From 03e69c98dddb1db49673917289f1f5b192d73117 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Wed, 8 Jul 2026 01:04:30 -0700 Subject: [PATCH] fix: batch of webui fixes, update webui deps, apply formatter, and regen protos --- flake.nix | 3 +- gen/go/v1/authentication_grpc.pb.go | 8 +- gen/go/v1/service.pb.go | 12 +-- gen/go/v1/service_grpc.pb.go | 48 +++++------ gen/go/v1sync/syncservice_grpc.pb.go | 12 +-- internal/api/backresthandler.go | 13 +-- proto/v1/service.proto | 2 +- webui/gen/ts/google/api/annotations_pb.ts | 2 +- webui/gen/ts/google/api/http_pb.ts | 2 +- webui/gen/ts/types/value_pb.ts | 2 +- webui/gen/ts/v1/authentication_pb.ts | 2 +- webui/gen/ts/v1/config_pb.ts | 2 +- webui/gen/ts/v1/crypto_pb.ts | 2 +- webui/gen/ts/v1/operations_pb.ts | 2 +- webui/gen/ts/v1/restic_pb.ts | 2 +- webui/gen/ts/v1/service_pb.ts | 8 +- webui/gen/ts/v1sync/syncservice_pb.ts | 2 +- webui/package.json | 1 - webui/pnpm-lock.yaml | 74 ++++++---------- webui/src/app/App.tsx | 86 +++++++++---------- webui/src/app/main.tsx | 2 +- webui/src/app/provider.tsx | 20 ++++- webui/src/components/ui/input-group.tsx | 2 +- webui/src/custom.d.ts | 2 + .../features/dashboard/SummaryDashboard.tsx | 19 ++-- .../src/features/operations/OperationRow.tsx | 12 ++- .../features/repositories/SnapshotBrowser.tsx | 6 +- webui/tsconfig.json | 2 + 28 files changed, 173 insertions(+), 177 deletions(-) diff --git a/flake.nix b/flake.nix index 46f7ef5a..7a1f0792 100644 --- a/flake.nix +++ b/flake.nix @@ -16,7 +16,7 @@ pkgs = nixpkgs.legacyPackages.${system}; devShellPkgs = with pkgs; [ go goreleaser nodejs_20 pnpm_9 - protobuf buf protoc-gen-go protoc-gen-go-grpc + protobuf buf protoc-gen-go protoc-gen-go-grpc protoc-gen-connect-go gnumake git restic rclone zsh oh-my-posh ]; in @@ -47,6 +47,7 @@ buf protoc-gen-go protoc-gen-go-grpc + protoc-gen-connect-go # General build tools gnumake diff --git a/gen/go/v1/authentication_grpc.pb.go b/gen/go/v1/authentication_grpc.pb.go index 9be66eda..77f3efb9 100644 --- a/gen/go/v1/authentication_grpc.pb.go +++ b/gen/go/v1/authentication_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.1 // - protoc (unknown) // source: v1/authentication.proto @@ -77,10 +77,10 @@ type AuthenticationServer interface { type UnimplementedAuthenticationServer struct{} func (UnimplementedAuthenticationServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Login not implemented") + return nil, status.Error(codes.Unimplemented, "method Login not implemented") } func (UnimplementedAuthenticationServer) HashPassword(context.Context, *types.StringValue) (*types.StringValue, error) { - return nil, status.Errorf(codes.Unimplemented, "method HashPassword not implemented") + return nil, status.Error(codes.Unimplemented, "method HashPassword not implemented") } func (UnimplementedAuthenticationServer) mustEmbedUnimplementedAuthenticationServer() {} func (UnimplementedAuthenticationServer) testEmbeddedByValue() {} @@ -93,7 +93,7 @@ type UnsafeAuthenticationServer interface { } func RegisterAuthenticationServer(s grpc.ServiceRegistrar, srv AuthenticationServer) { - // If the following call pancis, it indicates UnimplementedAuthenticationServer was + // If the following call panics, it indicates UnimplementedAuthenticationServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/gen/go/v1/service.pb.go b/gen/go/v1/service.pb.go index c4057402..395a8b6a 100644 --- a/gen/go/v1/service.pb.go +++ b/gen/go/v1/service.pb.go @@ -913,7 +913,7 @@ func (x *RestoreSnapshotRequest) GetTarget() string { type ListSnapshotFilesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - RepoGuid string `protobuf:"bytes,1,opt,name=repo_guid,json=repoGuid,proto3" json:"repo_guid,omitempty"` + RepoId string `protobuf:"bytes,1,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` SnapshotId string `protobuf:"bytes,2,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` unknownFields protoimpl.UnknownFields @@ -950,9 +950,9 @@ func (*ListSnapshotFilesRequest) Descriptor() ([]byte, []int) { return file_v1_service_proto_rawDescGZIP(), []int{14} } -func (x *ListSnapshotFilesRequest) GetRepoGuid() string { +func (x *ListSnapshotFilesRequest) GetRepoId() string { if x != nil { - return x.RepoGuid + return x.RepoId } return "" } @@ -2032,9 +2032,9 @@ const file_v1_service_proto_rawDesc = "" + "\vsnapshot_id\x18\x02 \x01(\tR\n" + "snapshotId\x12\x12\n" + "\x04path\x18\x03 \x01(\tR\x04path\x12\x16\n" + - "\x06target\x18\x04 \x01(\tR\x06target\"l\n" + - "\x18ListSnapshotFilesRequest\x12\x1b\n" + - "\trepo_guid\x18\x01 \x01(\tR\brepoGuid\x12\x1f\n" + + "\x06target\x18\x04 \x01(\tR\x06target\"h\n" + + "\x18ListSnapshotFilesRequest\x12\x17\n" + + "\arepo_id\x18\x01 \x01(\tR\x06repoId\x12\x1f\n" + "\vsnapshot_id\x18\x02 \x01(\tR\n" + "snapshotId\x12\x12\n" + "\x04path\x18\x03 \x01(\tR\x04path\"V\n" + diff --git a/gen/go/v1/service_grpc.pb.go b/gen/go/v1/service_grpc.pb.go index 5328938a..726d244b 100644 --- a/gen/go/v1/service_grpc.pb.go +++ b/gen/go/v1/service_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.1 // - protoc (unknown) // source: v1/service.proto @@ -382,70 +382,70 @@ type BackrestServer interface { type UnimplementedBackrestServer struct{} func (UnimplementedBackrestServer) GetConfig(context.Context, *emptypb.Empty) (*Config, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetConfig not implemented") + return nil, status.Error(codes.Unimplemented, "method GetConfig not implemented") } func (UnimplementedBackrestServer) SetConfig(context.Context, *Config) (*Config, error) { - return nil, status.Errorf(codes.Unimplemented, "method SetConfig not implemented") + return nil, status.Error(codes.Unimplemented, "method SetConfig not implemented") } func (UnimplementedBackrestServer) SetupSftp(context.Context, *SetupSftpRequest) (*SetupSftpResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method SetupSftp not implemented") + return nil, status.Error(codes.Unimplemented, "method SetupSftp not implemented") } func (UnimplementedBackrestServer) CheckRepoExists(context.Context, *CheckRepoExistsRequest) (*CheckRepoExistsResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method CheckRepoExists not implemented") + return nil, status.Error(codes.Unimplemented, "method CheckRepoExists not implemented") } func (UnimplementedBackrestServer) AddRepo(context.Context, *AddRepoRequest) (*Config, error) { - return nil, status.Errorf(codes.Unimplemented, "method AddRepo not implemented") + return nil, status.Error(codes.Unimplemented, "method AddRepo not implemented") } func (UnimplementedBackrestServer) RemoveRepo(context.Context, *RemoveRepoRequest) (*Config, error) { - return nil, status.Errorf(codes.Unimplemented, "method RemoveRepo not implemented") + return nil, status.Error(codes.Unimplemented, "method RemoveRepo not implemented") } func (UnimplementedBackrestServer) GetOperationEvents(*emptypb.Empty, grpc.ServerStreamingServer[OperationEvent]) error { - return status.Errorf(codes.Unimplemented, "method GetOperationEvents not implemented") + return status.Error(codes.Unimplemented, "method GetOperationEvents not implemented") } func (UnimplementedBackrestServer) GetOperations(context.Context, *GetOperationsRequest) (*OperationList, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetOperations not implemented") + return nil, status.Error(codes.Unimplemented, "method GetOperations not implemented") } func (UnimplementedBackrestServer) ListSnapshots(context.Context, *ListSnapshotsRequest) (*ResticSnapshotList, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListSnapshots not implemented") + return nil, status.Error(codes.Unimplemented, "method ListSnapshots not implemented") } func (UnimplementedBackrestServer) ListSnapshotFiles(context.Context, *ListSnapshotFilesRequest) (*ListSnapshotFilesResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListSnapshotFiles not implemented") + return nil, status.Error(codes.Unimplemented, "method ListSnapshotFiles not implemented") } func (UnimplementedBackrestServer) Backup(context.Context, *BackupRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method Backup not implemented") + return nil, status.Error(codes.Unimplemented, "method Backup not implemented") } func (UnimplementedBackrestServer) DoRepoTask(context.Context, *DoRepoTaskRequest) (*ScheduleTaskResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method DoRepoTask not implemented") + return nil, status.Error(codes.Unimplemented, "method DoRepoTask not implemented") } func (UnimplementedBackrestServer) Forget(context.Context, *ForgetRequest) (*ScheduleTaskResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Forget not implemented") + return nil, status.Error(codes.Unimplemented, "method Forget not implemented") } func (UnimplementedBackrestServer) Restore(context.Context, *RestoreSnapshotRequest) (*ScheduleTaskResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Restore not implemented") + return nil, status.Error(codes.Unimplemented, "method Restore not implemented") } func (UnimplementedBackrestServer) Cancel(context.Context, *CancelOperationRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method Cancel not implemented") + return nil, status.Error(codes.Unimplemented, "method Cancel not implemented") } func (UnimplementedBackrestServer) GetLogs(*LogDataRequest, grpc.ServerStreamingServer[types.BytesValue]) error { - return status.Errorf(codes.Unimplemented, "method GetLogs not implemented") + return status.Error(codes.Unimplemented, "method GetLogs not implemented") } func (UnimplementedBackrestServer) RunCommand(context.Context, *RunCommandRequest) (*RunCommandResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method RunCommand not implemented") + return nil, status.Error(codes.Unimplemented, "method RunCommand not implemented") } func (UnimplementedBackrestServer) GetDownloadURL(context.Context, *GetDownloadURLRequest) (*types.StringValue, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetDownloadURL not implemented") + return nil, status.Error(codes.Unimplemented, "method GetDownloadURL not implemented") } func (UnimplementedBackrestServer) ClearHistory(context.Context, *ClearHistoryRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method ClearHistory not implemented") + return nil, status.Error(codes.Unimplemented, "method ClearHistory not implemented") } func (UnimplementedBackrestServer) PathAutocomplete(context.Context, *types.StringValue) (*types.StringList, error) { - return nil, status.Errorf(codes.Unimplemented, "method PathAutocomplete not implemented") + return nil, status.Error(codes.Unimplemented, "method PathAutocomplete not implemented") } func (UnimplementedBackrestServer) GetSummaryDashboard(context.Context, *emptypb.Empty) (*SummaryDashboardResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetSummaryDashboard not implemented") + return nil, status.Error(codes.Unimplemented, "method GetSummaryDashboard not implemented") } func (UnimplementedBackrestServer) GeneratePairingToken(context.Context, *GeneratePairingTokenRequest) (*GeneratePairingTokenResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GeneratePairingToken not implemented") + return nil, status.Error(codes.Unimplemented, "method GeneratePairingToken not implemented") } func (UnimplementedBackrestServer) mustEmbedUnimplementedBackrestServer() {} func (UnimplementedBackrestServer) testEmbeddedByValue() {} @@ -458,7 +458,7 @@ type UnsafeBackrestServer interface { } func RegisterBackrestServer(s grpc.ServiceRegistrar, srv BackrestServer) { - // If the following call pancis, it indicates UnimplementedBackrestServer was + // If the following call panics, it indicates UnimplementedBackrestServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/gen/go/v1sync/syncservice_grpc.pb.go b/gen/go/v1sync/syncservice_grpc.pb.go index 5499bd37..fa4bf153 100644 --- a/gen/go/v1sync/syncservice_grpc.pb.go +++ b/gen/go/v1sync/syncservice_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.1 // - protoc (unknown) // source: v1sync/syncservice.proto @@ -72,7 +72,7 @@ type BackrestSyncServiceServer interface { type UnimplementedBackrestSyncServiceServer struct{} func (UnimplementedBackrestSyncServiceServer) Sync(grpc.BidiStreamingServer[SyncStreamItem, SyncStreamItem]) error { - return status.Errorf(codes.Unimplemented, "method Sync not implemented") + return status.Error(codes.Unimplemented, "method Sync not implemented") } func (UnimplementedBackrestSyncServiceServer) mustEmbedUnimplementedBackrestSyncServiceServer() {} func (UnimplementedBackrestSyncServiceServer) testEmbeddedByValue() {} @@ -85,7 +85,7 @@ type UnsafeBackrestSyncServiceServer interface { } func RegisterBackrestSyncServiceServer(s grpc.ServiceRegistrar, srv BackrestSyncServiceServer) { - // If the following call pancis, it indicates UnimplementedBackrestSyncServiceServer was + // If the following call panics, it indicates UnimplementedBackrestSyncServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. @@ -195,10 +195,10 @@ type BackrestSyncStateServiceServer interface { type UnimplementedBackrestSyncStateServiceServer struct{} func (UnimplementedBackrestSyncStateServiceServer) GetPeerSyncStatesStream(*SyncStateStreamRequest, grpc.ServerStreamingServer[PeerState]) error { - return status.Errorf(codes.Unimplemented, "method GetPeerSyncStatesStream not implemented") + return status.Error(codes.Unimplemented, "method GetPeerSyncStatesStream not implemented") } func (UnimplementedBackrestSyncStateServiceServer) SetRemoteClientConfig(context.Context, *SetRemoteClientConfigRequest) (*SetRemoteClientConfigResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method SetRemoteClientConfig not implemented") + return nil, status.Error(codes.Unimplemented, "method SetRemoteClientConfig not implemented") } func (UnimplementedBackrestSyncStateServiceServer) mustEmbedUnimplementedBackrestSyncStateServiceServer() { } @@ -212,7 +212,7 @@ type UnsafeBackrestSyncStateServiceServer interface { } func RegisterBackrestSyncStateServiceServer(s grpc.ServiceRegistrar, srv BackrestSyncStateServiceServer) { - // If the following call pancis, it indicates UnimplementedBackrestSyncStateServiceServer was + // If the following call panics, it indicates UnimplementedBackrestSyncStateServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/internal/api/backresthandler.go b/internal/api/backresthandler.go index 10a7f1c1..fa4465da 100644 --- a/internal/api/backresthandler.go +++ b/internal/api/backresthandler.go @@ -347,18 +347,7 @@ func (s *BackrestHandler) ListSnapshots(ctx context.Context, req *connect.Reques func (s *BackrestHandler) ListSnapshotFiles(ctx context.Context, req *connect.Request[v1.ListSnapshotFilesRequest]) (*connect.Response[v1.ListSnapshotFilesResponse], error) { query := req.Msg - // Lookup the repo by GUID to handle reimports correctly. - cfg, err := s.config.Get() - if err != nil { - return nil, fmt.Errorf("failed to get config: %w", err) - } - repoCfg := config.FindRepoByGUID(cfg, query.RepoGuid) - if repoCfg == nil { - return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("repo not found: %q", query.RepoGuid)) - } - - // Get the orchestrator for the repo if its configuration is available. - repo, err := s.orchestrator.GetRepoOrchestrator(repoCfg.Id) + repo, err := s.orchestrator.GetRepoOrchestrator(query.RepoId) if err != nil { return nil, withLookupCode(fmt.Errorf("failed to get repo: %w", err)) } diff --git a/proto/v1/service.proto b/proto/v1/service.proto index b4689d98..b6bb1644 100644 --- a/proto/v1/service.proto +++ b/proto/v1/service.proto @@ -164,7 +164,7 @@ message RestoreSnapshotRequest { } message ListSnapshotFilesRequest { - string repo_guid = 1; + string repo_id = 1; string snapshot_id = 2; string path = 3; } diff --git a/webui/gen/ts/google/api/annotations_pb.ts b/webui/gen/ts/google/api/annotations_pb.ts index 405b7c1a..0735d598 100644 --- a/webui/gen/ts/google/api/annotations_pb.ts +++ b/webui/gen/ts/google/api/annotations_pb.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// @generated by protoc-gen-es v2.10.0 with parameter "target=ts" +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts" // @generated from file google/api/annotations.proto (package google.api, syntax proto3) /* eslint-disable */ diff --git a/webui/gen/ts/google/api/http_pb.ts b/webui/gen/ts/google/api/http_pb.ts index 78cc0ed4..20b7bad4 100644 --- a/webui/gen/ts/google/api/http_pb.ts +++ b/webui/gen/ts/google/api/http_pb.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// @generated by protoc-gen-es v2.10.0 with parameter "target=ts" +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts" // @generated from file google/api/http.proto (package google.api, syntax proto3) /* eslint-disable */ diff --git a/webui/gen/ts/types/value_pb.ts b/webui/gen/ts/types/value_pb.ts index 8912a97c..8c70bea9 100644 --- a/webui/gen/ts/types/value_pb.ts +++ b/webui/gen/ts/types/value_pb.ts @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v2.10.0 with parameter "target=ts" +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts" // @generated from file types/value.proto (package types, syntax proto3) /* eslint-disable */ diff --git a/webui/gen/ts/v1/authentication_pb.ts b/webui/gen/ts/v1/authentication_pb.ts index bba495cd..7a77afee 100644 --- a/webui/gen/ts/v1/authentication_pb.ts +++ b/webui/gen/ts/v1/authentication_pb.ts @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v2.10.0 with parameter "target=ts" +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts" // @generated from file v1/authentication.proto (package v1, syntax proto3) /* eslint-disable */ diff --git a/webui/gen/ts/v1/config_pb.ts b/webui/gen/ts/v1/config_pb.ts index 3c5bcbc7..389387d8 100644 --- a/webui/gen/ts/v1/config_pb.ts +++ b/webui/gen/ts/v1/config_pb.ts @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v2.10.0 with parameter "target=ts" +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts" // @generated from file v1/config.proto (package v1, syntax proto3) /* eslint-disable */ diff --git a/webui/gen/ts/v1/crypto_pb.ts b/webui/gen/ts/v1/crypto_pb.ts index 4e4a61ab..45aee0cc 100644 --- a/webui/gen/ts/v1/crypto_pb.ts +++ b/webui/gen/ts/v1/crypto_pb.ts @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v2.10.0 with parameter "target=ts" +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts" // @generated from file v1/crypto.proto (package v1, syntax proto3) /* eslint-disable */ diff --git a/webui/gen/ts/v1/operations_pb.ts b/webui/gen/ts/v1/operations_pb.ts index f3b81f6a..981f096d 100644 --- a/webui/gen/ts/v1/operations_pb.ts +++ b/webui/gen/ts/v1/operations_pb.ts @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v2.10.0 with parameter "target=ts" +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts" // @generated from file v1/operations.proto (package v1, syntax proto3) /* eslint-disable */ diff --git a/webui/gen/ts/v1/restic_pb.ts b/webui/gen/ts/v1/restic_pb.ts index 90133e65..6053ccbf 100644 --- a/webui/gen/ts/v1/restic_pb.ts +++ b/webui/gen/ts/v1/restic_pb.ts @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v2.10.0 with parameter "target=ts" +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts" // @generated from file v1/restic.proto (package v1, syntax proto3) /* eslint-disable */ diff --git a/webui/gen/ts/v1/service_pb.ts b/webui/gen/ts/v1/service_pb.ts index 902027d9..06d195c2 100644 --- a/webui/gen/ts/v1/service_pb.ts +++ b/webui/gen/ts/v1/service_pb.ts @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v2.10.0 with parameter "target=ts" +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts" // @generated from file v1/service.proto (package v1, syntax proto3) /* eslint-disable */ @@ -21,7 +21,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file v1/service.proto. */ export const file_v1_service: GenFile = /*@__PURE__*/ - fileDesc("ChB2MS9zZXJ2aWNlLnByb3RvEgJ2MSIvCg1CYWNrdXBSZXF1ZXN0Eg0KBXZhbHVlGAEgASgJEg8KB2RyeV9ydW4YAiABKAgiLAoUU2NoZWR1bGVUYXNrUmVzcG9uc2USFAoMb3BlcmF0aW9uX2lkGAEgASgDIr8CCgpPcFNlbGVjdG9yEgsKA2lkcxgBIAMoAxIYCgtpbnN0YW5jZV9pZBgGIAEoCUgAiAEBEiQKF29yaWdpbmFsX2luc3RhbmNlX2tleWlkGAggASgJSAGIAQESFgoJcmVwb19ndWlkGAcgASgJSAKIAQESFAoHcGxhbl9pZBgDIAEoCUgDiAEBEhgKC3NuYXBzaG90X2lkGAQgASgJSASIAQESFAoHZmxvd19pZBgFIAEoA0gFiAEBEhYKCW1vZG5vX2d0ZRgJIAEoA0gGiAEBQg4KDF9pbnN0YW5jZV9pZEIaChhfb3JpZ2luYWxfaW5zdGFuY2Vfa2V5aWRCDAoKX3JlcG9fZ3VpZEIKCghfcGxhbl9pZEIOCgxfc25hcHNob3RfaWRCCgoIX2Zsb3dfaWRCDAoKX21vZG5vX2d0ZSJkChBTZXR1cFNmdHBSZXF1ZXN0EgwKBGhvc3QYASABKAkSDAoEcG9ydBgCIAEoCRIQCgh1c2VybmFtZRgDIAEoCRIVCghwYXNzd29yZBgEIAEoCUgAiAEBQgsKCV9wYXNzd29yZCJiChFTZXR1cFNmdHBSZXNwb25zZRISCgpwdWJsaWNfa2V5GAEgASgJEhAKCGtleV9wYXRoGAIgASgJEhgKEGtub3duX2hvc3RzX3BhdGgYAyABKAkSDQoFZXJyb3IYBCABKAkiMAoWQ2hlY2tSZXBvRXhpc3RzUmVxdWVzdBIWCgRyZXBvGAEgASgLMggudjEuUmVwbyJUChdDaGVja1JlcG9FeGlzdHNSZXNwb25zZRIOCgZleGlzdHMYASABKAgSDQoFZXJyb3IYAiABKAkSGgoSaG9zdF9rZXlfdW50cnVzdGVkGAUgASgIIigKDkFkZFJlcG9SZXF1ZXN0EhYKBHJlcG8YASABKAsyCC52MS5SZXBvItIBChFEb1JlcG9UYXNrUmVxdWVzdBIPCgdyZXBvX2lkGAEgASgJEigKBHRhc2sYAiABKA4yGi52MS5Eb1JlcG9UYXNrUmVxdWVzdC5UYXNrIoEBCgRUYXNrEg0KCVRBU0tfTk9ORRAAEhgKFFRBU0tfSU5ERVhfU05BUFNIT1RTEAESDgoKVEFTS19QUlVORRACEg4KClRBU0tfQ0hFQ0sQAxIOCgpUQVNLX1NUQVRTEAQSDwoLVEFTS19VTkxPQ0sQBRIPCgtUQVNLX0ZPUkdFVBAGIkwKE0NsZWFySGlzdG9yeVJlcXVlc3QSIAoIc2VsZWN0b3IYASABKAsyDi52MS5PcFNlbGVjdG9yEhMKC29ubHlfZmFpbGVkGAIgASgIIkYKDUZvcmdldFJlcXVlc3QSDwoHcmVwb19pZBgBIAEoCRIPCgdwbGFuX2lkGAIgASgJEhMKC3NuYXBzaG90X2lkGAMgASgJIjgKFExpc3RTbmFwc2hvdHNSZXF1ZXN0Eg8KB3JlcG9faWQYASABKAkSDwoHcGxhbl9pZBgCIAEoCSJIChRHZXRPcGVyYXRpb25zUmVxdWVzdBIgCghzZWxlY3RvchgBIAEoCzIOLnYxLk9wU2VsZWN0b3ISDgoGbGFzdF9uGAIgASgDIm0KFlJlc3RvcmVTbmFwc2hvdFJlcXVlc3QSDwoHcGxhbl9pZBgBIAEoCRIPCgdyZXBvX2lkGAUgASgJEhMKC3NuYXBzaG90X2lkGAIgASgJEgwKBHBhdGgYAyABKAkSDgoGdGFyZ2V0GAQgASgJIlAKGExpc3RTbmFwc2hvdEZpbGVzUmVxdWVzdBIRCglyZXBvX2d1aWQYASABKAkSEwoLc25hcHNob3RfaWQYAiABKAkSDAoEcGF0aBgDIAEoCSJHChlMaXN0U25hcHNob3RGaWxlc1Jlc3BvbnNlEgwKBHBhdGgYASABKAkSHAoHZW50cmllcxgCIAMoCzILLnYxLkxzRW50cnkiHQoOTG9nRGF0YVJlcXVlc3QSCwoDcmVmGAEgASgJIjkKFUdldERvd25sb2FkVVJMUmVxdWVzdBINCgVvcF9pZBgBIAEoAxIRCglmaWxlX3BhdGgYAiABKAkilgEKB0xzRW50cnkSDAoEbmFtZRgBIAEoCRIMCgR0eXBlGAIgASgJEgwKBHBhdGgYAyABKAkSCwoDdWlkGAQgASgDEgsKA2dpZBgFIAEoAxIMCgRzaXplGAYgASgDEgwKBG1vZGUYByABKAMSDQoFbXRpbWUYCCABKAkSDQoFYXRpbWUYCSABKAkSDQoFY3RpbWUYCiABKAkiNQoRUnVuQ29tbWFuZFJlcXVlc3QSDwoHcmVwb19pZBgBIAEoCRIPCgdjb21tYW5kGAIgASgJIioKElJ1bkNvbW1hbmRSZXNwb25zZRIUCgxvcGVyYXRpb25faWQYASABKAMiJAoRUmVtb3ZlUmVwb1JlcXVlc3QSDwoHcmVwb19pZBgBIAEoCSIuChZDYW5jZWxPcGVyYXRpb25SZXF1ZXN0EhQKDG9wZXJhdGlvbl9pZBgBIAEoAyKKCAoYU3VtbWFyeURhc2hib2FyZFJlc3BvbnNlEjwKDnJlcG9fc3VtbWFyaWVzGAEgAygLMiQudjEuU3VtbWFyeURhc2hib2FyZFJlc3BvbnNlLlN1bW1hcnkSPAoOcGxhbl9zdW1tYXJpZXMYAiADKAsyJC52MS5TdW1tYXJ5RGFzaGJvYXJkUmVzcG9uc2UuU3VtbWFyeRITCgtjb25maWdfcGF0aBgKIAEoCRIRCglkYXRhX3BhdGgYCyABKAka0gMKB1N1bW1hcnkSCgoCaWQYASABKAkSHQoVYmFja3Vwc19mYWlsZWRfMzBkYXlzGAIgASgDEiMKG2JhY2t1cHNfd2FybmluZ19sYXN0XzMwZGF5cxgDIAEoAxIjChtiYWNrdXBzX3N1Y2Nlc3NfbGFzdF8zMGRheXMYBCABKAMSIQoZYnl0ZXNfc2Nhbm5lZF9sYXN0XzMwZGF5cxgFIAEoAxIfChdieXRlc19hZGRlZF9sYXN0XzMwZGF5cxgGIAEoAxIXCg90b3RhbF9zbmFwc2hvdHMYByABKAMSGQoRYnl0ZXNfc2Nhbm5lZF9hdmcYCCABKAMSFwoPYnl0ZXNfYWRkZWRfYXZnGAkgASgDEhsKE25leHRfYmFja3VwX3RpbWVfbXMYCiABKAMSQAoOcmVjZW50X2JhY2t1cHMYCyABKAsyKC52MS5TdW1tYXJ5RGFzaGJvYXJkUmVzcG9uc2UuQmFja3VwQ2hhcnQSFwoPcHJvdGVjdGVkX2J5dGVzGAwgASgDEkkKE2hpc3RvcnlfbGFzdF8zMGRheXMYDSADKAsyLC52MS5TdW1tYXJ5RGFzaGJvYXJkUmVzcG9uc2UuRGF5U3RhdHVzQnVja2V0GoMBCgtCYWNrdXBDaGFydBIPCgdmbG93X2lkGAEgAygDEhQKDHRpbWVzdGFtcF9tcxgCIAMoAxITCgtkdXJhdGlvbl9tcxgDIAMoAxIjCgZzdGF0dXMYBCADKA4yEy52MS5PcGVyYXRpb25TdGF0dXMSEwoLYnl0ZXNfYWRkZWQYBSADKAMaqAEKD0RheVN0YXR1c0J1Y2tldBIUCgx0aW1lc3RhbXBfbXMYASABKAMSEwoLYnl0ZXNfYWRkZWQYAiABKAMSFQoNYnl0ZXNfc2Nhbm5lZBgDIAEoAxJCCg1zdGF0dXNfY291bnRzGAQgAygLMisudjEuU3VtbWFyeURhc2hib2FyZFJlc3BvbnNlLlN0YXR1c0FuZENvdW50Eg8KB292ZXJkdWUYBSABKAgaRAoOU3RhdHVzQW5kQ291bnQSDQoFY291bnQYASABKAMSIwoGc3RhdHVzGAIgASgOMhMudjEuT3BlcmF0aW9uU3RhdHVzIoIBChtHZW5lcmF0ZVBhaXJpbmdUb2tlblJlcXVlc3QSDQoFbGFiZWwYASABKAkSEwoLdHRsX3NlY29uZHMYAiABKAMSEAoIbWF4X3VzZXMYAyABKAUSLQoLcGVybWlzc2lvbnMYBCADKAsyGC52MS5NdWx0aWhvc3QuUGVybWlzc2lvbiItChxHZW5lcmF0ZVBhaXJpbmdUb2tlblJlc3BvbnNlEg0KBXRva2VuGAEgASgJMoULCghCYWNrcmVzdBIxCglHZXRDb25maWcSFi5nb29nbGUucHJvdG9idWYuRW1wdHkaCi52MS5Db25maWciABIlCglTZXRDb25maWcSCi52MS5Db25maWcaCi52MS5Db25maWciABI6CglTZXR1cFNmdHASFC52MS5TZXR1cFNmdHBSZXF1ZXN0GhUudjEuU2V0dXBTZnRwUmVzcG9uc2UiABJMCg9DaGVja1JlcG9FeGlzdHMSGi52MS5DaGVja1JlcG9FeGlzdHNSZXF1ZXN0GhsudjEuQ2hlY2tSZXBvRXhpc3RzUmVzcG9uc2UiABIrCgdBZGRSZXBvEhIudjEuQWRkUmVwb1JlcXVlc3QaCi52MS5Db25maWciABIxCgpSZW1vdmVSZXBvEhUudjEuUmVtb3ZlUmVwb1JlcXVlc3QaCi52MS5Db25maWciABJEChJHZXRPcGVyYXRpb25FdmVudHMSFi5nb29nbGUucHJvdG9idWYuRW1wdHkaEi52MS5PcGVyYXRpb25FdmVudCIAMAESPgoNR2V0T3BlcmF0aW9ucxIYLnYxLkdldE9wZXJhdGlvbnNSZXF1ZXN0GhEudjEuT3BlcmF0aW9uTGlzdCIAEkMKDUxpc3RTbmFwc2hvdHMSGC52MS5MaXN0U25hcHNob3RzUmVxdWVzdBoWLnYxLlJlc3RpY1NuYXBzaG90TGlzdCIAElIKEUxpc3RTbmFwc2hvdEZpbGVzEhwudjEuTGlzdFNuYXBzaG90RmlsZXNSZXF1ZXN0Gh0udjEuTGlzdFNuYXBzaG90RmlsZXNSZXNwb25zZSIAEjUKBkJhY2t1cBIRLnYxLkJhY2t1cFJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiABI/CgpEb1JlcG9UYXNrEhUudjEuRG9SZXBvVGFza1JlcXVlc3QaGC52MS5TY2hlZHVsZVRhc2tSZXNwb25zZSIAEjcKBkZvcmdldBIRLnYxLkZvcmdldFJlcXVlc3QaGC52MS5TY2hlZHVsZVRhc2tSZXNwb25zZSIAEkEKB1Jlc3RvcmUSGi52MS5SZXN0b3JlU25hcHNob3RSZXF1ZXN0GhgudjEuU2NoZWR1bGVUYXNrUmVzcG9uc2UiABI+CgZDYW5jZWwSGi52MS5DYW5jZWxPcGVyYXRpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IgASNAoHR2V0TG9ncxISLnYxLkxvZ0RhdGFSZXF1ZXN0GhEudHlwZXMuQnl0ZXNWYWx1ZSIAMAESPQoKUnVuQ29tbWFuZBIVLnYxLlJ1bkNvbW1hbmRSZXF1ZXN0GhYudjEuUnVuQ29tbWFuZFJlc3BvbnNlIgASQQoOR2V0RG93bmxvYWRVUkwSGS52MS5HZXREb3dubG9hZFVSTFJlcXVlc3QaEi50eXBlcy5TdHJpbmdWYWx1ZSIAEkEKDENsZWFySGlzdG9yeRIXLnYxLkNsZWFySGlzdG9yeVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiABI7ChBQYXRoQXV0b2NvbXBsZXRlEhIudHlwZXMuU3RyaW5nVmFsdWUaES50eXBlcy5TdHJpbmdMaXN0IgASTQoTR2V0U3VtbWFyeURhc2hib2FyZBIWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eRocLnYxLlN1bW1hcnlEYXNoYm9hcmRSZXNwb25zZSIAElsKFEdlbmVyYXRlUGFpcmluZ1Rva2VuEh8udjEuR2VuZXJhdGVQYWlyaW5nVG9rZW5SZXF1ZXN0GiAudjEuR2VuZXJhdGVQYWlyaW5nVG9rZW5SZXNwb25zZSIAQixaKmdpdGh1Yi5jb20vZ2FyZXRoZ2VvcmdlL2JhY2tyZXN0L2dlbi9nby92MWIGcHJvdG8z", [file_v1_config, file_v1_restic, file_v1_operations, file_types_value, file_google_protobuf_empty, file_google_api_annotations]); + fileDesc("ChB2MS9zZXJ2aWNlLnByb3RvEgJ2MSIvCg1CYWNrdXBSZXF1ZXN0Eg0KBXZhbHVlGAEgASgJEg8KB2RyeV9ydW4YAiABKAgiLAoUU2NoZWR1bGVUYXNrUmVzcG9uc2USFAoMb3BlcmF0aW9uX2lkGAEgASgDIr8CCgpPcFNlbGVjdG9yEgsKA2lkcxgBIAMoAxIYCgtpbnN0YW5jZV9pZBgGIAEoCUgAiAEBEiQKF29yaWdpbmFsX2luc3RhbmNlX2tleWlkGAggASgJSAGIAQESFgoJcmVwb19ndWlkGAcgASgJSAKIAQESFAoHcGxhbl9pZBgDIAEoCUgDiAEBEhgKC3NuYXBzaG90X2lkGAQgASgJSASIAQESFAoHZmxvd19pZBgFIAEoA0gFiAEBEhYKCW1vZG5vX2d0ZRgJIAEoA0gGiAEBQg4KDF9pbnN0YW5jZV9pZEIaChhfb3JpZ2luYWxfaW5zdGFuY2Vfa2V5aWRCDAoKX3JlcG9fZ3VpZEIKCghfcGxhbl9pZEIOCgxfc25hcHNob3RfaWRCCgoIX2Zsb3dfaWRCDAoKX21vZG5vX2d0ZSJkChBTZXR1cFNmdHBSZXF1ZXN0EgwKBGhvc3QYASABKAkSDAoEcG9ydBgCIAEoCRIQCgh1c2VybmFtZRgDIAEoCRIVCghwYXNzd29yZBgEIAEoCUgAiAEBQgsKCV9wYXNzd29yZCJiChFTZXR1cFNmdHBSZXNwb25zZRISCgpwdWJsaWNfa2V5GAEgASgJEhAKCGtleV9wYXRoGAIgASgJEhgKEGtub3duX2hvc3RzX3BhdGgYAyABKAkSDQoFZXJyb3IYBCABKAkiMAoWQ2hlY2tSZXBvRXhpc3RzUmVxdWVzdBIWCgRyZXBvGAEgASgLMggudjEuUmVwbyJUChdDaGVja1JlcG9FeGlzdHNSZXNwb25zZRIOCgZleGlzdHMYASABKAgSDQoFZXJyb3IYAiABKAkSGgoSaG9zdF9rZXlfdW50cnVzdGVkGAUgASgIIigKDkFkZFJlcG9SZXF1ZXN0EhYKBHJlcG8YASABKAsyCC52MS5SZXBvItIBChFEb1JlcG9UYXNrUmVxdWVzdBIPCgdyZXBvX2lkGAEgASgJEigKBHRhc2sYAiABKA4yGi52MS5Eb1JlcG9UYXNrUmVxdWVzdC5UYXNrIoEBCgRUYXNrEg0KCVRBU0tfTk9ORRAAEhgKFFRBU0tfSU5ERVhfU05BUFNIT1RTEAESDgoKVEFTS19QUlVORRACEg4KClRBU0tfQ0hFQ0sQAxIOCgpUQVNLX1NUQVRTEAQSDwoLVEFTS19VTkxPQ0sQBRIPCgtUQVNLX0ZPUkdFVBAGIkwKE0NsZWFySGlzdG9yeVJlcXVlc3QSIAoIc2VsZWN0b3IYASABKAsyDi52MS5PcFNlbGVjdG9yEhMKC29ubHlfZmFpbGVkGAIgASgIIkYKDUZvcmdldFJlcXVlc3QSDwoHcmVwb19pZBgBIAEoCRIPCgdwbGFuX2lkGAIgASgJEhMKC3NuYXBzaG90X2lkGAMgASgJIjgKFExpc3RTbmFwc2hvdHNSZXF1ZXN0Eg8KB3JlcG9faWQYASABKAkSDwoHcGxhbl9pZBgCIAEoCSJIChRHZXRPcGVyYXRpb25zUmVxdWVzdBIgCghzZWxlY3RvchgBIAEoCzIOLnYxLk9wU2VsZWN0b3ISDgoGbGFzdF9uGAIgASgDIm0KFlJlc3RvcmVTbmFwc2hvdFJlcXVlc3QSDwoHcGxhbl9pZBgBIAEoCRIPCgdyZXBvX2lkGAUgASgJEhMKC3NuYXBzaG90X2lkGAIgASgJEgwKBHBhdGgYAyABKAkSDgoGdGFyZ2V0GAQgASgJIk4KGExpc3RTbmFwc2hvdEZpbGVzUmVxdWVzdBIPCgdyZXBvX2lkGAEgASgJEhMKC3NuYXBzaG90X2lkGAIgASgJEgwKBHBhdGgYAyABKAkiRwoZTGlzdFNuYXBzaG90RmlsZXNSZXNwb25zZRIMCgRwYXRoGAEgASgJEhwKB2VudHJpZXMYAiADKAsyCy52MS5Mc0VudHJ5Ih0KDkxvZ0RhdGFSZXF1ZXN0EgsKA3JlZhgBIAEoCSI5ChVHZXREb3dubG9hZFVSTFJlcXVlc3QSDQoFb3BfaWQYASABKAMSEQoJZmlsZV9wYXRoGAIgASgJIpYBCgdMc0VudHJ5EgwKBG5hbWUYASABKAkSDAoEdHlwZRgCIAEoCRIMCgRwYXRoGAMgASgJEgsKA3VpZBgEIAEoAxILCgNnaWQYBSABKAMSDAoEc2l6ZRgGIAEoAxIMCgRtb2RlGAcgASgDEg0KBW10aW1lGAggASgJEg0KBWF0aW1lGAkgASgJEg0KBWN0aW1lGAogASgJIjUKEVJ1bkNvbW1hbmRSZXF1ZXN0Eg8KB3JlcG9faWQYASABKAkSDwoHY29tbWFuZBgCIAEoCSIqChJSdW5Db21tYW5kUmVzcG9uc2USFAoMb3BlcmF0aW9uX2lkGAEgASgDIiQKEVJlbW92ZVJlcG9SZXF1ZXN0Eg8KB3JlcG9faWQYASABKAkiLgoWQ2FuY2VsT3BlcmF0aW9uUmVxdWVzdBIUCgxvcGVyYXRpb25faWQYASABKAMiiggKGFN1bW1hcnlEYXNoYm9hcmRSZXNwb25zZRI8Cg5yZXBvX3N1bW1hcmllcxgBIAMoCzIkLnYxLlN1bW1hcnlEYXNoYm9hcmRSZXNwb25zZS5TdW1tYXJ5EjwKDnBsYW5fc3VtbWFyaWVzGAIgAygLMiQudjEuU3VtbWFyeURhc2hib2FyZFJlc3BvbnNlLlN1bW1hcnkSEwoLY29uZmlnX3BhdGgYCiABKAkSEQoJZGF0YV9wYXRoGAsgASgJGtIDCgdTdW1tYXJ5EgoKAmlkGAEgASgJEh0KFWJhY2t1cHNfZmFpbGVkXzMwZGF5cxgCIAEoAxIjChtiYWNrdXBzX3dhcm5pbmdfbGFzdF8zMGRheXMYAyABKAMSIwobYmFja3Vwc19zdWNjZXNzX2xhc3RfMzBkYXlzGAQgASgDEiEKGWJ5dGVzX3NjYW5uZWRfbGFzdF8zMGRheXMYBSABKAMSHwoXYnl0ZXNfYWRkZWRfbGFzdF8zMGRheXMYBiABKAMSFwoPdG90YWxfc25hcHNob3RzGAcgASgDEhkKEWJ5dGVzX3NjYW5uZWRfYXZnGAggASgDEhcKD2J5dGVzX2FkZGVkX2F2ZxgJIAEoAxIbChNuZXh0X2JhY2t1cF90aW1lX21zGAogASgDEkAKDnJlY2VudF9iYWNrdXBzGAsgASgLMigudjEuU3VtbWFyeURhc2hib2FyZFJlc3BvbnNlLkJhY2t1cENoYXJ0EhcKD3Byb3RlY3RlZF9ieXRlcxgMIAEoAxJJChNoaXN0b3J5X2xhc3RfMzBkYXlzGA0gAygLMiwudjEuU3VtbWFyeURhc2hib2FyZFJlc3BvbnNlLkRheVN0YXR1c0J1Y2tldBqDAQoLQmFja3VwQ2hhcnQSDwoHZmxvd19pZBgBIAMoAxIUCgx0aW1lc3RhbXBfbXMYAiADKAMSEwoLZHVyYXRpb25fbXMYAyADKAMSIwoGc3RhdHVzGAQgAygOMhMudjEuT3BlcmF0aW9uU3RhdHVzEhMKC2J5dGVzX2FkZGVkGAUgAygDGqgBCg9EYXlTdGF0dXNCdWNrZXQSFAoMdGltZXN0YW1wX21zGAEgASgDEhMKC2J5dGVzX2FkZGVkGAIgASgDEhUKDWJ5dGVzX3NjYW5uZWQYAyABKAMSQgoNc3RhdHVzX2NvdW50cxgEIAMoCzIrLnYxLlN1bW1hcnlEYXNoYm9hcmRSZXNwb25zZS5TdGF0dXNBbmRDb3VudBIPCgdvdmVyZHVlGAUgASgIGkQKDlN0YXR1c0FuZENvdW50Eg0KBWNvdW50GAEgASgDEiMKBnN0YXR1cxgCIAEoDjITLnYxLk9wZXJhdGlvblN0YXR1cyKCAQobR2VuZXJhdGVQYWlyaW5nVG9rZW5SZXF1ZXN0Eg0KBWxhYmVsGAEgASgJEhMKC3R0bF9zZWNvbmRzGAIgASgDEhAKCG1heF91c2VzGAMgASgFEi0KC3Blcm1pc3Npb25zGAQgAygLMhgudjEuTXVsdGlob3N0LlBlcm1pc3Npb24iLQocR2VuZXJhdGVQYWlyaW5nVG9rZW5SZXNwb25zZRINCgV0b2tlbhgBIAEoCTKFCwoIQmFja3Jlc3QSMQoJR2V0Q29uZmlnEhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5GgoudjEuQ29uZmlnIgASJQoJU2V0Q29uZmlnEgoudjEuQ29uZmlnGgoudjEuQ29uZmlnIgASOgoJU2V0dXBTZnRwEhQudjEuU2V0dXBTZnRwUmVxdWVzdBoVLnYxLlNldHVwU2Z0cFJlc3BvbnNlIgASTAoPQ2hlY2tSZXBvRXhpc3RzEhoudjEuQ2hlY2tSZXBvRXhpc3RzUmVxdWVzdBobLnYxLkNoZWNrUmVwb0V4aXN0c1Jlc3BvbnNlIgASKwoHQWRkUmVwbxISLnYxLkFkZFJlcG9SZXF1ZXN0GgoudjEuQ29uZmlnIgASMQoKUmVtb3ZlUmVwbxIVLnYxLlJlbW92ZVJlcG9SZXF1ZXN0GgoudjEuQ29uZmlnIgASRAoSR2V0T3BlcmF0aW9uRXZlbnRzEhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5GhIudjEuT3BlcmF0aW9uRXZlbnQiADABEj4KDUdldE9wZXJhdGlvbnMSGC52MS5HZXRPcGVyYXRpb25zUmVxdWVzdBoRLnYxLk9wZXJhdGlvbkxpc3QiABJDCg1MaXN0U25hcHNob3RzEhgudjEuTGlzdFNuYXBzaG90c1JlcXVlc3QaFi52MS5SZXN0aWNTbmFwc2hvdExpc3QiABJSChFMaXN0U25hcHNob3RGaWxlcxIcLnYxLkxpc3RTbmFwc2hvdEZpbGVzUmVxdWVzdBodLnYxLkxpc3RTbmFwc2hvdEZpbGVzUmVzcG9uc2UiABI1CgZCYWNrdXASES52MS5CYWNrdXBSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IgASPwoKRG9SZXBvVGFzaxIVLnYxLkRvUmVwb1Rhc2tSZXF1ZXN0GhgudjEuU2NoZWR1bGVUYXNrUmVzcG9uc2UiABI3CgZGb3JnZXQSES52MS5Gb3JnZXRSZXF1ZXN0GhgudjEuU2NoZWR1bGVUYXNrUmVzcG9uc2UiABJBCgdSZXN0b3JlEhoudjEuUmVzdG9yZVNuYXBzaG90UmVxdWVzdBoYLnYxLlNjaGVkdWxlVGFza1Jlc3BvbnNlIgASPgoGQ2FuY2VsEhoudjEuQ2FuY2VsT3BlcmF0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIAEjQKB0dldExvZ3MSEi52MS5Mb2dEYXRhUmVxdWVzdBoRLnR5cGVzLkJ5dGVzVmFsdWUiADABEj0KClJ1bkNvbW1hbmQSFS52MS5SdW5Db21tYW5kUmVxdWVzdBoWLnYxLlJ1bkNvbW1hbmRSZXNwb25zZSIAEkEKDkdldERvd25sb2FkVVJMEhkudjEuR2V0RG93bmxvYWRVUkxSZXF1ZXN0GhIudHlwZXMuU3RyaW5nVmFsdWUiABJBCgxDbGVhckhpc3RvcnkSFy52MS5DbGVhckhpc3RvcnlSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IgASOwoQUGF0aEF1dG9jb21wbGV0ZRISLnR5cGVzLlN0cmluZ1ZhbHVlGhEudHlwZXMuU3RyaW5nTGlzdCIAEk0KE0dldFN1bW1hcnlEYXNoYm9hcmQSFi5nb29nbGUucHJvdG9idWYuRW1wdHkaHC52MS5TdW1tYXJ5RGFzaGJvYXJkUmVzcG9uc2UiABJbChRHZW5lcmF0ZVBhaXJpbmdUb2tlbhIfLnYxLkdlbmVyYXRlUGFpcmluZ1Rva2VuUmVxdWVzdBogLnYxLkdlbmVyYXRlUGFpcmluZ1Rva2VuUmVzcG9uc2UiAEIsWipnaXRodWIuY29tL2dhcmV0aGdlb3JnZS9iYWNrcmVzdC9nZW4vZ28vdjFiBnByb3RvMw", [file_v1_config, file_v1_restic, file_v1_operations, file_types_value, file_google_protobuf_empty, file_google_api_annotations]); /** * @generated from message v1.BackupRequest @@ -456,9 +456,9 @@ export const RestoreSnapshotRequestSchema: GenMessage = */ export type ListSnapshotFilesRequest = Message<"v1.ListSnapshotFilesRequest"> & { /** - * @generated from field: string repo_guid = 1; + * @generated from field: string repo_id = 1; */ - repoGuid: string; + repoId: string; /** * @generated from field: string snapshot_id = 2; diff --git a/webui/gen/ts/v1sync/syncservice_pb.ts b/webui/gen/ts/v1sync/syncservice_pb.ts index 39efe1c6..b91ce04a 100644 --- a/webui/gen/ts/v1sync/syncservice_pb.ts +++ b/webui/gen/ts/v1sync/syncservice_pb.ts @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v2.10.0 with parameter "target=ts" +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts" // @generated from file v1sync/syncservice.proto (package v1sync, syntax proto3) /* eslint-disable */ diff --git a/webui/package.json b/webui/package.json index 56213eaa..fef32211 100644 --- a/webui/package.json +++ b/webui/package.json @@ -36,7 +36,6 @@ "react-icons": "^5.5.0", "react-js-cron": "^5.2.0", "react-router": "^8.1.0", - "react-router-dom": "^7.18.1", "react-virtualized": "^9.22.6", "recharts": "^3.9.0" }, diff --git a/webui/pnpm-lock.yaml b/webui/pnpm-lock.yaml index eeabbc1d..a1cbf514 100644 --- a/webui/pnpm-lock.yaml +++ b/webui/pnpm-lock.yaml @@ -68,9 +68,6 @@ importers: react-router: specifier: ^8.1.0 version: 8.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react-router-dom: - specifier: ^7.18.1 - version: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react-virtualized: specifier: ^9.22.6 version: 9.22.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -731,36 +728,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.6': resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} @@ -885,36 +888,42 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.1.3': resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.1.3': resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.1.3': resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.1.3': resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.1.3': resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.1.3': resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==} @@ -976,56 +985,67 @@ packages: resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.54.0': resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.54.0': resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.54.0': resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.54.0': resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.54.0': resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.54.0': resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.54.0': resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.54.0': resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.54.0': resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.54.0': resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openharmony-arm64@4.54.0': resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} @@ -1519,10 +1539,6 @@ packages: cookie-es@3.1.1: resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} - cookie@1.1.1: - resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} - engines: {node: '>=18'} - copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} @@ -1906,24 +1922,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -2360,23 +2380,6 @@ packages: redux: optional: true - react-router-dom@7.18.1: - resolution: {integrity: sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: '>=18' - react-dom: '>=18' - - react-router@7.18.1: - resolution: {integrity: sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: '>=18' - react-dom: '>=18' - peerDependenciesMeta: - react-dom: - optional: true - react-router@8.1.0: resolution: {integrity: sha512-Mdfi61uObuvWNN9OhChOC0HV6YWOIfKRzEWOvCHRSuQg8IM+Nv10edaM/2HE8ZixBpUTdQbruyWqC3sDkkh9vw==} engines: {node: '>=22.22.0'} @@ -2483,9 +2486,6 @@ packages: engines: {node: '>=10'} hasBin: true - set-cookie-parser@2.7.2: - resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} - sha.js@2.4.11: resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} hasBin: true @@ -4402,7 +4402,7 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.29.7 cosmiconfig: 7.1.0 resolve: 1.22.11 @@ -4479,8 +4479,6 @@ snapshots: cookie-es@3.1.1: {} - cookie@1.1.1: {} - copy-to-clipboard@3.3.3: dependencies: toggle-selection: 1.0.6 @@ -5324,20 +5322,6 @@ snapshots: '@types/react': 19.2.17 redux: 5.0.1 - react-router-dom@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - dependencies: - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-router: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - - react-router@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - dependencies: - cookie: 1.1.1 - react: 19.2.7 - set-cookie-parser: 2.7.2 - optionalDependencies: - react-dom: 19.2.7(react@19.2.7) - react-router@8.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: cookie-es: 3.1.1 @@ -5483,8 +5467,6 @@ snapshots: semver@7.7.4: {} - set-cookie-parser@2.7.2: {} - sha.js@2.4.11: dependencies: inherits: 2.0.4 diff --git a/webui/src/app/App.tsx b/webui/src/app/App.tsx index 5f187af9..12aa066a 100644 --- a/webui/src/app/App.tsx +++ b/webui/src/app/App.tsx @@ -1,4 +1,10 @@ -import React, { Suspense, useCallback, useEffect, useMemo, useState } from "react"; +import React, { + Suspense, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; import { FiCalendar, FiDatabase, @@ -66,12 +72,16 @@ import { Routes, useNavigate, useParams, - Link as RouterLink, useLocation, -} from "react-router-dom"; +} from "react-router"; import { MainContentAreaTemplate } from "../components/layout/MainContentArea"; import { create } from "@bufbuild/protobuf"; -import { PeerState, PlanMetadata, RepoMetadata, SetRemoteClientConfigRequestSchema } from "../../gen/ts/v1sync/syncservice_pb"; +import { + PeerState, + PlanMetadata, + RepoMetadata, + SetRemoteClientConfigRequestSchema, +} from "../../gen/ts/v1sync/syncservice_pb"; import { useSyncStates } from "../state/peerStates"; import * as m from "../paraglide/messages"; import { Link } from "../components/ui/link"; @@ -145,7 +155,11 @@ const RepoViewContainer = () => { borderColor="blue.200" fontSize="sm" color="blue.800" - _dark={{ bg: "blue.950", borderColor: "blue.800", color: "blue.200" }} + _dark={{ + bg: "blue.950", + borderColor: "blue.800", + color: "blue.200", + }} > This is a remote repo from{" "} {repo.originInstanceId}. Operation history only @@ -293,11 +307,7 @@ const PeerNavItem = ({ {name} {onEdit && ( - + onNav(planPath)} userSelect="none" > - + {plan.id} - + onNav(repoPath)} userSelect="none" > - + {repo.id} {repo.originInstanceId && ( @@ -565,11 +563,7 @@ const SidebarRepoItem = React.memo( )} - + void }) => { const isActive = (path: string) => location.pathname === path; - const reposById = useMemo(() => config ? keyBy(config.repos, (r) => r.id) : {}, [config?.repos]); + const reposById = useMemo( + () => (config ? keyBy(config.repos, (r) => r.id) : {}), + [config?.repos], + ); // Replicate getSidenavItems functionality with Chakra components if (!config) return null; const configPlans = config.plans || []; - const localRepos = (config.repos || []).filter( - (r) => !r.originInstanceId, - ); - const remoteRepos = (config.repos || []).filter( - (r) => !!r.originInstanceId, - ); + const localRepos = (config.repos || []).filter((r) => !r.originInstanceId); + const remoteRepos = (config.repos || []).filter((r) => !!r.originInstanceId); return ( ((_, reject) => setTimeout( () => - reject(new Error("Request timed out, backend may be unavailable")), + reject( + new Error("Request timed out, backend may be unavailable"), + ), 10000, ), ); @@ -1070,9 +1065,8 @@ const AuthenticationBoundary = ({ ])) as Config; setConfig(config); if (shouldShowSettings(config)) { - const { SettingsModal } = await import( - "../features/settings/SettingsModal" - ); + const { SettingsModal } = + await import("../features/settings/SettingsModal"); showModal(); } else { showModal(null); @@ -1134,12 +1128,10 @@ const AuthenticationBoundary = ({ return <>{children}; }; -const IconForResource = React.memo( - ({ selector }: { selector: OpSelector }) => { - const status = useResourceStatus(selector); - return iconForStatus(status); - }, -); +const IconForResource = React.memo(({ selector }: { selector: OpSelector }) => { + const status = useResourceStatus(selector); + return iconForStatus(status); +}); const iconForStatus = (status: OperationStatus) => { const color = colorForStatus(status); diff --git a/webui/src/app/main.tsx b/webui/src/app/main.tsx index 0b064c5c..47fbd542 100644 --- a/webui/src/app/main.tsx +++ b/webui/src/app/main.tsx @@ -6,7 +6,7 @@ import { Toaster } from "../components/ui/toaster"; import "react-js-cron/dist/styles.css"; -import { HashRouter } from "react-router-dom"; +import { HashRouter } from "react-router"; import { AppProvider } from "./provider"; const Root = ({ children }: { children: React.ReactNode }) => { diff --git a/webui/src/app/provider.tsx b/webui/src/app/provider.tsx index e2eb53ab..442ec704 100644 --- a/webui/src/app/provider.tsx +++ b/webui/src/app/provider.tsx @@ -2,7 +2,7 @@ import { ChakraProvider, defaultSystem } from "@chakra-ui/react"; import { ThemeProvider } from "next-themes"; import React, { useContext, useEffect, useRef, useState } from "react"; import { useUserPreferences } from "../lib/userPreferences"; -import { Config } from "../../gen/ts/v1/config_pb"; +import { Config, Repo } from "../../gen/ts/v1/config_pb"; import { backrestService } from "../api/client"; // Config Context Logic @@ -14,6 +14,24 @@ export const useConfig = (): ConfigCtx => { return context; }; +// Lookup of repos by their stable GUID, derived from the config. Cached on the +// config object's identity so it is built once per config instance and shared +// across all consumers (and garbage-collected when the config is replaced), +// giving an O(1) lookup instead of rescanning config.repos on every render. +const EMPTY_REPOS_BY_GUID: Map = new Map(); +const reposByGuidCache = new WeakMap>(); + +export const useReposByGuid = (): Map => { + const [config] = useConfig(); + if (!config) return EMPTY_REPOS_BY_GUID; + let byGuid = reposByGuidCache.get(config); + if (!byGuid) { + byGuid = new Map(config.repos.map((r) => [r.guid, r])); + reposByGuidCache.set(config, byGuid); + } + return byGuid; +}; + export function AppProvider(props: { children: React.ReactNode }) { useUserPreferences(); // Ensure locale is synced on startup const [config, setConfig] = useState(null); diff --git a/webui/src/components/ui/input-group.tsx b/webui/src/components/ui/input-group.tsx index a7315fd2..7513bfe9 100644 --- a/webui/src/components/ui/input-group.tsx +++ b/webui/src/components/ui/input-group.tsx @@ -31,7 +31,7 @@ export const InputGroup = React.forwardRef( {React.cloneElement(children, { ...(startElement && { ps: "calc(var(--input-height) - 6px)" }), ...(endElement && { pe: "calc(var(--input-height) - 6px)" }), - ...children.props, + ...(children.props as Record), })} {endElement && ( diff --git a/webui/src/custom.d.ts b/webui/src/custom.d.ts index 0a3495bf..a7caa19d 100644 --- a/webui/src/custom.d.ts +++ b/webui/src/custom.d.ts @@ -3,6 +3,8 @@ declare module "*.svg" { export default content; } +declare module "*.css"; + declare module "*/paraglide/messages" { const messages: any; export = messages; diff --git a/webui/src/features/dashboard/SummaryDashboard.tsx b/webui/src/features/dashboard/SummaryDashboard.tsx index 1b440e03..f9daf317 100644 --- a/webui/src/features/dashboard/SummaryDashboard.tsx +++ b/webui/src/features/dashboard/SummaryDashboard.tsx @@ -64,7 +64,8 @@ function agoText(ms: number): string { const s = Math.floor((Date.now() - ms) / 1000); if (s < 45) return m.dashboard_time_just_now(); if (s < 90) return m.dashboard_time_a_minute_ago(); - if (s < 3600) return m.dashboard_time_minutes_ago({ count: Math.floor(s / 60) }); + if (s < 3600) + return m.dashboard_time_minutes_ago({ count: Math.floor(s / 60) }); if (s < 5400) return m.dashboard_time_an_hour_ago(); if (s < 86400) { const h = Math.floor(s / 3600); @@ -82,7 +83,9 @@ function untilText(ms: number): string | null { const s = Math.floor((ms - Date.now()) / 1000); if (s <= 0) return m.dashboard_time_due_now(); if (s < 5400) - return m.dashboard_time_in_minutes({ count: Math.max(1, Math.round(s / 60)) }); + return m.dashboard_time_in_minutes({ + count: Math.max(1, Math.round(s / 60)), + }); if (s < 172800) return m.dashboard_time_in_hours({ count: Math.round(s / 3600) }); return m.dashboard_time_in_days({ count: Math.round(s / 86400) }); @@ -181,7 +184,9 @@ interface SummaryStatus { color: string; } -function summaryStatus(summary: SummaryDashboardResponse_Summary): SummaryStatus { +function summaryStatus( + summary: SummaryDashboardResponse_Summary, +): SummaryStatus { const rb = summary.recentBackups; const latestStatus = rb?.status[0]; const latestTs = Number(rb?.timestampMs[0] ?? 0); @@ -326,7 +331,7 @@ function progressFromOp(op: Operation): LiveProgress | null { const useLiveProgress = ( planId: string, running: boolean, - onFinished: () => void, + onFinished?: () => void, ) => { const [progress, setProgress] = useState(null); @@ -350,7 +355,7 @@ const useLiveProgress = ( if (finished) return; finished = true; setProgress(null); - onFinished(); + onFinished?.(); return; } const p = progressFromOp(op); @@ -942,7 +947,7 @@ const MultihostSummary = ({ return map; }, [config?.repos]); - const knownHostTiles: JSX.Element[] = []; + const knownHostTiles: React.JSX.Element[] = []; for (const cfgPeer of multihostConfig?.knownHosts ?? []) { const peerState = peerStates.get(cfgPeer.keyid); if (!peerState) continue; @@ -955,7 +960,7 @@ const MultihostSummary = ({ ); } - const authorizedClientTiles: JSX.Element[] = []; + const authorizedClientTiles: React.JSX.Element[] = []; for (const cfgPeer of multihostConfig?.authorizedClients ?? []) { const peerState = peerStates.get(cfgPeer.keyid); if (!peerState) continue; diff --git a/webui/src/features/operations/OperationRow.tsx b/webui/src/features/operations/OperationRow.tsx index ef997325..37bd3128 100644 --- a/webui/src/features/operations/OperationRow.tsx +++ b/webui/src/features/operations/OperationRow.tsx @@ -59,6 +59,7 @@ import { OperationIcon } from "./OperationIcon"; import { LogView } from "../../components/common/LogView"; import { create } from "@bufbuild/protobuf"; +import { useReposByGuid } from "../../app/provider"; import { OperationListView } from "./OperationListView"; import * as m from "../../paraglide/messages"; import { FormModal } from "../../components/common/FormModal"; @@ -107,9 +108,17 @@ export const OperationRow = ({ showDelete?: boolean; }>) => { const showModal = useShowModal(); + const reposByGuid = useReposByGuid(); const displayType = getTypeForDisplay(operation); const setRefresh = useState(0)[1]; + // The operation's repoId reflects the repo's name when the operation was + // recorded, which may be stale if the repo was removed and re-added under a + // different name. Resolve the current repoId from the stable repoGuid so that + // actions (e.g. browsing files, restoring) target the repo as it exists now. + const currentRepoId = + reposByGuid.get(operation.repoGuid)?.id ?? operation.repoId; + useEffect(() => { if (operation.status === OperationStatus.STATUS_INPROGRESS) { const interval = setInterval(() => { @@ -293,8 +302,7 @@ export const OperationRow = ({ ), diff --git a/webui/src/features/repositories/SnapshotBrowser.tsx b/webui/src/features/repositories/SnapshotBrowser.tsx index 290b62ba..9222d88e 100644 --- a/webui/src/features/repositories/SnapshotBrowser.tsx +++ b/webui/src/features/repositories/SnapshotBrowser.tsx @@ -117,7 +117,6 @@ const findInTree = ( export const SnapshotBrowser = ({ repoId, - repoGuid, planId, snapshotId, snapshotOpId, @@ -125,7 +124,6 @@ export const SnapshotBrowser = ({ snapshotId: string; snapshotOpId?: bigint; repoId: string; - repoGuid: string; planId?: string; }>) => { const showModal = useShowModal(); @@ -174,7 +172,7 @@ export const SnapshotBrowser = ({ // But we can trigger a load for root immediately to populate it if expanded. loadData("/"); setExpandedKeys(new Set(["/"])); // Auto-expand root for better UX - }, [repoId, repoGuid, snapshotId]); + }, [repoId, snapshotId]); const loadData = async (key: string) => { // Check if node exists first @@ -201,7 +199,7 @@ export const SnapshotBrowser = ({ const resp = await backrestService.listSnapshotFiles( create(ListSnapshotFilesRequestSchema, { path, - repoGuid, + repoId, snapshotId, }), ); diff --git a/webui/tsconfig.json b/webui/tsconfig.json index c11d1035..57d678a0 100644 --- a/webui/tsconfig.json +++ b/webui/tsconfig.json @@ -3,9 +3,11 @@ "target": "ESNext", "useDefineForClassFields": true, "lib": ["DOM", "DOM.Iterable", "ESNext"], + "types": ["node"], "allowJs": false, "skipLibCheck": true, "esModuleInterop": false, + "ignoreDeprecations": "6.0", "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true,