implement tree view

This commit is contained in:
Gareth George
2023-11-25 17:27:28 -08:00
parent 463870eb5c
commit 4cd84223cf
9 changed files with 254 additions and 207 deletions
+92 -83
View File
@@ -83,10 +83,11 @@ type GetOperationsRequest struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
RepoId string `protobuf:"bytes,1,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"`
PlanId string `protobuf:"bytes,2,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"`
SnapshotId string `protobuf:"bytes,4,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"`
LastN int64 `protobuf:"varint,3,opt,name=last_n,json=lastN,proto3" json:"last_n,omitempty"` // limit to the last n operations
RepoId string `protobuf:"bytes,1,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"`
PlanId string `protobuf:"bytes,2,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"`
SnapshotId string `protobuf:"bytes,4,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"`
Ids []int64 `protobuf:"varint,5,rep,packed,name=ids,proto3" json:"ids,omitempty"`
LastN int64 `protobuf:"varint,3,opt,name=last_n,json=lastN,proto3" json:"last_n,omitempty"` // limit to the last n operations
}
func (x *GetOperationsRequest) Reset() {
@@ -142,6 +143,13 @@ func (x *GetOperationsRequest) GetSnapshotId() string {
return ""
}
func (x *GetOperationsRequest) GetIds() []int64 {
if x != nil {
return x.Ids
}
return nil
}
func (x *GetOperationsRequest) GetLastN() int64 {
if x != nil {
return x.LastN
@@ -403,92 +411,93 @@ var file_v1_service_proto_rawDesc = []byte{
0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07,
0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70,
0x6c, 0x61, 0x6e, 0x49, 0x64, 0x22, 0x80, 0x01, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65,
0x6c, 0x61, 0x6e, 0x49, 0x64, 0x22, 0x92, 0x01, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65,
0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17,
0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6c, 0x61, 0x6e, 0x5f,
0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x61, 0x6e, 0x49, 0x64,
0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18,
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49,
0x64, 0x12, 0x15, 0x0a, 0x06, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28,
0x03, 0x52, 0x05, 0x6c, 0x61, 0x73, 0x74, 0x4e, 0x22, 0x68, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74,
0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x1f, 0x0a,
0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64, 0x12, 0x12,
0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61,
0x74, 0x68, 0x22, 0x56, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68,
0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70,
0x61, 0x74, 0x68, 0x12, 0x25, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x02,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x73, 0x45, 0x6e, 0x74, 0x72,
0x79, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xd3, 0x01, 0x0a, 0x07, 0x4c,
0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79,
0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12,
0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61,
0x74, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52,
0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28,
0x03, 0x52, 0x03, 0x67, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06,
0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f,
0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x14,
0x0a, 0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d,
0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20,
0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x74,
0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65,
0x32, 0x81, 0x06, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x55, 0x49, 0x12, 0x43, 0x0a,
0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
0x74, 0x79, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x12,
0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66,
0x69, 0x67, 0x12, 0x3a, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x0a, 0x2e, 0x76, 0x31,
0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x3a,
0x01, 0x2a, 0x22, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3b,
0x0a, 0x07, 0x41, 0x64, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x12, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x52,
0x65, 0x70, 0x6f, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22,
0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, 0x01, 0x2a, 0x22, 0x0f, 0x2f, 0x76, 0x31, 0x2f,
0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x72, 0x65, 0x70, 0x6f, 0x12, 0x61, 0x0a, 0x12, 0x47,
0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74,
0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4f,
0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x1d, 0x82,
0xd3, 0xe4, 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74,
0x73, 0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x30, 0x01, 0x12, 0x57,
0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12,
0x18, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x4f,
0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x19, 0x82, 0xd3,
0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x65,
0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x5b, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x53,
0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69,
0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e,
0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93,
0x02, 0x12, 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6e, 0x61, 0x70, 0x73,
0x68, 0x6f, 0x74, 0x73, 0x12, 0x70, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70,
0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x76, 0x31, 0x2e, 0x4c,
0x64, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x03, 0x52, 0x03,
0x69, 0x64, 0x73, 0x12, 0x15, 0x0a, 0x06, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6e, 0x18, 0x03, 0x20,
0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x61, 0x73, 0x74, 0x4e, 0x22, 0x68, 0x0a, 0x18, 0x4c, 0x69,
0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12,
0x1f, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64,
0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x70, 0x61, 0x74, 0x68, 0x22, 0x56, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70,
0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x25, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73,
0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x73, 0x45, 0x6e,
0x74, 0x72, 0x79, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xd3, 0x01, 0x0a,
0x07, 0x4c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04,
0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65,
0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x70, 0x61, 0x74, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28,
0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x05, 0x20,
0x01, 0x28, 0x03, 0x52, 0x03, 0x67, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65,
0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04,
0x6d, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65,
0x12, 0x14, 0x0a, 0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52,
0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18,
0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05,
0x63, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69,
0x6d, 0x65, 0x32, 0x81, 0x06, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x55, 0x49, 0x12,
0x43, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x22, 0x12, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f,
0x6e, 0x66, 0x69, 0x67, 0x12, 0x3a, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x67, 0x12, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x0a, 0x2e,
0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02,
0x0f, 0x3a, 0x01, 0x2a, 0x22, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x12, 0x3b, 0x0a, 0x07, 0x41, 0x64, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x12, 0x08, 0x2e, 0x76, 0x31,
0x2e, 0x52, 0x65, 0x70, 0x6f, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x67, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, 0x01, 0x2a, 0x22, 0x0f, 0x2f, 0x76,
0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x72, 0x65, 0x70, 0x6f, 0x12, 0x61, 0x0a,
0x12, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65,
0x6e, 0x74, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x76, 0x31,
0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22,
0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x76, 0x65,
0x6e, 0x74, 0x73, 0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x30, 0x01,
0x12, 0x57, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x76, 0x31,
0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x19,
0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x6f,
0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x5b, 0x0a, 0x0d, 0x4c, 0x69, 0x73,
0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e,
0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63,
0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x18, 0x82, 0xd3,
0xe4, 0x93, 0x02, 0x12, 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6e, 0x61,
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x70, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e,
0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x76, 0x31,
0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c,
0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x76, 0x31, 0x2e, 0x4c,
0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73,
0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x3a, 0x01,
0x2a, 0x22, 0x13, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73,
0x2f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x4f, 0x0a, 0x06, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70,
0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56,
0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x19, 0x82, 0xd3,
0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6d, 0x64,
0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x5b, 0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41,
0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79,
0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a,
0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69,
0x73, 0x74, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f,
0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x2f,
0x70, 0x61, 0x74, 0x68, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f,
0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18,
0x3a, 0x01, 0x2a, 0x22, 0x13, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
0x74, 0x73, 0x2f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x4f, 0x0a, 0x06, 0x42, 0x61, 0x63, 0x6b,
0x75, 0x70, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e,
0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x19,
0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x63,
0x6d, 0x64, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x5b, 0x0a, 0x10, 0x50, 0x61, 0x74,
0x68, 0x41, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e,
0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75,
0x65, 0x1a, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67,
0x4c, 0x69, 0x73, 0x74, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22,
0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74,
0x65, 0x2f, 0x70, 0x61, 0x74, 0x68, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67,
0x65, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
+9
View File
@@ -202,6 +202,15 @@ func (s *Server) GetOperations(ctx context.Context, req *v1.GetOperationsRequest
ops, err = s.oplog.GetByRepo(req.RepoId, collector)
} else if req.SnapshotId != "" {
ops, err = s.oplog.GetBySnapshotId(req.SnapshotId, collector)
} else if len(req.Ids) > 0 {
ops = make([]*v1.Operation, 0, len(req.Ids))
for i, id := range req.Ids {
op, err := s.oplog.Get(id)
if err != nil {
return nil, fmt.Errorf("failed to get operation %d: %w", i, err)
}
ops = append(ops, op)
}
} else {
ops, err = s.oplog.GetAll()
}
+1
View File
@@ -20,6 +20,7 @@ type RepoOrchestrator struct {
repo *restic.Repo
}
// newRepoOrchestrator accepts a config and a repo that is configured with the properties of that config object.
func newRepoOrchestrator(repoConfig *v1.Repo, repo *restic.Repo) *RepoOrchestrator {
return &RepoOrchestrator{
repoConfig: repoConfig,
+1
View File
@@ -85,6 +85,7 @@ message GetOperationsRequest {
string repo_id = 1;
string plan_id = 2;
string snapshot_id = 4;
repeated int64 ids = 5;
int64 last_n = 3; // limit to the last n operations
}
+1
View File
@@ -19,6 +19,7 @@ export type GetOperationsRequest = {
repoId?: string
planId?: string
snapshotId?: string
ids?: string[]
lastN?: string
}
+35 -23
View File
@@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { Operation, OperationStatus } from "../../gen/ts/v1/operations.pb";
import {
Card,
@@ -8,6 +8,7 @@ import {
List,
Progress,
Row,
Spin,
Typography,
} from "antd";
import {
@@ -16,7 +17,12 @@ import {
SaveOutlined,
} from "@ant-design/icons";
import { BackupProgressEntry, ResticSnapshot } from "../../gen/ts/v1/restic.pb";
import { EOperation } from "../state/oplog";
import {
EOperation,
buildOperationListListener,
subscribeToOperations,
unsubscribeFromOperations,
} from "../state/oplog";
import { SnapshotBrowser } from "./SnapshotBrowser";
import {
formatBytes,
@@ -24,11 +30,24 @@ import {
formatTime,
normalizeSnapshotId,
} from "../lib/formatting";
import _ from "lodash";
import { GetOperationsRequest } from "../../gen/ts/v1/service.pb";
export const OperationList = ({
operations,
}: React.PropsWithoutRef<{ operations: EOperation[] }>) => {
operations = [...operations].reverse();
req,
}: React.PropsWithoutRef<{ req: GetOperationsRequest }>) => {
const [operations, setOperations] = useState<EOperation[]>([]);
useEffect(() => {
const lis = buildOperationListListener(req, (event, operation, opList) => {
setOperations(opList);
});
subscribeToOperations(lis);
return () => {
unsubscribeFromOperations(lis);
};
}, [JSON.stringify(req)]);
if (operations.length === 0) {
return (
@@ -39,27 +58,20 @@ export const OperationList = ({
);
}
const groupBy = (ops: EOperation[], keyFunc: (op: EOperation) => string) => {
const groups: { [key: string]: EOperation[] } = {};
ops.forEach((op) => {
const key = keyFunc(op);
if (!(key in groups)) {
groups[key] = [];
}
groups[key].push(op);
});
return Object.values(groups);
};
// groups items by snapshotID if one can be identified, otherwise by operation ID.
const groupedItems = groupBy(operations, (op: EOperation) => {
return getSnapshotId(op) || op.id!;
});
const groupedItems = _.values(
_.groupBy(operations, (op: EOperation) => {
return getSnapshotId(op) || op.id!;
})
);
groupedItems.sort((a, b) => {
return b[0].parsedTime - a[0].parsedTime;
});
groupedItems.forEach((group) => {
group.sort((a, b) => {
return b.parsedTime - a.parsedTime;
});
});
return (
<List
@@ -82,7 +94,7 @@ export const OperationList = ({
pagination={
operations.length > 50
? { position: "both", align: "center", defaultPageSize: 50 }
: {}
: undefined
}
/>
);
+100 -81
View File
@@ -8,7 +8,7 @@ import {
toEop,
unsubscribeFromOperations,
} from "../state/oplog";
import { Tree } from "antd";
import { Col, Empty, Row, Tree } from "antd";
import _ from "lodash";
import { DataNode } from "antd/es/tree";
import {
@@ -23,30 +23,34 @@ import {
QuestionOutlined,
SaveOutlined,
} from "@ant-design/icons";
import {
OperationEvent,
OperationEventType,
OperationStatus,
} from "../../gen/ts/v1/operations.pb";
import { OperationEvent, OperationStatus } from "../../gen/ts/v1/operations.pb";
import { useAlertApi } from "./Alerts";
import { MAX_OPERATION_HISTORY } from "../constants";
import { OperationList } from "./OperationList";
import { GetOperationsRequest } from "../../gen/ts/v1/service.pb";
type OpTreeNode = DataNode & {
backup?: BackupInfo;
};
export const OperationTree = ({
planId,
repoId,
}: React.PropsWithoutRef<{ planId?: string; repoId?: string }>) => {
req,
}: React.PropsWithoutRef<{ req: GetOperationsRequest }>) => {
console.log("Loading operation tree with req: ", req);
const alertApi = useAlertApi();
const [backups, setBackups] = useState<BackupInfo[]>([]);
const [selectedBackupId, setSelectedBackupId] = useState<string | null>(null);
// track backups for this operation tree view.
useEffect(() => {
setSelectedBackupId(null);
const backupCollector = new BackupInfoCollector();
const lis = (opEvent: OperationEvent) => {
if (!!req.planId && opEvent.operation!.planId !== req.planId) {
return;
}
if (!!req.repoId && opEvent.operation!.repoId !== req.repoId) {
return;
}
backupCollector.addOperation(opEvent.type!, opEvent.operation!);
};
subscribeToOperations(lis);
@@ -59,12 +63,7 @@ export const OperationTree = ({
setBackups(backups);
});
getOperations({
planId,
repoId,
snapshotId,
lastN: "" + MAX_OPERATION_HISTORY,
})
getOperations(req)
.then((ops) => {
backupCollector.bulkAddOperations(ops);
})
@@ -74,7 +73,7 @@ export const OperationTree = ({
return () => {
unsubscribeFromOperations(lis);
};
}, [planId, repoId]);
}, [req]);
if (backups.length === 0) {
return (
@@ -86,73 +85,94 @@ export const OperationTree = ({
const treeData = buildTreeYear(backups);
return (
<Tree<OpTreeNode>
treeData={treeData}
showIcon
defaultExpandedKeys={[backups[0].id!]}
titleRender={(node: OpTreeNode): React.ReactNode => {
if (node.title) {
return <>{node.title}</>;
}
if (node.backup) {
const b = node.backup;
const details: string[] = [];
let oplist: React.ReactNode | null = null;
if (selectedBackupId) {
const backup = backups.find((b) => b.id === selectedBackupId);
if (!backup) {
oplist = <Empty description="Backup not found." />;
} else {
const req: GetOperationsRequest = {
ids: backup.operations.map((op) => op.id!),
};
oplist = (
<>
<h3>Backup at {formatTime(backup.displayTime)}</h3>
<OperationList req={req} />
</>
);
}
}
if (b.backupLastStatus) {
if (b.backupLastStatus.summary) {
const s = b.backupLastStatus.summary;
details.push(
`${formatBytes(s.totalBytesProcessed)} in ${formatDuration(
s.totalDuration!
)}`
);
} else if (b.backupLastStatus.status) {
const s = b.backupLastStatus.status;
const bytesDone = parseInt(s.bytesDone!);
const bytesTotal = parseInt(s.totalBytes!);
const percent = Math.floor((bytesDone / bytesTotal) * 100);
details.push(
`${percent}% processed ${formatBytes(
bytesDone
)} / ${formatBytes(bytesTotal)}`
return (
<Row>
<Col span={12}>
<h3>Browse Backups</h3>
<Tree<OpTreeNode>
treeData={treeData}
showIcon
defaultExpandedKeys={[backups[0].id!]}
onSelect={(keys, info) => {
setSelectedBackupId(
info.selectedNodes.length > 0
? info.selectedNodes[0].backup!.id!
: null
);
}}
titleRender={(node: OpTreeNode): React.ReactNode => {
if (node.title) {
return <>{node.title}</>;
}
if (node.backup) {
const b = node.backup;
const details: string[] = [];
if (b.backupLastStatus) {
if (b.backupLastStatus.summary) {
const s = b.backupLastStatus.summary;
details.push(
`${formatBytes(s.totalBytesProcessed)} in ${formatDuration(
s.totalDuration!
)}`
);
} else if (b.backupLastStatus.status) {
const s = b.backupLastStatus.status;
const bytesDone = parseInt(s.bytesDone!);
const bytesTotal = parseInt(s.totalBytes!);
const percent = Math.floor((bytesDone / bytesTotal) * 100);
details.push(
`${percent}% processed ${formatBytes(
bytesDone
)} / ${formatBytes(bytesTotal)}`
);
}
}
if (b.snapshotInfo) {
details.push(`ID: ${normalizeSnapshotId(b.snapshotInfo.id!)}`);
}
let detailsElem: React.ReactNode | null = null;
if (details.length > 0) {
detailsElem = (
<span className="resticui backup-details">
[{details.join(", ")}]
</span>
);
}
return (
<>
Backup {formatTime(b.displayTime)} {detailsElem}
</>
);
}
}
if (b.snapshotInfo) {
details.push(`ID: ${normalizeSnapshotId(b.snapshotInfo.id!)}`);
}
let detailsElem: React.ReactNode | null = null;
if (details.length > 0) {
detailsElem = (
<span className="resticui backup-details">
[{details.join(", ")}]
</span>
return (
<span>ERROR: this element should not appear, this is a bug.</span>
);
}
return (
<>
Backup {formatTime(b.displayTime)} {detailsElem}
</>
);
}
return (
<span>ERROR: this element should not appear, this is a bug.</span>
);
}}
></Tree>
);
};
const BackupInfoPanel = ({
backup,
}: React.PropsWithoutRef<{ backup: BackupInfo }>) => {
return (
<>
<OperationList operations={backup.operations.map(toEop)} />
</>
}}
></Tree>
</Col>
<Col span={12}>{oplist}</Col>
</Row>
);
};
@@ -181,7 +201,6 @@ const buildTreeMonth = (operations: BackupInfo[]): OpTreeNode[] => {
key: key,
title: value[0].displayTime.toLocaleString("default", {
month: "long",
year: "numeric",
}),
children: buildTreeDay(value),
};
+8 -2
View File
@@ -96,6 +96,12 @@ export const buildOperationListListener = (
if (!!req.repoId && op.repoId !== req.repoId) {
return;
}
if (req.snapshotId && op.snapshotId !== req.snapshotId) {
return;
}
if (req.ids && !req.ids.includes(op.id!)) {
return;
}
if (type === OperationEventType.EVENT_UPDATED) {
const index = operations.findIndex((o) => o.id === op.id);
if (index > -1) {
@@ -243,13 +249,13 @@ export class BackupInfoCollector {
}
public subscribe(
listener: (event: OperationEventType, info: BackupInfo) => void
listener: (event: OperationEventType, info: BackupInfo[]) => void
) {
this.listeners.push(listener);
}
public unsubscribe(
listener: (event: OperationEventType, info: BackupInfo) => void
listener: (event: OperationEventType, info: BackupInfo[]) => void
) {
const index = this.listeners.indexOf(listener);
if (index > -1) {
+7 -18
View File
@@ -21,21 +21,6 @@ import { MAX_OPERATION_HISTORY } from "../constants";
export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
const showModal = useShowModal();
const alertsApi = useAlertApi()!;
const [operations, setOperations] = useState<EOperation[]>([]);
useEffect(() => {
const listener = buildOperationListListener(
{ planId: plan.id, lastN: "" + MAX_OPERATION_HISTORY },
(event, changedOp, operations) => {
setOperations([...operations]);
}
);
subscribeToOperations(listener);
return () => {
unsubscribeFromOperations(listener);
};
}, [plan.id]);
// Gracefully handle deletions by checking if the plan is still in the config.
const config = useRecoilValue(configState);
@@ -88,7 +73,9 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
label: "Tree View",
children: (
<>
<OperationTree operations={operations} />
<OperationTree
req={{ planId: plan.id!, lastN: "" + MAX_OPERATION_HISTORY }}
/>
</>
),
},
@@ -97,8 +84,10 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
label: "Operation List",
children: (
<>
<h2>Backup Action History ({operations.length} loaded)</h2>
<OperationList operations={operations} />
<h2>Backup Action History</h2>
<OperationList
req={{ planId: plan.id!, lastN: "" + MAX_OPERATION_HISTORY }}
/>
</>
),
},