From 95f71c6255a9b1e9ccaf65100c48ea4359dbfa58 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Mon, 6 May 2024 16:17:59 -0700 Subject: [PATCH 01/24] Start defining a service for evaluation results. Use connect-go to generate the handler. --- app/pkg/eval/evaluator.go | 2 ++ protos/buf.gen.yaml | 3 +++ protos/foyle/v1alpha1/eval.proto | 14 ++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/app/pkg/eval/evaluator.go b/app/pkg/eval/evaluator.go index 3329649f..99e30fd4 100644 --- a/app/pkg/eval/evaluator.go +++ b/app/pkg/eval/evaluator.go @@ -501,6 +501,8 @@ func (e *Evaluator) loadFoyleFiles(ctx context.Context, db *pebble.DB, files []s continue } + // TODO(https://github.com/jlewi/foyle/issues/95): We should assign an ID to each example that is stable + // across experiments. id := uuid.NewString() example := &v1alpha1.Example{ Id: id, diff --git a/protos/buf.gen.yaml b/protos/buf.gen.yaml index 42137951..b0ee41a9 100644 --- a/protos/buf.gen.yaml +++ b/protos/buf.gen.yaml @@ -26,5 +26,8 @@ plugins: - import_extension=none out: ../frontend/foyle/src/gen - name: zap-marshaler + out: go/ + opt: paths=source_relative + - plugin: connect-go out: go/ opt: paths=source_relative \ No newline at end of file diff --git a/protos/foyle/v1alpha1/eval.proto b/protos/foyle/v1alpha1/eval.proto index 5a2c5c32..fb1ba41f 100644 --- a/protos/foyle/v1alpha1/eval.proto +++ b/protos/foyle/v1alpha1/eval.proto @@ -32,3 +32,17 @@ message EvalResult { // Status of the evaluation EvalResultStatus status = 6; } + +message EvalResultListRequest { + // The path of the database to fetch results for + string database = 1; +} + +message EvalResultListResponse { + repeated EvalResult items = 1; +} + + +service EvalService { + rpc List(EvalResultListRequest) returns (EvalResultListResponse) {} +} \ No newline at end of file From c17ed4ae88013c25f06dafb7b77e238bd48cc2cc Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Mon, 6 May 2024 16:54:13 -0700 Subject: [PATCH 02/24] The go_package option was missing a directory. Not sure why it wasn't a problem before but with the connect protocl it is a problem. --- app/pkg/eval/service.go | 1 + protos/README.md | 8 + protos/foyle/v1alpha1/agent.proto | 2 +- protos/foyle/v1alpha1/doc.proto | 2 +- protos/foyle/v1alpha1/eval.proto | 2 +- protos/foyle/v1alpha1/trainer.proto | 2 +- protos/go/foyle/v1alpha1/agent.pb.go | 6 +- protos/go/foyle/v1alpha1/doc.pb.go | 6 +- protos/go/foyle/v1alpha1/eval.pb.go | 182 ++++++++++++++--- protos/go/foyle/v1alpha1/eval.zap.go | 39 ++++ protos/go/foyle/v1alpha1/eval_grpc.pb.go | 105 ++++++++++ protos/go/foyle/v1alpha1/trainer.pb.go | 5 +- .../v1alpha1/v1alpha1connect/agent.connect.go | 191 ++++++++++++++++++ .../v1alpha1/v1alpha1connect/eval.connect.go | 112 ++++++++++ 14 files changed, 628 insertions(+), 35 deletions(-) create mode 100644 app/pkg/eval/service.go create mode 100644 protos/go/foyle/v1alpha1/eval_grpc.pb.go create mode 100644 protos/go/foyle/v1alpha1/v1alpha1connect/agent.connect.go create mode 100644 protos/go/foyle/v1alpha1/v1alpha1connect/eval.connect.go diff --git a/app/pkg/eval/service.go b/app/pkg/eval/service.go new file mode 100644 index 00000000..ab209193 --- /dev/null +++ b/app/pkg/eval/service.go @@ -0,0 +1 @@ +package eval diff --git a/protos/README.md b/protos/README.md index ab94d030..b51745a4 100644 --- a/protos/README.md +++ b/protos/README.md @@ -46,6 +46,14 @@ More documentation can be found [here](https://github.com/bufbuild/protobuf-es/b We [grpc-gateway](https://grpc-ecosystem.github.io/grpc-gateway/) to generate RESTful services from our grpc services. +## connect-go + +To install the connect-go plugin + +``` + go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest +``` + ## Zap Marshalers We use [zap marshalers](https://pkg.go.dev/go.uber.org/zap#hdr-JSON) to generate MarshalLogObject methods for our types. diff --git a/protos/foyle/v1alpha1/agent.proto b/protos/foyle/v1alpha1/agent.proto index 18edeeb3..c1a3ae15 100644 --- a/protos/foyle/v1alpha1/agent.proto +++ b/protos/foyle/v1alpha1/agent.proto @@ -3,7 +3,7 @@ syntax = "proto3"; import "google/protobuf/struct.proto"; import "foyle/v1alpha1/doc.proto"; -option go_package = "github.com/jlewi/foyle/protos/go/v1alpha1"; +option go_package = "github.com/jlewi/foyle/protos/go/foyle/v1alpha1"; // grpc-gateway // https://github.com/grpc-ecosystem/grpc-gateway diff --git a/protos/foyle/v1alpha1/doc.proto b/protos/foyle/v1alpha1/doc.proto index 3a7530bd..2c0fa8bb 100644 --- a/protos/foyle/v1alpha1/doc.proto +++ b/protos/foyle/v1alpha1/doc.proto @@ -2,7 +2,7 @@ syntax = "proto3"; import "google/protobuf/struct.proto"; -option go_package = "github.com/jlewi/foyle/protos/go/v1alpha1"; +option go_package = "github.com/jlewi/foyle/protos/go/foyle/v1alpha1"; // Doc represents a document in the editor. message Doc { diff --git a/protos/foyle/v1alpha1/eval.proto b/protos/foyle/v1alpha1/eval.proto index fb1ba41f..5599c524 100644 --- a/protos/foyle/v1alpha1/eval.proto +++ b/protos/foyle/v1alpha1/eval.proto @@ -5,7 +5,7 @@ import "foyle/v1alpha1/trainer.proto"; import "google/protobuf/struct.proto"; -option go_package = "github.com/jlewi/foyle/protos/go/v1alpha1"; +option go_package = "github.com/jlewi/foyle/protos/go/foyle/v1alpha1"; enum EvalResultStatus { UNKNOWN_EVAL_RESULT_STATUS = 0; diff --git a/protos/foyle/v1alpha1/trainer.proto b/protos/foyle/v1alpha1/trainer.proto index b9a33cf5..3dff4241 100644 --- a/protos/foyle/v1alpha1/trainer.proto +++ b/protos/foyle/v1alpha1/trainer.proto @@ -4,7 +4,7 @@ import "foyle/v1alpha1/doc.proto"; import "google/protobuf/struct.proto"; -option go_package = "github.com/jlewi/foyle/protos/go/v1alpha1"; +option go_package = "github.com/jlewi/foyle/protos/go/foyle/v1alpha1"; // Example represents an example to be used in few shot learning message Example { diff --git a/protos/go/foyle/v1alpha1/agent.pb.go b/protos/go/foyle/v1alpha1/agent.pb.go index e5c01613..be8d3fb1 100644 --- a/protos/go/foyle/v1alpha1/agent.pb.go +++ b/protos/go/foyle/v1alpha1/agent.pb.go @@ -245,10 +245,10 @@ var file_foyle_v1alpha1_agent_proto_rawDesc = []byte{ 0x74, 0x1a, 0x10, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x65, 0x78, - 0x65, 0x63, 0x75, 0x74, 0x65, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x65, 0x63, 0x75, 0x74, 0x65, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x6c, 0x65, 0x77, 0x69, 0x2f, 0x66, 0x6f, 0x79, 0x6c, 0x65, 0x2f, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, - 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x67, 0x6f, 0x2f, 0x66, 0x6f, 0x79, 0x6c, 0x65, 0x2f, + 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/protos/go/foyle/v1alpha1/doc.pb.go b/protos/go/foyle/v1alpha1/doc.pb.go index 6c45247c..d1ae5f4c 100644 --- a/protos/go/foyle/v1alpha1/doc.pb.go +++ b/protos/go/foyle/v1alpha1/doc.pb.go @@ -364,11 +364,11 @@ var file_foyle_v1alpha1_doc_proto_rawDesc = []byte{ 0x61, 0x2a, 0x39, 0x0a, 0x09, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x16, 0x0a, 0x12, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x4d, 0x41, 0x52, 0x4b, 0x55, 0x50, - 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x43, 0x4f, 0x44, 0x45, 0x10, 0x02, 0x42, 0x2b, 0x5a, 0x29, + 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x43, 0x4f, 0x44, 0x45, 0x10, 0x02, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x6c, 0x65, 0x77, 0x69, 0x2f, 0x66, 0x6f, 0x79, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x67, 0x6f, - 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x2f, 0x66, 0x6f, 0x79, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/protos/go/foyle/v1alpha1/eval.pb.go b/protos/go/foyle/v1alpha1/eval.pb.go index 8e4d9f5d..1784138d 100644 --- a/protos/go/foyle/v1alpha1/eval.pb.go +++ b/protos/go/foyle/v1alpha1/eval.pb.go @@ -171,6 +171,101 @@ func (x *EvalResult) GetStatus() EvalResultStatus { return EvalResultStatus_UNKNOWN_EVAL_RESULT_STATUS } +type EvalResultListRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The path of the database to fetch results for + Database string `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"` +} + +func (x *EvalResultListRequest) Reset() { + *x = EvalResultListRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_foyle_v1alpha1_eval_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EvalResultListRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EvalResultListRequest) ProtoMessage() {} + +func (x *EvalResultListRequest) ProtoReflect() protoreflect.Message { + mi := &file_foyle_v1alpha1_eval_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EvalResultListRequest.ProtoReflect.Descriptor instead. +func (*EvalResultListRequest) Descriptor() ([]byte, []int) { + return file_foyle_v1alpha1_eval_proto_rawDescGZIP(), []int{1} +} + +func (x *EvalResultListRequest) GetDatabase() string { + if x != nil { + return x.Database + } + return "" +} + +type EvalResultListResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Items []*EvalResult `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` +} + +func (x *EvalResultListResponse) Reset() { + *x = EvalResultListResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_foyle_v1alpha1_eval_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EvalResultListResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EvalResultListResponse) ProtoMessage() {} + +func (x *EvalResultListResponse) ProtoReflect() protoreflect.Message { + mi := &file_foyle_v1alpha1_eval_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EvalResultListResponse.ProtoReflect.Descriptor instead. +func (*EvalResultListResponse) Descriptor() ([]byte, []int) { + return file_foyle_v1alpha1_eval_proto_rawDescGZIP(), []int{2} +} + +func (x *EvalResultListResponse) GetItems() []*EvalResult { + if x != nil { + return x.Items + } + return nil +} + var File_foyle_v1alpha1_eval_proto protoreflect.FileDescriptor var file_foyle_v1alpha1_eval_proto_rawDesc = []byte{ @@ -197,15 +292,27 @@ var file_foyle_v1alpha1_eval_proto_rawDesc = []byte{ 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x29, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x2a, 0x47, 0x0a, 0x10, 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x73, - 0x75, 0x6c, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x1a, 0x55, 0x4e, 0x4b, - 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x45, 0x56, 0x41, 0x4c, 0x5f, 0x52, 0x45, 0x53, 0x55, 0x4c, 0x54, - 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x4f, 0x4e, - 0x45, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x42, 0x2b, - 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x6c, 0x65, - 0x77, 0x69, 0x2f, 0x66, 0x6f, 0x79, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, - 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x33, 0x0a, 0x15, 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x73, + 0x75, 0x6c, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, + 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x22, 0x3b, 0x0a, 0x16, 0x45, 0x76, + 0x61, 0x6c, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x2a, 0x47, 0x0a, 0x10, 0x45, 0x76, 0x61, 0x6c, 0x52, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x1a, 0x55, + 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x45, 0x56, 0x41, 0x4c, 0x5f, 0x52, 0x45, 0x53, 0x55, + 0x4c, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, + 0x4f, 0x4e, 0x45, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, + 0x32, 0x48, 0x0a, 0x0b, 0x45, 0x76, 0x61, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, + 0x39, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x2e, 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x17, 0x2e, 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x6c, 0x65, 0x77, 0x69, 0x2f, 0x66, + 0x6f, 0x79, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x67, 0x6f, 0x2f, 0x66, + 0x6f, 0x79, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -221,22 +328,27 @@ func file_foyle_v1alpha1_eval_proto_rawDescGZIP() []byte { } var file_foyle_v1alpha1_eval_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_foyle_v1alpha1_eval_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_foyle_v1alpha1_eval_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_foyle_v1alpha1_eval_proto_goTypes = []interface{}{ - (EvalResultStatus)(0), // 0: EvalResultStatus - (*EvalResult)(nil), // 1: EvalResult - (*Example)(nil), // 2: Example - (*Block)(nil), // 3: Block + (EvalResultStatus)(0), // 0: EvalResultStatus + (*EvalResult)(nil), // 1: EvalResult + (*EvalResultListRequest)(nil), // 2: EvalResultListRequest + (*EvalResultListResponse)(nil), // 3: EvalResultListResponse + (*Example)(nil), // 4: Example + (*Block)(nil), // 5: Block } var file_foyle_v1alpha1_eval_proto_depIdxs = []int32{ - 2, // 0: EvalResult.example:type_name -> Example - 3, // 1: EvalResult.actual:type_name -> Block + 4, // 0: EvalResult.example:type_name -> Example + 5, // 1: EvalResult.actual:type_name -> Block 0, // 2: EvalResult.status:type_name -> EvalResultStatus - 3, // [3:3] is the sub-list for method output_type - 3, // [3:3] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 1, // 3: EvalResultListResponse.items:type_name -> EvalResult + 2, // 4: EvalService.List:input_type -> EvalResultListRequest + 3, // 5: EvalService.List:output_type -> EvalResultListResponse + 5, // [5:6] is the sub-list for method output_type + 4, // [4:5] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_foyle_v1alpha1_eval_proto_init() } @@ -259,6 +371,30 @@ func file_foyle_v1alpha1_eval_proto_init() { return nil } } + file_foyle_v1alpha1_eval_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EvalResultListRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_foyle_v1alpha1_eval_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EvalResultListResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -266,9 +402,9 @@ func file_foyle_v1alpha1_eval_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_foyle_v1alpha1_eval_proto_rawDesc, NumEnums: 1, - NumMessages: 1, + NumMessages: 3, NumExtensions: 0, - NumServices: 0, + NumServices: 1, }, GoTypes: file_foyle_v1alpha1_eval_proto_goTypes, DependencyIndexes: file_foyle_v1alpha1_eval_proto_depIdxs, diff --git a/protos/go/foyle/v1alpha1/eval.zap.go b/protos/go/foyle/v1alpha1/eval.zap.go index ee20ea54..f6c9494a 100644 --- a/protos/go/foyle/v1alpha1/eval.zap.go +++ b/protos/go/foyle/v1alpha1/eval.zap.go @@ -63,3 +63,42 @@ func (m *EvalResult) MarshalLogObject(enc go_uber_org_zap_zapcore.ObjectEncoder) return nil } + +func (m *EvalResultListRequest) MarshalLogObject(enc go_uber_org_zap_zapcore.ObjectEncoder) error { + var keyName string + _ = keyName + + if m == nil { + return nil + } + + keyName = "database" // field database = 1 + enc.AddString(keyName, m.Database) + + return nil +} + +func (m *EvalResultListResponse) MarshalLogObject(enc go_uber_org_zap_zapcore.ObjectEncoder) error { + var keyName string + _ = keyName + + if m == nil { + return nil + } + + keyName = "items" // field items = 1 + enc.AddArray(keyName, go_uber_org_zap_zapcore.ArrayMarshalerFunc(func(aenc go_uber_org_zap_zapcore.ArrayEncoder) error { + for _, rv := range m.Items { + _ = rv + if rv != nil { + var vv interface{} = rv + if marshaler, ok := vv.(go_uber_org_zap_zapcore.ObjectMarshaler); ok { + aenc.AppendObject(marshaler) + } + } + } + return nil + })) + + return nil +} diff --git a/protos/go/foyle/v1alpha1/eval_grpc.pb.go b/protos/go/foyle/v1alpha1/eval_grpc.pb.go new file mode 100644 index 00000000..13687545 --- /dev/null +++ b/protos/go/foyle/v1alpha1/eval_grpc.pb.go @@ -0,0 +1,105 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc (unknown) +// source: foyle/v1alpha1/eval.proto + +package v1alpha1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// EvalServiceClient is the client API for EvalService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type EvalServiceClient interface { + List(ctx context.Context, in *EvalResultListRequest, opts ...grpc.CallOption) (*EvalResultListResponse, error) +} + +type evalServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewEvalServiceClient(cc grpc.ClientConnInterface) EvalServiceClient { + return &evalServiceClient{cc} +} + +func (c *evalServiceClient) List(ctx context.Context, in *EvalResultListRequest, opts ...grpc.CallOption) (*EvalResultListResponse, error) { + out := new(EvalResultListResponse) + err := c.cc.Invoke(ctx, "/EvalService/List", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// EvalServiceServer is the server API for EvalService service. +// All implementations must embed UnimplementedEvalServiceServer +// for forward compatibility +type EvalServiceServer interface { + List(context.Context, *EvalResultListRequest) (*EvalResultListResponse, error) + mustEmbedUnimplementedEvalServiceServer() +} + +// UnimplementedEvalServiceServer must be embedded to have forward compatible implementations. +type UnimplementedEvalServiceServer struct { +} + +func (UnimplementedEvalServiceServer) List(context.Context, *EvalResultListRequest) (*EvalResultListResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method List not implemented") +} +func (UnimplementedEvalServiceServer) mustEmbedUnimplementedEvalServiceServer() {} + +// UnsafeEvalServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to EvalServiceServer will +// result in compilation errors. +type UnsafeEvalServiceServer interface { + mustEmbedUnimplementedEvalServiceServer() +} + +func RegisterEvalServiceServer(s grpc.ServiceRegistrar, srv EvalServiceServer) { + s.RegisterService(&EvalService_ServiceDesc, srv) +} + +func _EvalService_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EvalResultListRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(EvalServiceServer).List(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/EvalService/List", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EvalServiceServer).List(ctx, req.(*EvalResultListRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// EvalService_ServiceDesc is the grpc.ServiceDesc for EvalService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var EvalService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "EvalService", + HandlerType: (*EvalServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "List", + Handler: _EvalService_List_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "foyle/v1alpha1/eval.proto", +} diff --git a/protos/go/foyle/v1alpha1/trainer.pb.go b/protos/go/foyle/v1alpha1/trainer.pb.go index a1e0642f..71e98c34 100644 --- a/protos/go/foyle/v1alpha1/trainer.pb.go +++ b/protos/go/foyle/v1alpha1/trainer.pb.go @@ -108,10 +108,11 @@ var file_foyle_v1alpha1_trainer_proto_rawDesc = []byte{ 0x1a, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x04, 0x2e, 0x44, 0x6f, 0x63, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x1e, 0x0a, 0x06, 0x61, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x06, 0x2e, 0x42, 0x6c, - 0x6f, 0x63, 0x6b, 0x52, 0x06, 0x61, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x42, 0x2b, 0x5a, 0x29, 0x67, + 0x6f, 0x63, 0x6b, 0x52, 0x06, 0x61, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x6c, 0x65, 0x77, 0x69, 0x2f, 0x66, 0x6f, 0x79, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x67, 0x6f, 0x2f, - 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x66, 0x6f, 0x79, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/protos/go/foyle/v1alpha1/v1alpha1connect/agent.connect.go b/protos/go/foyle/v1alpha1/v1alpha1connect/agent.connect.go new file mode 100644 index 00000000..1d453ed4 --- /dev/null +++ b/protos/go/foyle/v1alpha1/v1alpha1connect/agent.connect.go @@ -0,0 +1,191 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: foyle/v1alpha1/agent.proto + +package v1alpha1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1alpha1 "github.com/jlewi/foyle/protos/go/foyle/v1alpha1" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // GenerateServiceName is the fully-qualified name of the GenerateService service. + GenerateServiceName = "GenerateService" + // ExecuteServiceName is the fully-qualified name of the ExecuteService service. + ExecuteServiceName = "ExecuteService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // GenerateServiceGenerateProcedure is the fully-qualified name of the GenerateService's Generate + // RPC. + GenerateServiceGenerateProcedure = "/GenerateService/Generate" + // ExecuteServiceExecuteProcedure is the fully-qualified name of the ExecuteService's Execute RPC. + ExecuteServiceExecuteProcedure = "/ExecuteService/Execute" +) + +// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. +var ( + generateServiceServiceDescriptor = v1alpha1.File_foyle_v1alpha1_agent_proto.Services().ByName("GenerateService") + generateServiceGenerateMethodDescriptor = generateServiceServiceDescriptor.Methods().ByName("Generate") + executeServiceServiceDescriptor = v1alpha1.File_foyle_v1alpha1_agent_proto.Services().ByName("ExecuteService") + executeServiceExecuteMethodDescriptor = executeServiceServiceDescriptor.Methods().ByName("Execute") +) + +// GenerateServiceClient is a client for the GenerateService service. +type GenerateServiceClient interface { + // Generate generates new cells given an existing document. + Generate(context.Context, *connect.Request[v1alpha1.GenerateRequest]) (*connect.Response[v1alpha1.GenerateResponse], error) +} + +// NewGenerateServiceClient constructs a client for the GenerateService service. By default, it uses +// the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends +// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewGenerateServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) GenerateServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + return &generateServiceClient{ + generate: connect.NewClient[v1alpha1.GenerateRequest, v1alpha1.GenerateResponse]( + httpClient, + baseURL+GenerateServiceGenerateProcedure, + connect.WithSchema(generateServiceGenerateMethodDescriptor), + connect.WithClientOptions(opts...), + ), + } +} + +// generateServiceClient implements GenerateServiceClient. +type generateServiceClient struct { + generate *connect.Client[v1alpha1.GenerateRequest, v1alpha1.GenerateResponse] +} + +// Generate calls GenerateService.Generate. +func (c *generateServiceClient) Generate(ctx context.Context, req *connect.Request[v1alpha1.GenerateRequest]) (*connect.Response[v1alpha1.GenerateResponse], error) { + return c.generate.CallUnary(ctx, req) +} + +// GenerateServiceHandler is an implementation of the GenerateService service. +type GenerateServiceHandler interface { + // Generate generates new cells given an existing document. + Generate(context.Context, *connect.Request[v1alpha1.GenerateRequest]) (*connect.Response[v1alpha1.GenerateResponse], error) +} + +// NewGenerateServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewGenerateServiceHandler(svc GenerateServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + generateServiceGenerateHandler := connect.NewUnaryHandler( + GenerateServiceGenerateProcedure, + svc.Generate, + connect.WithSchema(generateServiceGenerateMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + return "/GenerateService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case GenerateServiceGenerateProcedure: + generateServiceGenerateHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedGenerateServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedGenerateServiceHandler struct{} + +func (UnimplementedGenerateServiceHandler) Generate(context.Context, *connect.Request[v1alpha1.GenerateRequest]) (*connect.Response[v1alpha1.GenerateResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("GenerateService.Generate is not implemented")) +} + +// ExecuteServiceClient is a client for the ExecuteService service. +type ExecuteServiceClient interface { + // Execute executes a cell in an existing document. + Execute(context.Context, *connect.Request[v1alpha1.ExecuteRequest]) (*connect.Response[v1alpha1.ExecuteResponse], error) +} + +// NewExecuteServiceClient constructs a client for the ExecuteService service. By default, it uses +// the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends +// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewExecuteServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ExecuteServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + return &executeServiceClient{ + execute: connect.NewClient[v1alpha1.ExecuteRequest, v1alpha1.ExecuteResponse]( + httpClient, + baseURL+ExecuteServiceExecuteProcedure, + connect.WithSchema(executeServiceExecuteMethodDescriptor), + connect.WithClientOptions(opts...), + ), + } +} + +// executeServiceClient implements ExecuteServiceClient. +type executeServiceClient struct { + execute *connect.Client[v1alpha1.ExecuteRequest, v1alpha1.ExecuteResponse] +} + +// Execute calls ExecuteService.Execute. +func (c *executeServiceClient) Execute(ctx context.Context, req *connect.Request[v1alpha1.ExecuteRequest]) (*connect.Response[v1alpha1.ExecuteResponse], error) { + return c.execute.CallUnary(ctx, req) +} + +// ExecuteServiceHandler is an implementation of the ExecuteService service. +type ExecuteServiceHandler interface { + // Execute executes a cell in an existing document. + Execute(context.Context, *connect.Request[v1alpha1.ExecuteRequest]) (*connect.Response[v1alpha1.ExecuteResponse], error) +} + +// NewExecuteServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewExecuteServiceHandler(svc ExecuteServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + executeServiceExecuteHandler := connect.NewUnaryHandler( + ExecuteServiceExecuteProcedure, + svc.Execute, + connect.WithSchema(executeServiceExecuteMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + return "/ExecuteService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case ExecuteServiceExecuteProcedure: + executeServiceExecuteHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedExecuteServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedExecuteServiceHandler struct{} + +func (UnimplementedExecuteServiceHandler) Execute(context.Context, *connect.Request[v1alpha1.ExecuteRequest]) (*connect.Response[v1alpha1.ExecuteResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ExecuteService.Execute is not implemented")) +} diff --git a/protos/go/foyle/v1alpha1/v1alpha1connect/eval.connect.go b/protos/go/foyle/v1alpha1/v1alpha1connect/eval.connect.go new file mode 100644 index 00000000..5cedccc5 --- /dev/null +++ b/protos/go/foyle/v1alpha1/v1alpha1connect/eval.connect.go @@ -0,0 +1,112 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: foyle/v1alpha1/eval.proto + +package v1alpha1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1alpha1 "github.com/jlewi/foyle/protos/go/foyle/v1alpha1" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // EvalServiceName is the fully-qualified name of the EvalService service. + EvalServiceName = "EvalService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // EvalServiceListProcedure is the fully-qualified name of the EvalService's List RPC. + EvalServiceListProcedure = "/EvalService/List" +) + +// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. +var ( + evalServiceServiceDescriptor = v1alpha1.File_foyle_v1alpha1_eval_proto.Services().ByName("EvalService") + evalServiceListMethodDescriptor = evalServiceServiceDescriptor.Methods().ByName("List") +) + +// EvalServiceClient is a client for the EvalService service. +type EvalServiceClient interface { + List(context.Context, *connect.Request[v1alpha1.EvalResultListRequest]) (*connect.Response[v1alpha1.EvalResultListResponse], error) +} + +// NewEvalServiceClient constructs a client for the EvalService service. By default, it uses the +// Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends +// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewEvalServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) EvalServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + return &evalServiceClient{ + list: connect.NewClient[v1alpha1.EvalResultListRequest, v1alpha1.EvalResultListResponse]( + httpClient, + baseURL+EvalServiceListProcedure, + connect.WithSchema(evalServiceListMethodDescriptor), + connect.WithClientOptions(opts...), + ), + } +} + +// evalServiceClient implements EvalServiceClient. +type evalServiceClient struct { + list *connect.Client[v1alpha1.EvalResultListRequest, v1alpha1.EvalResultListResponse] +} + +// List calls EvalService.List. +func (c *evalServiceClient) List(ctx context.Context, req *connect.Request[v1alpha1.EvalResultListRequest]) (*connect.Response[v1alpha1.EvalResultListResponse], error) { + return c.list.CallUnary(ctx, req) +} + +// EvalServiceHandler is an implementation of the EvalService service. +type EvalServiceHandler interface { + List(context.Context, *connect.Request[v1alpha1.EvalResultListRequest]) (*connect.Response[v1alpha1.EvalResultListResponse], error) +} + +// NewEvalServiceHandler builds an HTTP handler from the service implementation. It returns the path +// on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewEvalServiceHandler(svc EvalServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + evalServiceListHandler := connect.NewUnaryHandler( + EvalServiceListProcedure, + svc.List, + connect.WithSchema(evalServiceListMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + return "/EvalService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case EvalServiceListProcedure: + evalServiceListHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedEvalServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedEvalServiceHandler struct{} + +func (UnimplementedEvalServiceHandler) List(context.Context, *connect.Request[v1alpha1.EvalResultListRequest]) (*connect.Response[v1alpha1.EvalResultListResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("EvalService.List is not implemented")) +} From cbd47158141128185e524a034a5f96467b376f1c Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Mon, 6 May 2024 17:02:29 -0700 Subject: [PATCH 03/24] This is working in terms of setting up a connect protocol server using bug. You can send a curl request as curl \ --header "Content-Type: application/json" \ --data '{"name": "Jane"}' \ http://localhost:8080/EvalService/List --- app/go.mod | 7 +- app/go.sum | 4 +- app/pkg/eval/service.go | 29 +++++++ app/pkg/server/server.go | 7 ++ .../foyle/src/gen/foyle/v1alpha1/eval_pb.ts | 76 +++++++++++++++++++ 5 files changed, 118 insertions(+), 5 deletions(-) diff --git a/app/go.mod b/app/go.mod index 097a2b2c..770d0734 100644 --- a/app/go.mod +++ b/app/go.mod @@ -5,6 +5,7 @@ go 1.22.1 replace github.com/jlewi/foyle/protos/go => ../protos/go require ( + connectrpc.com/connect v1.16.1 github.com/Kunde21/markdownfmt/v3 v3.1.0 github.com/agnivade/levenshtein v1.1.1 github.com/cockroachdb/pebble v1.1.0 @@ -37,9 +38,12 @@ require ( go.opentelemetry.io/otel/trace v1.25.0 go.uber.org/zap v1.27.0 gonum.org/v1/gonum v0.15.0 + google.golang.org/api v0.162.0 google.golang.org/grpc v1.63.2 google.golang.org/protobuf v1.33.0 gopkg.in/yaml.v3 v3.0.1 + k8s.io/apimachinery v0.27.3 + sigs.k8s.io/kustomize/kyaml v0.13.9 ) require ( @@ -189,7 +193,6 @@ require ( golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.15.0 // indirect - google.golang.org/api v0.162.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda // indirect @@ -198,13 +201,11 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/apimachinery v0.27.3 // indirect k8s.io/klog/v2 v2.90.1 // indirect k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.12.1 // indirect - sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/app/go.sum b/app/go.sum index e8d18c8b..687bed91 100644 --- a/app/go.sum +++ b/app/go.sum @@ -48,6 +48,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.36.0 h1:P0mOkAcaJxhCTvAkMhxMfrTKiNcub4YmmPBtlhAyTr8= cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= +connectrpc.com/connect v1.16.1 h1:rOdrK/RTI/7TVnn3JsVxt3n028MlTRwmK5Q4heSpjis= +connectrpc.com/connect v1.16.1/go.mod h1:XpZAduBQUySsb4/KO5JffORVkDI4B6/EYPi7N8xpNZw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= @@ -348,8 +350,6 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= -github.com/jlewi/hydros v0.0.6 h1:gylMzSH5VvO+uTtz20JUtJPPr3ZLAQxvH3j/0FHK4FE= -github.com/jlewi/hydros v0.0.6/go.mod h1:4fV+JUCnexPY2ZbKzdfV/RsyrfralN832MsUSq/7FqE= github.com/jlewi/hydros v0.0.7-0.20240503183011-8f99ead373fb h1:2G2k606S3Qcg40czr7gnkeIG5KgQ2wXJ1BMxAuC+P3I= github.com/jlewi/hydros v0.0.7-0.20240503183011-8f99ead373fb/go.mod h1:4fV+JUCnexPY2ZbKzdfV/RsyrfralN832MsUSq/7FqE= github.com/jlewi/monogo v0.0.0-20240123191147-401afe194d74 h1:pbOw/rOMs0AZ494bGnI6DieGKwqoJQEjHWaJZrvxsJo= diff --git a/app/pkg/eval/service.go b/app/pkg/eval/service.go index ab209193..356bdfa2 100644 --- a/app/pkg/eval/service.go +++ b/app/pkg/eval/service.go @@ -1 +1,30 @@ package eval + +import ( + "connectrpc.com/connect" + "context" + "github.com/jlewi/foyle/protos/go/foyle/v1alpha1" + "log" +) + +// EvalServer is the server that implements the Eval service interface. +// This is used to make results available to the frontend. +type EvalServer struct{} + +func (s *EvalServer) List( + ctx context.Context, + req *connect.Request[v1alpha1.EvalResultListRequest], +) (*connect.Response[v1alpha1.EvalResultListResponse], error) { + log.Println("Request headers: ", req.Header()) + res := connect.NewResponse(&v1alpha1.EvalResultListResponse{ + Items: []*v1alpha1.EvalResult{ + { + Example: &v1alpha1.Example{ + Id: "hello-world", + }, + }, + }, + }) + res.Header().Set("Eval-Version", "v1alpha1") + return res, nil +} diff --git a/app/pkg/server/server.go b/app/pkg/server/server.go index f1d30342..9cd5f909 100644 --- a/app/pkg/server/server.go +++ b/app/pkg/server/server.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "fmt" + "github.com/jlewi/foyle/app/pkg/eval" + "github.com/jlewi/foyle/protos/go/foyle/v1alpha1/v1alpha1connect" "time" "github.com/jlewi/foyle/app/pkg/analyze" @@ -249,6 +251,11 @@ func (s *Server) createGinEngine() error { // because we need to leave the final slash in the path so that the route ends up matching. router.Any(logsviewer.AppPath+"/*any", gin.WrapH(http.StripPrefix(logsviewer.AppPath, viewerApp))) + path, handler := v1alpha1connect.NewEvalServiceHandler(&eval.EvalServer{}) + log.Info("Setting up eval service", "path", path) + router.Any(path+"*any", func(c *gin.Context) { + handler.ServeHTTP(c.Writer, c.Request) + }) s.engine = router return nil } diff --git a/frontend/foyle/src/gen/foyle/v1alpha1/eval_pb.ts b/frontend/foyle/src/gen/foyle/v1alpha1/eval_pb.ts index d476139b..6e067676 100644 --- a/frontend/foyle/src/gen/foyle/v1alpha1/eval_pb.ts +++ b/frontend/foyle/src/gen/foyle/v1alpha1/eval_pb.ts @@ -119,3 +119,79 @@ export class EvalResult extends Message { } } +/** + * @generated from message EvalResultListRequest + */ +export class EvalResultListRequest extends Message { + /** + * The path of the database to fetch results for + * + * @generated from field: string database = 1; + */ + database = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "EvalResultListRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "database", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): EvalResultListRequest { + return new EvalResultListRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): EvalResultListRequest { + return new EvalResultListRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): EvalResultListRequest { + return new EvalResultListRequest().fromJsonString(jsonString, options); + } + + static equals(a: EvalResultListRequest | PlainMessage | undefined, b: EvalResultListRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(EvalResultListRequest, a, b); + } +} + +/** + * @generated from message EvalResultListResponse + */ +export class EvalResultListResponse extends Message { + /** + * @generated from field: repeated EvalResult items = 1; + */ + items: EvalResult[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "EvalResultListResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "items", kind: "message", T: EvalResult, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): EvalResultListResponse { + return new EvalResultListResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): EvalResultListResponse { + return new EvalResultListResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): EvalResultListResponse { + return new EvalResultListResponse().fromJsonString(jsonString, options); + } + + static equals(a: EvalResultListResponse | PlainMessage | undefined, b: EvalResultListResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(EvalResultListResponse, a, b); + } +} + From 5fb957d493239ea0f330c059233a6f3a2ef2490c Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Mon, 6 May 2024 17:21:58 -0700 Subject: [PATCH 04/24] Add the api prefix to the route. --- app/pkg/server/server.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/app/pkg/server/server.go b/app/pkg/server/server.go index 9cd5f909..63016e0f 100644 --- a/app/pkg/server/server.go +++ b/app/pkg/server/server.go @@ -220,8 +220,22 @@ func (s *Server) createGinEngine() error { router.Use(corsMiddleWare) } + // N.B. don't include leading or trailing slashes in the prefix because the code below assumes there isn't any + apiPrefix := "api" // Add REST handlers for blocklogs - router.GET("api/blocklogs/:id", s.logsCrud.GetBlockLog) + // TODO(jeremy): We should probably standardize on connect-rpc + router.GET(apiPrefix+"/blocklogs/:id", s.logsCrud.GetBlockLog) + + // Set up the connect-rpc handlers for the EvalServer + path, handler := v1alpha1connect.NewEvalServiceHandler(&eval.EvalServer{}) + log.Info("Setting up eval service", "path", path) + // Since we want to add the prefix apiPrefix we need to strip it before passing it to the connect-rpc handler + // Refer to https://connectrpc.com/docs/go/routing#prefixing-routes. Note that grpc-go clients don't + // support prefixes. + router.Any(apiPrefix+"/"+path+"*any", gin.WrapH(http.StripPrefix("/"+apiPrefix, handler))) + s.engine = router + + // Setup the logs viewer app.Route("/", &logsviewer.MainApp{}) @@ -251,12 +265,6 @@ func (s *Server) createGinEngine() error { // because we need to leave the final slash in the path so that the route ends up matching. router.Any(logsviewer.AppPath+"/*any", gin.WrapH(http.StripPrefix(logsviewer.AppPath, viewerApp))) - path, handler := v1alpha1connect.NewEvalServiceHandler(&eval.EvalServer{}) - log.Info("Setting up eval service", "path", path) - router.Any(path+"*any", func(c *gin.Context) { - handler.ServeHTTP(c.Writer, c.Request) - }) - s.engine = router return nil } From db4ccd2ad17adadcf16c6a2f1cbc82fc41e661f3 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Mon, 6 May 2024 17:33:01 -0700 Subject: [PATCH 05/24] Implemented the EvalServiceList request. --- app/pkg/eval/service.go | 62 +++++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/app/pkg/eval/service.go b/app/pkg/eval/service.go index 356bdfa2..252db66f 100644 --- a/app/pkg/eval/service.go +++ b/app/pkg/eval/service.go @@ -3,8 +3,12 @@ package eval import ( "connectrpc.com/connect" "context" + "github.com/cockroachdb/pebble" + "github.com/jlewi/foyle/app/pkg/logs" "github.com/jlewi/foyle/protos/go/foyle/v1alpha1" - "log" + "github.com/jlewi/monogo/helpers" + "github.com/pkg/errors" + "google.golang.org/protobuf/proto" ) // EvalServer is the server that implements the Eval service interface. @@ -15,16 +19,52 @@ func (s *EvalServer) List( ctx context.Context, req *connect.Request[v1alpha1.EvalResultListRequest], ) (*connect.Response[v1alpha1.EvalResultListResponse], error) { - log.Println("Request headers: ", req.Header()) - res := connect.NewResponse(&v1alpha1.EvalResultListResponse{ - Items: []*v1alpha1.EvalResult{ - { - Example: &v1alpha1.Example{ - Id: "hello-world", - }, - }, - }, - }) + log := logs.FromContext(ctx) + + if req.Msg.GetDatabase() == "" { + err := connect.NewError(connect.CodeInvalidArgument, errors.New("Request is missing database")) + log.Error(err, "Invalid EvalResultListRequest") + return nil, err + } + + db, err := pebble.Open(req.Msg.GetDatabase(), &pebble.Options{}) + if err != nil { + log.Error(err, "Failed to open database") + return nil, connect.NewError(connect.CodeInternal, err) + } + defer helpers.DeferIgnoreError(db.Close) + + iter, err := db.NewIterWithContext(ctx, nil) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } + defer iter.Close() + + results := &v1alpha1.EvalResultListResponse{ + Items: make([]*v1alpha1.EvalResult, 0, 100), + } + + for iter.First(); iter.Valid(); iter.Next() { + key := iter.Key() + if key == nil { + break + } + + value, err := iter.ValueAndErr() + if err != nil { + log.Error(err, "Failed to read value for key", "key", string(key)) + continue + } + + result := &v1alpha1.EvalResult{} + if err := proto.Unmarshal(value, result); err != nil { + log.Error(err, "Failed to unmarshal value for", "key", string(key)) + continue + } + results.Items = append(results.Items, result) + } + + res := connect.NewResponse(results) res.Header().Set("Eval-Version", "v1alpha1") return res, nil } From ecb167ef624fed87dc1387461f5ac9443cfaad65 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Mon, 6 May 2024 19:01:01 -0700 Subject: [PATCH 06/24] Add a navigation bar for different pages. --- app/pkg/logsviewer/navigation.go | 28 ++++++++++++++++++++++++++++ app/pkg/logsviewer/viewer.go | 3 +++ 2 files changed, 31 insertions(+) create mode 100644 app/pkg/logsviewer/navigation.go diff --git a/app/pkg/logsviewer/navigation.go b/app/pkg/logsviewer/navigation.go new file mode 100644 index 00000000..4f482caf --- /dev/null +++ b/app/pkg/logsviewer/navigation.go @@ -0,0 +1,28 @@ +package logsviewer + +import "github.com/maxence-charriere/go-app/v9/pkg/app" + +// navigationBar is the left hand side navigation bar. +// It is used to select the different pages in the application +type navigationBar struct { + app.Compo +} + +func (s *navigationBar) Render() app.UI { + return app.Div().Body( + // Each button needs to be enclosed in a div. Otherwise events get triggered for all the buttons. + app.Div().Body( + app.Button().Text("Block Logs").OnClick(func(ctx app.Context, e app.Event) { + //ctx.NewActionWithValue(getAction, generatedBlockView) + }), + ), + app.Div().Body( + app.Button().Text("Eval Results")).OnClick(func(ctx app.Context, e app.Event) { + //ctx.NewActionWithValue(getAction, executedBlockView) + }), + //app.Div().Body( + // app.Button().Text("Raw")).OnClick(func(ctx app.Context, e app.Event) { + // ctx.NewActionWithValue(getAction, rawView) + //}) + ) +} diff --git a/app/pkg/logsviewer/viewer.go b/app/pkg/logsviewer/viewer.go index e7d6e3ff..c1bb992c 100644 --- a/app/pkg/logsviewer/viewer.go +++ b/app/pkg/logsviewer/viewer.go @@ -42,6 +42,9 @@ func (c *MainApp) Render() app.UI { &blockSelector{}, ), app.Div().Class("content").Body( + app.Div().Class("sidebar").Body( + &navigationBar{}, + ), app.Div().Class("sidebar").Body( &sideBar{}, ), From 29d8431ffb2317c341493a61eeca930ffabb1f5f Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Mon, 6 May 2024 19:01:29 -0700 Subject: [PATCH 07/24] Rename viewer to app to be the main application. --- app/pkg/logsviewer/{viewer.go => app.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/pkg/logsviewer/{viewer.go => app.go} (100%) diff --git a/app/pkg/logsviewer/viewer.go b/app/pkg/logsviewer/app.go similarity index 100% rename from app/pkg/logsviewer/viewer.go rename to app/pkg/logsviewer/app.go From 434c3570825e0817ef587860cdaebffdeebb8d11 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Mon, 6 May 2024 19:09:49 -0700 Subject: [PATCH 08/24] Add a left hand navigation bar to navigate between different pages. --- app/pkg/logsviewer/app.go | 39 ++----------------- app/pkg/logsviewer/block_viewer.go | 60 ++++++++++++++++++++++++++++++ app/web/viewer.css | 5 +++ 3 files changed, 69 insertions(+), 35 deletions(-) create mode 100644 app/pkg/logsviewer/block_viewer.go diff --git a/app/pkg/logsviewer/app.go b/app/pkg/logsviewer/app.go index c1bb992c..52819a44 100644 --- a/app/pkg/logsviewer/app.go +++ b/app/pkg/logsviewer/app.go @@ -30,50 +30,19 @@ const ( // MainApp is the main window of the application. type MainApp struct { app.Compo - main *mainWindow } func (c *MainApp) Render() app.UI { - if c.main == nil { - c.main = &mainWindow{} - } return app.Div().Class("main-layout").Body( - app.Div().Class("header").Body( - &blockSelector{}, - ), app.Div().Class("content").Body( app.Div().Class("sidebar").Body( &navigationBar{}, ), - app.Div().Class("sidebar").Body( - &sideBar{}, - ), - app.Div().Class("main-window").Body( - c.main, + app.Div().Class("page-window").Body( + // TODO(jeremy): How do we change this when the user clicks the left hand navigation bar? + // Do we need to find and update the div? + &BlockViewer{}, ), ), ) } - -// sideBar adds a navigation bar between the views to the left side. -type sideBar struct { - app.Compo -} - -func (s *sideBar) Render() app.UI { - return app.Div().Body( - // Each button needs to be enclosed in a div. Otherwise events get triggered for all the buttons. - app.Div().Body( - app.Button().Text("Generated Block").OnClick(func(ctx app.Context, e app.Event) { - ctx.NewActionWithValue(getAction, generatedBlockView) - }), - ), - app.Div().Body( - app.Button().Text("Executed Block")).OnClick(func(ctx app.Context, e app.Event) { - ctx.NewActionWithValue(getAction, executedBlockView) - }), - app.Div().Body( - app.Button().Text("Raw")).OnClick(func(ctx app.Context, e app.Event) { - ctx.NewActionWithValue(getAction, rawView) - })) -} diff --git a/app/pkg/logsviewer/block_viewer.go b/app/pkg/logsviewer/block_viewer.go new file mode 100644 index 00000000..6851c0ae --- /dev/null +++ b/app/pkg/logsviewer/block_viewer.go @@ -0,0 +1,60 @@ +package logsviewer + +import "github.com/maxence-charriere/go-app/v9/pkg/app" + +// BlockViewer is the page that displays the block logs. +// +// How it works: +// Clicking load fetches the blocklog from the server. +// The log is then stored in the application context (https://go-app.dev/states) +// this allows other components to use it. Load then fires off an UpdateView event to trigger +// the mainWindow to update its content. +// The UpdateView event takes a string argument which is what view should be rendered. +// There is a left hand navigation bar with buttons to display different views of the current log. +// Changing the view is achieved by sending UpdateView events to change the view +type BlockViewer struct { + app.Compo + main *mainWindow +} + +func (c *BlockViewer) Render() app.UI { + if c.main == nil { + c.main = &mainWindow{} + } + return app.Div().Class("main-layout").Body( + app.Div().Class("header").Body( + &blockSelector{}, + ), + app.Div().Class("content").Body( + app.Div().Class("sidebar").Body( + &sideBar{}, + ), + app.Div().Class("main-window").Body( + c.main, + ), + ), + ) +} + +// sideBar adds a navigation bar between the views to the left side. +type sideBar struct { + app.Compo +} + +func (s *sideBar) Render() app.UI { + return app.Div().Body( + // Each button needs to be enclosed in a div. Otherwise events get triggered for all the buttons. + app.Div().Body( + app.Button().Text("Generated Block").OnClick(func(ctx app.Context, e app.Event) { + ctx.NewActionWithValue(getAction, generatedBlockView) + }), + ), + app.Div().Body( + app.Button().Text("Executed Block")).OnClick(func(ctx app.Context, e app.Event) { + ctx.NewActionWithValue(getAction, executedBlockView) + }), + app.Div().Body( + app.Button().Text("Raw")).OnClick(func(ctx app.Context, e app.Event) { + ctx.NewActionWithValue(getAction, rawView) + })) +} diff --git a/app/web/viewer.css b/app/web/viewer.css index 8442ce2e..1febae66 100644 --- a/app/web/viewer.css +++ b/app/web/viewer.css @@ -19,6 +19,11 @@ background-color: #57d2ed; } +.page-window { + flex: 1; + background-color: #fff; +} + .main-window { flex: 1; background-color: #fff; From 19c3a3b1bb528471f452e389c4f8c1729330303f Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Mon, 6 May 2024 19:28:37 -0700 Subject: [PATCH 09/24] Start displaying the eval viewer. --- app/pkg/logsviewer/app.go | 50 +++++++++++++++++++++++++-- app/pkg/logsviewer/eval_viewer.go | 56 +++++++++++++++++++++++++++++++ app/pkg/logsviewer/navigation.go | 4 +-- 3 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 app/pkg/logsviewer/eval_viewer.go diff --git a/app/pkg/logsviewer/app.go b/app/pkg/logsviewer/app.go index 52819a44..5b847820 100644 --- a/app/pkg/logsviewer/app.go +++ b/app/pkg/logsviewer/app.go @@ -1,19 +1,27 @@ package logsviewer import ( + "github.com/go-logr/zapr" "github.com/maxence-charriere/go-app/v9/pkg/app" + "github.com/pkg/errors" + "go.uber.org/zap" ) +type page string type view string const ( - getAction = "/et" + getAction = "/get" + setPage = "/setPage" errorView view = "error" generatedBlockView view = "generatedBlock" executedBlockView view = "executedBlock" rawView view = "raw" + blockLogsView page = "blockLogs" + evalsView page = "evals" + getErrorState = "/getError" blockLogState = "/blocklog" ) @@ -30,9 +38,15 @@ const ( // MainApp is the main window of the application. type MainApp struct { app.Compo + // Page keeps track of the page to display in the right hand side. + page app.UI } -func (c *MainApp) Render() app.UI { +func (m *MainApp) Render() app.UI { + if m.page == nil { + // Default to the Blockvier + m.page = &BlockViewer{} + } return app.Div().Class("main-layout").Body( app.Div().Class("content").Body( app.Div().Class("sidebar").Body( @@ -41,8 +55,38 @@ func (c *MainApp) Render() app.UI { app.Div().Class("page-window").Body( // TODO(jeremy): How do we change this when the user clicks the left hand navigation bar? // Do we need to find and update the div? - &BlockViewer{}, + m.page, ), ), ) } + +func (m *MainApp) OnMount(ctx app.Context) { + // register to handle the setPage action + ctx.Handle(setPage, m.handleSetPage) +} + +// handleSetPage handles the setPage action. The event will tell us which view to display. +func (m *MainApp) handleSetPage(ctx app.Context, action app.Action) { + log := zapr.NewLogger(zap.L()) + pageValue, ok := action.Value.(page) // Checks if a name was given. + if !ok { + log.Error(errors.New("No page provided"), "Invalid action") + return + } + log.Info("Handling set page action", "page", pageValue) + switch pageValue { + case blockLogsView: + if _, ok := m.page.(*BlockViewer); !ok { + log.Info("Setting page to BlockViewer") + m.page = &BlockViewer{} + } + case evalsView: + if _, ok := m.page.(*EvalViewer); !ok { + log.Info("Setting page to EvalViewer") + m.page = &EvalViewer{} + } + } + // We need to call update to trigger a re-render of the component. + m.Update() +} diff --git a/app/pkg/logsviewer/eval_viewer.go b/app/pkg/logsviewer/eval_viewer.go new file mode 100644 index 00000000..0f072a03 --- /dev/null +++ b/app/pkg/logsviewer/eval_viewer.go @@ -0,0 +1,56 @@ +package logsviewer + +import "github.com/maxence-charriere/go-app/v9/pkg/app" + +// EvalViewer is the page that displays an eval result. +type EvalViewer struct { + app.Compo + main *mainWindow +} + +func (c *EvalViewer) Render() app.UI { + if c.main == nil { + c.main = &mainWindow{} + } + return app.Div().Class("main-layout").Body( + app.Div().Class("header").Body( + &blockSelector{}, + ), + app.Div().Class("content").Body( + app.Div().Class("sidebar").Body( + &evalSideBar{}, + ), + app.Div().Class("main-window").Body( + c.main, + ), + ), + ) +} + +// evalSideBar adds a navigation bar between the views to the left side. +type evalSideBar struct { + app.Compo +} + +func (s *evalSideBar) Render() app.UI { + return app.Div().Body( + // Each button needs to be enclosed in a div. Otherwise events get triggered for all the buttons. + app.Div().Body( + app.Button().Text("Query").OnClick(func(ctx app.Context, e app.Event) { + //ctx.NewActionWithValue(getAction, generatedBlockView) + }), + ), + app.Div().Body( + app.Button().Text("Actual Answer").OnClick(func(ctx app.Context, e app.Event) { + //ctx.NewActionWithValue(getAction, generatedBlockView) + }), + ), + app.Div().Body( + app.Button().Text("Expected Answer")).OnClick(func(ctx app.Context, e app.Event) { + //ctx.NewActionWithValue(getAction, executedBlockView) + }), + app.Div().Body( + app.Button().Text("Raw")).OnClick(func(ctx app.Context, e app.Event) { + //ctx.NewActionWithValue(getAction, rawView) + })) +} diff --git a/app/pkg/logsviewer/navigation.go b/app/pkg/logsviewer/navigation.go index 4f482caf..4fcbd94b 100644 --- a/app/pkg/logsviewer/navigation.go +++ b/app/pkg/logsviewer/navigation.go @@ -13,12 +13,12 @@ func (s *navigationBar) Render() app.UI { // Each button needs to be enclosed in a div. Otherwise events get triggered for all the buttons. app.Div().Body( app.Button().Text("Block Logs").OnClick(func(ctx app.Context, e app.Event) { - //ctx.NewActionWithValue(getAction, generatedBlockView) + ctx.NewActionWithValue(setPage, blockLogsView) }), ), app.Div().Body( app.Button().Text("Eval Results")).OnClick(func(ctx app.Context, e app.Event) { - //ctx.NewActionWithValue(getAction, executedBlockView) + ctx.NewActionWithValue(setPage, evalsView) }), //app.Div().Body( // app.Button().Text("Raw")).OnClick(func(ctx app.Context, e app.Event) { From 42596acbe6ebb49af1d2ff4c81078e4286851676 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Tue, 7 May 2024 06:14:29 -0700 Subject: [PATCH 10/24] Add a status bar to display the version. Try to update Version to force a reload. --- app/pkg/logsviewer/app.go | 15 +++++++- app/pkg/logsviewer/eval_viewer.go | 63 ++++++++++++++++++++++++++++++- app/pkg/server/server.go | 14 ++++++- app/pwa/main.go | 3 +- app/web/table.css | 4 ++ app/web/viewer.css | 10 +++++ 6 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 app/web/table.css diff --git a/app/pkg/logsviewer/app.go b/app/pkg/logsviewer/app.go index 5b847820..a048da16 100644 --- a/app/pkg/logsviewer/app.go +++ b/app/pkg/logsviewer/app.go @@ -44,7 +44,7 @@ type MainApp struct { func (m *MainApp) Render() app.UI { if m.page == nil { - // Default to the Blockvier + // Default to the Blockviewer m.page = &BlockViewer{} } return app.Div().Class("main-layout").Body( @@ -57,7 +57,7 @@ func (m *MainApp) Render() app.UI { // Do we need to find and update the div? m.page, ), - ), + ), &StatusBar{}, ) } @@ -90,3 +90,14 @@ func (m *MainApp) handleSetPage(ctx app.Context, action app.Action) { // We need to call update to trigger a re-render of the component. m.Update() } + +// StatusBar at the bottom of the page. Inspired by the vscode/intellij status bar. +// We use this to show useful information like the version number. +type StatusBar struct { + app.Compo +} + +func (s *StatusBar) Render() app.UI { + version := app.Getenv("GOAPP_VERSION") + return app.Div().Class("status-bar").Text("goapp version: " + version) +} diff --git a/app/pkg/logsviewer/eval_viewer.go b/app/pkg/logsviewer/eval_viewer.go index 0f072a03..6ccdb1a5 100644 --- a/app/pkg/logsviewer/eval_viewer.go +++ b/app/pkg/logsviewer/eval_viewer.go @@ -1,6 +1,10 @@ package logsviewer -import "github.com/maxence-charriere/go-app/v9/pkg/app" +import ( + "fmt" + "github.com/jlewi/foyle/protos/go/foyle/v1alpha1" + "github.com/maxence-charriere/go-app/v9/pkg/app" +) // EvalViewer is the page that displays an eval result. type EvalViewer struct { @@ -14,7 +18,9 @@ func (c *EvalViewer) Render() app.UI { } return app.Div().Class("main-layout").Body( app.Div().Class("header").Body( - &blockSelector{}, + &EvalResultsTable{ + SelectedRow: 1, + }, ), app.Div().Class("content").Body( app.Div().Class("sidebar").Body( @@ -54,3 +60,56 @@ func (s *evalSideBar) Render() app.UI { //ctx.NewActionWithValue(getAction, rawView) })) } + +type EvalResultsTable struct { + app.Compo + Data []*v1alpha1.EvalResult + SelectedRow int +} + +func (c *EvalResultsTable) Render() app.UI { + c.Data = make([]*v1alpha1.EvalResult, 0, 20) + + for i := 0; i < 20; i++ { + c.Data = append(c.Data, &v1alpha1.EvalResult{ + Example: &v1alpha1.Example{ + Id: fmt.Sprintf("%d", i), + }, + ExampleFile: fmt.Sprintf("file%d", i), + Distance: 1, + NormalizedDistance: 0.2, + }) + } + + return app.Table().Body( + app.Tr().Body( + app.Th().Text("ID"), + app.Th().Text("File"), + app.Th().Text("Distance"), + app.Th().Text("Normalized Distance"), + ), + app.Range(c.Data).Slice(func(i int) app.UI { + rowStyle := "" + if i == c.SelectedRow { + rowStyle = "selected-row" // This is a CSS class that you need to define + } + row := app.Tr().Class(rowStyle).Body( + app.Td().Text(c.Data[i].GetExample().GetId()), + app.Td().Text(c.Data[i].GetExampleFile()), + app.Td().Text(c.Data[i].GetDistance()), + app.Td().Text(c.Data[i].GetNormalizedDistance()), + ) + + // For each row we add a click handler to display the corresponding example. + row.OnClick(func(ctx app.Context, e app.Event) { + // Mark the selected row and trigger the update. + // This will redraw the table and change the style on the selected row. + c.SelectedRow = i + c.Update() + + // TODO(jeremy): We should fire an event and change the context to display the evaluation result. + }) + return row + }), + ) +} diff --git a/app/pkg/server/server.go b/app/pkg/server/server.go index 63016e0f..b785f52f 100644 --- a/app/pkg/server/server.go +++ b/app/pkg/server/server.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/google/uuid" "github.com/jlewi/foyle/app/pkg/eval" "github.com/jlewi/foyle/protos/go/foyle/v1alpha1/v1alpha1connect" "time" @@ -263,7 +264,18 @@ func (s *Server) createGinEngine() error { } // N.B. We need a trailing slash for the relativePath passed to router. Any but not in the stripprefix // because we need to leave the final slash in the path so that the route ends up matching. - router.Any(logsviewer.AppPath+"/*any", gin.WrapH(http.StripPrefix(logsviewer.AppPath, viewerApp))) + router.Any(logsviewer.AppPath+"/*any", func(c *gin.Context) { + // Ref: https://github.com/jlewi/foyle/issues/64 + // We update the version each time the app is served so that the browser doesn't cache the app. + // TODO(jeremy): I think this is a hack which might be ok during debugging but might cause pathological issues + // because the version number isn't stable. A better approach would be to do something like a hash + // of the app.wasm file. We could potentially watch the filesystem for changes to the app.wasm file and then + // update the version number. + viewerApp.Version = uuid.NewString() + log.Info("Serving logs viewer", "version", viewerApp.Version) + h := http.StripPrefix(logsviewer.AppPath, viewerApp) + h.ServeHTTP(c.Writer, c.Request) + }) return nil } diff --git a/app/pwa/main.go b/app/pwa/main.go index 4c64ac1c..7189d649 100644 --- a/app/pwa/main.go +++ b/app/pwa/main.go @@ -51,7 +51,8 @@ func main() { Description: "An Hello World! example", Resources: app.CustomProvider("", "/viewer"), Styles: []string{ - "/web/viewer.css", // Loads traceSelector.css file. + "/web/table.css", + "/web/viewer.css", }, Env: map[string]string{ logsviewer.EndpointEnvVar: "http://localhost:8000", diff --git a/app/web/table.css b/app/web/table.css new file mode 100644 index 00000000..77eb8527 --- /dev/null +++ b/app/web/table.css @@ -0,0 +1,4 @@ +/* This styles the selected row in a table */ +.selected-row { + background-color: #f0f0f0; +} \ No newline at end of file diff --git a/app/web/viewer.css b/app/web/viewer.css index 1febae66..0457160b 100644 --- a/app/web/viewer.css +++ b/app/web/viewer.css @@ -27,4 +27,14 @@ .main-window { flex: 1; background-color: #fff; +} + +/* style the status bar at the bottom */ +.status-bar { + position: fixed; + bottom: 0; + width: 100%; + background-color: #f0f0f0; + text-align: center; + padding: 10px 0; } \ No newline at end of file From b561fce911c564c03c533536104ab1d6ba664068 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Tue, 7 May 2024 06:42:39 -0700 Subject: [PATCH 11/24] We have rudimentary client app working; can display a table and click on items. So next step is to try to actually get the data and load it. --- app/pkg/logsviewer/eval_viewer.go | 8 +++++++- app/pkg/server/server.go | 25 ++++++++++++------------- app/web/table.css | 6 ++++++ 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/app/pkg/logsviewer/eval_viewer.go b/app/pkg/logsviewer/eval_viewer.go index 6ccdb1a5..d2633ce7 100644 --- a/app/pkg/logsviewer/eval_viewer.go +++ b/app/pkg/logsviewer/eval_viewer.go @@ -2,8 +2,10 @@ package logsviewer import ( "fmt" + "github.com/go-logr/zapr" "github.com/jlewi/foyle/protos/go/foyle/v1alpha1" "github.com/maxence-charriere/go-app/v9/pkg/app" + "go.uber.org/zap" ) // EvalViewer is the page that displays an eval result. @@ -81,7 +83,7 @@ func (c *EvalResultsTable) Render() app.UI { }) } - return app.Table().Body( + table := app.Table().Body( app.Tr().Body( app.Th().Text("ID"), app.Th().Text("File"), @@ -102,6 +104,8 @@ func (c *EvalResultsTable) Render() app.UI { // For each row we add a click handler to display the corresponding example. row.OnClick(func(ctx app.Context, e app.Event) { + log := zapr.NewLogger(zap.L()) + log.Info("Selected row", "row", i) // Mark the selected row and trigger the update. // This will redraw the table and change the style on the selected row. c.SelectedRow = i @@ -112,4 +116,6 @@ func (c *EvalResultsTable) Render() app.UI { return row }), ) + div := app.Div().Class("scrollable-table").Body(table) + return div } diff --git a/app/pkg/server/server.go b/app/pkg/server/server.go index b785f52f..566d45b6 100644 --- a/app/pkg/server/server.go +++ b/app/pkg/server/server.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/google/uuid" "github.com/jlewi/foyle/app/pkg/eval" "github.com/jlewi/foyle/protos/go/foyle/v1alpha1/v1alpha1connect" "time" @@ -237,7 +236,15 @@ func (s *Server) createGinEngine() error { s.engine = router // Setup the logs viewer + if err := s.setupViewerApp(router); err != nil { + return err + } + return nil +} +// setupViewerApp sets up the viewer app +func (s *Server) setupViewerApp(router *gin.Engine) error { + log := zapr.NewLogger(zap.L()) app.Route("/", &logsviewer.MainApp{}) if strings.HasSuffix(logsviewer.AppPath, "/") { @@ -250,6 +257,7 @@ func (s *Server) createGinEngine() error { endpoint := fmt.Sprintf("http://%s:%d", s.config.Server.BindAddress, s.config.Server.HttpPort) log.Info("Setting up logs viewer", "endpoint", endpoint, "path", logsviewer.AppPath) + viewerApp := &app.Handler{ Name: "FoyleLogsViewer", Description: "View Foyle Logs", @@ -257,25 +265,16 @@ func (s *Server) createGinEngine() error { Resources: app.CustomProvider("", logsviewer.AppPath), Styles: []string{ "/web/viewer.css", // Loads traceSelector.css file. + "/web/table.css", // Loads table.css file. }, Env: map[string]string{ logsviewer.EndpointEnvVar: endpoint, }, } + // N.B. We need a trailing slash for the relativePath passed to router. Any but not in the stripprefix // because we need to leave the final slash in the path so that the route ends up matching. - router.Any(logsviewer.AppPath+"/*any", func(c *gin.Context) { - // Ref: https://github.com/jlewi/foyle/issues/64 - // We update the version each time the app is served so that the browser doesn't cache the app. - // TODO(jeremy): I think this is a hack which might be ok during debugging but might cause pathological issues - // because the version number isn't stable. A better approach would be to do something like a hash - // of the app.wasm file. We could potentially watch the filesystem for changes to the app.wasm file and then - // update the version number. - viewerApp.Version = uuid.NewString() - log.Info("Serving logs viewer", "version", viewerApp.Version) - h := http.StripPrefix(logsviewer.AppPath, viewerApp) - h.ServeHTTP(c.Writer, c.Request) - }) + router.Any(logsviewer.AppPath+"/*any", gin.WrapH(http.StripPrefix(logsviewer.AppPath, viewerApp))) return nil } diff --git a/app/web/table.css b/app/web/table.css index 77eb8527..96751424 100644 --- a/app/web/table.css +++ b/app/web/table.css @@ -1,3 +1,9 @@ +/* make the table scrollable */ +.scrollable-table { + height: 300px; /* Adjust as needed */ + overflow-y: auto; +} + /* This styles the selected row in a table */ .selected-row { background-color: #f0f0f0; From e8e19b6ac47c88ed986c2d1c079b7ccf98265895 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Tue, 7 May 2024 07:29:14 -0700 Subject: [PATCH 12/24] Add a text box to specify the data Load the data from the backend using the connect client. --- app/pkg/logsviewer/eval_viewer.go | 92 +++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 16 deletions(-) diff --git a/app/pkg/logsviewer/eval_viewer.go b/app/pkg/logsviewer/eval_viewer.go index d2633ce7..9b2f8c40 100644 --- a/app/pkg/logsviewer/eval_viewer.go +++ b/app/pkg/logsviewer/eval_viewer.go @@ -1,28 +1,50 @@ package logsviewer import ( - "fmt" + "connectrpc.com/connect" + "context" "github.com/go-logr/zapr" "github.com/jlewi/foyle/protos/go/foyle/v1alpha1" + "github.com/jlewi/foyle/protos/go/foyle/v1alpha1/v1alpha1connect" "github.com/maxence-charriere/go-app/v9/pkg/app" "go.uber.org/zap" + "net/http" + "strings" +) + +const ( + loadEvalResults = "/loadEvalResults" + databaseInputID = "databaseInput" ) // EvalViewer is the page that displays an eval result. type EvalViewer struct { app.Compo - main *mainWindow + main *mainWindow + resultsTable *EvalResultsTable } func (c *EvalViewer) Render() app.UI { if c.main == nil { c.main = &mainWindow{} } + if c.resultsTable == nil { + c.resultsTable = &EvalResultsTable{} + } + loadButton := app.Button(). + Text("Load"). + OnClick(func(ctx app.Context, e app.Event) { + ctx.NewAction(loadEvalResults) + }) + return app.Div().Class("main-layout").Body( app.Div().Class("header").Body( - &EvalResultsTable{ - SelectedRow: 1, - }, + app.Input(). + Type("text"). + ID(databaseInputID). + Value("/Users/jlewi/foyle_experiments/learning"), + loadButton, + c.resultsTable, ), app.Div().Class("content").Body( app.Div().Class("sidebar").Body( @@ -69,18 +91,56 @@ type EvalResultsTable struct { SelectedRow int } +func (c *EvalResultsTable) OnMount(ctx app.Context) { + ctx.Handle(loadEvalResults, c.handleLoadEvalResults) +} + +func (c *EvalResultsTable) handleLoadEvalResults(ctx app.Context, action app.Action) { + log := zapr.NewLogger(zap.L()) + log.Info("Handling loadEvalResults") + + database := app.Window().GetElementByID(databaseInputID).Get("value").String() + database = strings.TrimSpace(database) + if database == "" { + // TODO(jeremy): We should surface an error message. We could use the status bar to show the error message + log.Info("No database provided; can't load eval results") + return + } + + // TODO(jeremy): We should cache the client; see GetClient in block_viewer.go for an example. + client := v1alpha1connect.NewEvalServiceClient( + http.DefaultClient, + // TODO(jeremy): How should we make this configurable? + "http://localhost:8080/api", + ) + + listRequest := &v1alpha1.EvalResultListRequest{ + // TODO(jeremy): We need a UI element to let you enter this + Database: database, + } + + res, err := client.List( + context.Background(), + connect.NewRequest(listRequest), + ) + + if err != nil { + log.Error(err, "Error listing eval results") + return + } + + c.Data = res.Msg.Items + log.Info("Loaded eval results", "numResults", len(c.Data), "instance", c) + c.SelectedRow = 1 + c.Update() +} + func (c *EvalResultsTable) Render() app.UI { - c.Data = make([]*v1alpha1.EvalResult, 0, 20) - - for i := 0; i < 20; i++ { - c.Data = append(c.Data, &v1alpha1.EvalResult{ - Example: &v1alpha1.Example{ - Id: fmt.Sprintf("%d", i), - }, - ExampleFile: fmt.Sprintf("file%d", i), - Distance: 1, - NormalizedDistance: 0.2, - }) + log := zapr.NewLogger(zap.L()) + log.Info("Rendering EvalResultsTable", "instance", c) + if c.Data == nil { + log.Info("Data is nil", "instance", c) + c.Data = make([]*v1alpha1.EvalResult, 0) } table := app.Table().Body( From bc9515abb0e668f6a597ac4616f055dfabfb2181 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Tue, 7 May 2024 07:48:19 -0700 Subject: [PATCH 13/24] Fix styling of the table for the eval results. --- app/pkg/logsviewer/eval_viewer.go | 25 ++++++++++++++++--------- app/web/table.css | 22 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/app/pkg/logsviewer/eval_viewer.go b/app/pkg/logsviewer/eval_viewer.go index 9b2f8c40..3cdf2a3d 100644 --- a/app/pkg/logsviewer/eval_viewer.go +++ b/app/pkg/logsviewer/eval_viewer.go @@ -37,24 +37,31 @@ func (c *EvalViewer) Render() app.UI { ctx.NewAction(loadEvalResults) }) - return app.Div().Class("main-layout").Body( - app.Div().Class("header").Body( + // The top row will contain the input box to select the database + // and the results table to scroll though them. + // These will be arranged vertically in the row + topRow := app.Div().Class("row").Body( + app.Div().Body( app.Input(). Type("text"). ID(databaseInputID). Value("/Users/jlewi/foyle_experiments/learning"), loadButton, + ), + app.Div().Body( c.resultsTable, + )) + + // The bottom row will contain the main window. + bottomRow := app.Div().Class("row").Body( + app.Div().Class("sidebar").Body( + &evalSideBar{}, ), - app.Div().Class("content").Body( - app.Div().Class("sidebar").Body( - &evalSideBar{}, - ), - app.Div().Class("main-window").Body( - c.main, - ), + app.Div().Class("main-window").Body( + c.main, ), ) + return app.Div().Body(topRow, bottomRow) } // evalSideBar adds a navigation bar between the views to the left side. diff --git a/app/web/table.css b/app/web/table.css index 96751424..5cd0b9c3 100644 --- a/app/web/table.css +++ b/app/web/table.css @@ -1,3 +1,25 @@ +/* We want to dive the eval panel into a column with two rows. + The top row will be a table with the evaluation results, and the bottom row will be the viewer + showing the current selected row. + We will use flexbox to achieve this and specify the heights in percentages. + */ +.row { + flex: 1; /* Take up all available space */ + display: flex; + flex-direction: column; + border: 1px solid #ccc; + box-sizing: border-box; +} + +.row:first-child { + flex: 0 0 10%; /* 10% height, no grow, no shrink */ +} + +.row:last-child { + flex: 0 0 90%; /* 90% height, no grow, no shrink */ +} + + /* make the table scrollable */ .scrollable-table { height: 300px; /* Adjust as needed */ From 885aaf5ef9b87251c213fe058c7407af97e19256 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Tue, 7 May 2024 08:25:16 -0700 Subject: [PATCH 14/24] Can render the view although it isn't displayed well. --- app/pkg/logsviewer/eval_viewer.go | 138 +++++++++++++++++++++++++++--- app/pkg/logsviewer/mainwindow.go | 2 + app/pkg/logsviewer/views.go | 22 +++++ 3 files changed, 149 insertions(+), 13 deletions(-) diff --git a/app/pkg/logsviewer/eval_viewer.go b/app/pkg/logsviewer/eval_viewer.go index 3cdf2a3d..f7be6f29 100644 --- a/app/pkg/logsviewer/eval_viewer.go +++ b/app/pkg/logsviewer/eval_viewer.go @@ -3,30 +3,46 @@ package logsviewer import ( "connectrpc.com/connect" "context" + "fmt" "github.com/go-logr/zapr" "github.com/jlewi/foyle/protos/go/foyle/v1alpha1" "github.com/jlewi/foyle/protos/go/foyle/v1alpha1/v1alpha1connect" "github.com/maxence-charriere/go-app/v9/pkg/app" + "github.com/pkg/errors" "go.uber.org/zap" "net/http" "strings" ) +type evalViews string + const ( loadEvalResults = "/loadEvalResults" databaseInputID = "databaseInput" + + setEvalView = "/setEvalView" + + evalQueryView evalViews = "evalQueryView" + evalActualAnswerView evalViews = "evalActualAnswerView" + evalExpectedAnswerView evalViews = "evalExpectedAnswerView" +) + +var ( + // resultSet keeps track of the current loaded result set. This allows us to easily access it from multiple + // elements. I guess we could also pass it around using go-app context but this seems easier. + resultSet *ResultSet ) // EvalViewer is the page that displays an eval result. type EvalViewer struct { app.Compo - main *mainWindow + main *evalView resultsTable *EvalResultsTable } func (c *EvalViewer) Render() app.UI { if c.main == nil { - c.main = &mainWindow{} + c.main = &evalView{} } if c.resultsTable == nil { c.resultsTable = &EvalResultsTable{} @@ -74,7 +90,7 @@ func (s *evalSideBar) Render() app.UI { // Each button needs to be enclosed in a div. Otherwise events get triggered for all the buttons. app.Div().Body( app.Button().Text("Query").OnClick(func(ctx app.Context, e app.Event) { - //ctx.NewActionWithValue(getAction, generatedBlockView) + ctx.NewActionWithValue(setEvalView, evalQueryView) }), ), app.Div().Body( @@ -94,7 +110,6 @@ func (s *evalSideBar) Render() app.UI { type EvalResultsTable struct { app.Compo - Data []*v1alpha1.EvalResult SelectedRow int } @@ -106,6 +121,10 @@ func (c *EvalResultsTable) handleLoadEvalResults(ctx app.Context, action app.Act log := zapr.NewLogger(zap.L()) log.Info("Handling loadEvalResults") + if resultSet == nil { + resultSet = &ResultSet{} + } + database := app.Window().GetElementByID(databaseInputID).Get("value").String() database = strings.TrimSpace(database) if database == "" { @@ -136,8 +155,9 @@ func (c *EvalResultsTable) handleLoadEvalResults(ctx app.Context, action app.Act return } - c.Data = res.Msg.Items - log.Info("Loaded eval results", "numResults", len(c.Data), "instance", c) + resultSet.data = res.Msg.Items + resultSet.selected = 0 + log.Info("Loaded eval results", "numResults", len(resultSet.data), "instance", c) c.SelectedRow = 1 c.Update() } @@ -145,9 +165,13 @@ func (c *EvalResultsTable) handleLoadEvalResults(ctx app.Context, action app.Act func (c *EvalResultsTable) Render() app.UI { log := zapr.NewLogger(zap.L()) log.Info("Rendering EvalResultsTable", "instance", c) - if c.Data == nil { + if resultSet == nil { log.Info("Data is nil", "instance", c) - c.Data = make([]*v1alpha1.EvalResult, 0) + resultSet = &ResultSet{} + } + + if resultSet.data == nil { + resultSet.data = make([]*v1alpha1.EvalResult, 0) } table := app.Table().Body( @@ -157,16 +181,16 @@ func (c *EvalResultsTable) Render() app.UI { app.Th().Text("Distance"), app.Th().Text("Normalized Distance"), ), - app.Range(c.Data).Slice(func(i int) app.UI { + app.Range(resultSet.data).Slice(func(i int) app.UI { rowStyle := "" if i == c.SelectedRow { rowStyle = "selected-row" // This is a CSS class that you need to define } row := app.Tr().Class(rowStyle).Body( - app.Td().Text(c.Data[i].GetExample().GetId()), - app.Td().Text(c.Data[i].GetExampleFile()), - app.Td().Text(c.Data[i].GetDistance()), - app.Td().Text(c.Data[i].GetNormalizedDistance()), + app.Td().Text(resultSet.data[i].GetExample().GetId()), + app.Td().Text(resultSet.data[i].GetExampleFile()), + app.Td().Text(resultSet.data[i].GetDistance()), + app.Td().Text(resultSet.data[i].GetNormalizedDistance()), ) // For each row we add a click handler to display the corresponding example. @@ -176,6 +200,7 @@ func (c *EvalResultsTable) Render() app.UI { // Mark the selected row and trigger the update. // This will redraw the table and change the style on the selected row. c.SelectedRow = i + resultSet.selected = i c.Update() // TODO(jeremy): We should fire an event and change the context to display the evaluation result. @@ -186,3 +211,90 @@ func (c *EvalResultsTable) Render() app.UI { div := app.Div().Class("scrollable-table").Body(table) return div } + +// evalView is the main viewer of the evaluation viewer. +// What it displays will change depending on the view selected. +// The content of the window is HTML which gets set by the action handler for different events. +// +// The view registers a handler for the setEvalViewAction event. The setEvalViewAction event is triggered when ever +// the view needs to be changed; e.g. because the view has changed or the selected data has changed +type evalView struct { + app.Compo + HTMLContent string +} + +func (m *evalView) Render() app.UI { + // Raw requires the value to have a single root element. So we enclose the HTML content in a div to ensure + // that is all ways true. + return app.Raw("
" + m.HTMLContent + "
") +} + +func (m *evalView) OnMount(ctx app.Context) { + ctx.Handle(setEvalView, m.handleSetEvalView) +} + +func (m *evalView) handleSetEvalView(ctx app.Context, action app.Action) { + log := zapr.NewLogger(zap.L()) + viewValue, ok := action.Value.(evalViews) // Checks if a name was given. + if !ok { + log.Error(errors.New("No view provided"), "Invalid action") + return + } + log.Info("Handling get action", "view", viewValue) + switch viewValue { + case evalQueryView: + current := resultSet.GetSelected() + if current == nil { + m.HTMLContent = fmt.Sprintf("No evaluation result is currently selected") + } + value, err := docToHTML(current.Example.Query) + if err == nil { + m.HTMLContent = value + } else { + log.Error(err, "Failed to convert generated block to html") + m.HTMLContent = fmt.Sprintf("Failed to convert generated block to html : error %+v", err) + } + //case executedBlockView: + // block := &api.BlockLog{} + // ctx.GetState(blockLogState, block) + // value, err := renderExecutedBlock(block) + // if err == nil { + // m.HTMLContent = value + // } else { + // log.Error(err, "Failed to convert executed block to html") + // m.HTMLContent = fmt.Sprintf("Failed to convert executed block to html: error %+v", err) + // } + //case rawView: + // block := &api.BlockLog{} + // ctx.GetState(blockLogState, block) + // blockJson, err := json.MarshalIndent(block, "", " ") + // if err != nil { + // log.Error(err, "Failed to turn blocklog into json") + // m.HTMLContent = fmt.Sprintf("Failed to turn blocklog into json: error %+v", err) + // } else { + // raw := "
" + string(blockJson) + "
" + // m.HTMLContent = raw + // } + default: + m.HTMLContent = "Unknown view: " + string(viewValue) + } + // We need to call update to trigger a re-render of the component. + m.Update() +} + +// ResultSet keeps track of the current loaded result set. This allows us to easily access it from multiple components. +// N.B. we also might want to wrap the data with accessors so we can access data in a thread safe way +type ResultSet struct { + data []*v1alpha1.EvalResult + selected int +} + +func (c *ResultSet) GetSelected() *v1alpha1.EvalResult { + if c.data == nil || len(c.data) == 0 { + return nil + } + if c.selected < 0 || c.selected >= len(c.data) { + return nil + } + return c.data[c.selected] +} diff --git a/app/pkg/logsviewer/mainwindow.go b/app/pkg/logsviewer/mainwindow.go index 664cc9c0..1a239170 100644 --- a/app/pkg/logsviewer/mainwindow.go +++ b/app/pkg/logsviewer/mainwindow.go @@ -11,6 +11,8 @@ import ( "go.uber.org/zap" ) +// TODO(jeremy): This is just the viewer for blog log view. We should probably rename it. + // mainWindow is the main window of the application. // What it displays will change depending on the view selected. // The content of the main window is HTML which gets set by the action handler for different events. diff --git a/app/pkg/logsviewer/views.go b/app/pkg/logsviewer/views.go index 6143e907..c0f2a82d 100644 --- a/app/pkg/logsviewer/views.go +++ b/app/pkg/logsviewer/views.go @@ -2,6 +2,8 @@ package logsviewer import ( "bytes" + "github.com/jlewi/foyle/app/pkg/docs" + "github.com/jlewi/foyle/protos/go/foyle/v1alpha1" "github.com/go-logr/zapr" "github.com/jlewi/foyle/app/api" @@ -49,3 +51,23 @@ func renderExecutedBlock(block *api.BlockLog) (string, error) { return buf.String(), nil } + +// docToHTML returns the dock as html +func docToHTML(doc *v1alpha1.Doc) (string, error) { + if doc == nil { + return "", errors.New("doc is nil") + } + log := zapr.NewLogger(zap.L()) + + // Convert it to markdown + md := docs.DocToMarkdown(doc) + + // Conver the markdown to html + var buf bytes.Buffer + if err := goldmark.Convert([]byte(md), &buf); err != nil { + log.Error(err, "Failed to convert markdown") + return "", err + } + + return buf.String(), nil +} From 369e0db0a08bcecf8ab4b12d429c359f89ec7b11 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Tue, 7 May 2024 13:03:58 -0700 Subject: [PATCH 15/24] Implement all the different viewer options. --- app/pkg/logsviewer/eval_viewer.go | 79 +++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/app/pkg/logsviewer/eval_viewer.go b/app/pkg/logsviewer/eval_viewer.go index f7be6f29..07d9522c 100644 --- a/app/pkg/logsviewer/eval_viewer.go +++ b/app/pkg/logsviewer/eval_viewer.go @@ -10,6 +10,7 @@ import ( "github.com/maxence-charriere/go-app/v9/pkg/app" "github.com/pkg/errors" "go.uber.org/zap" + "google.golang.org/protobuf/encoding/protojson" "net/http" "strings" ) @@ -25,6 +26,7 @@ const ( evalQueryView evalViews = "evalQueryView" evalActualAnswerView evalViews = "evalActualAnswerView" evalExpectedAnswerView evalViews = "evalExpectedAnswerView" + evalRawView evalViews = "evalRawView" ) var ( @@ -95,16 +97,16 @@ func (s *evalSideBar) Render() app.UI { ), app.Div().Body( app.Button().Text("Actual Answer").OnClick(func(ctx app.Context, e app.Event) { - //ctx.NewActionWithValue(getAction, generatedBlockView) + ctx.NewActionWithValue(setEvalView, evalActualAnswerView) }), ), app.Div().Body( app.Button().Text("Expected Answer")).OnClick(func(ctx app.Context, e app.Event) { - //ctx.NewActionWithValue(getAction, executedBlockView) + ctx.NewActionWithValue(setEvalView, evalExpectedAnswerView) }), app.Div().Body( app.Button().Text("Raw")).OnClick(func(ctx app.Context, e app.Event) { - //ctx.NewActionWithValue(getAction, rawView) + ctx.NewActionWithValue(setEvalView, evalRawView) })) } @@ -246,6 +248,7 @@ func (m *evalView) handleSetEvalView(ctx app.Context, action app.Action) { current := resultSet.GetSelected() if current == nil { m.HTMLContent = fmt.Sprintf("No evaluation result is currently selected") + break } value, err := docToHTML(current.Example.Query) if err == nil { @@ -254,27 +257,55 @@ func (m *evalView) handleSetEvalView(ctx app.Context, action app.Action) { log.Error(err, "Failed to convert generated block to html") m.HTMLContent = fmt.Sprintf("Failed to convert generated block to html : error %+v", err) } - //case executedBlockView: - // block := &api.BlockLog{} - // ctx.GetState(blockLogState, block) - // value, err := renderExecutedBlock(block) - // if err == nil { - // m.HTMLContent = value - // } else { - // log.Error(err, "Failed to convert executed block to html") - // m.HTMLContent = fmt.Sprintf("Failed to convert executed block to html: error %+v", err) - // } - //case rawView: - // block := &api.BlockLog{} - // ctx.GetState(blockLogState, block) - // blockJson, err := json.MarshalIndent(block, "", " ") - // if err != nil { - // log.Error(err, "Failed to turn blocklog into json") - // m.HTMLContent = fmt.Sprintf("Failed to turn blocklog into json: error %+v", err) - // } else { - // raw := "
" + string(blockJson) + "
" - // m.HTMLContent = raw - // } + case evalActualAnswerView: + current := resultSet.GetSelected() + if current == nil { + m.HTMLContent = fmt.Sprintf("No evaluation result is currently selected") + break + } + doc := &v1alpha1.Doc{ + Blocks: current.Actual, + } + value, err := docToHTML(doc) + if err == nil { + m.HTMLContent = value + } else { + log.Error(err, "Failed to convert actual answer to html") + m.HTMLContent = fmt.Sprintf("Failed to convert actual answer to html : error %+v", err) + } + case evalExpectedAnswerView: + current := resultSet.GetSelected() + if current == nil { + m.HTMLContent = fmt.Sprintf("No evaluation result is currently selected") + break + } + doc := &v1alpha1.Doc{ + Blocks: current.Example.Answer, + } + value, err := docToHTML(doc) + if err == nil { + m.HTMLContent = value + } else { + log.Error(err, "Failed to convert expected blocks to html") + m.HTMLContent = fmt.Sprintf("Failed to convert expected blocks to html : error %+v", err) + } + case evalRawView: + current := resultSet.GetSelected() + if current == nil { + m.HTMLContent = fmt.Sprintf("No evaluation result is currently selected") + break + } + marshaler := protojson.MarshalOptions{ + Indent: " ", + } + blockJson, err := marshaler.Marshal(current) + if err != nil { + log.Error(err, "Failed to turn result into json") + m.HTMLContent = fmt.Sprintf("Failed to turn blocklog into json: error %+v", err) + } else { + raw := "
" + string(blockJson) + "
" + m.HTMLContent = raw + } default: m.HTMLContent = "Unknown view: " + string(viewValue) } From e196d32bb87d2d73b2f6ab358834cd3953077590 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Tue, 7 May 2024 13:04:15 -0700 Subject: [PATCH 16/24] Tidy --- app/pkg/eval/service.go | 3 ++- app/pkg/logsviewer/eval_viewer.go | 7 ++++--- app/pkg/logsviewer/views.go | 1 + app/pkg/server/server.go | 3 ++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/pkg/eval/service.go b/app/pkg/eval/service.go index 252db66f..464ff8e0 100644 --- a/app/pkg/eval/service.go +++ b/app/pkg/eval/service.go @@ -1,8 +1,9 @@ package eval import ( - "connectrpc.com/connect" "context" + + "connectrpc.com/connect" "github.com/cockroachdb/pebble" "github.com/jlewi/foyle/app/pkg/logs" "github.com/jlewi/foyle/protos/go/foyle/v1alpha1" diff --git a/app/pkg/logsviewer/eval_viewer.go b/app/pkg/logsviewer/eval_viewer.go index 07d9522c..395bcb45 100644 --- a/app/pkg/logsviewer/eval_viewer.go +++ b/app/pkg/logsviewer/eval_viewer.go @@ -1,9 +1,12 @@ package logsviewer import ( - "connectrpc.com/connect" "context" "fmt" + "net/http" + "strings" + + "connectrpc.com/connect" "github.com/go-logr/zapr" "github.com/jlewi/foyle/protos/go/foyle/v1alpha1" "github.com/jlewi/foyle/protos/go/foyle/v1alpha1/v1alpha1connect" @@ -11,8 +14,6 @@ import ( "github.com/pkg/errors" "go.uber.org/zap" "google.golang.org/protobuf/encoding/protojson" - "net/http" - "strings" ) type evalViews string diff --git a/app/pkg/logsviewer/views.go b/app/pkg/logsviewer/views.go index c0f2a82d..deef8c50 100644 --- a/app/pkg/logsviewer/views.go +++ b/app/pkg/logsviewer/views.go @@ -2,6 +2,7 @@ package logsviewer import ( "bytes" + "github.com/jlewi/foyle/app/pkg/docs" "github.com/jlewi/foyle/protos/go/foyle/v1alpha1" diff --git a/app/pkg/server/server.go b/app/pkg/server/server.go index 566d45b6..d13fe03b 100644 --- a/app/pkg/server/server.go +++ b/app/pkg/server/server.go @@ -4,9 +4,10 @@ import ( "context" "encoding/json" "fmt" + "time" + "github.com/jlewi/foyle/app/pkg/eval" "github.com/jlewi/foyle/protos/go/foyle/v1alpha1/v1alpha1connect" - "time" "github.com/jlewi/foyle/app/pkg/analyze" "github.com/jlewi/foyle/app/pkg/logsviewer" From 589cd543c89113691cea169016dc277cce75593b Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Tue, 7 May 2024 13:04:47 -0700 Subject: [PATCH 17/24] Fix lint. --- app/pkg/logsviewer/eval_viewer.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/pkg/logsviewer/eval_viewer.go b/app/pkg/logsviewer/eval_viewer.go index 395bcb45..1cba1ec0 100644 --- a/app/pkg/logsviewer/eval_viewer.go +++ b/app/pkg/logsviewer/eval_viewer.go @@ -261,7 +261,7 @@ func (m *evalView) handleSetEvalView(ctx app.Context, action app.Action) { case evalActualAnswerView: current := resultSet.GetSelected() if current == nil { - m.HTMLContent = fmt.Sprintf("No evaluation result is currently selected") + m.HTMLContent = "No evaluation result is currently selected" break } doc := &v1alpha1.Doc{ @@ -277,7 +277,7 @@ func (m *evalView) handleSetEvalView(ctx app.Context, action app.Action) { case evalExpectedAnswerView: current := resultSet.GetSelected() if current == nil { - m.HTMLContent = fmt.Sprintf("No evaluation result is currently selected") + m.HTMLContent = "No evaluation result is currently selected" break } doc := &v1alpha1.Doc{ @@ -293,7 +293,7 @@ func (m *evalView) handleSetEvalView(ctx app.Context, action app.Action) { case evalRawView: current := resultSet.GetSelected() if current == nil { - m.HTMLContent = fmt.Sprintf("No evaluation result is currently selected") + m.HTMLContent = "No evaluation result is currently selected" break } marshaler := protojson.MarshalOptions{ From dbe20c23bb74f08f1f1accf144a6ffb4d7729282 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Tue, 7 May 2024 13:09:29 -0700 Subject: [PATCH 18/24] Tidy --- app/pkg/logsviewer/app.go | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/app/pkg/logsviewer/app.go b/app/pkg/logsviewer/app.go index a048da16..e803a30c 100644 --- a/app/pkg/logsviewer/app.go +++ b/app/pkg/logsviewer/app.go @@ -26,16 +26,12 @@ const ( blockLogState = "/blocklog" ) -// How it works: -// Clicking load fetches the blocklog from the server. -// The log is then stored in the application context (https://go-app.dev/states) -// this allows other components to use it. Load then fires off an UpdateView event to trigger -// the mainWindow to update its content. -// The UpdateView event takes a string argument which is what view should be rendered. -// There is a left hand navigation bar with buttons to display different views of the current log. -// Changing the view is achieved by sending UpdateView events to change the view - // MainApp is the main window of the application. +// +// The main application consists of a left hand navigation bar and a right hand side component that is the page +// to display. When you click on one of the left hand navigation buttons it fires of an action setPage to change the +// view. The handler for this action loads the appropriate page and sets MainApp.page to the component for that +// page. type MainApp struct { app.Compo // Page keeps track of the page to display in the right hand side. @@ -44,7 +40,9 @@ type MainApp struct { func (m *MainApp) Render() app.UI { if m.page == nil { - // Default to the Blockviewer + // TODO(jeremy): Could we keep track of the last view so if we refresh we show the same data? + // One way to do that is to update the URL with query arguments containing the relevant state information. + // Then when we click refresh we could get the information directly from the URL m.page = &BlockViewer{} } return app.Div().Class("main-layout").Body( @@ -53,8 +51,6 @@ func (m *MainApp) Render() app.UI { &navigationBar{}, ), app.Div().Class("page-window").Body( - // TODO(jeremy): How do we change this when the user clicks the left hand navigation bar? - // Do we need to find and update the div? m.page, ), ), &StatusBar{}, @@ -69,7 +65,7 @@ func (m *MainApp) OnMount(ctx app.Context) { // handleSetPage handles the setPage action. The event will tell us which view to display. func (m *MainApp) handleSetPage(ctx app.Context, action app.Action) { log := zapr.NewLogger(zap.L()) - pageValue, ok := action.Value.(page) // Checks if a name was given. + pageValue, ok := action.Value.(page) if !ok { log.Error(errors.New("No page provided"), "Invalid action") return From c2a2d39633f9ccd95788035d863b56a71d752c5c Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Tue, 7 May 2024 13:12:39 -0700 Subject: [PATCH 19/24] Rename mainWindow to BlockLogView --- app/pkg/logsviewer/block_viewer.go | 6 +++--- app/pkg/logsviewer/mainwindow.go | 12 +++++------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/pkg/logsviewer/block_viewer.go b/app/pkg/logsviewer/block_viewer.go index 6851c0ae..a38f3771 100644 --- a/app/pkg/logsviewer/block_viewer.go +++ b/app/pkg/logsviewer/block_viewer.go @@ -8,18 +8,18 @@ import "github.com/maxence-charriere/go-app/v9/pkg/app" // Clicking load fetches the blocklog from the server. // The log is then stored in the application context (https://go-app.dev/states) // this allows other components to use it. Load then fires off an UpdateView event to trigger -// the mainWindow to update its content. +// the blockLogView to update its content. // The UpdateView event takes a string argument which is what view should be rendered. // There is a left hand navigation bar with buttons to display different views of the current log. // Changing the view is achieved by sending UpdateView events to change the view type BlockViewer struct { app.Compo - main *mainWindow + main *blockLogView } func (c *BlockViewer) Render() app.UI { if c.main == nil { - c.main = &mainWindow{} + c.main = &blockLogView{} } return app.Div().Class("main-layout").Body( app.Div().Class("header").Body( diff --git a/app/pkg/logsviewer/mainwindow.go b/app/pkg/logsviewer/mainwindow.go index 1a239170..4ac0af98 100644 --- a/app/pkg/logsviewer/mainwindow.go +++ b/app/pkg/logsviewer/mainwindow.go @@ -11,30 +11,28 @@ import ( "go.uber.org/zap" ) -// TODO(jeremy): This is just the viewer for blog log view. We should probably rename it. - -// mainWindow is the main window of the application. +// blockLogView is the main window of the application. // What it displays will change depending on the view selected. // The content of the main window is HTML which gets set by the action handler for different events. // // The main window registers a handler for the getAction event. The getAction event is triggered when ever // a blockLog is loaded. The handler for the getAction event will set the HTML content of the main windowß -type mainWindow struct { +type blockLogView struct { app.Compo HTMLContent string } -func (m *mainWindow) Render() app.UI { +func (m *blockLogView) Render() app.UI { // Raw requires the value to have a single root element. So we enclose the HTML content in a div to ensure // that is all ways true. return app.Raw("
" + m.HTMLContent + "
") } -func (m *mainWindow) OnMount(ctx app.Context) { +func (m *blockLogView) OnMount(ctx app.Context) { ctx.Handle(getAction, m.handleGetAction) } -func (m *mainWindow) handleGetAction(ctx app.Context, action app.Action) { +func (m *blockLogView) handleGetAction(ctx app.Context, action app.Action) { log := zapr.NewLogger(zap.L()) viewValue, ok := action.Value.(view) // Checks if a name was given. if !ok { From fa25204e8c65d3328271435f9bb1fb715397b04c Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Tue, 7 May 2024 13:13:55 -0700 Subject: [PATCH 20/24] Move the blockLogView into the block_viewer.go --- app/pkg/logsviewer/block_viewer.go | 81 +++++++++++++++++++++++++++- app/pkg/logsviewer/mainwindow.go | 85 ------------------------------ 2 files changed, 80 insertions(+), 86 deletions(-) delete mode 100644 app/pkg/logsviewer/mainwindow.go diff --git a/app/pkg/logsviewer/block_viewer.go b/app/pkg/logsviewer/block_viewer.go index a38f3771..9e7a1a0c 100644 --- a/app/pkg/logsviewer/block_viewer.go +++ b/app/pkg/logsviewer/block_viewer.go @@ -1,6 +1,12 @@ package logsviewer -import "github.com/maxence-charriere/go-app/v9/pkg/app" +import ( + "fmt" + "github.com/go-logr/zapr" + "github.com/jlewi/foyle/app/api" + "github.com/maxence-charriere/go-app/v9/pkg/app" + "go.uber.org/zap" +) // BlockViewer is the page that displays the block logs. // @@ -58,3 +64,76 @@ func (s *sideBar) Render() app.UI { ctx.NewActionWithValue(getAction, rawView) })) } + +// blockLogView is the main window of the application. +// What it displays will change depending on the view selected. +// The content of the main window is HTML which gets set by the action handler for different events. +// +// The main window registers a handler for the getAction event. The getAction event is triggered when ever +// a blockLog is loaded. The handler for the getAction event will set the HTML content of the main windowß +type blockLogView struct { + app.Compo + HTMLContent string +} + +func (m *blockLogView) Render() app.UI { + // Raw requires the value to have a single root element. So we enclose the HTML content in a div to ensure + // that is all ways true. + return app.Raw("
" + m.HTMLContent + "
") +} + +func (m *blockLogView) OnMount(ctx app.Context) { + ctx.Handle(getAction, m.handleGetAction) +} + +func (m *blockLogView) handleGetAction(ctx app.Context, action app.Action) { + log := zapr.NewLogger(zap.L()) + viewValue, ok := action.Value.(view) // Checks if a name was given. + if !ok { + log.Error(errors.New("No view provided"), "Invalid action") + return + } + log.Info("Handling get action", "view", viewValue) + switch viewValue { + case errorView: + errState := "" + ctx.GetState(getErrorState, &errState) + + m.HTMLContent = "

Error getting blocklog:


" + errState + case generatedBlockView: + block := &api.BlockLog{} + ctx.GetState(blockLogState, block) + value, err := renderGeneratedBlock(block) + if err == nil { + m.HTMLContent = value + } else { + log.Error(err, "Failed to convert generated block to html") + m.HTMLContent = fmt.Sprintf("Failed to convert generated block to html : error %+v", err) + } + case executedBlockView: + block := &api.BlockLog{} + ctx.GetState(blockLogState, block) + value, err := renderExecutedBlock(block) + if err == nil { + m.HTMLContent = value + } else { + log.Error(err, "Failed to convert executed block to html") + m.HTMLContent = fmt.Sprintf("Failed to convert executed block to html: error %+v", err) + } + case rawView: + block := &api.BlockLog{} + ctx.GetState(blockLogState, block) + blockJson, err := jsßon.MarshalIndent(block, "", " ") + if err != nil { + log.Error(err, "Failed to turn blocklog into json") + m.HTMLContent = fmt.Sprintf("Failed to turn blocklog into json: error %+v", err) + } else { + raw := "
" + string(blockJson) + "
" + m.HTMLContent = raw + } + default: + m.HTMLContent = "Unknown view: " + string(viewValue) + } + // We need to call update to trigger a re-render of the component. + m.Update() +} diff --git a/app/pkg/logsviewer/mainwindow.go b/app/pkg/logsviewer/mainwindow.go deleted file mode 100644 index 4ac0af98..00000000 --- a/app/pkg/logsviewer/mainwindow.go +++ /dev/null @@ -1,85 +0,0 @@ -package logsviewer - -import ( - "encoding/json" - "fmt" - - "github.com/go-logr/zapr" - "github.com/jlewi/foyle/app/api" - "github.com/maxence-charriere/go-app/v9/pkg/app" - "github.com/pkg/errors" - "go.uber.org/zap" -) - -// blockLogView is the main window of the application. -// What it displays will change depending on the view selected. -// The content of the main window is HTML which gets set by the action handler for different events. -// -// The main window registers a handler for the getAction event. The getAction event is triggered when ever -// a blockLog is loaded. The handler for the getAction event will set the HTML content of the main windowß -type blockLogView struct { - app.Compo - HTMLContent string -} - -func (m *blockLogView) Render() app.UI { - // Raw requires the value to have a single root element. So we enclose the HTML content in a div to ensure - // that is all ways true. - return app.Raw("
" + m.HTMLContent + "
") -} - -func (m *blockLogView) OnMount(ctx app.Context) { - ctx.Handle(getAction, m.handleGetAction) -} - -func (m *blockLogView) handleGetAction(ctx app.Context, action app.Action) { - log := zapr.NewLogger(zap.L()) - viewValue, ok := action.Value.(view) // Checks if a name was given. - if !ok { - log.Error(errors.New("No view provided"), "Invalid action") - return - } - log.Info("Handling get action", "view", viewValue) - switch viewValue { - case errorView: - errState := "" - ctx.GetState(getErrorState, &errState) - - m.HTMLContent = "

Error getting blocklog:


" + errState - case generatedBlockView: - block := &api.BlockLog{} - ctx.GetState(blockLogState, block) - value, err := renderGeneratedBlock(block) - if err == nil { - m.HTMLContent = value - } else { - log.Error(err, "Failed to convert generated block to html") - m.HTMLContent = fmt.Sprintf("Failed to convert generated block to html : error %+v", err) - } - case executedBlockView: - block := &api.BlockLog{} - ctx.GetState(blockLogState, block) - value, err := renderExecutedBlock(block) - if err == nil { - m.HTMLContent = value - } else { - log.Error(err, "Failed to convert executed block to html") - m.HTMLContent = fmt.Sprintf("Failed to convert executed block to html: error %+v", err) - } - case rawView: - block := &api.BlockLog{} - ctx.GetState(blockLogState, block) - blockJson, err := json.MarshalIndent(block, "", " ") - if err != nil { - log.Error(err, "Failed to turn blocklog into json") - m.HTMLContent = fmt.Sprintf("Failed to turn blocklog into json: error %+v", err) - } else { - raw := "
" + string(blockJson) + "
" - m.HTMLContent = raw - } - default: - m.HTMLContent = "Unknown view: " + string(viewValue) - } - // We need to call update to trigger a re-render of the component. - m.Update() -} From 76a2ff9df4315029615e79fbb7f1badaa4112dab Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Tue, 7 May 2024 13:15:19 -0700 Subject: [PATCH 21/24] Fix compile errors. --- app/pkg/logsviewer/block_viewer.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/pkg/logsviewer/block_viewer.go b/app/pkg/logsviewer/block_viewer.go index 9e7a1a0c..0d549362 100644 --- a/app/pkg/logsviewer/block_viewer.go +++ b/app/pkg/logsviewer/block_viewer.go @@ -1,10 +1,12 @@ package logsviewer import ( + "encoding/json" "fmt" "github.com/go-logr/zapr" "github.com/jlewi/foyle/app/api" "github.com/maxence-charriere/go-app/v9/pkg/app" + "github.com/pkg/errors" "go.uber.org/zap" ) @@ -123,7 +125,7 @@ func (m *blockLogView) handleGetAction(ctx app.Context, action app.Action) { case rawView: block := &api.BlockLog{} ctx.GetState(blockLogState, block) - blockJson, err := jsßon.MarshalIndent(block, "", " ") + blockJson, err := json.MarshalIndent(block, "", " ") if err != nil { log.Error(err, "Failed to turn blocklog into json") m.HTMLContent = fmt.Sprintf("Failed to turn blocklog into json: error %+v", err) From 27cc3fe60fd0afef851741165cd092d76856d7b1 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Tue, 7 May 2024 13:16:11 -0700 Subject: [PATCH 22/24] More cleanup. --- app/pkg/logsviewer/navigation.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/pkg/logsviewer/navigation.go b/app/pkg/logsviewer/navigation.go index 4fcbd94b..82343e09 100644 --- a/app/pkg/logsviewer/navigation.go +++ b/app/pkg/logsviewer/navigation.go @@ -20,9 +20,5 @@ func (s *navigationBar) Render() app.UI { app.Button().Text("Eval Results")).OnClick(func(ctx app.Context, e app.Event) { ctx.NewActionWithValue(setPage, evalsView) }), - //app.Div().Body( - // app.Button().Text("Raw")).OnClick(func(ctx app.Context, e app.Event) { - // ctx.NewActionWithValue(getAction, rawView) - //}) ) } From b6eb7f4a919dca8247cf2ce1139581c6935e9c5b Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Tue, 7 May 2024 13:19:36 -0700 Subject: [PATCH 23/24] Fix imports. --- app/pkg/logsviewer/block_viewer.go | 1 + 1 file changed, 1 insertion(+) diff --git a/app/pkg/logsviewer/block_viewer.go b/app/pkg/logsviewer/block_viewer.go index 0d549362..f1f5a807 100644 --- a/app/pkg/logsviewer/block_viewer.go +++ b/app/pkg/logsviewer/block_viewer.go @@ -3,6 +3,7 @@ package logsviewer import ( "encoding/json" "fmt" + "github.com/go-logr/zapr" "github.com/jlewi/foyle/app/api" "github.com/maxence-charriere/go-app/v9/pkg/app" From 17e8029104c49a654bc122bfc75342541ee07966 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Tue, 7 May 2024 13:34:57 -0700 Subject: [PATCH 24/24] Fix lint. --- app/pkg/logsviewer/eval_viewer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pkg/logsviewer/eval_viewer.go b/app/pkg/logsviewer/eval_viewer.go index 1cba1ec0..f25339ea 100644 --- a/app/pkg/logsviewer/eval_viewer.go +++ b/app/pkg/logsviewer/eval_viewer.go @@ -248,7 +248,7 @@ func (m *evalView) handleSetEvalView(ctx app.Context, action app.Action) { case evalQueryView: current := resultSet.GetSelected() if current == nil { - m.HTMLContent = fmt.Sprintf("No evaluation result is currently selected") + m.HTMLContent = "No evaluation result is currently selected" break } value, err := docToHTML(current.Example.Query)