diff --git a/internal/orchestrator/taskbackup.go b/internal/orchestrator/taskbackup.go index ae4abeef..e4f7678d 100644 --- a/internal/orchestrator/taskbackup.go +++ b/internal/orchestrator/taskbackup.go @@ -190,7 +190,6 @@ func backupHelper(ctx context.Context, t Task, orchestrator *Orchestrator, plan orchestrator.ScheduleTask(NewOneoffForgetTask(orchestrator, plan, op.SnapshotId, at), TaskPriorityForget) } orchestrator.ScheduleTask(NewOneoffIndexSnapshotsTask(orchestrator, plan.Repo, at), TaskPriorityIndexSnapshots) - orchestrator.ScheduleTask(NewOneoffStatsTask(orchestrator, plan, op.SnapshotId, at), TaskPriorityStats) return nil } diff --git a/internal/orchestrator/taskcollectgarbage.go b/internal/orchestrator/taskcollectgarbage.go index 792a0b7c..5e75c10f 100644 --- a/internal/orchestrator/taskcollectgarbage.go +++ b/internal/orchestrator/taskcollectgarbage.go @@ -18,6 +18,8 @@ const ( // - it has a forgotten snapshot associated with it gcHistoryAge = 30 * 24 * time.Hour gcHistoryMaxCount = 1000 + // keep stats operations for 1 year (they're small and useful for long term trends) + gcHistoryStatsAge = 365 * 24 * time.Hour ) type CollectGarbageTask struct { @@ -72,9 +74,11 @@ func (t *CollectGarbageTask) gcOperations() error { operationsByPlan := make(map[string][]gcOpInfo) if err := oplog.ForAll(func(op *v1.Operation) error { if op.SnapshotId == "" || snapshotIsForgotten[op.SnapshotId] { + _, isStats := op.Op.(*v1.Operation_OperationStats) operationsByPlan[op.PlanId] = append(operationsByPlan[op.PlanId], gcOpInfo{ id: op.Id, timestamp: op.UnixTimeStartMs, + isStats: isStats, }) } return nil @@ -94,7 +98,11 @@ func (t *CollectGarbageTask) gcOperations() error { // check if each operation timestamp is old. for _, opInfo := range opInfos { - if curTime-opInfo.timestamp > gcHistoryAge.Milliseconds() { + maxAgeForType := gcHistoryAge.Milliseconds() + if opInfo.isStats { + maxAgeForType = gcHistoryStatsAge.Milliseconds() + } + if curTime-opInfo.timestamp > maxAgeForType { gcOps = append(gcOps, opInfo.id) } } @@ -122,4 +130,5 @@ func (t *CollectGarbageTask) OperationId() int64 { type gcOpInfo struct { id int64 // operation ID timestamp int64 // unix time milliseconds + isStats bool // true if this is a stats operation } diff --git a/internal/orchestrator/taskprune.go b/internal/orchestrator/taskprune.go index a5ab3712..5bfc1d3a 100644 --- a/internal/orchestrator/taskprune.go +++ b/internal/orchestrator/taskprune.go @@ -176,6 +176,9 @@ func (t *PruneTask) Run(ctx context.Context) error { }) return err } + + t.orch.ScheduleTask(NewOneoffStatsTask(t.orch, t.plan, time.Now()), TaskPriorityStats) + return nil } diff --git a/internal/orchestrator/taskstats.go b/internal/orchestrator/taskstats.go index 9efec95c..75ccaca7 100644 --- a/internal/orchestrator/taskstats.go +++ b/internal/orchestrator/taskstats.go @@ -8,32 +8,25 @@ import ( v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/hook" - "github.com/garethgeorge/backrest/internal/oplog" - "github.com/garethgeorge/backrest/internal/oplog/indexutil" "go.uber.org/zap" ) -var statBytesThreshold int64 = 10 * 1024 * 1024 * 1024 // 10 GB added. -var statOperationsThreshold int = 100 // run a stat command every 100 operations. - // StatsTask tracks a restic stats operation. type StatsTask struct { TaskWithOperation - plan *v1.Plan - linkSnapshot string // snapshot to link the task to (if any) - at *time.Time + plan *v1.Plan + at *time.Time } var _ Task = &StatsTask{} -func NewOneoffStatsTask(orchestrator *Orchestrator, plan *v1.Plan, linkSnapshot string, at time.Time) *StatsTask { +func NewOneoffStatsTask(orchestrator *Orchestrator, plan *v1.Plan, at time.Time) *StatsTask { return &StatsTask{ TaskWithOperation: TaskWithOperation{ orch: orchestrator, }, - plan: plan, - at: &at, - linkSnapshot: linkSnapshot, + plan: plan, + at: &at, } } @@ -41,58 +34,14 @@ func (t *StatsTask) Name() string { return fmt.Sprintf("stats for plan %q", t.plan.Id) } -func (t *StatsTask) shouldRun() (bool, error) { - var bytesSinceLastStat int64 = -1 - var howFarBack int = 0 - if err := t.orch.OpLog.ForEachByRepo(t.plan.Repo, indexutil.Reversed(indexutil.CollectAll()), func(op *v1.Operation) error { - if op.Status == v1.OperationStatus_STATUS_PENDING || op.Status == v1.OperationStatus_STATUS_INPROGRESS { - return nil - } - howFarBack++ - if _, ok := op.Op.(*v1.Operation_OperationStats); ok { - if bytesSinceLastStat == -1 { - bytesSinceLastStat = 0 - } - return oplog.ErrStopIteration - } else if backup, ok := op.Op.(*v1.Operation_OperationBackup); ok && backup.OperationBackup.LastStatus != nil { - if summary, ok := backup.OperationBackup.LastStatus.Entry.(*v1.BackupProgressEntry_Summary); ok { - bytesSinceLastStat += summary.Summary.DataAdded - } - } - return nil - }); err != nil { - return false, fmt.Errorf("iterate oplog: %w", err) - } - - zap.L().Debug("distance since last stat", zap.Int64("bytes", bytesSinceLastStat), zap.String("repo", t.plan.Repo), zap.Int("opsBack", howFarBack)) - if howFarBack >= statOperationsThreshold { - zap.S().Debugf("distance since last stat (%v) is exceeds threshold (%v)", howFarBack, statOperationsThreshold) - return true, nil - } - if bytesSinceLastStat == -1 || bytesSinceLastStat > statBytesThreshold { - zap.S().Debugf("bytes since last stat (%v) exceeds threshold (%v)", bytesSinceLastStat, statBytesThreshold) - return true, nil - } - return false, nil -} - func (t *StatsTask) Next(now time.Time) *time.Time { ret := t.at if ret != nil { t.at = nil - shouldRun, err := t.shouldRun() - if err != nil { - zap.S().Errorf("task %v failed to check if it should run: %v", t.Name(), err) - } - if !shouldRun { - return nil - } - if err := t.setOperation(&v1.Operation{ PlanId: t.plan.Id, RepoId: t.plan.Repo, - SnapshotId: t.linkSnapshot, UnixTimeStartMs: timeToUnixMillis(*ret), Status: v1.OperationStatus_STATUS_PENDING, Op: &v1.Operation_OperationStats{}, diff --git a/webui/package-lock.json b/webui/package-lock.json index 297feb9b..6894f9c8 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -13,6 +13,9 @@ "@bufbuild/protobuf": "^1.6.0", "@connectrpc/connect": "^1.2.0", "@connectrpc/connect-web": "^1.2.0", + "@emotion/styled": "^11.11.0", + "@mui/material": "^5.15.11", + "@mui/x-charts": "^6.19.5", "@types/lodash": "^4.14.202", "@types/node": "^20.9.0", "@types/react": "^18.2.37", @@ -168,6 +171,25 @@ "node": ">=4" } }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", @@ -256,6 +278,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@bufbuild/protobuf": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.7.2.tgz", @@ -286,16 +321,218 @@ "node": ">=10" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "node_modules/@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/react": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", + "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz", + "integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/serialize/node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/serialize/node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "node_modules/@emotion/styled": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", + "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.1", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@emotion/unitless": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", + "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "dependencies": { + "@floating-ui/dom": "^1.6.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -533,6 +770,261 @@ "win32" ] }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.37", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.37.tgz", + "integrity": "sha512-/o3anbb+DeCng8jNsd3704XtmmLDZju1Fo8R2o7ugrVtPQ/QpcqddwKNzKPZwa0J5T8YNW3ZVuHyQgbTnQLisQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@floating-ui/react-dom": "^2.0.8", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.11", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.15.11", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.11.tgz", + "integrity": "sha512-JVrJ9Jo4gyU707ujnRzmE8ABBWpXd6FwL9GYULmwZRtfPg89ggXs/S3MStQkpJ1JRWfdLL6S5syXmgQGq5EDAw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/material": { + "version": "5.15.11", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.11.tgz", + "integrity": "sha512-FA3eEuEZaDaxgN3CgfXezMWbCZ4VCeU/sv0F0/PK5n42qIgsPVD6q+j71qS7/62sp6wRFMHtDMpXRlN+tT/7NA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.37", + "@mui/core-downloads-tracker": "^5.15.11", + "@mui/system": "^5.15.11", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.11", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "5.15.11", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.11.tgz", + "integrity": "sha512-jY/696SnSxSzO1u86Thym7ky5T9CgfidU3NFJjguldqK4f3Z5S97amZ6nffg8gTD0HBjY9scB+4ekqDEUmxZOA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.15.11", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.15.11", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.11.tgz", + "integrity": "sha512-So21AhAngqo07ces4S/JpX5UaMU2RHXpEA6hNzI6IQjd/1usMPxpgK8wkGgTe3JKmC2KDmH8cvoycq5H3Ii7/w==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.15.11", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.11.tgz", + "integrity": "sha512-9j35suLFq+MgJo5ktVSHPbkjDLRMBCV17NMBdEQurh6oWyGnLM4uhU4QGZZQ75o0vuhjJghOCA1jkO3+79wKsA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.15.11", + "@mui/styled-engine": "^5.15.11", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.11", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.13", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz", + "integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.15.11", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.11.tgz", + "integrity": "sha512-D6bwqprUa9Stf8ft0dcMqWyWDKEo7D+6pB1k8WajbqlYIRA8J8Kw9Ra7PSZKKePGBGWO+/xxrX1U8HpG/aXQCw==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@types/prop-types": "^15.7.11", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-charts": { + "version": "6.19.5", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-6.19.5.tgz", + "integrity": "sha512-BBRGLup5gpaLkhECv+J2ahFbDDgqK4BgLyLXLHKUASoWSU3YRCyDt9ifBREspEPfTZXgrcqNkybAl5b+l6baFQ==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@mui/base": "^5.0.0-beta.22", + "@react-spring/rafz": "^9.7.3", + "@react-spring/web": "^9.7.3", + "clsx": "^2.0.0", + "d3-color": "^3.1.0", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.4.1", + "@mui/system": "^5.4.1", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, "node_modules/@parcel/bundler-default": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/@parcel/bundler-default/-/bundler-default-2.11.0.tgz", @@ -867,6 +1359,69 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@parcel/optimizer-htmlnano/node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/@parcel/optimizer-htmlnano/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@parcel/optimizer-htmlnano/node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@parcel/optimizer-htmlnano/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "node_modules/@parcel/optimizer-htmlnano/node_modules/svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@parcel/optimizer-image": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/@parcel/optimizer-image/-/optimizer-image-2.11.0.tgz", @@ -909,6 +1464,69 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@parcel/optimizer-svgo/node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/@parcel/optimizer-svgo/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@parcel/optimizer-svgo/node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@parcel/optimizer-svgo/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "node_modules/@parcel/optimizer-svgo/node_modules/svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@parcel/optimizer-swc": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/@parcel/optimizer-swc/-/optimizer-swc-2.11.0.tgz", @@ -1893,6 +2511,15 @@ "node": ">=14" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@rc-component/color-picker": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-1.5.2.tgz", @@ -2005,6 +2632,71 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@react-spring/animated": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz", + "integrity": "sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==", + "dependencies": { + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.3.tgz", + "integrity": "sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==", + "dependencies": { + "@react-spring/animated": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.3.tgz", + "integrity": "sha512-9vzW1zJPcC4nS3aCV+GgcsK/WLaB520Iyvm55ARHfM5AuyBqycjvh1wbmWmgCyJuX4VPoWigzemq1CaaeRSHhQ==" + }, + "node_modules/@react-spring/shared": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz", + "integrity": "sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==", + "dependencies": { + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.3.tgz", + "integrity": "sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw==" + }, + "node_modules/@react-spring/web": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.3.tgz", + "integrity": "sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==", + "dependencies": { + "@react-spring/animated": "~9.7.3", + "@react-spring/core": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@swc/core": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.2.tgz", @@ -2231,6 +2923,11 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, "node_modules/@types/prop-types": { "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", @@ -2254,6 +2951,14 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", @@ -2376,6 +3081,35 @@ "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-macros/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2595,6 +3329,14 @@ "node": ">=0.8" } }, + "node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2624,6 +3366,11 @@ "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==" }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, "node_modules/copy-to-clipboard": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", @@ -2690,30 +3437,93 @@ } }, "node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "optional": true, + "peer": true, "dependencies": { "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", "nth-check": "^2.0.1" }, "funding": { "url": "https://github.com/sponsors/fb55" } }, - "node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "node_modules/css-select/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "optional": true, + "peer": true, "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/css-select/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "optional": true, + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0" }, "engines": { - "node": ">=8.0.0" + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/css-select/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "optional": true, + "peer": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/css-select/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "optional": true, + "peer": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, "node_modules/css-what": { @@ -2728,21 +3538,140 @@ } }, "node_modules/csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "optional": true, + "peer": true, "dependencies": { - "css-tree": "^1.1.2" + "css-tree": "~2.2.0" }, "engines": { - "node": ">=8.0.0" + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" } }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "optional": true, + "peer": true, + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "optional": true, + "peer": true + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/dayjs": { "version": "1.11.10", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", @@ -2759,6 +3688,15 @@ "node": ">=0.10" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -2902,6 +3840,11 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -2932,6 +3875,14 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-port": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz", @@ -2996,6 +3947,32 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "peer": true, + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "peer": true + }, "node_modules/htmlnano": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-2.1.0.tgz", @@ -3106,6 +4083,14 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -3123,6 +4108,17 @@ "node": ">=8" } }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3486,9 +4482,11 @@ } }, "node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "optional": true, + "peer": true }, "node_modules/micromatch": { "version": "4.0.5", @@ -3526,6 +4524,16 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "optional": true, + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/msgpackr": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.10.1.tgz", @@ -3625,6 +4633,14 @@ "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ordered-binary": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.1.tgz", @@ -3698,6 +4714,11 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, "node_modules/path-scurry": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", @@ -3794,6 +4815,21 @@ "node": ">= 0.6.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/qrcode.react": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-3.1.0.tgz", @@ -4425,6 +5461,21 @@ "node": ">=0.10.0" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -4461,6 +5512,22 @@ "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4598,7 +5665,7 @@ "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, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -4706,24 +5773,41 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz", + "integrity": "sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==", + "optional": true, + "peer": true, "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", - "css-select": "^4.1.3", - "css-tree": "^1.1.3", - "csso": "^4.2.0", - "picocolors": "^1.0.0", - "stable": "^0.1.8" + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" }, "bin": { "svgo": "bin/svgo" }, "engines": { - "node": ">=10.13.0" + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" } }, "node_modules/term-size": { @@ -4750,6 +5834,14 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==" }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4957,6 +6049,14 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } } } } diff --git a/webui/package.json b/webui/package.json index 1369fa1e..37c36b95 100644 --- a/webui/package.json +++ b/webui/package.json @@ -17,6 +17,9 @@ "@bufbuild/protobuf": "^1.6.0", "@connectrpc/connect": "^1.2.0", "@connectrpc/connect-web": "^1.2.0", + "@emotion/styled": "^11.11.0", + "@mui/material": "^5.15.11", + "@mui/x-charts": "^6.19.5", "@types/lodash": "^4.14.202", "@types/node": "^20.9.0", "@types/react": "^18.2.37", diff --git a/webui/src/components/OperationList.tsx b/webui/src/components/OperationList.tsx index 636a275b..d6b3110a 100644 --- a/webui/src/components/OperationList.tsx +++ b/webui/src/components/OperationList.tsx @@ -3,50 +3,22 @@ import { Operation, OperationEvent, OperationEventType, - OperationForget, - OperationRunHook, - OperationStatus, } from "../../gen/ts/v1/operations_pb"; import { - Button, - Col, - Collapse, Empty, List, - Progress, - Row, - Typography, } from "antd"; -import { - PaperClipOutlined, - SaveOutlined, - DeleteOutlined, - DownloadOutlined, - RobotOutlined, -} from "@ant-design/icons"; -import { BackupProgressEntry, ResticSnapshot } from "../../gen/ts/v1/restic_pb"; import { BackupInfo, BackupInfoCollector, - DisplayType, - detailsForOperation, - displayTypeToString, getOperations, - getTypeForDisplay, subscribeToOperations, unsubscribeFromOperations, } from "../state/oplog"; -import { SnapshotBrowser } from "./SnapshotBrowser"; -import { - formatBytes, - formatTime, - normalizeSnapshotId, -} from "../lib/formatting"; import _ from "lodash"; -import { GetOperationsRequest, LogDataRequest } from "../../gen/ts/v1/service_pb"; +import { GetOperationsRequest } from "../../gen/ts/v1/service_pb"; import { useAlertApi } from "./Alerts"; -import { MessageInstance } from "antd/es/message/interface"; -import { backrestService } from "../api"; +import { OperationRow } from "./OperationRow"; // OperationList displays a list of operations that are either fetched based on 'req' or passed in via 'useBackups'. // If showPlan is provided the planId will be displayed next to each operation in the operation list. @@ -59,7 +31,7 @@ export const OperationList = ({ req?: GetOperationsRequest; useBackups?: BackupInfo[]; showPlan?: boolean, - filter?: (op: Operation) => boolean, + filter?: (op: Operation) => boolean, // if provided, only operations that pass this filter will be displayed. }>) => { const alertApi = useAlertApi(); @@ -74,7 +46,7 @@ export const OperationList = ({ return; } - const backupCollector = new BackupInfoCollector(); + const backupCollector = new BackupInfoCollector(filter); const lis = (opEvent: OperationEvent) => { if (!!req.planId && opEvent.operation!.planId !== req.planId) { return; @@ -91,7 +63,7 @@ export const OperationList = ({ subscribeToOperations(lis); backupCollector.subscribe(_.debounce(() => { - let backups = backupCollector.getAll(false); + let backups = backupCollector.getAll(); backups.sort((a, b) => { return b.startTimeMs - a.startTimeMs; }); @@ -122,10 +94,7 @@ export const OperationList = ({ ); } - let operations = backups.flatMap((b) => b.operations); - if (filter) { - operations = operations.filter(filter); - } + let operations = backups.flatMap((b) => b.operations) operations.sort((a, b) => { return Number(b.unixTimeStartMs - a.unixTimeStartMs) }); @@ -146,385 +115,3 @@ export const OperationList = ({ ); }; -export const OperationRow = ({ - operation, - alertApi, - showPlan, -}: React.PropsWithoutRef<{ operation: Operation, alertApi?: MessageInstance, showPlan: boolean }>) => { - const details = detailsForOperation(operation); - const displayType = getTypeForDisplay(operation); - let avatar: React.ReactNode; - switch (displayType) { - case DisplayType.BACKUP: - avatar = ( - - ); - break; - case DisplayType.FORGET: - avatar = ( - - ); - break; - case DisplayType.SNAPSHOT: - avatar = ; - break; - case DisplayType.RESTORE: - avatar = ; - break; - case DisplayType.PRUNE: - avatar = ; - break; - case DisplayType.RUNHOOK: - avatar = ; - - } - - const opName = displayTypeToString(getTypeForDisplay(operation)); - let title = ( - <> - {showPlan ? operation.planId + " - " : undefined} {formatTime(Number(operation.unixTimeStartMs))} - {opName}{" "} - {details.displayState} - - ); - - if (operation.status === OperationStatus.STATUS_PENDING || operation.status == OperationStatus.STATUS_INPROGRESS) { - title = <> - {title} - - - } - - let body: React.ReactNode | undefined; - - if (operation.op.case === "operationBackup") { - const backupOp = operation.op.value; - const items: { key: number, label: string, children: React.ReactNode }[] = [ - { - key: 1, - label: "Backup Details", - children: , - }, - ]; - - if (backupOp.errors.length > 0) { - items.splice(0, 0, { - key: 2, - label: "Item Errors", - children:
{backupOp.errors.map(e => "Error on item: " + e.item).join("\n")}
, - }); - } - - body = ( - <> - - - ); - } else if (operation.op.case === "operationIndexSnapshot") { - const snapshotOp = operation.op.value; - body = ( - - ); - } else if (operation.op.case === "operationForget") { - const forgetOp = operation.op.value; - body = - } else if (operation.op.case === "operationPrune") { - const prune = operation.op.value; - body = ( - {prune.output}, - }, - ]} - /> - ); - } else if (operation.op.case === "operationRestore") { - const restore = operation.op.value; - body = ( - <> - Restore {restore.path} to {restore.target} - {details.percentage !== undefined ? ( - - ) : null} - - ); - } else if (operation.op.case === "operationRunHook") { - const hook = operation.op.value; - body = - } - - if (operation.displayMessage) { - body = ( - <> -
{details.state}: {operation.displayMessage}
- {body} - - ); - } - - return ( - - - - ); -}; - -const SnapshotInfo = ({ - snapshot, - repoId, - planId, -}: { - snapshot: ResticSnapshot; - repoId: string; - planId?: string; -}) => { - return ( - - - Snapshot ID: - {normalizeSnapshotId(snapshot.id!)} - - - - Host -
- {snapshot.hostname} - - - Username -
- {snapshot.hostname} - - - Tags -
- {snapshot.tags?.join(", ")} - -
- - ), - }, - { - key: 2, - label: "Browse and Restore Files in Backup", - children: ( - - ), - }, - ]} - /> - ); -}; - -const BackupOperationStatus = ({ - status, -}: { - status?: BackupProgressEntry; -}) => { - if (!status) { - return <>No status yet.; - } - - if (status.entry.case === "status") { - const st = status.entry.value; - const progress = - Math.round( - (Number(st.bytesDone) / Math.max(Number(st.totalBytes), 1)) * 1000 - ) / 10; - return ( - <> - -
- - - Bytes Done/Total -
- {formatBytes(Number(st.bytesDone))}/{formatBytes(Number(st.totalBytes))} - - - Files Done/Total -
- {Number(st.filesDone)}/{Number(st.totalFiles)} - -
- {st.currentFile && st.currentFile.length > 0 ? ( -
Current file: {st.currentFile.join("\n")}
- ) : null} - - ); - } else if (status.entry.case === "summary") { - const sum = status.entry.value; - return ( - <> - - Snapshot ID: - {normalizeSnapshotId(sum.snapshotId!)} - - - - Files Added -
- {sum.filesNew.toString()} - - - Files Changed -
- {sum.filesChanged.toString()} - - - Files Unmodified -
- {sum.filesUnmodified.toString()} - -
- - - Bytes Added -
- {formatBytes(Number(sum.dataAdded))} - - - Total Bytes Processed -
- {formatBytes(Number(sum.totalBytesProcessed))} - - - Total Files Processed -
- {sum.totalFilesProcessed.toString()} - -
- - ); - } else { - console.error("GOT UNEXPECTED STATUS: ", status); - return <>No fields set. This shouldn't happen; - } -}; - -const ForgetOperationDetails = ({ forgetOp }: { forgetOp: OperationForget }) => { - const policy = forgetOp.policy! || {}; - const policyDesc = []; - if (policy.keepLastN) { - policyDesc.push(`Keep Last ${policy.keepLastN} Snapshots`); - } - if (policy.keepHourly) { - policyDesc.push(`Keep Hourly for ${policy.keepHourly} Hours`); - } - if (policy.keepDaily) { - policyDesc.push(`Keep Daily for ${policy.keepDaily} Days`); - } - if (policy.keepWeekly) { - policyDesc.push(`Keep Weekly for ${policy.keepWeekly} Weeks`); - } - if (policy.keepMonthly) { - policyDesc.push(`Keep Monthly for ${policy.keepMonthly} Months`); - } - if (policy.keepYearly) { - policyDesc.push(`Keep Yearly for ${policy.keepYearly} Years`); - } - - return ( - - Removed snapshots: -
{forgetOp.forget?.map((f) => (
-              
- {"removed snapshot " + normalizeSnapshotId(f.id!) + " taken at " + formatTime(Number(f.unixTimeMs))}
-
- ))}
- Policy: -
    - {policyDesc.map((desc, idx) => ( -
  • {desc}
  • - ))} -
- , - }, - ]} - /> - ); -} - -const RunHookOperationStatus = ({ op }: { op: Operation }) => { - if (op.op.case !== "operationRunHook") { - return <>Wrong operation type; - } - - const hook = op.op.value; - - return <> - - - - }, - ]} /> - -} - -// TODO: refactor this to use the provider pattern -const BigOperationDataVerbatim = ({ logref }: { logref: string }) => { - const [output, setOutput] = useState(undefined); - - useEffect(() => { - if (!logref) { - return; - } - backrestService.getLogs(new LogDataRequest({ - ref: logref, - })).then((resp) => { - setOutput(new TextDecoder("utf-8").decode(resp.value)); - }).catch((e) => { - console.error("Failed to fetch hook output: ", e); - }); - }, [logref]); - - return
{output}
; -} \ No newline at end of file diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx new file mode 100644 index 00000000..63a60632 --- /dev/null +++ b/webui/src/components/OperationRow.tsx @@ -0,0 +1,431 @@ +import React, { useEffect, useState } from "react"; +import { + Operation, + OperationEvent, + OperationEventType, + OperationForget, + OperationRunHook, + OperationStatus, +} from "../../gen/ts/v1/operations_pb"; +import { + Button, + Col, + Collapse, + Empty, + List, + Progress, + Row, + Typography, +} from "antd"; +import { + PaperClipOutlined, + SaveOutlined, + DeleteOutlined, + DownloadOutlined, + RobotOutlined, + InfoCircleOutlined, +} from "@ant-design/icons"; +import { BackupProgressEntry, ResticSnapshot } from "../../gen/ts/v1/restic_pb"; +import { + DisplayType, + detailsForOperation, + displayTypeToString, + getTypeForDisplay, +} from "../state/oplog"; +import { SnapshotBrowser } from "./SnapshotBrowser"; +import { + formatBytes, + formatTime, + normalizeSnapshotId, +} from "../lib/formatting"; +import _ from "lodash"; +import { LogDataRequest } from "../../gen/ts/v1/service_pb"; +import { MessageInstance } from "antd/es/message/interface"; +import { backrestService } from "../api"; + + +export const OperationRow = ({ + operation, + alertApi, + showPlan, +}: React.PropsWithoutRef<{ operation: Operation, alertApi?: MessageInstance, showPlan: boolean }>) => { + const details = detailsForOperation(operation); + const displayType = getTypeForDisplay(operation); + let avatar: React.ReactNode; + switch (displayType) { + case DisplayType.BACKUP: + avatar = ( + + ); + break; + case DisplayType.FORGET: + avatar = ( + + ); + break; + case DisplayType.SNAPSHOT: + avatar = ; + break; + case DisplayType.RESTORE: + avatar = ; + break; + case DisplayType.PRUNE: + avatar = ; + break; + case DisplayType.RUNHOOK: + avatar = ; + break; + case DisplayType.STATS: + avatar = ; + break; + } + + const opName = displayTypeToString(getTypeForDisplay(operation)); + let title = ( + <> + {showPlan ? operation.planId + " - " : undefined} {formatTime(Number(operation.unixTimeStartMs))} - {opName}{" "} + {details.displayState} + + ); + + if (operation.status === OperationStatus.STATUS_PENDING || operation.status == OperationStatus.STATUS_INPROGRESS) { + title = <> + {title} + + + } + + let body: React.ReactNode | undefined; + + if (operation.op.case === "operationBackup") { + const backupOp = operation.op.value; + const items: { key: number, label: string, children: React.ReactNode }[] = [ + { + key: 1, + label: "Backup Details", + children: , + }, + ]; + + if (backupOp.errors.length > 0) { + items.splice(0, 0, { + key: 2, + label: "Item Errors", + children:
{backupOp.errors.map(e => "Error on item: " + e.item).join("\n")}
, + }); + } + + body = ( + <> + + + ); + } else if (operation.op.case === "operationIndexSnapshot") { + const snapshotOp = operation.op.value; + body = ( + + ); + } else if (operation.op.case === "operationForget") { + const forgetOp = operation.op.value; + body = + } else if (operation.op.case === "operationPrune") { + const prune = operation.op.value; + body = ( + {prune.output}, + }, + ]} + /> + ); + } else if (operation.op.case === "operationRestore") { + const restore = operation.op.value; + body = ( + <> + Restore {restore.path} to {restore.target} + {details.percentage !== undefined ? ( + + ) : null} + + ); + } else if (operation.op.case === "operationRunHook") { + const hook = operation.op.value; + body = + } + + if (operation.displayMessage) { + body = ( + <> +
{details.state}: {operation.displayMessage}
+ {body} + + ); + } + + return ( + + + + ); +}; + +const SnapshotInfo = ({ + snapshot, + repoId, + planId, +}: { + snapshot: ResticSnapshot; + repoId: string; + planId?: string; +}) => { + return ( + + + Snapshot ID: + {normalizeSnapshotId(snapshot.id!)} + + + + Host +
+ {snapshot.hostname} + + + Username +
+ {snapshot.hostname} + + + Tags +
+ {snapshot.tags?.join(", ")} + +
+ + ), + }, + { + key: 2, + label: "Browse and Restore Files in Backup", + children: ( + + ), + }, + ]} + /> + ); +}; + +const BackupOperationStatus = ({ + status, +}: { + status?: BackupProgressEntry; +}) => { + if (!status) { + return <>No status yet.; + } + + if (status.entry.case === "status") { + const st = status.entry.value; + const progress = + Math.round( + (Number(st.bytesDone) / Math.max(Number(st.totalBytes), 1)) * 1000 + ) / 10; + return ( + <> + +
+ + + Bytes Done/Total +
+ {formatBytes(Number(st.bytesDone))}/{formatBytes(Number(st.totalBytes))} + + + Files Done/Total +
+ {Number(st.filesDone)}/{Number(st.totalFiles)} + +
+ {st.currentFile && st.currentFile.length > 0 ? ( +
Current file: {st.currentFile.join("\n")}
+ ) : null} + + ); + } else if (status.entry.case === "summary") { + const sum = status.entry.value; + return ( + <> + + Snapshot ID: + {normalizeSnapshotId(sum.snapshotId!)} + + + + Files Added +
+ {sum.filesNew.toString()} + + + Files Changed +
+ {sum.filesChanged.toString()} + + + Files Unmodified +
+ {sum.filesUnmodified.toString()} + +
+ + + Bytes Added +
+ {formatBytes(Number(sum.dataAdded))} + + + Total Bytes Processed +
+ {formatBytes(Number(sum.totalBytesProcessed))} + + + Total Files Processed +
+ {sum.totalFilesProcessed.toString()} + +
+ + ); + } else { + console.error("GOT UNEXPECTED STATUS: ", status); + return <>No fields set. This shouldn't happen; + } +}; + +const ForgetOperationDetails = ({ forgetOp }: { forgetOp: OperationForget }) => { + const policy = forgetOp.policy! || {}; + const policyDesc = []; + if (policy.keepLastN) { + policyDesc.push(`Keep Last ${policy.keepLastN} Snapshots`); + } + if (policy.keepHourly) { + policyDesc.push(`Keep Hourly for ${policy.keepHourly} Hours`); + } + if (policy.keepDaily) { + policyDesc.push(`Keep Daily for ${policy.keepDaily} Days`); + } + if (policy.keepWeekly) { + policyDesc.push(`Keep Weekly for ${policy.keepWeekly} Weeks`); + } + if (policy.keepMonthly) { + policyDesc.push(`Keep Monthly for ${policy.keepMonthly} Months`); + } + if (policy.keepYearly) { + policyDesc.push(`Keep Yearly for ${policy.keepYearly} Years`); + } + + return ( + + Removed snapshots: +
{forgetOp.forget?.map((f) => (
+              
+ {"removed snapshot " + normalizeSnapshotId(f.id!) + " taken at " + formatTime(Number(f.unixTimeMs))}
+
+ ))}
+ Policy: +
    + {policyDesc.map((desc, idx) => ( +
  • {desc}
  • + ))} +
+ , + }, + ]} + /> + ); +} + +const RunHookOperationStatus = ({ op }: { op: Operation }) => { + if (op.op.case !== "operationRunHook") { + return <>Wrong operation type; + } + + const hook = op.op.value; + + return <> + + + + }, + ]} /> + +} + +// TODO: refactor this to use the provider pattern +const BigOperationDataVerbatim = ({ logref }: { logref: string }) => { + const [output, setOutput] = useState(undefined); + + useEffect(() => { + if (!logref) { + return; + } + backrestService.getLogs(new LogDataRequest({ + ref: logref, + })).then((resp) => { + setOutput(new TextDecoder("utf-8").decode(resp.value)); + }).catch((e) => { + console.error("Failed to fetch hook output: ", e); + }); + }, [logref]); + + return
{output}
; +} diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index 894f2515..ecfaf5f3 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -79,7 +79,7 @@ export const OperationTree = ({ backupCollector.bulkAddOperations(ops); }) .catch((e) => { - alertApi!.error("Failed to fetch operations: " + e.message); + alertApi!.error("Failed to fetch operations: " + e.messag); }); return () => { unsubscribeFromOperations(lis); @@ -314,7 +314,7 @@ const BackupView = ({ backup }: { backup?: BackupInfo }) => { {backup.status !== OperationStatus.STATUS_PENDING && backup.status != OperationStatus.STATUS_INPROGRESS ? deleteButton : null} - op && !shouldHideOperation(op)} /> + ; } } \ No newline at end of file diff --git a/webui/src/index.tsx b/webui/src/index.tsx index 27457f7c..92d58ea1 100644 --- a/webui/src/index.tsx +++ b/webui/src/index.tsx @@ -8,6 +8,7 @@ import "react-js-cron/dist/styles.css"; import { ConfigProvider as AntdConfigProvider, theme } from "antd"; import { ConfigContextProvider } from "./components/ConfigProvider"; import { MainContentProvider } from "./views/MainContentArea"; +import { ThemeProvider, createTheme } from "@mui/material"; const Root = ({ children }: { children: React.ReactNode }) => { return ( @@ -34,8 +35,14 @@ el && ], }} > - - - + + + + + ); diff --git a/webui/src/lib/formatting.ts b/webui/src/lib/formatting.ts index 188d6d8f..c4c70c58 100644 --- a/webui/src/lib/formatting.ts +++ b/webui/src/lib/formatting.ts @@ -1,6 +1,6 @@ export const formatBytes = (bytes?: number | string) => { if (!bytes) { - return 0; + return "0B"; } if (typeof bytes === "string") { bytes = parseInt(bytes); diff --git a/webui/src/state/oplog.ts b/webui/src/state/oplog.ts index e27413b4..d0d93407 100644 --- a/webui/src/state/oplog.ts +++ b/webui/src/state/oplog.ts @@ -123,7 +123,6 @@ export interface BackupInfo { backupLastStatus?: BackupProgressEntry; snapshotInfo?: ResticSnapshot; forgotten: boolean; - hidden: boolean; } // BackupInfoCollector maps multiple operations to single aggregate 'BackupInfo' objects. @@ -136,6 +135,12 @@ export class BackupInfoCollector { private backupByOpId: Map = new Map(); private backupBySnapshotId: Map = new Map(); + /** + * + * @param filter a function that returns true if an operation should be displayed, false otherwise. + */ + constructor(private filter: (op: Operation) => boolean = (op) => !shouldHideOperation(op)) { } + private createBackup(operations: Operation[]): BackupInfo { // deduplicate and sort operations. operations.sort((a, b) => { @@ -155,7 +160,7 @@ export class BackupInfoCollector { displayType = getTypeForDisplay(operations[0]); } - // use the latest status that is not cancelled. + // use the latest status that is not a hidden status let statusIdx = operations.length - 1; let status = OperationStatus.STATUS_SYSTEM_CANCELLED; while (statusIdx !== -1) { @@ -174,7 +179,6 @@ export class BackupInfoCollector { let backupLastStatus = undefined; let snapshotInfo = undefined; let forgotten = false; - let hidden = true; for (const op of operations) { if (op.op.case === "operationBackup") { backupLastStatus = op.op.value.lastStatus; @@ -182,9 +186,6 @@ export class BackupInfoCollector { snapshotInfo = op.op.value.snapshot; forgotten = op.op.value.forgot || false; } - if (hidden && !shouldHideOperation(op)) { - hidden = false; - } } return { @@ -198,7 +199,6 @@ export class BackupInfoCollector { backupLastStatus, snapshotInfo, forgotten, - hidden, snapshotId: operations[0].snapshotId, planId: operations[0].planId, repoId: operations[0].repoId, @@ -233,7 +233,10 @@ export class BackupInfoCollector { } } - public addOperation(event: OperationEventType, op: Operation): BackupInfo { + public addOperation(event: OperationEventType, op: Operation): BackupInfo | null { + if (!this.filter(op)) { + return null; + } const backupInfo = this.addHelper(op); this.listeners.forEach((l) => l(event, [backupInfo])); return backupInfo; @@ -253,6 +256,7 @@ export class BackupInfoCollector { } public bulkAddOperations(ops: Operation[]): BackupInfo[] { + ops = ops.filter(this.filter); let grouped = _.groupBy(ops, (op) => op.snapshotId ? op.snapshotId : op.id ); @@ -284,17 +288,12 @@ export class BackupInfoCollector { return info; } - public getAll(filter: boolean = true): BackupInfo[] { + public getAll(): BackupInfo[] { const arr = [ ...this.backupByOpId.values(), ...this.backupBySnapshotId.values(), ]; - if (!filter) { - return arr.filter((b) => !b.forgotten); - } - return arr.filter( - (b) => !b.forgotten && !b.hidden && !shouldHideStatus(b.status) - ); + return arr.filter((b) => !b.forgotten); } public subscribe( diff --git a/webui/src/views/PlanView.tsx b/webui/src/views/PlanView.tsx index 8c14cda4..65ccc95b 100644 --- a/webui/src/views/PlanView.tsx +++ b/webui/src/views/PlanView.tsx @@ -91,6 +91,7 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { /> ), + destroyInactiveTabPane: true, }, { key: "2", @@ -100,10 +101,11 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {

Backup Action History

shouldHideStatus(operation.status)} + filter={(op) => !shouldHideStatus(op.status)} /> ), + destroyInactiveTabPane: true, }, ]} /> diff --git a/webui/src/views/RepoView.tsx b/webui/src/views/RepoView.tsx index 98ef646d..5cd21de5 100644 --- a/webui/src/views/RepoView.tsx +++ b/webui/src/views/RepoView.tsx @@ -5,40 +5,22 @@ import { OperationList } from "../components/OperationList"; import { OperationTree } from "../components/OperationTree"; import { MAX_OPERATION_HISTORY, STATS_OPERATION_HISTORY } from "../constants"; import { GetOperationsRequest } from "../../gen/ts/v1/service_pb"; -import { getOperations } from "../state/oplog"; +import { BackupInfo, BackupInfoCollector, getOperations, shouldHideStatus } from "../state/oplog"; import { RepoStats } from "../../gen/ts/v1/restic_pb"; -import { formatBytes, formatTime } from "../lib/formatting"; -import { Operation } from "../../gen/ts/v1/operations_pb"; +import { formatBytes, formatDate, formatTime } from "../lib/formatting"; +import { Operation, OperationStats, OperationStatus } from "../../gen/ts/v1/operations_pb"; import { backrestService } from "../api"; import { StringValue } from "@bufbuild/protobuf"; import { SpinButton } from "../components/SpinButton"; import { ConfigContext } from "antd/es/config-provider"; import { useConfig } from "../components/ConfigProvider"; +import { useAlertApi } from "../components/Alerts"; +import { LineChart } from "@mui/x-charts"; + export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => { - const [loading, setLoading] = useState(true); - const [statsOperation, setStatsOperation] = useState(null); const [config, setConfig] = useConfig(); - useEffect(() => { - setLoading(true); - setStatsOperation(null); - getOperations(new GetOperationsRequest({ repoId: repo.id!, lastN: BigInt(STATS_OPERATION_HISTORY) })).then((operations) => { - for (const op of operations) { - if (op.op.case === "operationStats") { - const stats = op.op.value.stats; - if (stats) { - setStatsOperation(op); - } - } - } - }).catch((e) => { - console.error(e); - }).finally(() => { - setLoading(false); - }); - }, [repo.id]); - // Task handlers const handleIndexNow = async () => { await backrestService.indexSnapshots(new StringValue({ value: repo.id! })); @@ -56,23 +38,13 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => { } repo = repoInConfig; - if (loading) { - return ; - } - const items = [ { key: "1", label: "Stats", children: ( <> - {statsOperation === null ? : - <> -

Repo stats computed on {formatTime(Number(statsOperation.unixTimeStartMs))}

- {statsOperation.op.case === "operationStats" && } - Stats are refreshed periodically in the background as new data is added (e.g. every 10GB added or every 50 operations). - - } + ), destroyInactiveTabPane: true, @@ -99,6 +71,7 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => { !shouldHideStatus(op.status)} /> ), @@ -127,21 +100,132 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => { ); }; -const StatsTable = ({ stats }: { stats: RepoStats }) => { - return - -

Total Size:

-

Total Size Uncompressed:

-

Blob Count:

-

Snapshot Count:

-

Compression Ratio:

- - -

{formatBytes(Number(stats.totalSize))}

-

{formatBytes(Number(stats.totalUncompressedSize))}

-

{Number(stats.totalBlobCount)} blobs

-

{Number(stats.snapshotCount)} snapshots

-

{Math.round(stats.compressionRatio * 1000) / 1000}

- -
+const StatsPanel = ({ repoId }: { repoId: string }) => { + const [operations, setOperations] = useState([]); + const alertApi = useAlertApi(); + + useEffect(() => { + if (!repoId) { + return; + } + + const backupCollector = new BackupInfoCollector((op) => { + return op.status === OperationStatus.STATUS_SUCCESS && op.op.case === "operationStats" && !!op.op.value.stats + }); + + getOperations(new GetOperationsRequest({ repoId: repoId, lastN: BigInt(MAX_OPERATION_HISTORY) })) + .then((ops) => { + backupCollector.bulkAddOperations(ops); + + const operations = backupCollector.getAll().flatMap((b) => b.operations); + operations.sort((a, b) => { + return Number(b.unixTimeEndMs - a.unixTimeEndMs); + }); + setOperations(operations); + }) + .catch((e) => { + alertApi!.error("Failed to fetch operations: " + e.message); + }); + }, [repoId]); + + if (operations.length === 0) { + return + } + + const dataset: { + time: number, + totalSizeMb: number, + compressionRatio: number, + snapshotCount: number, + totalBlobCount: number, + }[] = operations.map((op) => { + const stats = (op.op.value! as OperationStats).stats!; + return { + time: Number(op.unixTimeEndMs!), + totalSizeMb: Number(stats.totalSize) / 1000000, + compressionRatio: Number(stats.compressionRatio), + snapshotCount: Number(stats.snapshotCount), + totalBlobCount: Number(stats.totalBlobCount), + } + }); + + const minTime = Math.min(...dataset.map((d) => d.time)); + const maxTime = Math.max(...dataset.map((d) => d.time)); + + return <> + + + formatDate(v as number), + min: minTime, + max: maxTime, + }]} + series={[ + { + dataKey: "totalSizeMb", + label: "Total Size (MB)", + valueFormatter: (v: any) => formatBytes(v * 1000000 as number), + }, + ]} + height={300} + dataset={dataset} + /> + + formatDate(v as number), + min: minTime, + max: maxTime, + }]} + series={[ + { + dataKey: "compressionRatio", + label: "Compression Ratio", + }, + ]} + height={300} + dataset={dataset} + /> + + + formatDate(v as number), + min: minTime, + max: maxTime, + }]} + series={[ + { + dataKey: "snapshotCount", + label: "Snapshot Count", + }, + ]} + height={300} + dataset={dataset} + /> + + formatDate(v as number), + min: minTime, + max: maxTime, + }]} + series={[ + { + dataKey: "totalBlobCount", + label: "Blob Count", + }, + ]} + height={300} + dataset={dataset} + /> + + + + } \ No newline at end of file