diff --git a/gen/go/v1/operations.pb.go b/gen/go/v1/operations.pb.go index 641c1cba..e04ab7eb 100644 --- a/gen/go/v1/operations.pb.go +++ b/gen/go/v1/operations.pb.go @@ -78,6 +78,7 @@ const ( OperationStatus_STATUS_INPROGRESS OperationStatus = 2 OperationStatus_STATUS_SUCCESS OperationStatus = 3 OperationStatus_STATUS_ERROR OperationStatus = 4 + OperationStatus_STATUS_CANCELLED OperationStatus = 5 ) // Enum value maps for OperationStatus. @@ -88,6 +89,7 @@ var ( 2: "STATUS_INPROGRESS", 3: "STATUS_SUCCESS", 4: "STATUS_ERROR", + 5: "STATUS_CANCELLED", } OperationStatus_value = map[string]int32{ "STATUS_UNKNOWN": 0, @@ -95,6 +97,7 @@ var ( "STATUS_INPROGRESS": 2, "STATUS_SUCCESS": 3, "STATUS_ERROR": 4, + "STATUS_CANCELLED": 5, } ) @@ -177,14 +180,21 @@ type Operation struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` - RepoId string `protobuf:"bytes,2,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` // repo id if associated with a repo (always true) - PlanId string `protobuf:"bytes,3,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"` // plan id if associated with a plan (always true) - SnapshotId string `protobuf:"bytes,8,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` // snapshot id if associated with a snapshot. - Status OperationStatus `protobuf:"varint,4,opt,name=status,proto3,enum=v1.OperationStatus" json:"status,omitempty"` - UnixTimeStartMs int64 `protobuf:"varint,5,opt,name=unix_time_start_ms,json=unixTimeStartMs,proto3" json:"unix_time_start_ms,omitempty"` - UnixTimeEndMs int64 `protobuf:"varint,6,opt,name=unix_time_end_ms,json=unixTimeEndMs,proto3" json:"unix_time_end_ms,omitempty"` - DisplayMessage string `protobuf:"bytes,7,opt,name=display_message,json=displayMessage,proto3" json:"display_message,omitempty"` // human readable context message (if any) + // required, primary ID of the operation. + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + // required, repo id if associated with a repo + RepoId string `protobuf:"bytes,2,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` + // required, plan id if associated with a plan + PlanId string `protobuf:"bytes,3,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"` + // optional snapshot id if associated with a snapshot. + SnapshotId string `protobuf:"bytes,8,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` + Status OperationStatus `protobuf:"varint,4,opt,name=status,proto3,enum=v1.OperationStatus" json:"status,omitempty"` + // required, unix time in milliseconds of the operation's creation (ID is derived from this) + UnixTimeStartMs int64 `protobuf:"varint,5,opt,name=unix_time_start_ms,json=unixTimeStartMs,proto3" json:"unix_time_start_ms,omitempty"` + // optional, unix time in milliseconds of the operation's completion + UnixTimeEndMs int64 `protobuf:"varint,6,opt,name=unix_time_end_ms,json=unixTimeEndMs,proto3" json:"unix_time_end_ms,omitempty"` + // optional, human readable context message, typically an error message. + DisplayMessage string `protobuf:"bytes,7,opt,name=display_message,json=displayMessage,proto3" json:"display_message,omitempty"` // Types that are assignable to Op: // // *Operation_OperationBackup @@ -526,18 +536,19 @@ var file_v1_operations_proto_rawDesc = []byte{ 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, - 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x2a, 0x76, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, - 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, - 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, - 0x47, 0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, - 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, - 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x10, - 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, - 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, + 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x2a, 0x8c, 0x01, 0x0a, 0x0f, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, + 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, + 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, + 0x4e, 0x47, 0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, + 0x4e, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, + 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, + 0x04, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x41, 0x4e, 0x43, + 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x05, 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 ( diff --git a/gen/go/v1/service.pb.go b/gen/go/v1/service.pb.go index a288305d..4a612e1e 100644 --- a/gen/go/v1/service.pb.go +++ b/gen/go/v1/service.pb.go @@ -83,9 +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"` - 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() { @@ -134,6 +136,20 @@ func (x *GetOperationsRequest) GetPlanId() string { return "" } +func (x *GetOperationsRequest) GetSnapshotId() string { + if x != nil { + return x.SnapshotId + } + 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 @@ -395,90 +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, 0x5f, 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, - 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, 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, + 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, 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, 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 ( diff --git a/internal/api/server.go b/internal/api/server.go index 0acfb58e..87eac8fe 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -96,7 +96,7 @@ func (s *Server) AddRepo(ctx context.Context, repo *v1.Repo) (*v1.Config, error) return c, nil } -// ListSnapshots implements GET /v1/snapshots/{repo.id}/{plan.id?} +// ListSnapshots implements POST /v1/snapshots func (s *Server) ListSnapshots(ctx context.Context, query *v1.ListSnapshotsRequest) (*v1.ResticSnapshotList, error) { repo, err := s.orchestrator.GetRepo(query.RepoId) if err != nil { @@ -200,6 +200,17 @@ func (s *Server) GetOperations(ctx context.Context, req *v1.GetOperationsRequest ops, err = s.oplog.GetByPlan(req.PlanId, collector) } else if req.RepoId != "" { 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() } diff --git a/internal/oplog/oplog.go b/internal/oplog/oplog.go index be356dd9..b4c10c3a 100644 --- a/internal/oplog/oplog.go +++ b/internal/oplog/oplog.go @@ -195,17 +195,25 @@ func (o *OpLog) getOperationHelper(b *bolt.Bucket, id int64) (*v1.Operation, err return &op, nil } +func (o *OpLog) nextOperationId(b *bolt.Bucket, unixTimeMs int64) (int64, error) { + seq, err := b.NextSequence() + if err != nil { + return 0, fmt.Errorf("next sequence: %w", err) + } + return int64(unixTimeMs<<20) | int64(seq&((1<<20)-1)), nil +} + func (o *OpLog) addOperationHelper(tx *bolt.Tx, op *v1.Operation) error { b := tx.Bucket(OpLogBucket) if op.Id == 0 { - seq, err := b.NextSequence() - if err != nil { - return fmt.Errorf("error getting next sequence: %w", err) - } if op.UnixTimeStartMs == 0 { return fmt.Errorf("operation must have a start time") } - op.Id = op.UnixTimeStartMs<<20 | int64(seq&(1<<20-1)) + var err error + op.Id, err = o.nextOperationId(b, op.UnixTimeStartMs) + if err != nil { + return fmt.Errorf("create next operation ID: %w", err) + } } op.SnapshotId = NormalizeSnapshotId(op.SnapshotId) diff --git a/internal/orchestrator/repo.go b/internal/orchestrator/repo.go index 968d756b..77d7e2bf 100644 --- a/internal/orchestrator/repo.go +++ b/internal/orchestrator/repo.go @@ -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, @@ -47,6 +48,11 @@ func (r *RepoOrchestrator) SnapshotsForPlan(ctx context.Context, plan *v1.Plan) func (r *RepoOrchestrator) Backup(ctx context.Context, plan *v1.Plan, progressCallback func(event *restic.BackupProgressEntry)) (*restic.BackupProgressEntry, error) { zap.L().Debug("repo orchestrator starting backup", zap.String("repo", r.repoConfig.Id)) + + if err := r.repo.Init(ctx); err != nil { + return nil, fmt.Errorf("failed to init repo: %w", err) + } + snapshots, err := r.SnapshotsForPlan(ctx, plan) if err != nil { return nil, fmt.Errorf("failed to get snapshots for plan: %w", err) diff --git a/internal/orchestrator/repo_test.go b/internal/orchestrator/repo_test.go index b348191e..6994beea 100644 --- a/internal/orchestrator/repo_test.go +++ b/internal/orchestrator/repo_test.go @@ -18,15 +18,15 @@ func TestBackup(t *testing.T) { // create a new repo with cache disabled for testing r := &v1.Repo{ - Id: "test", - Uri: repo, + Id: "test", + Uri: repo, Password: "test", - Flags: []string{"--no-cache"}, + Flags: []string{"--no-cache"}, } - + plan := &v1.Plan{ - Id: "test", - Repo: "test", + Id: "test", + Repo: "test", Paths: []string{testData}, } @@ -48,34 +48,34 @@ func TestBackup(t *testing.T) { func TestSnapshotParenting(t *testing.T) { t.Parallel() - + repo := t.TempDir() testData := test.CreateTestData(t) // create a new repo with cache disabled for testing r := &v1.Repo{ - Id: "test", - Uri: repo, + Id: "test", + Uri: repo, Password: "test", - Flags: []string{"--no-cache"}, + Flags: []string{"--no-cache"}, } plans := []*v1.Plan{ { - Id: "test", - Repo: "test", + Id: "test", + Repo: "test", Paths: []string{testData}, }, { - Id: "test2", - Repo: "test", + Id: "test2", + Repo: "test", Paths: []string{testData}, }, } orchestrator := newRepoOrchestrator(r, restic.NewRepo(r, restic.WithFlags("--no-cache"))) - for i := 0; i < 4; i ++{ + for i := 0; i < 4; i++ { for _, plan := range plans { summary, err := orchestrator.Backup(context.Background(), plan, nil) if err != nil { @@ -92,7 +92,7 @@ func TestSnapshotParenting(t *testing.T) { } } } - + for _, plan := range plans { snapshots, err := orchestrator.SnapshotsForPlan(context.Background(), plan) if err != nil { @@ -105,7 +105,7 @@ func TestSnapshotParenting(t *testing.T) { } for i := 1; i < len(snapshots); i++ { - prev := snapshots[i - 1] + prev := snapshots[i-1] curr := snapshots[i] if prev.ToProto().UnixTimeMs >= curr.ToProto().UnixTimeMs { @@ -129,4 +129,4 @@ func TestSnapshotParenting(t *testing.T) { if len(snapshots) != 8 { t.Errorf("expected 8 snapshots, got %d", len(snapshots)) } -} \ No newline at end of file +} diff --git a/internal/orchestrator/tasks.go b/internal/orchestrator/tasks.go index 6e2889a3..eb574506 100644 --- a/internal/orchestrator/tasks.go +++ b/internal/orchestrator/tasks.go @@ -47,21 +47,6 @@ func (t *ScheduledBackupTask) Name() string { } func (t *ScheduledBackupTask) Next(now time.Time) *time.Time { - if ops, err := t.orchestrator.OpLog.GetByPlan(t.plan.Id, indexutil.CollectLastN(10)); err == nil { - var lastBackupOp *v1.Operation - for _, op := range ops { - if _, ok := op.Op.(*v1.Operation_OperationBackup); ok { - lastBackupOp = op - } - } - - if lastBackupOp != nil { - now = time.Unix(0, lastBackupOp.UnixTimeEndMs*int64(time.Millisecond)) - } - } else { - zap.S().Errorf("error getting last operation for plan %q when computing backup schedule: %v", t.plan.Id, err) - } - next := t.schedule.Next(now) return &next } @@ -116,7 +101,7 @@ func backupHelper(ctx context.Context, orchestrator *Orchestrator, plan *v1.Plan startTime := time.Now() err := WithOperation(orchestrator.OpLog, op, func() error { - zap.L().Info("Starting backup", zap.String("plan", plan.Id)) + zap.L().Info("Starting backup", zap.String("plan", plan.Id), zap.Int64("opId", op.Id)) repo, err := orchestrator.GetRepo(plan.Repo) if err != nil { return fmt.Errorf("couldn't get repo %q: %w", plan.Repo, err) @@ -124,7 +109,7 @@ func backupHelper(ctx context.Context, orchestrator *Orchestrator, plan *v1.Plan lastSent := time.Now() // debounce progress updates, these can endup being very frequent. summary, err := repo.Backup(ctx, plan, func(entry *restic.BackupProgressEntry) { - if time.Since(lastSent) < 200*time.Millisecond { + if time.Since(lastSent) < 250*time.Millisecond { return } lastSent = time.Now() @@ -230,7 +215,7 @@ func WithOperation(oplog *oplog.OpLog, op *v1.Operation, do func() error) error if op.Status == v1.OperationStatus_STATUS_INPROGRESS { op.Status = v1.OperationStatus_STATUS_SUCCESS } - if e := oplog.Update(op); err != nil { + if e := oplog.Update(op); e != nil { return multierror.Append(err, fmt.Errorf("failed to update operation in oplog: %w", e)) } return err diff --git a/pkg/restic/restic.go b/pkg/restic/restic.go index 9978fa9d..5b73e7cb 100644 --- a/pkg/restic/restic.go +++ b/pkg/restic/restic.go @@ -77,7 +77,6 @@ func (r *Repo) init(ctx context.Context) error { func (r *Repo) Init(ctx context.Context) error { r.mu.Lock() defer r.mu.Unlock() - r.initialized = false return r.init(ctx) } diff --git a/proto/v1/operations.proto b/proto/v1/operations.proto index 6c2cb26c..f6256b3d 100644 --- a/proto/v1/operations.proto +++ b/proto/v1/operations.proto @@ -11,18 +11,20 @@ message OperationList { } message Operation { + // required, primary ID of the operation. int64 id = 1; - // repo id if associated with a repo (always true) + // required, repo id if associated with a repo string repo_id = 2; - // plan id if associated with a plan (always true) + // required, plan id if associated with a plan string plan_id = 3; - // snapshot id if associated with a snapshot. + // optional snapshot id if associated with a snapshot. string snapshot_id = 8; OperationStatus status = 4; - // unix time in milliseconds of the operation's creation (ID is derived from this) + // required, unix time in milliseconds of the operation's creation (ID is derived from this) int64 unix_time_start_ms = 5; + // optional, unix time in milliseconds of the operation's completion int64 unix_time_end_ms = 6; - // human readable context message, typically an error message. + // optional, human readable context message, typically an error message. string display_message = 7; oneof op { @@ -50,6 +52,7 @@ enum OperationStatus { STATUS_INPROGRESS = 2; STATUS_SUCCESS = 3; STATUS_ERROR = 4; + STATUS_CANCELLED = 5; } message OperationBackup { diff --git a/proto/v1/service.proto b/proto/v1/service.proto index 180f3a1e..8fdf39be 100644 --- a/proto/v1/service.proto +++ b/proto/v1/service.proto @@ -84,6 +84,8 @@ message ListSnapshotsRequest { 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 } diff --git a/webui/gen/ts/v1/operations.pb.ts b/webui/gen/ts/v1/operations.pb.ts index 474b5cbf..c984d9ec 100644 --- a/webui/gen/ts/v1/operations.pb.ts +++ b/webui/gen/ts/v1/operations.pb.ts @@ -27,6 +27,7 @@ export enum OperationStatus { STATUS_INPROGRESS = "STATUS_INPROGRESS", STATUS_SUCCESS = "STATUS_SUCCESS", STATUS_ERROR = "STATUS_ERROR", + STATUS_CANCELLED = "STATUS_CANCELLED", } export type OperationList = { diff --git a/webui/gen/ts/v1/service.pb.ts b/webui/gen/ts/v1/service.pb.ts index 8dc10d96..6fb4ba4b 100644 --- a/webui/gen/ts/v1/service.pb.ts +++ b/webui/gen/ts/v1/service.pb.ts @@ -18,6 +18,8 @@ export type ListSnapshotsRequest = { export type GetOperationsRequest = { repoId?: string planId?: string + snapshotId?: string + ids?: string[] lastN?: string } diff --git a/webui/package-lock.json b/webui/package-lock.json index 4cc0c5d3..ac797873 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -887,6 +887,17 @@ "react-refresh": "^0.9.0" } }, + "@parcel/transformer-sass": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@parcel/transformer-sass/-/transformer-sass-2.10.3.tgz", + "integrity": "sha512-Q8pTsMO+YuczBjmW8NdFcUjYpCdvY6EzYhPYw4eTyvldalGkaUVs1mJoKmWDHIDsIpdiaARxTpXP946B9cgE3w==", + "dev": true, + "requires": { + "@parcel/plugin": "2.10.3", + "@parcel/source-map": "^2.1.1", + "sass": "^1.38.0" + } + }, "@parcel/transformer-svg": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@parcel/transformer-svg/-/transformer-svg-2.10.3.tgz", @@ -1224,6 +1235,11 @@ "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==" }, + "@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" + }, "@types/node": { "version": "20.9.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.1.tgz", @@ -1328,6 +1344,16 @@ "throttle-debounce": "^5.0.0" } }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1356,6 +1382,12 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -1408,6 +1440,22 @@ "supports-color": "^7.1.0" } }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, "chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -1606,11 +1654,27 @@ "to-regex-range": "^5.0.1" } }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, "get-port": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz", "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==" }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, "globals": { "version": "13.23.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", @@ -1655,6 +1719,12 @@ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, + "immutable": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", + "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", + "dev": true + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -1675,6 +1745,15 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1830,6 +1909,11 @@ } } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1916,6 +2000,12 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, "nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -2460,6 +2550,15 @@ "util-deprecate": "^1.0.1" } }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, "recoil": { "version": "0.7.7", "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", @@ -2488,6 +2587,17 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, + "sass": { + "version": "1.69.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", + "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, "scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -2517,6 +2627,12 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, "srcset": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", diff --git a/webui/package.json b/webui/package.json index 8e0380b4..2440c524 100644 --- a/webui/package.json +++ b/webui/package.json @@ -11,11 +11,13 @@ "license": "ISC", "dependencies": { "@ant-design/icons": "^5.2.6", + "@types/lodash": "^4.14.202", "@types/node": "^20.9.0", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", "antd": "^5.11.1", "buffer": "^6.0.3", + "lodash": "^4.17.21", "parcel": "^2.10.2", "process": "^0.11.10", "react": "^18.2.0", @@ -25,6 +27,7 @@ "typescript": "^5.2.2" }, "devDependencies": { + "@parcel/transformer-sass": "^2.10.3", "events": "^3.3.0", "stream-browserify": "^3.0.0" } diff --git a/webui/src/components/OperationList.tsx b/webui/src/components/OperationList.tsx index 20d05b44..078cd2fd 100644 --- a/webui/src/components/OperationList.tsx +++ b/webui/src/components/OperationList.tsx @@ -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.sort((a, b) => b.parsedTime - a.parsedTime); + req, +}: React.PropsWithoutRef<{ req: GetOperationsRequest }>) => { + const [operations, setOperations] = useState([]); + + 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 ( { if (group.length === 1) { - return ; + return ; } return ( {group.map((op) => ( - + ))} ); @@ -82,7 +94,7 @@ export const OperationList = ({ pagination={ operations.length > 50 ? { position: "both", align: "center", defaultPageSize: 50 } - : {} + : undefined } /> ); diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx new file mode 100644 index 00000000..40ef3e39 --- /dev/null +++ b/webui/src/components/OperationTree.tsx @@ -0,0 +1,274 @@ +import React, { useEffect, useState } from "react"; +import { + BackupInfo, + BackupInfoCollector, + EOperation, + getOperations, + subscribeToOperations, + toEop, + unsubscribeFromOperations, +} from "../state/oplog"; +import { Col, Empty, Row, Tree } from "antd"; +import _ from "lodash"; +import { DataNode } from "antd/es/tree"; +import { + formatBytes, + formatDate, + formatDuration, + formatTime, + normalizeSnapshotId, +} from "../lib/formatting"; +import { + ExclamationOutlined, + QuestionOutlined, + SaveOutlined, +} from "@ant-design/icons"; +import { OperationEvent, OperationStatus } from "../../gen/ts/v1/operations.pb"; +import { useAlertApi } from "./Alerts"; +import { OperationList } from "./OperationList"; +import { GetOperationsRequest } from "../../gen/ts/v1/service.pb"; + +type OpTreeNode = DataNode & { + backup?: BackupInfo; +}; + +export const OperationTree = ({ + req, +}: React.PropsWithoutRef<{ req: GetOperationsRequest }>) => { + console.log("Loading operation tree with req: ", req); + const alertApi = useAlertApi(); + const [backups, setBackups] = useState([]); + const [selectedBackupId, setSelectedBackupId] = useState(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); + + backupCollector.subscribe(() => { + const backups = backupCollector.getAll(); + backups.sort((a, b) => { + return b.startTimeMs - a.startTimeMs; + }); + setBackups(backups); + }); + + getOperations(req) + .then((ops) => { + backupCollector.bulkAddOperations(ops); + }) + .catch((e) => { + alertApi!.error("Failed to fetch operations: " + e.message); + }); + return () => { + unsubscribeFromOperations(lis); + }; + }, [req]); + + if (backups.length === 0) { + return ( +
+ No operations yet. +
+ ); + } + + const treeData = buildTreeYear(backups); + + let oplist: React.ReactNode | null = null; + if (selectedBackupId) { + const backup = backups.find((b) => b.id === selectedBackupId); + if (!backup) { + oplist = ; + } else { + const req: GetOperationsRequest = { + ids: backup.operations.map((op) => op.id!), + }; + oplist = ( + <> +

Backup at {formatTime(backup.displayTime)}

+ + + ); + } + } + + return ( + + +

Browse Backups

+ + 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 = ( + + [{details.join(", ")}] + + ); + } + + return ( + <> + Backup {formatTime(b.displayTime)} {detailsElem} + + ); + } + return ( + ERROR: this element should not appear, this is a bug. + ); + }} + > + + {oplist} +
+ ); +}; + +const buildTreeYear = (operations: BackupInfo[]): OpTreeNode[] => { + const grouped = _.groupBy(operations, (op) => { + return op.displayTime.getFullYear(); + }); + + const entries: OpTreeNode[] = _.map(grouped, (value, key) => { + return { + key: "y" + key, + title: "" + key, + children: buildTreeMonth(value), + }; + }); + entries.sort(sortByKey); + return entries; +}; + +const buildTreeMonth = (operations: BackupInfo[]): OpTreeNode[] => { + const grouped = _.groupBy(operations, (op) => { + return `y${op.displayTime.getFullYear()}m${op.displayTime.getMonth()}`; + }); + const entries: OpTreeNode[] = _.map(grouped, (value, key) => { + return { + key: key, + title: value[0].displayTime.toLocaleString("default", { + month: "long", + }), + children: buildTreeDay(value), + }; + }); + entries.sort(sortByKey); + return entries; +}; + +const buildTreeDay = (operations: BackupInfo[]): OpTreeNode[] => { + const grouped = _.groupBy(operations, (op) => { + return `y${op.displayTime.getFullYear()}m${op.displayTime.getMonth()}d${op.displayTime.getDate()}`; + }); + + const entries = _.map(grouped, (value, key) => { + return { + key: "d" + key, + title: formatDate(value[0].displayTime), + children: buildTreeLeaf(value), + }; + }); + entries.sort(sortByKey); + return entries; +}; + +const buildTreeLeaf = (operations: BackupInfo[]): OpTreeNode[] => { + const entries = _.map(operations, (b): OpTreeNode => { + let iconColor = "grey"; + let icon: React.ReactNode | null = ; + + switch (b.status) { + case OperationStatus.STATUS_PENDING: + iconColor = "grey"; + break; + case OperationStatus.STATUS_SUCCESS: + iconColor = "green"; + break; + case OperationStatus.STATUS_ERROR: + iconColor = "red"; + break; + case OperationStatus.STATUS_INPROGRESS: + iconColor = "blue"; + break; + case OperationStatus.STATUS_CANCELLED: + iconColor = "orange"; + break; + } + + if (b.status === OperationStatus.STATUS_ERROR) { + icon = ; + } else { + icon = ; + } + + return { + key: b.id!, + backup: b, + icon: icon, + }; + }); + entries.sort(sortByKey); + return entries; +}; + +const sortByKey = (a: OpTreeNode, b: OpTreeNode) => { + if (a.key < b.key) { + return 1; + } else if (a.key > b.key) { + return -1; + } + return 0; +}; diff --git a/webui/src/components/SnapshotBrowser.tsx b/webui/src/components/SnapshotBrowser.tsx index 9be49c98..6ebf8197 100644 --- a/webui/src/components/SnapshotBrowser.tsx +++ b/webui/src/components/SnapshotBrowser.tsx @@ -20,6 +20,9 @@ const replaceKeyInTree = ( if (curNode.key === setKey) { return setValue; } + if (setKey.indexOf(curNode.key as string) === -1) { + return null; + } if (!curNode.children) { return null; } @@ -83,8 +86,6 @@ export const SnapshotBrowser = ({ return; } - console.log("Loading data for key: " + key); - const resp = await ResticUI.ListSnapshotFiles( { path: (key + "/") as string, @@ -103,33 +104,20 @@ export const SnapshotBrowser = ({ } if (!toUpdate) { - console.log("No node to update found!"); return; } const toUpdateCopy = { ...toUpdate }; toUpdateCopy.children = respToNodes(resp); - console.log( - "Replacing key: " + - key + - " with: " + - JSON.stringify(toUpdateCopy, null, 2) - ); - console.log("In tree: " + JSON.stringify(treeData, null, 2)); - const newTree = treeData.map((node) => { - console.log("trying replace in tree..."); const didUpdate = replaceKeyInTree(node, key as string, toUpdateCopy); if (didUpdate) { - console.log("Replaced in tree successfully!"); return didUpdate; } return node; }); - console.log("New tree: ", JSON.stringify(newTree, null, 2)); - setTreeData(newTree); }; diff --git a/webui/src/constants.ts b/webui/src/constants.ts new file mode 100644 index 00000000..c11d281d --- /dev/null +++ b/webui/src/constants.ts @@ -0,0 +1,2 @@ +export const API_PREFIX = "/api"; +export const MAX_OPERATION_HISTORY = 10000; diff --git a/webui/src/index.css b/webui/src/index.css deleted file mode 100644 index 0e956968..00000000 --- a/webui/src/index.css +++ /dev/null @@ -1,3 +0,0 @@ -body { - margin: 0px; -} \ No newline at end of file diff --git a/webui/src/index.html b/webui/src/index.html index 59067bbe..dc64a937 100644 --- a/webui/src/index.html +++ b/webui/src/index.html @@ -2,7 +2,7 @@ ResticUI - +
diff --git a/webui/src/index.sass b/webui/src/index.sass new file mode 100644 index 00000000..9ad2e53a --- /dev/null +++ b/webui/src/index.sass @@ -0,0 +1,8 @@ +body + margin: 0px + +.resticui + &.backup-details + color: grey + font-family: monospace + font-size: 0.7em diff --git a/webui/src/lib/formatting.ts b/webui/src/lib/formatting.ts index ca55e62a..7510b505 100644 --- a/webui/src/lib/formatting.ts +++ b/webui/src/lib/formatting.ts @@ -16,14 +16,35 @@ export const formatBytes = (bytes?: number | string) => { }; const timezoneOffsetMs = new Date().getTimezoneOffset() * 60 * 1000; -export const formatTime = (time: number | string) => { +// formatTime formats a time as YYYY-MM-DD at HH:MM AM/PM +export const formatTime = (time: number | string | Date) => { if (typeof time === "string") { time = parseInt(time); + } else if (time instanceof Date) { + time = time.getTime(); } const d = new Date(); d.setTime(time - timezoneOffsetMs); const isoStr = d.toISOString(); - return `${isoStr.substring(0, 10)} ${d.getUTCHours()}h${d.getUTCMinutes()}m`; + const hours = d.getUTCHours() % 12 == 0 ? 12 : d.getUTCHours() % 12; + const minutes = + d.getUTCMinutes() < 10 ? "0" + d.getUTCMinutes() : d.getUTCMinutes(); + return `${isoStr.substring(0, 10)} at ${hours}:${minutes} ${ + d.getUTCHours() > 12 ? "PM" : "AM" + }`; +}; + +// formatDate formats a time as YYYY-MM-DD +export const formatDate = (time: number | string | Date) => { + if (typeof time === "string") { + time = parseInt(time); + } else if (time instanceof Date) { + time = time.getTime(); + } + let d = new Date(); + d.setTime(time - timezoneOffsetMs); + const isoStr = d.toISOString(); + return isoStr.substring(0, 10); }; export const formatDuration = (ms: number) => { diff --git a/webui/src/state/config.ts b/webui/src/state/config.ts index 9b8f0d21..8ff09728 100644 --- a/webui/src/state/config.ts +++ b/webui/src/state/config.ts @@ -1,6 +1,7 @@ import { atom, useSetRecoilState } from "recoil"; import { Config, Repo } from "../../gen/ts/v1/config.pb"; import { ResticUI } from "../../gen/ts/v1/service.pb"; +import { API_PREFIX } from "../constants"; export const configState = atom({ key: "config", @@ -8,17 +9,17 @@ export const configState = atom({ }); export const fetchConfig = async (): Promise => { - return await ResticUI.GetConfig({}, { pathPrefix: "/api" }); + return await ResticUI.GetConfig({}, { pathPrefix: API_PREFIX }); }; export const addRepo = async (repo: Repo): Promise => { return await ResticUI.AddRepo(repo, { - pathPrefix: "/api", + pathPrefix: API_PREFIX, }); }; export const updateConfig = async (config: Config): Promise => { return await ResticUI.SetConfig(config, { - pathPrefix: "/api", + pathPrefix: API_PREFIX, }); -}; \ No newline at end of file +}; diff --git a/webui/src/state/oplog.ts b/webui/src/state/oplog.ts index d3a75b0a..f46afe2b 100644 --- a/webui/src/state/oplog.ts +++ b/webui/src/state/oplog.ts @@ -1,4 +1,3 @@ -import { atom } from "recoil"; import { Operation, OperationEvent, @@ -6,12 +5,13 @@ import { OperationStatus, } from "../../gen/ts/v1/operations.pb"; import { GetOperationsRequest, ResticUI } from "../../gen/ts/v1/service.pb"; -import { EventEmitter } from "events"; -import { useAlertApi } from "../components/Alerts"; +import { API_PREFIX } from "../constants"; +import { BackupProgressEntry, ResticSnapshot } from "../../gen/ts/v1/restic.pb"; +import _ from "lodash"; export type EOperation = Operation & { - parsedId: number; parsedTime: number; + parsedDate: Date; }; const subscribers: ((event: OperationEvent) => void)[] = []; @@ -28,7 +28,7 @@ const subscribers: ((event: OperationEvent) => void)[] = []; subscribers.forEach((subscriber) => subscriber(event)); }, { - pathPrefix: "/api", + pathPrefix: API_PREFIX, } ); } catch (e: any) { @@ -40,21 +40,12 @@ const subscribers: ((event: OperationEvent) => void)[] = []; } })(); -export const getOperations = async ({ - planId, - repoId, - lastN, -}: GetOperationsRequest): Promise => { - const opList = await ResticUI.GetOperations( - { - planId, - repoId, - lastN, - }, - { - pathPrefix: "/api", - } - ); +export const getOperations = async ( + req: GetOperationsRequest +): Promise => { + const opList = await ResticUI.GetOperations(req, { + pathPrefix: API_PREFIX, + }); return (opList.operations || []).map(toEop); }; @@ -87,10 +78,10 @@ export const buildOperationListListener = ( (async () => { let opsFromServer = await getOperations(req); operations = opsFromServer.filter( - (o) => !operations.find((op) => op.parsedId === o.parsedId) + (o) => !operations.find((op) => op.id === o.id) ); operations.sort((a, b) => { - return a.parsedId - b.parsedId; + return a.parsedTime! - b.parsedTime!; }); callback(null, null, operations); @@ -105,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) { @@ -112,7 +109,7 @@ export const buildOperationListListener = ( } else { operations.push(op); operations.sort((a, b) => { - return parseInt(a.id!) - parseInt(b.id!); + return a.parsedTime! - b.parsedTime!; }); } } else if (type === OperationEventType.EVENT_CREATED) { @@ -124,12 +121,146 @@ export const buildOperationListListener = ( }; export const toEop = (op: Operation): EOperation => { - const time = - op.operationIndexSnapshot?.snapshot?.unixTimeMs || op.unixTimeStartMs; + const time = parseInt(op.unixTimeStartMs!); + const date = new Date(); + date.setTime(time); return { ...op, - parsedId: parseInt(op.id!), - parsedTime: parseInt(time!), + parsedTime: time, + parsedDate: date, }; }; + +export interface BackupInfo { + id: string; // id of the first operation that generated this backup. + displayTime: Date; + startTimeMs: number; + endTimeMs: number; + status: OperationStatus; + operations: Operation[]; + repoId?: string; + planId?: string; + snapshotId?: string; + backupLastStatus?: BackupProgressEntry; + snapshotInfo?: ResticSnapshot; +} + +// BackupInfoCollector maps multiple operations to single aggregate 'BackupInfo' objects. +// A backup info object aggregates the backup status (if available), snapshot info (if available), and possibly the forget status (if available). +export class BackupInfoCollector { + private listeners: (( + event: OperationEventType, + info: BackupInfo[] + ) => void)[] = []; + private backupByOpId: { [key: string]: BackupInfo } = {}; + private backupBySnapshotId: { [key: string]: BackupInfo } = {}; + + private mergeBackups(existing: BackupInfo, newInfo: BackupInfo) { + existing.startTimeMs = Math.min(existing.startTimeMs, newInfo.startTimeMs); + existing.endTimeMs = Math.max(existing.endTimeMs, newInfo.endTimeMs); + existing.displayTime = new Date(existing.startTimeMs); + if (newInfo.startTimeMs >= existing.startTimeMs) { + existing.status = newInfo.status; // use the latest status + } + existing.operations = _.uniqBy( + [...existing.operations, ...newInfo.operations], + (o) => o.id! + ); + if (newInfo.backupLastStatus) { + existing.backupLastStatus = newInfo.backupLastStatus; + } + if (newInfo.snapshotInfo) { + existing.snapshotInfo = newInfo.snapshotInfo; + } + return existing; + } + + private operationToBackup(op: Operation): BackupInfo { + const startTimeMs = parseInt(op.unixTimeStartMs!); + const endTimeMs = parseInt(op.unixTimeEndMs!); + + const b: BackupInfo = { + id: op.id!, + startTimeMs, + endTimeMs, + status: op.status!, + displayTime: new Date(startTimeMs), + repoId: op.repoId, + planId: op.planId, + snapshotId: op.snapshotId, + operations: [op], + }; + + if (op.operationBackup) { + const ob = op.operationBackup; + b.backupLastStatus = ob.lastStatus; + } + if (op.operationIndexSnapshot) { + const oi = op.operationIndexSnapshot; + b.snapshotInfo = oi.snapshot; + } + return b; + } + + private addHelper(op: Operation) { + if (op.snapshotId) { + delete this.backupByOpId[op.id!]; + + let newInfo = this.operationToBackup(op); + const existing = this.backupBySnapshotId[op.snapshotId!]; + if (existing) { + this.mergeBackups(existing, newInfo); + return existing; + } else { + this.backupBySnapshotId[op.snapshotId] = newInfo; + return newInfo; + } + } else { + const newInfo = this.operationToBackup(op); + this.backupByOpId[op.id!] = newInfo; + return newInfo; + } + } + + public addOperation(event: OperationEventType, op: Operation): BackupInfo { + const backupInfo = this.addHelper(op); + this.listeners.forEach((l) => l(event, [backupInfo])); + return backupInfo; + } + + public bulkAddOperations(ops: Operation[]): BackupInfo[] { + const backupInfos = ops.map((op) => this.addHelper(op)); + this.listeners.forEach((l) => + l(OperationEventType.EVENT_UNKNOWN, backupInfos) + ); + return backupInfos; + } + + public addOperationNoNotify(op: Operation) { + this.addHelper(op); + } + + public getAll(): BackupInfo[] { + const arr = []; + arr.push(...Object.values(this.backupByOpId)); + arr.push(...Object.values(this.backupBySnapshotId)); + return arr; + } + + public subscribe( + listener: (event: OperationEventType, info: BackupInfo[]) => void + ) { + this.listeners.push(listener); + } + + public unsubscribe( + listener: (event: OperationEventType, info: BackupInfo[]) => void + ) { + const index = this.listeners.indexOf(listener); + if (index > -1) { + this.listeners[index] = this.listeners[this.listeners.length - 1]; + this.listeners.pop(); + } + } +} diff --git a/webui/src/views/AddRepoModel.tsx b/webui/src/views/AddRepoModel.tsx index 25e7e955..e62d7b83 100644 --- a/webui/src/views/AddRepoModel.tsx +++ b/webui/src/views/AddRepoModel.tsx @@ -106,7 +106,8 @@ export const AddRepoModel = ({ showModal(null); alertsApi.success("Updated repo " + repo.uri); - // Update the snapshots for the repo + // Update the snapshots for the repo to confirm the config works. + // TODO: this operation is only used here, find a different RPC for this purpose. await ResticUI.ListSnapshots( { repoId: repo.id, diff --git a/webui/src/views/App.tsx b/webui/src/views/App.tsx index 500fc23b..d7e2bb2e 100644 --- a/webui/src/views/App.tsx +++ b/webui/src/views/App.tsx @@ -9,7 +9,7 @@ import { import type { MenuProps } from "antd"; import { Button, Layout, List, Menu, Modal, Spin, theme } from "antd"; import { configState, fetchConfig } from "../state/config"; -import { useRecoilState } from "recoil"; +import { useRecoilState, useRecoilValue } from "recoil"; import { Config, Plan } from "../../gen/ts/v1/config.pb"; import { useAlertApi } from "../components/Alerts"; import { useShowModal } from "../components/ModalManager"; @@ -25,7 +25,7 @@ import { toEop, unsubscribeFromOperations, } from "../state/oplog"; -import { formatTime } from "../lib/formatting"; +import { formatTime, normalizeSnapshotId } from "../lib/formatting"; import { SnapshotBrowser } from "../components/SnapshotBrowser"; import { OperationRow } from "../components/OperationList"; import { @@ -33,6 +33,7 @@ import { OperationEvent, OperationEventType, } from "../../gen/ts/v1/operations.pb"; +import { MessageInstance } from "antd/es/message/interface"; const { Header, Content, Sider } = Layout; @@ -57,7 +58,8 @@ export const App: React.FC = () => { .catch((err) => { alertApi.error(err.message, 0); alertApi.error( - "Failed to fetch initial config, typically this means the UI could not connect to the backend" + "Failed to fetch initial config, typically this means the UI could not connect to the backend", + 0 ); }); }, []); @@ -66,6 +68,7 @@ export const App: React.FC = () => { return ( +

{ const getSidenavItems = (config: Config | null): MenuProps["items"] => { const showModal = useShowModal(); const setContent = useSetContent(); - const [snapshotsByPlan, setSnapshotsByPlan] = useState<{ - [planId: string]: EOperation[]; - }>({}); - - const addSnapshots = (planId: string, ops: EOperation[]) => { - const snapsByPlanCpy = { ...snapshotsByPlan }; - let snapsForPlanCpy = [...(snapsByPlanCpy[planId] || [])]; - for (const op of ops) { - snapsForPlanCpy.push(toEop(op)); - } - snapsForPlanCpy.sort((a, b) => { - return a.parsedTime > b.parsedTime ? -1 : 1; - }); - if (snapsForPlanCpy.length > 5) { - snapsForPlanCpy = snapsForPlanCpy.slice(0, 5); - } - snapsByPlanCpy[planId] = snapsForPlanCpy; - setSnapshotsByPlan(snapsByPlanCpy); - }; - - // Track newly created snapshots in the set. - useEffect(() => { - const listener = (event: OperationEvent) => { - if (event.type !== OperationEventType.EVENT_CREATED) return; - const op = event.operation!; - if (!op.planId) return; - if (!op.operationIndexSnapshot) return; - addSnapshots(op.planId!, [toEop(op)]); - }; - - subscribeToOperations(listener); - - return () => { - unsubscribeFromOperations(listener); - }; - }, [snapshotsByPlan]); if (!config) return []; const configPlans = config.plans || []; const configRepos = config.repos || []; - const onSelectPlan = (plan: Plan) => { - setContent(, [ - { title: "Plans" }, - { title: plan.id || "" }, - ]); - - if (!snapshotsByPlan[plan.id!]) { - (async () => { - const ops = await getOperations({ planId: plan.id!, lastN: "20" }); - // avoid races by checking again after the request - if (!snapshotsByPlan[plan.id!]) { - const snapshots = ops.filter((op) => !!op.operationIndexSnapshot); - addSnapshots(plan.id!, snapshots); - } - })(); - } - }; - const plans: MenuProps["items"] = [ { key: "add-plan", @@ -168,47 +117,16 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => { }, }, ...configPlans.map((plan) => { - const children: MenuProps["items"] = ( - snapshotsByPlan[plan.id!] || [] - ).map((snapshot) => { - return { - key: "s-" + snapshot.id, - icon: , - label: ( - {"Operation " + formatTime(snapshot.parsedTime)} - ), - onClick: () => { - showModal( - showModal(null)} - footer={[ - , - ]} - > - - - - - ); - }, - }; - }); - return { key: "p-" + plan.id, icon: , label: plan.id, - children: children, - onTitleClick: onSelectPlan.bind(null, plan), // if children - onClick: onSelectPlan.bind(null, plan), // if no children + onClick: () => { + setContent(, [ + { title: "Plans" }, + { title: plan.id || "" }, + ]); + }, }; }), ]; @@ -249,3 +167,49 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => { }, ]; }; + +const OperationNotificationGenerator = () => { + const alertApi = useAlertApi()!; + const setContent = useSetContent(); + const config = useRecoilValue(configState); + + useEffect(() => { + // TODO: factor notification generator into a separate file. + const listener = (event: OperationEvent) => { + if (event.type != OperationEventType.EVENT_CREATED) return; + const planId = event.operation!.planId!; + const repoId = event.operation!.repoId!; + + const onClick = () => { + const plan = config.plans!.find((p) => p.id == planId); + if (!plan) return; + setContent(, [ + { title: "Plans" }, + { title: planId || "" }, + ]); + }; + + if (event.operation?.operationBackup) { + alertApi.info({ + content: `Backup started for plan ${planId}.`, + onClick: onClick, + }); + } else if (event.operation?.operationIndexSnapshot) { + const indexOp = event.operation.operationIndexSnapshot; + alertApi.info({ + content: `Indexed snapshot ${normalizeSnapshotId( + indexOp.snapshot!.id! + )} for plan ${planId}.`, + onClick: onClick, + }); + } + }; + subscribeToOperations(listener); + + return () => { + unsubscribeFromOperations(listener); + }; + }, [config]); + + return <>; +}; diff --git a/webui/src/views/PlanView.tsx b/webui/src/views/PlanView.tsx index bbf82f51..899f84e7 100644 --- a/webui/src/views/PlanView.tsx +++ b/webui/src/views/PlanView.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; import { Plan } from "../../gen/ts/v1/config.pb"; -import { Button, Flex } from "antd"; +import { Button, Flex, Tabs } from "antd"; import { SettingOutlined } from "@ant-design/icons"; import { AddPlanModal } from "./AddPlanModel"; import { useShowModal } from "../components/ModalManager"; @@ -15,25 +15,12 @@ import { unsubscribeFromOperations, } from "../state/oplog"; import { OperationList } from "../components/OperationList"; +import { OperationTree } from "../components/OperationTree"; +import { MAX_OPERATION_HISTORY } from "../constants"; export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { const showModal = useShowModal(); const alertsApi = useAlertApi()!; - const [operations, setOperations] = useState([]); - - useEffect(() => { - const listener = buildOperationListListener( - { planId: plan.id, lastN: "1000" }, - (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); @@ -78,8 +65,34 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { Prune Now -

Backup Action History ({operations.length} loaded)

- + + + + ), + }, + { + key: "2", + label: "Operation List", + children: ( + <> +

Backup Action History

+ + + ), + }, + ]} + /> ); };