Merge pull request #9 from garethgeorge/optree

feat: implement tree view for backup operations
This commit is contained in:
Gareth
2023-11-25 17:34:19 -08:00
committed by GitHub
27 changed files with 923 additions and 345 deletions
+31 -20
View File
@@ -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 (
+106 -87
View File
@@ -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 (
+12 -1
View File
@@ -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()
}
+13 -5
View File
@@ -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)
+6
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,
@@ -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)
+18 -18
View File
@@ -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))
}
}
}
+3 -18
View File
@@ -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
-1
View File
@@ -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)
}
+8 -5
View File
@@ -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 {
+2
View File
@@ -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
}
+1
View File
@@ -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 = {
+2
View File
@@ -18,6 +18,8 @@ export type ListSnapshotsRequest = {
export type GetOperationsRequest = {
repoId?: string
planId?: string
snapshotId?: string
ids?: string[]
lastN?: string
}
+116
View File
@@ -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",
+3
View File
@@ -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"
}
+37 -25
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.sort((a, b) => b.parsedTime - a.parsedTime);
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
@@ -68,13 +80,13 @@ export const OperationList = ({
dataSource={groupedItems}
renderItem={(group, index) => {
if (group.length === 1) {
return <OperationRow key={group[0].parsedId} operation={group[0]} />;
return <OperationRow key={group[0].id!} operation={group[0]} />;
}
return (
<Card size="small" style={{ margin: "5px" }}>
{group.map((op) => (
<OperationRow key={op.parsedId} operation={op} />
<OperationRow key={op.id!} operation={op} />
))}
</Card>
);
@@ -82,7 +94,7 @@ export const OperationList = ({
pagination={
operations.length > 50
? { position: "both", align: "center", defaultPageSize: 50 }
: {}
: undefined
}
/>
);
+274
View File
@@ -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<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);
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 (
<div>
<QuestionOutlined /> No operations yet.
</div>
);
}
const treeData = buildTreeYear(backups);
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} />
</>
);
}
}
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}
</>
);
}
return (
<span>ERROR: this element should not appear, this is a bug.</span>
);
}}
></Tree>
</Col>
<Col span={12}>{oplist}</Col>
</Row>
);
};
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 = <QuestionOutlined />;
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 = <ExclamationOutlined style={{ color: iconColor }} />;
} else {
icon = <SaveOutlined style={{ color: iconColor }} />;
}
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;
};
+3 -15
View File
@@ -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);
};
+2
View File
@@ -0,0 +1,2 @@
export const API_PREFIX = "/api";
export const MAX_OPERATION_HISTORY = 10000;
-3
View File
@@ -1,3 +0,0 @@
body {
margin: 0px;
}
+1 -1
View File
@@ -2,7 +2,7 @@
<html>
<head>
<title>ResticUI</title>
<link rel="stylesheet" href="index.css">
<link rel="stylesheet" href="index.sass" />
</head>
<body>
<div id="app"></div>
+8
View File
@@ -0,0 +1,8 @@
body
margin: 0px
.resticui
&.backup-details
color: grey
font-family: monospace
font-size: 0.7em
+23 -2
View File
@@ -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) => {
+5 -4
View File
@@ -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<Config>({
key: "config",
@@ -8,17 +9,17 @@ export const configState = atom<Config>({
});
export const fetchConfig = async (): Promise<Config> => {
return await ResticUI.GetConfig({}, { pathPrefix: "/api" });
return await ResticUI.GetConfig({}, { pathPrefix: API_PREFIX });
};
export const addRepo = async (repo: Repo): Promise<Config> => {
return await ResticUI.AddRepo(repo, {
pathPrefix: "/api",
pathPrefix: API_PREFIX,
});
};
export const updateConfig = async (config: Config): Promise<Config> => {
return await ResticUI.SetConfig(config, {
pathPrefix: "/api",
pathPrefix: API_PREFIX,
});
};
};
+158 -27
View File
@@ -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<EOperation[]> => {
const opList = await ResticUI.GetOperations(
{
planId,
repoId,
lastN,
},
{
pathPrefix: "/api",
}
);
export const getOperations = async (
req: GetOperationsRequest
): Promise<EOperation[]> => {
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();
}
}
}
+2 -1
View File
@@ -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,
+58 -94
View File
@@ -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 (
<Layout style={{ height: "auto" }}>
<OperationNotificationGenerator />
<Header style={{ display: "flex", alignItems: "center" }}>
<h1>
<a
@@ -98,66 +101,12 @@ export const App: React.FC = () => {
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(<PlanView plan={plan} />, [
{ 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: <PaperClipOutlined />,
label: (
<small>{"Operation " + formatTime(snapshot.parsedTime)}</small>
),
onClick: () => {
showModal(
<Modal
title="View Snapshot"
open={true}
onCancel={() => showModal(null)}
footer={[
<Button
key="done"
onClick={() => showModal(null)}
type="primary"
>
Done
</Button>,
]}
>
<List>
<OperationRow operation={snapshot} />
</List>
</Modal>
);
},
};
});
return {
key: "p-" + plan.id,
icon: <CheckCircleOutlined style={{ color: "green" }} />,
label: plan.id,
children: children,
onTitleClick: onSelectPlan.bind(null, plan), // if children
onClick: onSelectPlan.bind(null, plan), // if no children
onClick: () => {
setContent(<PlanView plan={plan} />, [
{ 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(<PlanView plan={plan} />, [
{ 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 <></>;
};
+31 -18
View File
@@ -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<EOperation[]>([]);
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
</Button>
</Flex>
<h2>Backup Action History ({operations.length} loaded)</h2>
<OperationList operations={operations} />
<Tabs
defaultActiveKey="1"
items={[
{
key: "1",
label: "Tree View",
children: (
<>
<OperationTree
req={{ planId: plan.id!, lastN: "" + MAX_OPERATION_HISTORY }}
/>
</>
),
},
{
key: "2",
label: "Operation List",
children: (
<>
<h2>Backup Action History</h2>
<OperationList
req={{ planId: plan.id!, lastN: "" + MAX_OPERATION_HISTORY }}
/>
</>
),
},
]}
/>
</>
);
};