Ver Fonte

Merge branch 'develop' into dependabot/go_modules/go_modules-654f3690b0

Matt Ray há 2 anos atrás
pai
commit
ee6ce1f7f5
82 ficheiros alterados com 1269 adições e 10597 exclusões
  1. 2 2
      .github/ISSUE_TEMPLATE/opencost-bug-report.md
  2. 0 6
      .github/dependabot.yml
  3. 0 16
      .github/workflows/build-and-publish-release.yml
  4. 1 37
      .github/workflows/build-test.yaml
  5. 0 5
      .gitignore
  6. 7 1
      MAINTAINERS.md
  7. 3 1
      README.md
  8. 2 3
      core/go.mod
  9. 5 10
      core/go.sum
  10. 4 0
      core/pkg/util/buffer.go
  11. 274 0
      core/pkg/util/buffer_test.go
  12. 90 5
      core/pkg/util/typeutil/typeutil.go
  13. 123 0
      core/pkg/util/typeutil/typeutil_test.go
  14. 4 0
      pkg/cloud/aws/athenaquerier.go
  15. 116 7
      pkg/cloud/aws/authorizer.go
  16. 83 0
      pkg/cloud/aws/authorizer_test.go
  17. 70 0
      pkg/cloud/aws/webidentity.go
  18. 1 1
      pkg/cloud/azure/billingexportparser.go
  19. 3 3
      pkg/cloud/azure/storagebillingparser.go
  20. 6 0
      pkg/cloud/azure/storageconnection.go
  21. 247 0
      pkg/cloud/azure/streamreader.go
  22. 11 0
      pkg/cloud/provider/provider.go
  23. 79 35
      pkg/cmd/costmodel/costmodel.go
  24. 7 7
      pkg/costmodel/costmodel.go
  25. 113 178
      pkg/costmodel/router.go
  26. 7 0
      pkg/env/costmodelenv.go
  27. 1 1
      pkg/storage/prefixedbucketstorage.go
  28. 7 7
      pkg/storage/s3storage.go
  29. 3 0
      pkg/storage/storage.go
  30. 0 3
      ui/.babelrc
  31. 0 2
      ui/.dockerignore
  32. 0 1
      ui/.nvmrc
  33. 0 48
      ui/Dockerfile
  34. 0 38
      ui/Dockerfile.cross
  35. 0 40
      ui/Dockerfile.debug
  36. 0 65
      ui/README.md
  37. 0 79
      ui/default.nginx.conf.template
  38. 0 26
      ui/docker-entrypoint.sh
  39. 0 41
      ui/justfile
  40. 0 36
      ui/nginx.conf
  41. 0 6093
      ui/package-lock.json
  42. 0 47
      ui/package.json
  43. 0 314
      ui/src/Reports.js
  44. 0 5
      ui/src/app.js
  45. 0 217
      ui/src/cloudCost/cloudCost.js
  46. 0 14
      ui/src/cloudCost/cloudCostChart/index.js
  47. 0 275
      ui/src/cloudCost/cloudCostChart/rangeChart.js
  48. 0 178
      ui/src/cloudCost/cloudCostDetails.js
  49. 0 48
      ui/src/cloudCost/cloudCostRow.js
  50. 0 91
      ui/src/cloudCost/controls/cloudCostEditControls.js
  51. 0 49
      ui/src/cloudCost/tokens.js
  52. 0 337
      ui/src/cloudCostReports.js
  53. 0 149
      ui/src/components/AllocationChart/RangeChart.js
  54. 0 96
      ui/src/components/AllocationChart/SummaryChart.js
  55. 0 81
      ui/src/components/AllocationChart/index.js
  56. 0 89
      ui/src/components/Controls/Download.js
  57. 0 74
      ui/src/components/Controls/Edit.js
  58. 0 48
      ui/src/components/Controls/index.js
  59. 0 227
      ui/src/components/Details.js
  60. 0 13
      ui/src/components/Footer.js
  61. 0 60
      ui/src/components/Header.js
  62. 0 78
      ui/src/components/Nav/NavItem.js
  63. 0 70
      ui/src/components/Nav/SidebarNav.js
  64. 0 3
      ui/src/components/Nav/index.js
  65. 0 46
      ui/src/components/Page.js
  66. 0 190
      ui/src/components/SelectWindow.js
  67. 0 44
      ui/src/components/Subtitle.js
  68. 0 37
      ui/src/components/Warnings.js
  69. 0 241
      ui/src/components/allocationReport.js
  70. 0 38
      ui/src/constants/colors.js
  71. 0 1
      ui/src/constants/currencyCodes.js
  72. 0 20
      ui/src/css/index.css
  73. BIN
      ui/src/images/favicon.ico
  74. BIN
      ui/src/images/logo.png
  75. 0 16
      ui/src/index.html
  76. BIN
      ui/src/opencost-ui.png
  77. 0 25
      ui/src/route.js
  78. 0 28
      ui/src/services/allocation.js
  79. 0 42
      ui/src/services/cloudCostDayTotals.js
  80. 0 57
      ui/src/services/cloudCostTop.js
  81. BIN
      ui/src/thumbnail.png
  82. 0 452
      ui/src/util.js

+ 2 - 2
.github/ISSUE_TEMPLATE/opencost-bug-report.md

@@ -8,7 +8,7 @@ assignees: ''
 ---
 ---
 
 
 **Describe the bug**
 **Describe the bug**
-A clear and concise description of what the OpenCost bug is. Please ensure this is an issue related to the OpenCost cost model, API, UI or specification. Public Kubecost bugs may be opened at https://github.com/kubecost/features-bugs
+A clear and concise description of what the OpenCost bug is. Please ensure this is an issue related to the OpenCost cost model, API or specification. UI issues may be opened in the [OpenCost UI repository](https://github.com/opencost/opencost-ui).
 
 
 **To Reproduce**
 **To Reproduce**
 Steps to reproduce the behavior:
 Steps to reproduce the behavior:
@@ -24,7 +24,7 @@ A clear and concise description of what you expected to happen.
 If applicable, add screenshots to help explain your problem.
 If applicable, add screenshots to help explain your problem.
 
 
 **Which version of OpenCost are you using?**
 **Which version of OpenCost are you using?**
-This may be the Kubecost release.
+You can find the version from the container's startup logging or from the bottom of the page in the UI.
 
 
 **Additional context**
 **Additional context**
 Add any other context about the problem here. Kubernetes versions and which public clouds you are working with are especially important.
 Add any other context about the problem here. Kubernetes versions and which public clouds you are working with are especially important.

+ 0 - 6
.github/dependabot.yml

@@ -11,9 +11,3 @@ updates:
     directory: "/"
     directory: "/"
     schedule:
     schedule:
       interval: "weekly"
       interval: "weekly"
-
-  # Dependencies listed in ui/package.json
-  - package-ecosystem: "npm"
-    directory: "/ui"
-    schedule:
-      interval: "weekly"

+ 0 - 16
.github/workflows/build-and-publish-release.yml

@@ -88,15 +88,9 @@ jobs:
           echo "IMAGE_TAG=ghcr.io/opencost/opencost:${{ steps.sha.outputs.OC_SHORTHASH }}" >> $GITHUB_OUTPUT
           echo "IMAGE_TAG=ghcr.io/opencost/opencost:${{ steps.sha.outputs.OC_SHORTHASH }}" >> $GITHUB_OUTPUT
           echo "IMAGE_TAG_LATEST=ghcr.io/opencost/opencost:latest" >> $GITHUB_OUTPUT
           echo "IMAGE_TAG_LATEST=ghcr.io/opencost/opencost:latest" >> $GITHUB_OUTPUT
           echo "IMAGE_TAG_VERSION=ghcr.io/opencost/opencost:${{ steps.version_number.outputs.RELEASE_VERSION }}" >> $GITHUB_OUTPUT
           echo "IMAGE_TAG_VERSION=ghcr.io/opencost/opencost:${{ steps.version_number.outputs.RELEASE_VERSION }}" >> $GITHUB_OUTPUT
-          echo "IMAGE_TAG_UI=ghcr.io/opencost/opencost-ui:${{ steps.sha.outputs.OC_SHORTHASH }}" >> $GITHUB_OUTPUT
-          echo "IMAGE_TAG_UI_LATEST=ghcr.io/opencost/opencost-ui:latest" >> $GITHUB_OUTPUT
-          echo "IMAGE_TAG_UI_VERSION=ghcr.io/opencost/opencost-ui:${{ steps.version_number.outputs.RELEASE_VERSION }}" >> $GITHUB_OUTPUT
         #  echo "IMAGE_TAG_QUAY=quay.io/kubecost1/kubecost-cost-model:${{ steps.sha.outputs.OC_SHORTHASH }}" >> $GITHUB_OUTPUT
         #  echo "IMAGE_TAG_QUAY=quay.io/kubecost1/kubecost-cost-model:${{ steps.sha.outputs.OC_SHORTHASH }}" >> $GITHUB_OUTPUT
         #  echo "IMAGE_TAG_LATEST_QUAY=quay.io/kubecost1/kubecost-cost-model:latest" >> $GITHUB_OUTPUT
         #  echo "IMAGE_TAG_LATEST_QUAY=quay.io/kubecost1/kubecost-cost-model:latest" >> $GITHUB_OUTPUT
         #  echo "IMAGE_TAG_VERSION_QUAY=quay.io/kubecost1/kubecost-cost-model:prod-${{ steps.version_number.outputs.RELEASE_VERSION }}" >> $GITHUB_OUTPUT
         #  echo "IMAGE_TAG_VERSION_QUAY=quay.io/kubecost1/kubecost-cost-model:prod-${{ steps.version_number.outputs.RELEASE_VERSION }}" >> $GITHUB_OUTPUT
-        #  echo "IMAGE_TAG_UI_QUAY=quay.io/kubecost1/opencost-ui:${{ steps.sha.outputs.OC_SHORTHASH }}" >> $GITHUB_OUTPUT
-        #  echo "IMAGE_TAG_UI_LATEST_QUAY=quay.io/kubecost1/opencost-ui:latest" >> $GITHUB_OUTPUT
-        #  echo "IMAGE_TAG_UI_VERSION_QUAY=quay.io/kubecost1/opencost-ui:prod-${{ inputs.release_version }}" >> $GITHUB_OUTPUT
 
 
       - name: Set up Docker Buildx
       - name: Set up Docker Buildx
         uses: docker/setup-buildx-action@v3
         uses: docker/setup-buildx-action@v3
@@ -141,13 +135,3 @@ jobs:
         #  crane copy '${{ steps.tags.outputs.IMAGE_TAG }}' '${steps.tags.outputs.IMAGE_TAG_QUAY}'
         #  crane copy '${{ steps.tags.outputs.IMAGE_TAG }}' '${steps.tags.outputs.IMAGE_TAG_QUAY}'
         #  crane copy '${{ steps.tags.outputs.IMAGE_TAG }}' '${steps.tags.outputs.IMAGE_TAG_LATEST_QUAY}'
         #  crane copy '${{ steps.tags.outputs.IMAGE_TAG }}' '${steps.tags.outputs.IMAGE_TAG_LATEST_QUAY}'
         #  crane copy '${{ steps.tags.outputs.IMAGE_TAG }}' '${steps.tags.outputs.IMAGE_TAG_VERSION_QUAY}'
         #  crane copy '${{ steps.tags.outputs.IMAGE_TAG }}' '${steps.tags.outputs.IMAGE_TAG_VERSION_QUAY}'
-
-      - name: Build and push (multiarch) OpenCost UI
-        working-directory: ./opencost/ui
-        run: |
-          just build '${{ steps.tags.outputs.IMAGE_TAG_UI }}' '${{ steps.version_number.outputs.RELEASE_VERSION }}'
-          crane copy '${{ steps.tags.outputs.IMAGE_TAG_UI }}' '${{ steps.tags.outputs.IMAGE_TAG_UI_LATEST }}'
-          crane copy '${{ steps.tags.outputs.IMAGE_TAG_UI }}' '${{ steps.tags.outputs.IMAGE_TAG_UI_VERSION }}'
-        #  crane copy '${steps.tags.outputs.IMAGE_TAG_UI}' '${steps.tags.outputs.IMAGE_TAG_UI_QUAY}'
-        #  crane copy '${steps.tags.outputs.IMAGE_TAG_UI}' '${steps.tags.outputs.IMAGE_TAG_UI_LATEST_QUAY}'
-        #  crane copy '${steps.tags.outputs.IMAGE_TAG_UI}' '${steps.tags.outputs.IMAGE_TAG_UI_VERSION_QUAY}'

+ 1 - 37
.github/workflows/build-test.yaml

@@ -21,7 +21,7 @@ jobs:
         uses: actions/setup-go@v5
         uses: actions/setup-go@v5
         with:
         with:
           go-version: 'stable'
           go-version: 'stable'
-          
+
       -
       -
         name: Install protoc
         name: Install protoc
         uses: arduino/setup-protoc@v3
         uses: arduino/setup-protoc@v3
@@ -93,39 +93,3 @@ jobs:
            pr_num.txt
            pr_num.txt
            base.txt
            base.txt
            head.txt
            head.txt
-
-  frontend:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v4
-        with:
-          path: ./
-
-      -
-        name: Install just
-        uses: extractions/setup-just@v1
-
-      -
-        name: Install node
-        uses: actions/setup-node@v4
-        with:
-          node-version: '18.3.0'
-
-      - name: Get npm cache directory
-        id: npm-cache-dir
-        shell: bash
-        run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT}
-
-      - uses: actions/cache@v4
-        id: npm-cache # use this to check for `cache-hit` ==> if: steps.npm-cache.outputs.cache-hit != 'true'
-        with:
-          path: ${{ steps.npm-cache-dir.outputs.dir }}
-          key: ${{ runner.os }}-node-${{ hashFiles('./ui/**/package-lock.json') }}
-          restore-keys: |
-            ${{ runner.os }}-node-
-
-      -
-        name: Build
-        working-directory: ./ui
-        run: |
-          just build-local

+ 0 - 5
.gitignore

@@ -2,11 +2,6 @@
 .idea
 .idea
 *.iml
 *.iml
 
 
-ui/.parcel-cache
-ui/.cache
-ui/dist
-ui/.env
-ui/node_modules/
 cmd/costmodel/costmodel
 cmd/costmodel/costmodel
 cmd/costmodel/costmodel-amd64
 cmd/costmodel/costmodel-amd64
 cmd/costmodel/costmodel-arm64
 cmd/costmodel/costmodel-arm64

+ 7 - 1
MAINTAINERS.md

@@ -7,10 +7,16 @@ Official list of [OpenCost Maintainers](https://github.com/orgs/opencost/teams/o
 | Maintainer | GitHub ID | Affiliation | Email |
 | Maintainer | GitHub ID | Affiliation | Email |
 | --------------- | --------- | ----------- | ----------- |
 | --------------- | --------- | ----------- | ----------- |
 | Ajay Tripathy | @AjayTripathy | Kubecost | <Ajay@kubecost.com> |
 | Ajay Tripathy | @AjayTripathy | Kubecost | <Ajay@kubecost.com> |
+| Alex Meijer | @ameijer | Kubecost | <ameijer@kubecost.com> |
 | Artur Khantimirov | @r2k1 | Microsoft | |
 | Artur Khantimirov | @r2k1 | Microsoft | |
 | Matt Bolt | @​mbolt35 | Kubecost | <matt@kubecost.com> |
 | Matt Bolt | @​mbolt35 | Kubecost | <matt@kubecost.com> |
 | Matt Ray | @mattray | Kubecost | <mattray@kubecost.com> |
 | Matt Ray | @mattray | Kubecost | <mattray@kubecost.com> |
-| Michael Dresser | @michaelmdresser | Kubecost | <michael@kubecost.com> |
 | Niko Kovacevic | @nikovacevic | Kubecost | <niko@kubecost.com> |
 | Niko Kovacevic | @nikovacevic | Kubecost | <niko@kubecost.com> |
 | Sean Holcomb | @Sean-Holcomb | Kubecost | <Sean@kubecost.com> |
 | Sean Holcomb | @Sean-Holcomb | Kubecost | <Sean@kubecost.com> |
 | Thomas Evans | @teevans | Kubecost | <thomas@kubecost.com> |
 | Thomas Evans | @teevans | Kubecost | <thomas@kubecost.com> |
+
+## Opencost Emeritus Committers
+We would like to acknowledge previous committers and their huge contributions to our collective success:
+| Maintainer | GitHub ID | Affiliation | Email |
+| --------------- | --------- | ----------- | ----------- |
+| Michael Dresser | @michaelmdresser | Kubecost | <michaelmdresser@gmail.com> |

+ 3 - 1
README.md

@@ -9,7 +9,7 @@ OpenCost give teams visibility into current and historical Kubernetes and cloud
 These models provide cost transparency in Kubernetes environments that support multiple applications, teams, departments, etc.
 These models provide cost transparency in Kubernetes environments that support multiple applications, teams, departments, etc.
 It also provides visibility into the cloud costs across multiple providers.
 It also provides visibility into the cloud costs across multiple providers.
 
 
-OpenCost was originally developed and open sourced by [Kubecost](https://kubecost.com). This project combines a [specification](/spec/) as well as a Golang implementation of these detailed requirements.
+OpenCost was originally developed and open sourced by [Kubecost](https://kubecost.com). This project combines a [specification](/spec/) as well as a Golang implementation of these detailed requirements. The web UI is available in the [opencost/opencost-ui](http://github.com/opencost/opencost-ui) repository.
 
 
 [![OpenCost UI Walkthrough](./ui/src/thumbnail.png)](https://youtu.be/lCP4Ci9Kcdg)
 [![OpenCost UI Walkthrough](./ui/src/thumbnail.png)](https://youtu.be/lCP4Ci9Kcdg)
 *OpenCost UI Walkthrough*
 *OpenCost UI Walkthrough*
@@ -22,6 +22,8 @@ To see the full functionality of OpenCost you can view [OpenCost features](https
 - Supports on-prem k8s clusters with custom CSV pricing
 - Supports on-prem k8s clusters with custom CSV pricing
 - Allocation for in-cluster K8s resources like CPU, GPU, memory, and persistent volumes
 - Allocation for in-cluster K8s resources like CPU, GPU, memory, and persistent volumes
 - Easily export pricing data to Prometheus with /metrics endpoint ([learn more](https://www.opencost.io/docs/installation/prometheus))
 - Easily export pricing data to Prometheus with /metrics endpoint ([learn more](https://www.opencost.io/docs/installation/prometheus))
+- Carbon costs for cloud resources
+- Support for external costs like Datadog through [OpenCost Plugins](https://github.com/opencost/opencost-plugins)
 - Free and open source distribution ([Apache2 license](LICENSE))
 - Free and open source distribution ([Apache2 license](LICENSE))
 
 
 ## Getting Started
 ## Getting Started

+ 2 - 3
core/go.mod

@@ -7,6 +7,7 @@ require (
 	github.com/goccy/go-json v0.9.11
 	github.com/goccy/go-json v0.9.11
 	github.com/google/go-cmp v0.6.0
 	github.com/google/go-cmp v0.6.0
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/hashicorp/go-multierror v1.1.1
+	github.com/hashicorp/go-plugin v1.6.0
 	github.com/json-iterator/go v1.1.12
 	github.com/json-iterator/go v1.1.12
 	github.com/patrickmn/go-cache v2.1.0+incompatible
 	github.com/patrickmn/go-cache v2.1.0+incompatible
 	github.com/rs/zerolog v1.26.1
 	github.com/rs/zerolog v1.26.1
@@ -14,6 +15,7 @@ require (
 	golang.org/x/exp v0.0.0-20221031165847-c99f073a8326
 	golang.org/x/exp v0.0.0-20221031165847-c99f073a8326
 	golang.org/x/sync v0.6.0
 	golang.org/x/sync v0.6.0
 	golang.org/x/text v0.14.0
 	golang.org/x/text v0.14.0
+	google.golang.org/grpc v1.62.0
 	google.golang.org/protobuf v1.32.0
 	google.golang.org/protobuf v1.32.0
 	k8s.io/api v0.25.3
 	k8s.io/api v0.25.3
 	k8s.io/apimachinery v0.25.3
 	k8s.io/apimachinery v0.25.3
@@ -28,7 +30,6 @@ require (
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/hashicorp/errwrap v1.0.0 // indirect
 	github.com/hashicorp/errwrap v1.0.0 // indirect
 	github.com/hashicorp/go-hclog v1.6.2 // indirect
 	github.com/hashicorp/go-hclog v1.6.2 // indirect
-	github.com/hashicorp/go-plugin v1.6.0 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/hashicorp/yamux v0.1.1 // indirect
 	github.com/hashicorp/yamux v0.1.1 // indirect
 	github.com/magiconair/properties v1.8.5 // indirect
 	github.com/magiconair/properties v1.8.5 // indirect
@@ -48,9 +49,7 @@ require (
 	github.com/subosito/gotenv v1.2.0 // indirect
 	github.com/subosito/gotenv v1.2.0 // indirect
 	golang.org/x/net v0.21.0 // indirect
 	golang.org/x/net v0.21.0 // indirect
 	golang.org/x/sys v0.17.0 // indirect
 	golang.org/x/sys v0.17.0 // indirect
-	google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
-	google.golang.org/grpc v1.62.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect

+ 5 - 10
core/go.sum

@@ -45,6 +45,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
 github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
+github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
+github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
@@ -186,6 +188,8 @@ github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE
 github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
 github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
+github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
 github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
@@ -382,8 +386,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
 golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
-golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
 golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -409,8 +411,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -462,8 +463,6 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
-golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
 golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -606,8 +605,6 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
 google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
 google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
 google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
-google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -644,8 +641,6 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
-google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
 google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
 google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

+ 4 - 0
core/pkg/util/buffer.go

@@ -114,6 +114,10 @@ func (b *Buffer) WriteFloat64(i float64) {
 // WriteString writes the string's length as a uint16 followed by the string contents.
 // WriteString writes the string's length as a uint16 followed by the string contents.
 func (b *Buffer) WriteString(i string) {
 func (b *Buffer) WriteString(i string) {
 	s := stringToBytes(i)
 	s := stringToBytes(i)
+	// string lengths are limited to uint16 - See ReadString()
+	if len(s) > math.MaxUint16 {
+		s = s[:math.MaxUint16]
+	}
 	write(b.b, uint16(len(s)))
 	write(b.b, uint16(len(s)))
 	b.b.Write(s)
 	b.b.Write(s)
 }
 }

+ 274 - 0
core/pkg/util/buffer_test.go

@@ -0,0 +1,274 @@
+package util
+
+import (
+	"bytes"
+	"math"
+	"math/rand"
+	"testing"
+)
+
+func TestBufferReadWrite(t *testing.T) {
+	buf := NewBuffer()
+
+	buf.WriteBool(true)
+	buf.WriteInt(42)
+	buf.WriteFloat64(3.14)
+	buf.WriteString("Testing, 1, 2, 3!")
+
+	readBuf := NewBufferFromBytes(buf.Bytes())
+
+	boolVal := readBuf.ReadBool()
+	intVal := readBuf.ReadInt()
+	floatVal := readBuf.ReadFloat64()
+	stringVal := readBuf.ReadString()
+
+	if boolVal != true {
+		t.Errorf("Expected bool value to be true, got %v", boolVal)
+	}
+	if intVal != 42 {
+		t.Errorf("Expected int value to be 42, got %v", intVal)
+	}
+	if floatVal != 3.14 {
+		t.Errorf("Expected float value to be 3.14, got %v", floatVal)
+	}
+	if stringVal != "Testing, 1, 2, 3!" {
+		t.Errorf("Expected string value to be 'Hello, World!', got %v", stringVal)
+	}
+}
+
+func TestBufferWriteReadBytes(t *testing.T) {
+	buf := NewBuffer()
+
+	bytesToWrite := []byte{0x01, 0x02, 0x03, 0x04}
+	buf.WriteBytes(bytesToWrite)
+
+	readBuf := NewBufferFromBytes(buf.Bytes())
+	readBytes := readBuf.ReadBytes(len(bytesToWrite))
+
+	if !bytes.Equal(readBytes, bytesToWrite) {
+		t.Errorf("Expected bytes to be %v, got %v", bytesToWrite, readBytes)
+	}
+}
+
+func TestBufferWriteReadUInt64(t *testing.T) {
+	buf := NewBuffer()
+
+	uint64Val := uint64(1234567890)
+	buf.WriteUInt64(uint64Val)
+
+	readBuf := NewBufferFromBytes(buf.Bytes())
+	readUInt64 := readBuf.ReadUInt64()
+
+	if readUInt64 != uint64Val {
+		t.Errorf("Expected uint64 value to be %v, got %v", uint64Val, readUInt64)
+	}
+}
+
+func TestBufferWriteReadFloat32(t *testing.T) {
+	buf := NewBuffer()
+
+	float32Val := float32(3.14159)
+	buf.WriteFloat32(float32Val)
+
+	readBuf := NewBufferFromBytes(buf.Bytes())
+	readFloat32 := readBuf.ReadFloat32()
+
+	if readFloat32 != float32Val {
+		t.Errorf("Expected float32 value to be %v, got %v", float32Val, readFloat32)
+	}
+}
+
+func TestBufferWriteReadInt8(t *testing.T) {
+	buf := NewBuffer()
+
+	int8Val := int8(-42)
+	buf.WriteInt8(int8Val)
+
+	readBuf := NewBufferFromBytes(buf.Bytes())
+	readInt8 := readBuf.ReadInt8()
+
+	if readInt8 != int8Val {
+		t.Errorf("Expected int8 value to be %v, got %v", int8Val, readInt8)
+	}
+}
+
+func TestBufferWriteReadUInt16(t *testing.T) {
+	buf := NewBuffer()
+
+	uint16Val := uint16(65535)
+	buf.WriteUInt16(uint16Val)
+
+	readBuf := NewBufferFromBytes(buf.Bytes())
+	readUInt16 := readBuf.ReadUInt16()
+
+	if readUInt16 != uint16Val {
+		t.Errorf("Expected uint16 value to be %v, got %v", uint16Val, readUInt16)
+	}
+}
+
+func TestBufferWriteReadInt32(t *testing.T) {
+	buf := NewBuffer()
+
+	int32Val := int32(-1234567890)
+	buf.WriteInt32(int32Val)
+
+	readBuf := NewBufferFromBytes(buf.Bytes())
+	readInt32 := readBuf.ReadInt32()
+
+	if readInt32 != int32Val {
+		t.Errorf("Expected int32 value to be %v, got %v", int32Val, readInt32)
+	}
+}
+
+func TestBufferWriteReadUInt8(t *testing.T) {
+	buf := NewBuffer()
+
+	uint8Val := uint8(255)
+	buf.WriteUInt8(uint8Val)
+
+	readBuf := NewBufferFromBytes(buf.Bytes())
+	readUInt8 := readBuf.ReadUInt8()
+
+	if readUInt8 != uint8Val {
+		t.Errorf("Expected uint8 value to be %v, got %v", uint8Val, readUInt8)
+	}
+}
+
+func TestBufferWriteReadInt16(t *testing.T) {
+	buf := NewBuffer()
+
+	int16Val := int16(-32768)
+	buf.WriteInt16(int16Val)
+
+	readBuf := NewBufferFromBytes(buf.Bytes())
+	readInt16 := readBuf.ReadInt16()
+
+	if readInt16 != int16Val {
+		t.Errorf("Expected int16 value to be %v, got %v", int16Val, readInt16)
+	}
+}
+
+func TestBufferWriteReadUInt32(t *testing.T) {
+	buf := NewBuffer()
+
+	uint32Val := uint32(4294967295)
+	buf.WriteUInt32(uint32Val)
+
+	readBuf := NewBufferFromBytes(buf.Bytes())
+	readUInt32 := readBuf.ReadUInt32()
+
+	if readUInt32 != uint32Val {
+		t.Errorf("Expected uint32 value to be %v, got %v", uint32Val, readUInt32)
+	}
+}
+
+func TestBufferWriteReadInt64(t *testing.T) {
+	buf := NewBuffer()
+
+	int64Val := int64(-9223372036854775808)
+	buf.WriteInt64(int64Val)
+
+	readBuf := NewBufferFromBytes(buf.Bytes())
+	readInt64 := readBuf.ReadInt64()
+
+	if readInt64 != int64Val {
+		t.Errorf("Expected int64 value to be %v, got %v", int64Val, readInt64)
+	}
+}
+
+func TestBufferBytes(t *testing.T) {
+	buf := NewBuffer()
+
+	buf.WriteInt(42)
+	buf.WriteFloat64(3.14)
+
+	unreadBytes := buf.Bytes()
+
+	newBuf := NewBufferFromBytes(unreadBytes)
+
+	intVal := newBuf.ReadInt()
+	floatVal := newBuf.ReadFloat64()
+
+	if intVal != 42 {
+		t.Errorf("Expected int value to be 42, got %v", intVal)
+	}
+	if floatVal != 3.14 {
+		t.Errorf("Expected float value to be 3.14, got %v", floatVal)
+	}
+}
+
+func TestBufferNewBufferFrom(t *testing.T) {
+	buf := NewBuffer()
+
+	buf.WriteInt(42)
+	buf.WriteFloat64(3.14)
+
+	newBuf := NewBufferFrom(buf)
+
+	intVal := newBuf.ReadInt()
+	floatVal := newBuf.ReadFloat64()
+
+	if intVal != 42 {
+		t.Errorf("Expected int value to be 42, got %v", intVal)
+	}
+	if floatVal != 3.14 {
+		t.Errorf("Expected float value to be 3.14, got %v", floatVal)
+	}
+}
+
+const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+func generateRandomString(ln int) string {
+	b := make([]byte, ln)
+	for i := range b {
+		b[i] = letters[rand.Intn(len(letters))]
+	}
+	return string(b)
+}
+
+func TestTooLargeStringTruncate(t *testing.T) {
+	normalStr := generateRandomString(100)
+	bigStr := generateRandomString(math.MaxUint16 + (math.MaxUint16 / 2))
+	expectedBigStrRead := bigStr[:math.MaxUint16]
+
+	otherBigStr := generateRandomString(math.MaxUint16)
+	plusOne := generateRandomString(math.MaxUint16 + 1)
+	expectedPlusOne := plusOne[:math.MaxUint16]
+
+	buf := NewBuffer()
+
+	buf.WriteInt(42)
+	buf.WriteFloat64(3.14)
+	buf.WriteString(normalStr)
+	buf.WriteString(bigStr)
+	buf.WriteString(otherBigStr)
+	buf.WriteString(plusOne)
+
+	readBuf := NewBufferFromBytes(buf.Bytes())
+
+	intVal := readBuf.ReadInt()
+	floatVal := readBuf.ReadFloat64()
+	normalStrRead := readBuf.ReadString()
+	bigStrRead := readBuf.ReadString()
+	otherBigStrRead := readBuf.ReadString()
+	plusOneRead := readBuf.ReadString()
+
+	if intVal != 42 {
+		t.Errorf("Expected int value to be 42, got %v", intVal)
+	}
+	if floatVal != 3.14 {
+		t.Errorf("Expected float value to be 3.14, got %v", floatVal)
+	}
+	if normalStrRead != normalStr {
+		t.Errorf("Expected string value to be %v, got %v", normalStr, normalStrRead)
+	}
+	if bigStrRead != expectedBigStrRead {
+		t.Errorf("Expected large string values to be equivalent!")
+	}
+	if otherBigStrRead != otherBigStr {
+		t.Errorf("Expected large string values to be equivalent!")
+	}
+	if plusOneRead != expectedPlusOne {
+		t.Errorf("Expected large string values to be equivalent!")
+	}
+}

+ 90 - 5
core/pkg/util/typeutil/typeutil.go

@@ -3,15 +3,15 @@ package typeutil
 import (
 import (
 	"fmt"
 	"fmt"
 	"reflect"
 	"reflect"
+	"runtime"
+	"strings"
 )
 )
 
 
 // TypeOf is a utility that can covert a T type to a package + type name for generic types.
 // TypeOf is a utility that can covert a T type to a package + type name for generic types.
 func TypeOf[T any]() string {
 func TypeOf[T any]() string {
-	var inst T
 	var prefix string
 	var prefix string
 
 
-	// get a reflect.Type of a variable with type T
-	t := reflect.TypeOf(inst)
+	t := reflect.TypeFor[T]()
 
 
 	// pointer types do not carry the adequate type information, so we need to extract the
 	// pointer types do not carry the adequate type information, so we need to extract the
 	// underlying types until we reach the non-pointer type, we prepend a * each depth
 	// underlying types until we reach the non-pointer type, we prepend a * each depth
@@ -22,9 +22,94 @@ func TypeOf[T any]() string {
 
 
 	// this should not be possible, but in the event that it does, we want to be loud about it
 	// this should not be possible, but in the event that it does, we want to be loud about it
 	if t == nil {
 	if t == nil {
-		panic(fmt.Sprintf("Unable to generate a key for type: %+v", reflect.TypeOf(inst)))
+		panic(fmt.Sprintf("failed to locate non-pointer type: %+v", reflect.TypeFor[T]()))
+	}
+
+	name := t.Name()
+
+	// special cases for built-ins struct{} and interface{}
+	if name == "" {
+		name = t.String()
+	}
+
+	// no package path, do not use a / separator
+	if t.PkgPath() == "" {
+		return prefix + name
 	}
 	}
 
 
 	// combine the prefix, package path, and the type name
 	// combine the prefix, package path, and the type name
-	return fmt.Sprintf("%s%s/%s", prefix, t.PkgPath(), t.Name())
+	return fmt.Sprintf("%s%s/%s", prefix, t.PkgPath(), name)
+}
+
+// TypeFor uses type inferencing to accept a value and returns the fully qualified package
+// and type name
+func TypeFor[T any](value T) string {
+	return TypeOf[T]()
+}
+
+// PackageOf is a utility that can return the package name for the type provided.
+func PackageOf[T any]() string {
+	t := reflect.TypeFor[T]()
+
+	for t != nil && t.Kind() == reflect.Pointer {
+		t = t.Elem()
+	}
+
+	// this should not be possible, but in the event that it does, we want to be loud about it
+	if t == nil {
+		panic(fmt.Sprintf("failed to locate package for: %+v", reflect.TypeFor[T]()))
+	}
+
+	return t.PkgPath()
+}
+
+// PackageFor uses type inferencing to accepts a value and returns
+// the package name for the type of the value.
+func PackageFor[T any](value T) string {
+	return PackageOf[T]()
+}
+
+// PackageFromCaller returns the package name of the caller at the specified depth.
+func PackageFromCaller(depth int) string {
+	// get program counter for the first depth caller into this function
+	if pc, _, _, ok := runtime.Caller(depth); ok {
+		f := runtime.FuncForPC(pc)
+		if f == nil {
+			return ""
+		}
+
+		parentPkg := ""
+		funcName := f.Name()
+		pkg := funcName
+
+		// if there are slashes in the fully qualified path, we want to split
+		// everything before the last slash as the parent package, and everything
+		// after is the package + calling convention. If there are no slashes, then
+		// it's a root level package, so it's just package + calling convention
+		slashIndex := strings.LastIndex(funcName, "/")
+		if slashIndex >= 0 {
+			parentPkg = funcName[:slashIndex]
+			pkg = funcName[slashIndex:]
+		}
+
+		// the package + calling convention can be in a few forms, but since we only
+		// care about the package, we can return everything up until a '.'.
+		// We can make a hard assertion here that unless the go spec changes, we can
+		// rely on the function calling convention to have the form <package>.<function>
+		dotIndex := strings.Index(pkg, ".")
+		if dotIndex < 0 {
+			panic("Unable to parse package name from function call convention: " + pkg)
+		}
+
+		// the fully qualified package name is the parent package + resolved caller package
+		return parentPkg + pkg[:dotIndex]
+	}
+	return ""
+}
+
+// CurrentPackage returns the package name of the caller. This is especially handy for automatically
+// generating package scoped tracing identifiers.
+func CurrentPackage() string {
+	// Depth is from: (2) Caller -> (1) CurrentPackage -> (0) PackageFromCaller
+	return PackageFromCaller(2)
 }
 }

+ 123 - 0
core/pkg/util/typeutil/typeutil_test.go

@@ -0,0 +1,123 @@
+package typeutil_test
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/opencost/opencost/core/pkg/opencost"
+	"github.com/opencost/opencost/core/pkg/util/typeutil"
+)
+
+type TestType struct{}
+type GenericTestType[T any] struct{}
+
+type CurrentPackageTester struct{}
+
+func (c *CurrentPackageTester) TestFromInstance(t *testing.T, expected string) {
+	cmp(t, typeutil.CurrentPackage(), expected)
+}
+
+func (c *CurrentPackageTester) TestFromNestedInstance(t *testing.T, expected string) {
+	nested := func() string {
+		return typeutil.CurrentPackage()
+	}
+
+	result := nested()
+	cmp(t, result, expected)
+}
+
+// cmp compares two comparable values and fails the test if they are not equal.
+func cmp[T comparable](t *testing.T, result, expected T) {
+	if result != expected {
+		t.Errorf("Expected: %+v. Got: %+v", expected, result)
+	}
+}
+
+type InterfaceType interface{}
+
+var packageScoped = typeutil.CurrentPackage()
+
+func TestTypeOf(t *testing.T) {
+	const packageName = "github.com/opencost/opencost/core/pkg/util/typeutil_test"
+	const testTypeName = packageName + "/TestType"
+	const genericTestTypeName = packageName + "/GenericTestType"
+	const genericTypeParameterTypeName = packageName + ".GenericTestType"
+	const interfaceTypeName = packageName + "/InterfaceType"
+
+	// Basic Types
+	cmp(t, typeutil.TypeOf[int](), "int")
+	cmp(t, typeutil.TypeOf[int8](), "int8")
+	cmp(t, typeutil.TypeOf[any](), "interface {}")
+	cmp(t, typeutil.TypeOf[interface{}](), "interface {}")
+	cmp(t, typeutil.TypeOf[struct{}](), "struct {}")
+
+	// Specific Types
+	cmp(t, typeutil.TypeOf[TestType](), testTypeName)
+	cmp(t, typeutil.TypeOf[*TestType](), "*"+testTypeName)
+	cmp(t, typeutil.TypeOf[**TestType](), "**"+testTypeName)
+	cmp(t, typeutil.TypeOf[GenericTestType[string]](), genericTestTypeName+"[string]")
+	cmp(t, typeutil.TypeOf[GenericTestType[GenericTestType[string]]](), genericTestTypeName+"["+genericTypeParameterTypeName+"[string]"+"]")
+	cmp(t, typeutil.TypeOf[GenericTestType[*GenericTestType[string]]](), genericTestTypeName+"[*"+genericTypeParameterTypeName+"[string]"+"]")
+	cmp(t, typeutil.TypeOf[GenericTestType[*GenericTestType[map[int][]float64]]](), genericTestTypeName+"[*"+genericTypeParameterTypeName+"[map[int][]float64]"+"]")
+
+	// interface types
+	cmp(t, typeutil.TypeOf[InterfaceType](), interfaceTypeName)
+	cmp(t, typeutil.TypeOf[*InterfaceType](), "*"+interfaceTypeName)
+	cmp(t, typeutil.TypeOf[**InterfaceType](), "**"+interfaceTypeName)
+
+	// TypeFor variants
+	var value any
+	cmp(t, typeutil.TypeFor(value), "interface {}")
+
+	var ivalue InterfaceType
+	cmp(t, typeutil.TypeFor(ivalue), interfaceTypeName)
+
+	var testType **TestType
+	cmp(t, typeutil.TypeFor(testType), "**"+testTypeName)
+}
+
+func DeferredCurrentPackage() (result string) {
+	defer func() {
+		result = typeutil.CurrentPackage()
+	}()
+
+	return
+}
+
+func TestPackageOf(t *testing.T) {
+	const currentPackageName = "github.com/opencost/opencost/core/pkg/util/typeutil_test"
+	const jsonEncoderPackageName = "encoding/json"
+	const opencostPackageName = "github.com/opencost/opencost/core/pkg/opencost"
+
+	cmp(t, typeutil.PackageOf[TestType](), currentPackageName)
+	cmp(t, typeutil.PackageOf[*TestType](), currentPackageName)
+	cmp(t, typeutil.PackageOf[json.Encoder](), jsonEncoderPackageName)
+	cmp(t, typeutil.PackageOf[*opencost.Allocation](), opencostPackageName)
+
+	cmp(t, typeutil.CurrentPackage(), currentPackageName)
+
+	deferredResult := DeferredCurrentPackage()
+	cmp(t, deferredResult, currentPackageName)
+
+	// Tests the CurrentPackage function within an instance function
+	// this will return something like:
+	// "github.com/opencost/opencost/core/pkg/util/typeutil_test.(*CurrentPackageTester).TestFromInstance"
+	new(CurrentPackageTester).TestFromInstance(t, currentPackageName)
+
+	// Tests the CurrentPackage function within an instance function that contains a nested anonymous function
+	// this will return something like:
+	// "github.com/opencost/opencost/core/pkg/util/typeutil_test.(*CurrentPackageTester).TestFromNestedInstance.func1"
+	new(CurrentPackageTester).TestFromNestedInstance(t, currentPackageName)
+
+	// This test the package scoped variable which calls the CurrentPackage function in the package scope
+	// this will normally return something like:
+	// "github.com/opencost/opencost/core/pkg/util/typeutil_test.init"
+	cmp(t, packageScoped, currentPackageName)
+
+	// PackageFor variants
+	var value any
+	cmp(t, typeutil.PackageFor(value), "")
+
+	var ivalue InterfaceType
+	cmp(t, typeutil.PackageFor(ivalue), currentPackageName)
+}

+ 4 - 0
pkg/cloud/aws/athenaquerier.go

@@ -208,6 +208,10 @@ func SelectAWSCategory(providerID, usageType, service string) string {
 	// The node and volume conditions are mutually exclusive.
 	// The node and volume conditions are mutually exclusive.
 	// Provider ID has prefix "i-"
 	// Provider ID has prefix "i-"
 	if strings.HasPrefix(providerID, "i-") {
 	if strings.HasPrefix(providerID, "i-") {
+		// GuardDuty has a ProviderID prefix of "i-", but should not be categorized as compute
+		if strings.ToUpper(service) == "AMAZONGUARDDUTY" {
+			return opencost.OtherCategory
+		}
 		return opencost.ComputeCategory
 		return opencost.ComputeCategory
 	}
 	}
 	// Provider ID has prefix "vol-"
 	// Provider ID has prefix "vol-"

+ 116 - 7
pkg/cloud/aws/authorizer.go

@@ -15,6 +15,7 @@ import (
 const AccessKeyAuthorizerType = "AWSAccessKey"
 const AccessKeyAuthorizerType = "AWSAccessKey"
 const ServiceAccountAuthorizerType = "AWSServiceAccount"
 const ServiceAccountAuthorizerType = "AWSServiceAccount"
 const AssumeRoleAuthorizerType = "AWSAssumeRole"
 const AssumeRoleAuthorizerType = "AWSAssumeRole"
+const WebIdentityAuthorizerType = "AWSWebIdentity"
 
 
 // Authorizer implementations provide aws.Config for AWS SDK calls
 // Authorizer implementations provide aws.Config for AWS SDK calls
 type Authorizer interface {
 type Authorizer interface {
@@ -31,6 +32,8 @@ func SelectAuthorizerByType(typeStr string) (Authorizer, error) {
 		return &ServiceAccount{}, nil
 		return &ServiceAccount{}, nil
 	case AssumeRoleAuthorizerType:
 	case AssumeRoleAuthorizerType:
 		return &AssumeRole{}, nil
 		return &AssumeRole{}, nil
+	case WebIdentityAuthorizerType:
+		return &WebIdentity{}, nil
 	default:
 	default:
 		return nil, fmt.Errorf("AWS: provider authorizer type '%s' is not valid", typeStr)
 		return nil, fmt.Errorf("AWS: provider authorizer type '%s' is not valid", typeStr)
 	}
 	}
@@ -129,11 +132,7 @@ func (sa *ServiceAccount) Equals(config cloud.Config) bool {
 		return false
 		return false
 	}
 	}
 	_, ok := config.(*ServiceAccount)
 	_, ok := config.(*ServiceAccount)
-	if !ok {
-		return false
-	}
-
-	return true
+	return ok
 }
 }
 
 
 func (sa *ServiceAccount) Sanitize() cloud.Config {
 func (sa *ServiceAccount) Sanitize() cloud.Config {
@@ -204,7 +203,7 @@ func (ara *AssumeRole) CreateAWSConfig(region string) (aws.Config, error) {
 
 
 func (ara *AssumeRole) Validate() error {
 func (ara *AssumeRole) Validate() error {
 	if ara.Authorizer == nil {
 	if ara.Authorizer == nil {
-		return fmt.Errorf("AssumeRole: misisng base Authorizer")
+		return fmt.Errorf("AssumeRole: missing base Authorizer")
 	}
 	}
 	err := ara.Authorizer.Validate()
 	err := ara.Authorizer.Validate()
 	if err != nil {
 	if err != nil {
@@ -212,7 +211,7 @@ func (ara *AssumeRole) Validate() error {
 	}
 	}
 
 
 	if ara.RoleARN == "" {
 	if ara.RoleARN == "" {
-		return fmt.Errorf("AssumeRole: misisng RoleARN configuration")
+		return fmt.Errorf("AssumeRole: missing RoleARN configuration")
 	}
 	}
 
 
 	return nil
 	return nil
@@ -249,3 +248,113 @@ func (ara *AssumeRole) Sanitize() cloud.Config {
 		RoleARN:    ara.RoleARN,
 		RoleARN:    ara.RoleARN,
 	}
 	}
 }
 }
+
+type WebIdentity struct {
+	RoleARN          string           `json:"roleARN"`
+	IdentityProvider string           `json:"identityProvider"`
+	TokenRetriever   IDTokenRetriever `json:"tokenRetriever"`
+}
+
+func (wea *WebIdentity) CreateAWSConfig(region string) (aws.Config, error) {
+	cfg, err := awsconfig.LoadDefaultConfig(context.TODO(), awsconfig.WithRegion(region))
+	if err != nil {
+		return cfg, fmt.Errorf("failed to initialize AWS SDK config for region from annotation %s: %s", region, err)
+	}
+
+	stsSvc := sts.NewFromConfig(cfg)
+	creds := stscreds.NewWebIdentityRoleProvider(stsSvc, wea.RoleARN, wea.TokenRetriever)
+
+	cfg.Credentials = aws.NewCredentialsCache(creds)
+	return cfg, nil
+}
+
+func (wea *WebIdentity) MarshalJSON() ([]byte, error) {
+	fmap := make(map[string]any, 4)
+	fmap[cloud.AuthorizerTypeProperty] = WebIdentityAuthorizerType
+	fmap["roleARN"] = wea.RoleARN
+	fmap["identityProvider"] = wea.IdentityProvider
+	fmap["tokenRetriever"] = wea.TokenRetriever
+	return json.Marshal(fmap)
+}
+
+func (wea *WebIdentity) UnmarshalJSON(b []byte) error {
+	var f interface{}
+	err := json.Unmarshal(b, &f)
+	if err != nil {
+		return err
+	}
+
+	fmap := f.(map[string]interface{})
+
+	roleARN, err := cloud.GetInterfaceValue[string](fmap, "roleARN")
+	if err != nil {
+		return fmt.Errorf("WebIdentity: UnmarshalJSON: %s", err.Error())
+	}
+	wea.RoleARN = roleARN
+
+	idp, err := cloud.GetInterfaceValue[string](fmap, "identityProvider")
+	if err != nil {
+		return fmt.Errorf("WebIdentity: UnmarshalJSON: %s", err.Error())
+	}
+	wea.IdentityProvider = idp
+
+	var tr interface{}
+
+	tr, err = cloud.GetInterfaceValue[interface{}](fmap, "tokenRetriever")
+	if err != nil {
+		return fmt.Errorf("WebIdentity: UnmarshalJSON: %s", err.Error())
+	}
+
+	trb, err := json.Marshal(tr)
+	if err != nil {
+		return fmt.Errorf("WebIdentity: UnmarshalJSON: %s", err.Error())
+	}
+
+	var tokenRetriever IDTokenRetriever
+	switch idp {
+	case "Google":
+		tokenRetriever = &GoogleIDTokenRetriever{}
+	}
+
+	err = json.Unmarshal(trb, &tokenRetriever)
+	if err != nil {
+		return fmt.Errorf("WebIdentity: UnmarshalJSON: %s", err.Error())
+	}
+
+	wea.TokenRetriever = tokenRetriever
+
+	return nil
+}
+
+func (wea *WebIdentity) Validate() error {
+
+	if wea.RoleARN == "" {
+		return fmt.Errorf("WebIdentity: missing RoleARN configuration")
+	}
+
+	if wea.TokenRetriever == nil {
+		return fmt.Errorf("WebIdentity: missing TokenRetriever configuration")
+	}
+
+	return wea.TokenRetriever.Validate()
+}
+
+func (wea *WebIdentity) Equals(config cloud.Config) bool {
+	if config == nil {
+		return false
+	}
+	thatConfig, ok := config.(*WebIdentity)
+	if !ok {
+		return false
+	}
+
+	return wea.RoleARN == thatConfig.RoleARN && wea.IdentityProvider == thatConfig.IdentityProvider && wea.TokenRetriever.Equals(thatConfig.TokenRetriever)
+}
+
+func (wea *WebIdentity) Sanitize() cloud.Config {
+	return &WebIdentity{
+		RoleARN:          wea.RoleARN,
+		IdentityProvider: wea.IdentityProvider,
+		TokenRetriever:   wea.TokenRetriever.Sanitize(),
+	}
+}

+ 83 - 0
pkg/cloud/aws/authorizer_test.go

@@ -3,6 +3,7 @@ package aws
 import (
 import (
 	"testing"
 	"testing"
 
 
+	"github.com/opencost/opencost/core/pkg/util/json"
 	"github.com/opencost/opencost/pkg/cloud"
 	"github.com/opencost/opencost/pkg/cloud"
 )
 )
 
 
@@ -52,6 +53,22 @@ func TestAuthorizerJSON_Sanitize(t *testing.T) {
 				RoleARN:    "role arn",
 				RoleARN:    "role arn",
 			},
 			},
 		},
 		},
+		"Google Web Identity": {
+			input: &WebIdentity{
+				RoleARN:          "role arn",
+				IdentityProvider: "Google",
+				TokenRetriever: &GoogleIDTokenRetriever{
+					Aud: "aud",
+				},
+			},
+			expected: &WebIdentity{
+				RoleARN:          "role arn",
+				IdentityProvider: "Google",
+				TokenRetriever: &GoogleIDTokenRetriever{
+					Aud: "aud",
+				},
+			},
+		},
 	}
 	}
 	for name, tc := range testCases {
 	for name, tc := range testCases {
 		t.Run(name, func(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
@@ -65,3 +82,69 @@ func TestAuthorizerJSON_Sanitize(t *testing.T) {
 		})
 		})
 	}
 	}
 }
 }
+
+func TestAuthorizerJSON_Encode(t *testing.T) {
+
+	testCases := map[string]struct {
+		authorizer Authorizer
+	}{
+		"Access Key": {
+			authorizer: &AccessKey{
+				ID:     "ID",
+				Secret: "Secret",
+			},
+		},
+		"Service Account": {
+			authorizer: &ServiceAccount{},
+		},
+		"Master Payer Access Key": {
+			authorizer: &AssumeRole{
+				Authorizer: &AccessKey{
+					ID:     "ID",
+					Secret: "Secret",
+				},
+				RoleARN: "role arn",
+			},
+		},
+		"Master Payer Service Account": {
+			authorizer: &AssumeRole{
+				Authorizer: &ServiceAccount{},
+				RoleARN:    "role arn",
+			},
+		},
+		"Google Web Identity": {
+			authorizer: &WebIdentity{
+				RoleARN:          "role arn",
+				IdentityProvider: "Google",
+				TokenRetriever: &GoogleIDTokenRetriever{
+					Aud: "aud",
+				},
+			},
+		},
+	}
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+
+			b, err := tc.authorizer.MarshalJSON()
+			if err != nil {
+				t.Errorf("Failed to Marshal Authorizer: %s", err)
+			}
+
+			var f interface{}
+			err = json.Unmarshal(b, &f)
+			if err != nil {
+				t.Errorf("Failed to Unmarshal Authorizer: %s", err)
+			}
+
+			authorizer, err := cloud.AuthorizerFromInterface(f, SelectAuthorizerByType)
+			if err != nil {
+				t.Errorf("Failed to Unmarshal Authorizer: %s", err)
+			}
+
+			if !tc.authorizer.Equals(authorizer) {
+				t.Error("Authorizer was not as expected after Sanitization")
+			}
+
+		})
+	}
+}

+ 70 - 0
pkg/cloud/aws/webidentity.go

@@ -0,0 +1,70 @@
+package aws
+
+import (
+	"context"
+	"fmt"
+
+	"golang.org/x/oauth2/google"
+	"google.golang.org/api/idtoken"
+	"google.golang.org/api/option"
+)
+
+type IDTokenRetriever interface {
+	GetIdentityToken() ([]byte, error)
+	Validate() error
+	Sanitize() IDTokenRetriever
+	Equals(IDTokenRetriever) bool
+}
+
+type GoogleIDTokenRetriever struct {
+	Aud string `json:"aud"`
+}
+
+func (gitr GoogleIDTokenRetriever) GetIdentityToken() ([]byte, error) {
+	ctx := context.Background()
+	res := []byte{}
+
+	credentials, err := google.FindDefaultCredentials(ctx)
+	if err != nil {
+		return res, fmt.Errorf("failed to find default credentials: %v", err)
+	}
+
+	ts, err := idtoken.NewTokenSource(ctx, gitr.Aud, option.WithCredentials(credentials))
+	if err != nil {
+		return res, fmt.Errorf("failed to create ID token source: %w", err)
+	}
+
+	t, err := ts.Token()
+	if err != nil {
+		return res, fmt.Errorf("failed to receive ID token from metadata server: %w", err)
+	}
+
+	return []byte(t.AccessToken), nil
+}
+
+func (gitr GoogleIDTokenRetriever) Validate() error {
+	if gitr.Aud == "" {
+		return fmt.Errorf("GoogleIDTokenRetriever: missing audience configuration")
+	}
+
+	return nil
+}
+
+func (gitr GoogleIDTokenRetriever) Equals(other IDTokenRetriever) bool {
+	that, ok := other.(*GoogleIDTokenRetriever)
+	if !ok {
+		return false
+	}
+
+	if gitr.Aud != that.Aud {
+		return false
+	}
+
+	return true
+}
+
+func (gitr GoogleIDTokenRetriever) Sanitize() IDTokenRetriever {
+	return &GoogleIDTokenRetriever{
+		Aud: gitr.Aud,
+	}
+}

+ 1 - 1
pkg/cloud/azure/billingexportparser.go

@@ -285,7 +285,7 @@ func AzureSetProviderID(abv *BillingRowValues) (providerID string, isVMSSShared
 }
 }
 
 
 func SelectAzureCategory(meterCategory string) string {
 func SelectAzureCategory(meterCategory string) string {
-	if meterCategory == "Virtual Machines" {
+	if meterCategory == "Virtual Machines" || meterCategory == "Virtual Machines Licenses" {
 		return opencost.ComputeCategory
 		return opencost.ComputeCategory
 	} else if meterCategory == "Storage" {
 	} else if meterCategory == "Storage" {
 		return opencost.StorageCategory
 		return opencost.StorageCategory

+ 3 - 3
pkg/cloud/azure/storagebillingparser.go

@@ -1,7 +1,6 @@
 package azure
 package azure
 
 
 import (
 import (
-	"bytes"
 	"context"
 	"context"
 	"encoding/csv"
 	"encoding/csv"
 	"fmt"
 	"fmt"
@@ -86,12 +85,13 @@ func (asbp *AzureStorageBillingParser) ParseBillingData(start, end time.Time, re
 				return err
 				return err
 			}
 			}
 		} else {
 		} else {
-			blobBytes, err2 := asbp.DownloadBlob(blobName, client, ctx)
+			streamReader, err2 := asbp.StreamBlob(blobName, client)
 			if err2 != nil {
 			if err2 != nil {
 				asbp.ConnectionStatus = cloud.FailedConnection
 				asbp.ConnectionStatus = cloud.FailedConnection
 				return err2
 				return err2
 			}
 			}
-			err2 = asbp.parseCSV(start, end, csv.NewReader(bytes.NewReader(blobBytes)), resultFn)
+
+			err2 = asbp.parseCSV(start, end, csv.NewReader(streamReader), resultFn)
 			if err2 != nil {
 			if err2 != nil {
 				asbp.ConnectionStatus = cloud.ParseError
 				asbp.ConnectionStatus = cloud.ParseError
 				return err2
 				return err2

+ 6 - 0
pkg/cloud/azure/storageconnection.go

@@ -70,6 +70,12 @@ func (sc *StorageConnection) DownloadBlob(blobName string, client *azblob.Client
 	return downloadedData.Bytes(), nil
 	return downloadedData.Bytes(), nil
 }
 }
 
 
+// StreamBlob returns an io.Reader for the given blob which uses a re-usable double buffer approach to stream directly
+// from blob storage.
+func (sc *StorageConnection) StreamBlob(blobName string, client *azblob.Client) (*StreamReader, error) {
+	return NewStreamReader(client, sc.Container, blobName)
+}
+
 // DownloadBlobToFile downloads the Azure Billing CSV to a local file
 // DownloadBlobToFile downloads the Azure Billing CSV to a local file
 func (sc *StorageConnection) DownloadBlobToFile(localFilePath string, blobName string, client *azblob.Client, ctx context.Context) error {
 func (sc *StorageConnection) DownloadBlobToFile(localFilePath string, blobName string, client *azblob.Client, ctx context.Context) error {
 	// If file exists, don't download it again
 	// If file exists, don't download it again

+ 247 - 0
pkg/cloud/azure/streamreader.go

@@ -0,0 +1,247 @@
+package azure
+
+import (
+	"bytes"
+	"context"
+	"io"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+)
+
+const defaultBlockSize = int(8 * 1024 * 1024) // 8MB
+
+// StreamReader is a double buffered streaming reader for Azure Blob Storage.
+type StreamReader struct {
+	client    *azblob.Client
+	container string
+	blobName  string
+	block     *bytes.Buffer
+	next      *streamingBlock
+	position  int64
+	size      int64
+}
+
+// NewStreamReader creates a new streaming reader for the specified blob.
+func NewStreamReader(client *azblob.Client, container string, blobName string) (*StreamReader, error) {
+	sar := &StreamReader{
+		client:    client,
+		container: container,
+		blobName:  blobName,
+		block:     nil,
+		next:      nil,
+		position:  0,
+		size:      0,
+	}
+
+	// get the size of the blob
+	blobClient := client.ServiceClient().NewContainerClient(container).NewBlobClient(blobName)
+	gr, err := blobClient.GetProperties(context.Background(), nil)
+	if err != nil {
+		return nil, err
+	}
+
+	sar.size = *gr.ContentLength
+
+	return sar, nil
+}
+
+// See io.Reader.Read
+func (r *StreamReader) Read(p []byte) (n int, err error) {
+	if r.position >= r.size {
+		return 0, io.EOF
+	}
+
+	// fetch the blocks on demand
+	if r.block == nil || r.block.Len() == 0 {
+		err := r.nextBlock()
+		if err != nil {
+			return 0, err
+		}
+	}
+
+	// block.Next() constrains the bytes read even if len(p) is larger
+	// than the rest of the block
+	copied := copy(p, r.block.Next(len(p)))
+	r.position += int64(copied)
+
+	return copied, nil
+}
+
+// nextBlock fetches the next block of data from the blob and starts the download of
+// the next block in the background.
+func (r *StreamReader) nextBlock() error {
+	// if we don't have a block, we need to fetch the first block, and start fetching
+	// the next block in the background
+	if r.block == nil {
+		current := newStreamBlock(
+			r.client,
+			r.container,
+			r.blobName,
+			nil,
+			r.position,
+			int64(defaultBlockSize),
+			r.size,
+		)
+
+		// explicitly wait here for the first block
+		err := current.Wait()
+		if err != nil {
+			return err
+		}
+
+		// set the current block and start the next block download
+		r.block = current.buffer
+
+		// if the block size capacity was reduced to a value different than the default block size,
+		// we can assume there is no more data beyond this block, so we don't need to start the next block
+		if current.capacity != int64(defaultBlockSize) {
+			return nil
+		}
+
+		// start next block stream
+		r.next = newStreamBlock(
+			r.client,
+			r.container,
+			r.blobName,
+			nil,
+			r.position+current.capacity,
+			int64(defaultBlockSize),
+			r.size,
+		)
+
+		return nil
+	}
+
+	// we have a block and a next block, so we need to wait for the next block to finish
+	// buffering, then we can swap current and next buffers
+	err := r.next.Wait()
+	if err != nil {
+		return err
+	}
+
+	// save the current buffer to re-use in the next block and set the current to the next block
+	currentBuffer := r.block
+	r.block = r.next.buffer
+
+	if r.next.capacity != int64(defaultBlockSize) {
+		return nil
+	}
+
+	// start next block stream
+	r.next = newStreamBlock(
+		r.client,
+		r.container,
+		r.blobName,
+		currentBuffer, // recycle the old current buffer as the next buffer
+		r.position+int64(defaultBlockSize),
+		int64(defaultBlockSize),
+		r.size,
+	)
+
+	return nil
+}
+
+// streamingBlock is a buffered block of data that runs in a separate goroutine
+// to allow the next block to download while the current block is being read.
+type streamingBlock struct {
+	client    *azblob.Client
+	container string
+	blob      string
+
+	done   chan struct{}
+	buffer *bytes.Buffer
+	err    error
+
+	start    int64
+	capacity int64
+}
+
+// newStreamBlock creates a new buffered block of data the down the specific
+// range of the blob. While the block download runs in a separate goroutine,
+// we will never attempt to access the passed buffer until after the Wait()
+// returns. This just ensures that we will never attempt to swap buffers
+// mid-download.
+func newStreamBlock(
+	client *azblob.Client,
+	container string,
+	blob string,
+	buffer *bytes.Buffer,
+	start int64,
+	capacity int64,
+	max int64,
+) *streamingBlock {
+	sb := &streamingBlock{
+		client:    client,
+		container: container,
+		blob:      blob,
+		done:      make(chan struct{}),
+		buffer:    buffer,
+		start:     start,
+		capacity:  capacity,
+	}
+
+	// determine if we need to reallocate a new block buffer or if we can re-use the existing storage
+	blockSize := capacity
+	if start+blockSize > max {
+		blockSize = max - start
+	}
+
+	// if the provided buffer is nil or the blockSize is different than the provided capacity, we need to reallocate
+	// reallocation will likely happen once at the end of the stream
+	if sb.buffer == nil || blockSize != capacity {
+		sb.buffer = bytes.NewBuffer(make([]byte, 0, blockSize))
+		sb.capacity = blockSize
+	} else {
+		sb.buffer.Reset()
+	}
+
+	// start a goroutine to fetch the block of data, close the done channel when the block
+	// is fetched or an error occurs
+	go func(block *streamingBlock) {
+		ctx := context.Background()
+
+		opts := azblob.DownloadStreamOptions{
+			Range: azblob.HTTPRange{
+				Offset: block.start,
+				Count:  block.capacity,
+			},
+		}
+
+		resp, err := block.client.DownloadStream(ctx, block.container, block.blob, &opts)
+		if err != nil {
+			block.err = err
+			close(block.done)
+			return
+		}
+
+		retryOpts := &azblob.RetryReaderOptions{
+			MaxRetries: 3,
+		}
+
+		var body io.ReadCloser = resp.NewRetryReader(ctx, retryOpts)
+		_, err = io.Copy(block.buffer, body)
+		if err != nil {
+			block.err = err
+			close(block.done)
+			return
+		}
+
+		err = body.Close()
+		if err != nil {
+			block.err = err
+			close(block.done)
+			return
+		}
+
+		close(block.done)
+	}(sb)
+
+	return sb
+}
+
+// Wait blocks until the block is downloaded and returns any error that occurred.
+func (sb *streamingBlock) Wait() error {
+	<-sb.done
+
+	return sb.err
+}

+ 11 - 0
pkg/cloud/provider/provider.go

@@ -278,6 +278,16 @@ func getClusterProperties(node *v1.Node) clusterProperties {
 		accountID:      "",
 		accountID:      "",
 		projectID:      "",
 		projectID:      "",
 	}
 	}
+
+	// Check for custom provider settings
+	if env.IsUseCustomProvider() {
+		// Use CSV provider if set
+		if env.IsUseCSVProvider() {
+			cp.provider = opencost.CSVProvider
+		}
+		return cp
+	}
+
 	// The second conditional is mainly if you're running opencost outside of GCE, say in a local environment.
 	// The second conditional is mainly if you're running opencost outside of GCE, say in a local environment.
 	if metadata.OnGCE() || strings.HasPrefix(providerID, "gce") {
 	if metadata.OnGCE() || strings.HasPrefix(providerID, "gce") {
 		cp.provider = opencost.GCPProvider
 		cp.provider = opencost.GCPProvider
@@ -303,6 +313,7 @@ func getClusterProperties(node *v1.Node) clusterProperties {
 		cp.provider = opencost.OracleProvider
 		cp.provider = opencost.OracleProvider
 		cp.configFileName = "oracle.json"
 		cp.configFileName = "oracle.json"
 	}
 	}
+	// Override provider to CSV if CSVProvider is used and custom provider is not set
 	if env.IsUseCSVProvider() {
 	if env.IsUseCSVProvider() {
 		cp.provider = opencost.CSVProvider
 		cp.provider = opencost.CSVProvider
 	}
 	}

+ 79 - 35
pkg/cmd/costmodel/costmodel.go

@@ -8,7 +8,9 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/julienschmidt/httprouter"
 	"github.com/julienschmidt/httprouter"
-	"github.com/opencost/opencost/pkg/cloudcost"
+	"github.com/opencost/opencost/core/pkg/util/json"
+	"github.com/opencost/opencost/pkg/cloud/models"
+	"github.com/opencost/opencost/pkg/customcost"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 	"github.com/rs/cors"
 	"github.com/rs/cors"
 
 
@@ -36,59 +38,60 @@ func Execute(opts *CostModelOpts) error {
 	log.Infof("Starting cost-model version %s", version.FriendlyVersion())
 	log.Infof("Starting cost-model version %s", version.FriendlyVersion())
 	log.Infof("Kubernetes enabled: %t", env.IsKubernetesEnabled())
 	log.Infof("Kubernetes enabled: %t", env.IsKubernetesEnabled())
 
 
+	router := httprouter.New()
 	var a *costmodel.Accesses
 	var a *costmodel.Accesses
-
+	var cp models.Provider
 	if env.IsKubernetesEnabled() {
 	if env.IsKubernetesEnabled() {
-		a = costmodel.Initialize()
+		a = costmodel.Initialize(router)
 		err := StartExportWorker(context.Background(), a.Model)
 		err := StartExportWorker(context.Background(), a.Model)
 		if err != nil {
 		if err != nil {
 			log.Errorf("couldn't start CSV export worker: %v", err)
 			log.Errorf("couldn't start CSV export worker: %v", err)
 		}
 		}
-	} else {
-		a = costmodel.InitializeWithoutKubernetes()
-		log.Debugf("Cloud Cost config path: %s", env.GetCloudCostConfigPath())
+
+		// Register OpenCost Specific Endpoints
+		router.GET("/allocation", a.ComputeAllocationHandler)
+		router.GET("/allocation/summary", a.ComputeAllocationHandlerSummary)
+		router.GET("/assets", a.ComputeAssetsHandler)
+		if env.IsCarbonEstimatesEnabled() {
+			router.GET("/assets/carbon", a.ComputeAssetsCarbonHandler)
+		}
+
+		// set cloud provider for cloud cost
+		cp = a.CloudProvider
 	}
 	}
 
 
 	log.Infof("Cloud Costs enabled: %t", env.IsCloudCostEnabled())
 	log.Infof("Cloud Costs enabled: %t", env.IsCloudCostEnabled())
 	if env.IsCloudCostEnabled() {
 	if env.IsCloudCostEnabled() {
-		repo := cloudcost.NewMemoryRepository()
-		a.CloudCostPipelineService = cloudcost.NewPipelineService(repo, a.CloudConfigController, cloudcost.DefaultIngestorConfiguration())
-		repoQuerier := cloudcost.NewRepositoryQuerier(repo)
-		a.CloudCostQueryService = cloudcost.NewQueryService(repoQuerier, repoQuerier)
+		costmodel.InitializeCloudCost(router, cp)
 	}
 	}
 
 
-	rootMux := http.NewServeMux()
-	a.Router.GET("/healthz", Healthz)
-
-	if env.IsKubernetesEnabled() {
-		a.Router.GET("/allocation", a.ComputeAllocationHandler)
-		a.Router.GET("/allocation/summary", a.ComputeAllocationHandlerSummary)
-		a.Router.GET("/assets", a.ComputeAssetsHandler)
-		if env.IsCarbonEstimatesEnabled() {
-			a.Router.GET("/assets/carbon", a.ComputeAssetsCarbonHandler)
-		}
+	log.Infof("Custom Costs enabled: %t", env.IsCustomCostEnabled())
+	var customCostPipelineService *customcost.PipelineService
+	if env.IsCustomCostEnabled() {
+		customCostPipelineService = costmodel.InitializeCustomCost(router)
 	}
 	}
 
 
-	a.Router.GET("/cloudCost", a.CloudCostQueryService.GetCloudCostHandler())
-	a.Router.GET("/cloudCost/view/graph", a.CloudCostQueryService.GetCloudCostViewGraphHandler())
-	a.Router.GET("/cloudCost/view/totals", a.CloudCostQueryService.GetCloudCostViewTotalsHandler())
-	a.Router.GET("/cloudCost/view/table", a.CloudCostQueryService.GetCloudCostViewTableHandler())
+	// this endpoint is intentionally left out of the "if env.IsCustomCostEnabled()" conditional; in the handler, it is
+	// valid for CustomCostPipelineService to be nil
+	router.GET("/customCost/status", customCostPipelineService.GetCustomCostStatusHandler())
 
 
-	a.Router.GET("/cloudCost/status", a.CloudCostPipelineService.GetCloudCostStatusHandler())
-	a.Router.GET("/cloudCost/rebuild", a.CloudCostPipelineService.GetCloudCostRebuildHandler())
-	a.Router.GET("/cloudCost/repair", a.CloudCostPipelineService.GetCloudCostRepairHandler())
+	router.GET("/healthz", Healthz)
+
+	router.GET("/logs/level", GetLogLevel)
+	router.POST("/logs/level", SetLogLevel)
 
 
 	if env.IsPProfEnabled() {
 	if env.IsPProfEnabled() {
-		a.Router.HandlerFunc(http.MethodGet, "/debug/pprof/", pprof.Index)
-		a.Router.HandlerFunc(http.MethodGet, "/debug/pprof/cmdline", pprof.Cmdline)
-		a.Router.HandlerFunc(http.MethodGet, "/debug/pprof/profile", pprof.Profile)
-		a.Router.HandlerFunc(http.MethodGet, "/debug/pprof/symbol", pprof.Symbol)
-		a.Router.HandlerFunc(http.MethodGet, "/debug/pprof/trace", pprof.Trace)
-		a.Router.Handler(http.MethodGet, "/debug/pprof/goroutine", pprof.Handler("goroutine"))
-		a.Router.Handler(http.MethodGet, "/debug/pprof/heap", pprof.Handler("heap"))
+		router.HandlerFunc(http.MethodGet, "/debug/pprof/", pprof.Index)
+		router.HandlerFunc(http.MethodGet, "/debug/pprof/cmdline", pprof.Cmdline)
+		router.HandlerFunc(http.MethodGet, "/debug/pprof/profile", pprof.Profile)
+		router.HandlerFunc(http.MethodGet, "/debug/pprof/symbol", pprof.Symbol)
+		router.HandlerFunc(http.MethodGet, "/debug/pprof/trace", pprof.Trace)
+		router.Handler(http.MethodGet, "/debug/pprof/goroutine", pprof.Handler("goroutine"))
+		router.Handler(http.MethodGet, "/debug/pprof/heap", pprof.Handler("heap"))
 	}
 	}
 
 
-	rootMux.Handle("/", a.Router)
+	rootMux := http.NewServeMux()
+	rootMux.Handle("/", router)
 	rootMux.Handle("/metrics", promhttp.Handler())
 	rootMux.Handle("/metrics", promhttp.Handler())
 	telemetryHandler := metrics.ResponseMetricMiddleware(rootMux)
 	telemetryHandler := metrics.ResponseMetricMiddleware(rootMux)
 	handler := cors.AllowAll().Handler(telemetryHandler)
 	handler := cors.AllowAll().Handler(telemetryHandler)
@@ -130,3 +133,44 @@ func StartExportWorker(ctx context.Context, model costmodel.AllocationModel) err
 	}()
 	}()
 	return nil
 	return nil
 }
 }
+
+type LogLevelRequestResponse struct {
+	Level string `json:"level"`
+}
+
+func GetLogLevel(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+	w.Header().Set("Content-Type", "application/json")
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+
+	level := log.GetLogLevel()
+	llrr := LogLevelRequestResponse{
+		Level: level,
+	}
+
+	body, err := json.Marshal(llrr)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("unable to retrive log level"), http.StatusInternalServerError)
+		return
+	}
+	_, err = w.Write(body)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("unable to write response: %s", body), http.StatusInternalServerError)
+		return
+	}
+}
+
+func SetLogLevel(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+	params := LogLevelRequestResponse{}
+	err := json.NewDecoder(r.Body).Decode(&params)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("unable to decode request body, error: %s", err), http.StatusBadRequest)
+		return
+	}
+
+	err = log.SetLogLevel(params.Level)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("level must be a valid log level according to zerolog; level given: %s, error: %s", params.Level, err), http.StatusBadRequest)
+		return
+	}
+	w.WriteHeader(http.StatusOK)
+}

+ 7 - 7
pkg/costmodel/costmodel.go

@@ -844,12 +844,12 @@ func getContainerAllocation(req *util.Vector, used *util.Vector, allocationType
 	if req != nil && used != nil {
 	if req != nil && used != nil {
 		x1 := req.Value
 		x1 := req.Value
 		if math.IsNaN(x1) {
 		if math.IsNaN(x1) {
-			log.Warnf("NaN value found during %s allocation calculation for requests.", allocationType)
+			log.Debugf("NaN value found during %s allocation calculation for requests.", allocationType)
 			x1 = 0.0
 			x1 = 0.0
 		}
 		}
 		y1 := used.Value
 		y1 := used.Value
 		if math.IsNaN(y1) {
 		if math.IsNaN(y1) {
-			log.Warnf("NaN value found during %s allocation calculation for used.", allocationType)
+			log.Debugf("NaN value found during %s allocation calculation for used.", allocationType)
 			y1 = 0.0
 			y1 = 0.0
 		}
 		}
 		result = []*util.Vector{
 		result = []*util.Vector{
@@ -859,7 +859,7 @@ func getContainerAllocation(req *util.Vector, used *util.Vector, allocationType
 			},
 			},
 		}
 		}
 		if result[0].Value == 0 && result[0].Timestamp == 0 {
 		if result[0].Value == 0 && result[0].Timestamp == 0 {
-			log.Warnf("No request or usage data found during %s allocation calculation. Setting allocation to 0.", allocationType)
+			log.Debugf("No request or usage data found during %s allocation calculation. Setting allocation to 0.", allocationType)
 		}
 		}
 	} else if req != nil {
 	} else if req != nil {
 		result = []*util.Vector{
 		result = []*util.Vector{
@@ -876,7 +876,7 @@ func getContainerAllocation(req *util.Vector, used *util.Vector, allocationType
 			},
 			},
 		}
 		}
 	} else {
 	} else {
-		log.Warnf("No request or usage data found during %s allocation calculation. Setting allocation to 0.", allocationType)
+		log.Debugf("No request or usage data found during %s allocation calculation. Setting allocation to 0.", allocationType)
 		result = []*util.Vector{
 		result = []*util.Vector{
 			{
 			{
 				Value:     0,
 				Value:     0,
@@ -1159,9 +1159,9 @@ func (cm *CostModel) GetNodeCost(cp costAnalyzerCloud.Provider) (map[string]*cos
 			nodeCost := cpuCost + gpuCost + ramCost
 			nodeCost := cpuCost + gpuCost + ramCost
 
 
 			newCnode.Cost = fmt.Sprintf("%f", nodeCost)
 			newCnode.Cost = fmt.Sprintf("%f", nodeCost)
-			newCnode.VCPUCost = fmt.Sprintf("%f", cpuCost)
-			newCnode.GPUCost = fmt.Sprintf("%f", gpuCost)
-			newCnode.RAMCost = fmt.Sprintf("%f", ramCost)
+			newCnode.VCPUCost = fmt.Sprintf("%f", defaultCPUCorePrice)
+			newCnode.GPUCost = fmt.Sprintf("%f", defaultGPUPrice)
+			newCnode.RAMCost = fmt.Sprintf("%f", defaultRAMPrice)
 			newCnode.RAMBytes = fmt.Sprintf("%f", ram)
 			newCnode.RAMBytes = fmt.Sprintf("%f", ram)
 
 
 		} else if newCnode.GPU != "" && newCnode.GPUCost == "" {
 		} else if newCnode.GPU != "" && newCnode.GPUCost == "" {

+ 113 - 178
pkg/costmodel/router.go

@@ -86,28 +86,22 @@ var (
 // Accesses defines a singleton application instance, providing access to
 // Accesses defines a singleton application instance, providing access to
 // Prometheus, Kubernetes, the cloud provider, and caches.
 // Prometheus, Kubernetes, the cloud provider, and caches.
 type Accesses struct {
 type Accesses struct {
-	Router                    *httprouter.Router
-	PrometheusClient          prometheus.Client
-	ThanosClient              prometheus.Client
-	KubeClientSet             kubernetes.Interface
-	ClusterCache              clustercache.ClusterCache
-	ClusterMap                clusters.ClusterMap
-	CloudProvider             models.Provider
-	ConfigFileManager         *config.ConfigFileManager
-	CloudConfigController     *cloudconfig.Controller
-	CloudCostPipelineService  *cloudcost.PipelineService
-	CloudCostQueryService     *cloudcost.QueryService
-	CustomCostQueryService    *customcost.QueryService
-	CustomCostPipelineService *customcost.PipelineService
-	ClusterInfoProvider       clusters.ClusterInfoProvider
-	Model                     *CostModel
-	MetricsEmitter            *CostModelMetricsEmitter
-	OutOfClusterCache         *cache.Cache
-	AggregateCache            *cache.Cache
-	CostDataCache             *cache.Cache
-	ClusterCostsCache         *cache.Cache
-	CacheExpiration           map[time.Duration]time.Duration
-	AggAPI                    Aggregator
+	PrometheusClient    prometheus.Client
+	ThanosClient        prometheus.Client
+	KubeClientSet       kubernetes.Interface
+	ClusterCache        clustercache.ClusterCache
+	ClusterMap          clusters.ClusterMap
+	CloudProvider       models.Provider
+	ConfigFileManager   *config.ConfigFileManager
+	ClusterInfoProvider clusters.ClusterInfoProvider
+	Model               *CostModel
+	MetricsEmitter      *CostModelMetricsEmitter
+	OutOfClusterCache   *cache.Cache
+	AggregateCache      *cache.Cache
+	CostDataCache       *cache.Cache
+	ClusterCostsCache   *cache.Cache
+	CacheExpiration     map[time.Duration]time.Duration
+	AggAPI              Aggregator
 	// SettingsCache stores current state of app settings
 	// SettingsCache stores current state of app settings
 	SettingsCache *cache.Cache
 	SettingsCache *cache.Cache
 	// settingsSubscribers tracks channels through which changes to different
 	// settingsSubscribers tracks channels through which changes to different
@@ -1430,47 +1424,6 @@ func (a *Accesses) Status(w http.ResponseWriter, r *http.Request, _ httprouter.P
 	}
 	}
 }
 }
 
 
-type LogLevelRequestResponse struct {
-	Level string `json:"level"`
-}
-
-func (a *Accesses) GetLogLevel(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Header().Set("Access-Control-Allow-Origin", "*")
-
-	level := log.GetLogLevel()
-	llrr := LogLevelRequestResponse{
-		Level: level,
-	}
-
-	body, err := json.Marshal(llrr)
-	if err != nil {
-		http.Error(w, fmt.Sprintf("unable to retrive log level"), http.StatusInternalServerError)
-		return
-	}
-	_, err = w.Write(body)
-	if err != nil {
-		http.Error(w, fmt.Sprintf("unable to write response: %s", body), http.StatusInternalServerError)
-		return
-	}
-}
-
-func (a *Accesses) SetLogLevel(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
-	params := LogLevelRequestResponse{}
-	err := json.NewDecoder(r.Body).Decode(&params)
-	if err != nil {
-		http.Error(w, fmt.Sprintf("unable to decode request body, error: %s", err), http.StatusBadRequest)
-		return
-	}
-
-	err = log.SetLogLevel(params.Level)
-	if err != nil {
-		http.Error(w, fmt.Sprintf("level must be a valid log level according to zerolog; level given: %s, error: %s", params.Level, err), http.StatusBadRequest)
-		return
-	}
-	w.WriteHeader(http.StatusOK)
-}
-
 // captures the panic event in sentry
 // captures the panic event in sentry
 func capturePanicEvent(err string, stack string) {
 func capturePanicEvent(err string, stack string) {
 	msg := fmt.Sprintf("Panic: %s\nStackTrace: %s\n", err, stack)
 	msg := fmt.Sprintf("Panic: %s\nStackTrace: %s\n", err, stack)
@@ -1501,7 +1454,7 @@ func handlePanic(p errors.Panic) bool {
 	return p.Type == errors.PanicTypeHTTP
 	return p.Type == errors.PanicTypeHTTP
 }
 }
 
 
-func Initialize(additionalConfigWatchers ...*watcher.ConfigMapWatcher) *Accesses {
+func Initialize(router *httprouter.Router, additionalConfigWatchers ...*watcher.ConfigMapWatcher) *Accesses {
 	configWatchers := watcher.NewConfigMapWatchers(additionalConfigWatchers...)
 	configWatchers := watcher.NewConfigMapWatchers(additionalConfigWatchers...)
 
 
 	var err error
 	var err error
@@ -1733,25 +1686,23 @@ func Initialize(additionalConfigWatchers ...*watcher.ConfigMapWatcher) *Accesses
 	metricsEmitter := NewCostModelMetricsEmitter(promCli, k8sCache, cloudProvider, clusterInfoProvider, costModel)
 	metricsEmitter := NewCostModelMetricsEmitter(promCli, k8sCache, cloudProvider, clusterInfoProvider, costModel)
 
 
 	a := &Accesses{
 	a := &Accesses{
-		Router:                httprouter.New(),
-		PrometheusClient:      promCli,
-		ThanosClient:          thanosClient,
-		KubeClientSet:         kubeClientset,
-		ClusterCache:          k8sCache,
-		ClusterMap:            clusterMap,
-		CloudProvider:         cloudProvider,
-		CloudConfigController: cloudconfig.NewController(cloudProvider),
-		ConfigFileManager:     confManager,
-		ClusterInfoProvider:   clusterInfoProvider,
-		Model:                 costModel,
-		MetricsEmitter:        metricsEmitter,
-		AggregateCache:        aggregateCache,
-		CostDataCache:         costDataCache,
-		ClusterCostsCache:     clusterCostsCache,
-		OutOfClusterCache:     outOfClusterCache,
-		SettingsCache:         settingsCache,
-		CacheExpiration:       cacheExpiration,
-		httpServices:          services.NewCostModelServices(),
+		httpServices:        services.NewCostModelServices(),
+		PrometheusClient:    promCli,
+		ThanosClient:        thanosClient,
+		KubeClientSet:       kubeClientset,
+		ClusterCache:        k8sCache,
+		ClusterMap:          clusterMap,
+		CloudProvider:       cloudProvider,
+		ConfigFileManager:   confManager,
+		ClusterInfoProvider: clusterInfoProvider,
+		Model:               costModel,
+		MetricsEmitter:      metricsEmitter,
+		AggregateCache:      aggregateCache,
+		CostDataCache:       costDataCache,
+		ClusterCostsCache:   clusterCostsCache,
+		OutOfClusterCache:   outOfClusterCache,
+		SettingsCache:       settingsCache,
+		CacheExpiration:     cacheExpiration,
 	}
 	}
 
 
 	// Use the Accesses instance, itself, as the CostModelAggregator. This is
 	// Use the Accesses instance, itself, as the CostModelAggregator. This is
@@ -1779,120 +1730,104 @@ func Initialize(additionalConfigWatchers ...*watcher.ConfigMapWatcher) *Accesses
 		a.MetricsEmitter.Start()
 		a.MetricsEmitter.Start()
 	}
 	}
 
 
-	log.Infof("Custom Costs enabled: %t", env.IsCustomCostEnabled())
-	if env.IsCustomCostEnabled() {
-		hourlyRepo := customcost.NewMemoryRepository()
-		dailyRepo := customcost.NewMemoryRepository()
-		ingConfig := customcost.DefaultIngestorConfiguration()
-		var err error
-		a.CustomCostPipelineService, err = customcost.NewPipelineService(hourlyRepo, dailyRepo, ingConfig)
-		if err != nil {
-			log.Errorf("error instantiating custom cost pipeline service: %v", err)
-			return nil
-		}
-
-		customCostQuerier := customcost.NewRepositoryQuerier(hourlyRepo, dailyRepo, ingConfig.HourlyDuration, ingConfig.DailyDuration)
-		a.CustomCostQueryService = customcost.NewQueryService(customCostQuerier)
-	}
-
-	a.Router.GET("/costDataModel", a.CostDataModel)
-	a.Router.GET("/costDataModelRange", a.CostDataModelRange)
-	a.Router.GET("/aggregatedCostModel", a.AggregateCostModelHandler)
-	a.Router.GET("/allocation/compute", a.ComputeAllocationHandler)
-	a.Router.GET("/allocation/compute/summary", a.ComputeAllocationHandlerSummary)
-	a.Router.GET("/allNodePricing", a.GetAllNodePricing)
-	a.Router.POST("/refreshPricing", a.RefreshPricingData)
-	a.Router.GET("/clusterCostsOverTime", a.ClusterCostsOverTime)
-	a.Router.GET("/clusterCosts", a.ClusterCosts)
-	a.Router.GET("/clusterCostsFromCache", a.ClusterCostsFromCacheHandler)
-	a.Router.GET("/validatePrometheus", a.GetPrometheusMetadata)
-	a.Router.GET("/managementPlatform", a.ManagementPlatform)
-	a.Router.GET("/clusterInfo", a.ClusterInfo)
-	a.Router.GET("/clusterInfoMap", a.GetClusterInfoMap)
-	a.Router.GET("/serviceAccountStatus", a.GetServiceAccountStatus)
-	a.Router.GET("/pricingSourceStatus", a.GetPricingSourceStatus)
-	a.Router.GET("/pricingSourceSummary", a.GetPricingSourceSummary)
-	a.Router.GET("/pricingSourceCounts", a.GetPricingSourceCounts)
+	a.httpServices.RegisterAll(router)
+
+	router.GET("/costDataModel", a.CostDataModel)
+	router.GET("/costDataModelRange", a.CostDataModelRange)
+	router.GET("/aggregatedCostModel", a.AggregateCostModelHandler)
+	router.GET("/allocation/compute", a.ComputeAllocationHandler)
+	router.GET("/allocation/compute/summary", a.ComputeAllocationHandlerSummary)
+	router.GET("/allNodePricing", a.GetAllNodePricing)
+	router.POST("/refreshPricing", a.RefreshPricingData)
+	router.GET("/clusterCostsOverTime", a.ClusterCostsOverTime)
+	router.GET("/clusterCosts", a.ClusterCosts)
+	router.GET("/clusterCostsFromCache", a.ClusterCostsFromCacheHandler)
+	router.GET("/validatePrometheus", a.GetPrometheusMetadata)
+	router.GET("/managementPlatform", a.ManagementPlatform)
+	router.GET("/clusterInfo", a.ClusterInfo)
+	router.GET("/clusterInfoMap", a.GetClusterInfoMap)
+	router.GET("/serviceAccountStatus", a.GetServiceAccountStatus)
+	router.GET("/pricingSourceStatus", a.GetPricingSourceStatus)
+	router.GET("/pricingSourceSummary", a.GetPricingSourceSummary)
+	router.GET("/pricingSourceCounts", a.GetPricingSourceCounts)
 
 
 	// endpoints migrated from server
 	// endpoints migrated from server
-	a.Router.GET("/allPersistentVolumes", a.GetAllPersistentVolumes)
-	a.Router.GET("/allDeployments", a.GetAllDeployments)
-	a.Router.GET("/allStorageClasses", a.GetAllStorageClasses)
-	a.Router.GET("/allStatefulSets", a.GetAllStatefulSets)
-	a.Router.GET("/allNodes", a.GetAllNodes)
-	a.Router.GET("/allPods", a.GetAllPods)
-	a.Router.GET("/allNamespaces", a.GetAllNamespaces)
-	a.Router.GET("/allDaemonSets", a.GetAllDaemonSets)
-	a.Router.GET("/pod/:namespace/:name", a.GetPod)
-	a.Router.GET("/prometheusRecordingRules", a.PrometheusRecordingRules)
-	a.Router.GET("/prometheusConfig", a.PrometheusConfig)
-	a.Router.GET("/prometheusTargets", a.PrometheusTargets)
-	a.Router.GET("/orphanedPods", a.GetOrphanedPods)
-	a.Router.GET("/installNamespace", a.GetInstallNamespace)
-	a.Router.GET("/installInfo", a.GetInstallInfo)
-	a.Router.GET("/podLogs", a.GetPodLogs)
-	a.Router.POST("/serviceKey", a.AddServiceKey)
-	a.Router.GET("/helmValues", a.GetHelmValues)
-	a.Router.GET("/status", a.Status)
+	router.GET("/allPersistentVolumes", a.GetAllPersistentVolumes)
+	router.GET("/allDeployments", a.GetAllDeployments)
+	router.GET("/allStorageClasses", a.GetAllStorageClasses)
+	router.GET("/allStatefulSets", a.GetAllStatefulSets)
+	router.GET("/allNodes", a.GetAllNodes)
+	router.GET("/allPods", a.GetAllPods)
+	router.GET("/allNamespaces", a.GetAllNamespaces)
+	router.GET("/allDaemonSets", a.GetAllDaemonSets)
+	router.GET("/pod/:namespace/:name", a.GetPod)
+	router.GET("/prometheusRecordingRules", a.PrometheusRecordingRules)
+	router.GET("/prometheusConfig", a.PrometheusConfig)
+	router.GET("/prometheusTargets", a.PrometheusTargets)
+	router.GET("/orphanedPods", a.GetOrphanedPods)
+	router.GET("/installNamespace", a.GetInstallNamespace)
+	router.GET("/installInfo", a.GetInstallInfo)
+	router.GET("/podLogs", a.GetPodLogs)
+	router.POST("/serviceKey", a.AddServiceKey)
+	router.GET("/helmValues", a.GetHelmValues)
+	router.GET("/status", a.Status)
 
 
 	// prom query proxies
 	// prom query proxies
-	a.Router.GET("/prometheusQuery", a.PrometheusQuery)
-	a.Router.GET("/prometheusQueryRange", a.PrometheusQueryRange)
-	a.Router.GET("/thanosQuery", a.ThanosQuery)
-	a.Router.GET("/thanosQueryRange", a.ThanosQueryRange)
+	router.GET("/prometheusQuery", a.PrometheusQuery)
+	router.GET("/prometheusQueryRange", a.PrometheusQueryRange)
+	router.GET("/thanosQuery", a.ThanosQuery)
+	router.GET("/thanosQueryRange", a.ThanosQueryRange)
 
 
 	// diagnostics
 	// diagnostics
-	a.Router.GET("/diagnostics/requestQueue", a.GetPrometheusQueueState)
-	a.Router.GET("/diagnostics/prometheusMetrics", a.GetPrometheusMetrics)
+	router.GET("/diagnostics/requestQueue", a.GetPrometheusQueueState)
+	router.GET("/diagnostics/prometheusMetrics", a.GetPrometheusMetrics)
 
 
-	a.Router.GET("/logs/level", a.GetLogLevel)
-	a.Router.POST("/logs/level", a.SetLogLevel)
+	return a
+}
 
 
-	a.Router.GET("/cloud/config/export", a.CloudConfigController.GetExportConfigHandler())
-	a.Router.GET("/cloud/config/enable", a.CloudConfigController.GetEnableConfigHandler())
-	a.Router.GET("/cloud/config/disable", a.CloudConfigController.GetDisableConfigHandler())
-	a.Router.GET("/cloud/config/delete", a.CloudConfigController.GetDeleteConfigHandler())
+// InitializeCloudCost Initializes Cloud Cost pipeline and querier and registers endpoints
+func InitializeCloudCost(router *httprouter.Router, cp models.Provider) {
+	log.Debugf("Cloud Cost config path: %s", env.GetCloudCostConfigPath())
+	cloudConfigController := cloudconfig.NewController(cp)
 
 
-	if env.IsCustomCostEnabled() {
-		a.Router.GET("/customCost/total", a.CustomCostQueryService.GetCustomCostTotalHandler())
-		a.Router.GET("/customCost/timeseries", a.CustomCostQueryService.GetCustomCostTimeseriesHandler())
-	}
+	repo := cloudcost.NewMemoryRepository()
+	cloudCostPipelineService := cloudcost.NewPipelineService(repo, cloudConfigController, cloudcost.DefaultIngestorConfiguration())
+	repoQuerier := cloudcost.NewRepositoryQuerier(repo)
+	cloudCostQueryService := cloudcost.NewQueryService(repoQuerier, repoQuerier)
 
 
-	// this endpoint is intentionally left out of the "if env.IsCustomCostEnabled()" conditional; in the handler, it is
-	// valid for CustomCostPipelineService to be nil
-	a.Router.GET("/customCost/status", a.CustomCostPipelineService.GetCustomCostStatusHandler())
+	router.GET("/cloud/config/export", cloudConfigController.GetExportConfigHandler())
+	router.GET("/cloud/config/enable", cloudConfigController.GetEnableConfigHandler())
+	router.GET("/cloud/config/disable", cloudConfigController.GetDisableConfigHandler())
+	router.GET("/cloud/config/delete", cloudConfigController.GetDeleteConfigHandler())
 
 
-	a.httpServices.RegisterAll(a.Router)
+	router.GET("/cloudCost", cloudCostQueryService.GetCloudCostHandler())
+	router.GET("/cloudCost/view/graph", cloudCostQueryService.GetCloudCostViewGraphHandler())
+	router.GET("/cloudCost/view/totals", cloudCostQueryService.GetCloudCostViewTotalsHandler())
+	router.GET("/cloudCost/view/table", cloudCostQueryService.GetCloudCostViewTableHandler())
 
 
-	return a
+	router.GET("/cloudCost/status", cloudCostPipelineService.GetCloudCostStatusHandler())
+	router.GET("/cloudCost/rebuild", cloudCostPipelineService.GetCloudCostRebuildHandler())
+	router.GET("/cloudCost/repair", cloudCostPipelineService.GetCloudCostRepairHandler())
 }
 }
 
 
-func InitializeWithoutKubernetes() *Accesses {
+func InitializeCustomCost(router *httprouter.Router) *customcost.PipelineService {
+	hourlyRepo := customcost.NewMemoryRepository()
+	dailyRepo := customcost.NewMemoryRepository()
+	ingConfig := customcost.DefaultIngestorConfiguration()
 	var err error
 	var err error
-	if errorReportingEnabled {
-		err = sentry.Init(sentry.ClientOptions{Release: version.FriendlyVersion()})
-		if err != nil {
-			log.Infof("Failed to initialize sentry for error reporting")
-		} else {
-			err = errors.SetPanicHandler(handlePanic)
-			if err != nil {
-				log.Infof("Failed to set panic handler: %s", err)
-			}
-		}
-	}
-
-	a := &Accesses{
-		Router:                httprouter.New(),
-		CloudConfigController: cloudconfig.NewController(nil),
-		httpServices:          services.NewCostModelServices(),
+	customCostPipelineService, err := customcost.NewPipelineService(hourlyRepo, dailyRepo, ingConfig)
+	if err != nil {
+		log.Errorf("error instantiating custom cost pipeline service: %v", err)
+		return nil
 	}
 	}
 
 
-	a.Router.GET("/logs/level", a.GetLogLevel)
-	a.Router.POST("/logs/level", a.SetLogLevel)
+	customCostQuerier := customcost.NewRepositoryQuerier(hourlyRepo, dailyRepo, ingConfig.HourlyDuration, ingConfig.DailyDuration)
+	customCostQueryService := customcost.NewQueryService(customCostQuerier)
 
 
-	a.httpServices.RegisterAll(a.Router)
+	router.GET("/customCost/total", customCostQueryService.GetCustomCostTotalHandler())
+	router.GET("/customCost/timeseries", customCostQueryService.GetCustomCostTimeseriesHandler())
 
 
-	return a
+	return customCostPipelineService
 }
 }
 
 
 func writeErrorResponse(w http.ResponseWriter, code int, message string) {
 func writeErrorResponse(w http.ResponseWriter, code int, message string) {

+ 7 - 0
pkg/env/costmodelenv.go

@@ -36,6 +36,7 @@ const (
 	RemotePWEnvVar                 = "REMOTE_WRITE_PASSWORD"
 	RemotePWEnvVar                 = "REMOTE_WRITE_PASSWORD"
 	SQLAddressEnvVar               = "SQL_ADDRESS"
 	SQLAddressEnvVar               = "SQL_ADDRESS"
 	UseCSVProviderEnvVar           = "USE_CSV_PROVIDER"
 	UseCSVProviderEnvVar           = "USE_CSV_PROVIDER"
+	UseCustomProviderEnvVar        = "USE_CUSTOM_PROVIDER"
 	CSVRegionEnvVar                = "CSV_REGION"
 	CSVRegionEnvVar                = "CSV_REGION"
 	CSVEndpointEnvVar              = "CSV_ENDPOINT"
 	CSVEndpointEnvVar              = "CSV_ENDPOINT"
 	CSVPathEnvVar                  = "CSV_PATH"
 	CSVPathEnvVar                  = "CSV_PATH"
@@ -409,6 +410,12 @@ func IsUseCSVProvider() bool {
 	return env.GetBool(UseCSVProviderEnvVar, false)
 	return env.GetBool(UseCSVProviderEnvVar, false)
 }
 }
 
 
+// IsUseCustomProvider returns the environment variable value for UseCustomProviderEnvVar which represents
+// whether or not the use of a custom cost provider is enabled.
+func IsUseCustomProvider() bool {
+	return env.GetBool(UseCustomProviderEnvVar, false)
+}
+
 // GetCSVRegion returns the environment variable value for CSVRegionEnvVar which represents the
 // GetCSVRegion returns the environment variable value for CSVRegionEnvVar which represents the
 // region configured for a CSV provider.
 // region configured for a CSV provider.
 func GetCSVRegion() string {
 func GetCSVRegion() string {

+ 1 - 1
pkg/storage/prefixedbucketstorage.go

@@ -32,7 +32,7 @@ func validPrefix(prefix string) bool {
 }
 }
 
 
 func conditionalPrefix(prefix, name string) string {
 func conditionalPrefix(prefix, name string) string {
-	if len(name) > 0 {
+	if len(name) > 0 && !strings.HasPrefix(name, prefix) {
 		return withPrefix(prefix, name)
 		return withPrefix(prefix, name)
 	}
 	}
 
 

+ 7 - 7
pkg/storage/s3storage.go

@@ -347,7 +347,7 @@ func (s3 *S3Storage) FullPath(name string) string {
 func (s3 *S3Storage) Read(name string) ([]byte, error) {
 func (s3 *S3Storage) Read(name string) ([]byte, error) {
 	name = trimLeading(name)
 	name = trimLeading(name)
 
 
-	log.Debugf("S3Storage::Read(%s)", name)
+	log.Tracef("S3Storage::Read(%s)", name)
 	ctx := context.Background()
 	ctx := context.Background()
 
 
 	return s3.getRange(ctx, name, 0, -1)
 	return s3.getRange(ctx, name, 0, -1)
@@ -357,7 +357,7 @@ func (s3 *S3Storage) Read(name string) ([]byte, error) {
 // Exists checks if the given object exists.
 // Exists checks if the given object exists.
 func (s3 *S3Storage) Exists(name string) (bool, error) {
 func (s3 *S3Storage) Exists(name string) (bool, error) {
 	name = trimLeading(name)
 	name = trimLeading(name)
-	//log.Debugf("S3Storage::Exists(%s)", name)
+	log.Tracef("S3Storage::Exists(%s)", name)
 
 
 	ctx := context.Background()
 	ctx := context.Background()
 
 
@@ -376,7 +376,7 @@ func (s3 *S3Storage) Exists(name string) (bool, error) {
 func (s3 *S3Storage) Write(name string, data []byte) error {
 func (s3 *S3Storage) Write(name string, data []byte) error {
 	name = trimLeading(name)
 	name = trimLeading(name)
 
 
-	log.Debugf("S3Storage::Write(%s)", name)
+	log.Tracef("S3Storage::Write(%s)", name)
 
 
 	ctx := context.Background()
 	ctx := context.Background()
 	sse, err := s3.getServerSideEncryption(ctx)
 	sse, err := s3.getServerSideEncryption(ctx)
@@ -410,7 +410,7 @@ func (s3 *S3Storage) Write(name string, data []byte) error {
 func (s3 *S3Storage) Stat(name string) (*StorageInfo, error) {
 func (s3 *S3Storage) Stat(name string) (*StorageInfo, error) {
 	name = trimLeading(name)
 	name = trimLeading(name)
 
 
-	//log.Debugf("S3Storage::Stat(%s)", name)
+	log.Tracef("S3Storage::Stat(%s)", name)
 	ctx := context.Background()
 	ctx := context.Background()
 
 
 	objInfo, err := s3.client.StatObject(ctx, s3.name, name, minio.StatObjectOptions{})
 	objInfo, err := s3.client.StatObject(ctx, s3.name, name, minio.StatObjectOptions{})
@@ -432,7 +432,7 @@ func (s3 *S3Storage) Stat(name string) (*StorageInfo, error) {
 func (s3 *S3Storage) Remove(name string) error {
 func (s3 *S3Storage) Remove(name string) error {
 	name = trimLeading(name)
 	name = trimLeading(name)
 
 
-	log.Debugf("S3Storage::Remove(%s)", name)
+	log.Tracef("S3Storage::Remove(%s)", name)
 	ctx := context.Background()
 	ctx := context.Background()
 
 
 	return s3.client.RemoveObject(ctx, s3.name, name, minio.RemoveObjectOptions{})
 	return s3.client.RemoveObject(ctx, s3.name, name, minio.RemoveObjectOptions{})
@@ -441,7 +441,7 @@ func (s3 *S3Storage) Remove(name string) error {
 func (s3 *S3Storage) List(path string) ([]*StorageInfo, error) {
 func (s3 *S3Storage) List(path string) ([]*StorageInfo, error) {
 	path = trimLeading(path)
 	path = trimLeading(path)
 
 
-	log.Debugf("S3Storage::List(%s)", path)
+	log.Tracef("S3Storage::List(%s)", path)
 	ctx := context.Background()
 	ctx := context.Background()
 
 
 	// Ensure the object name actually ends with a dir suffix. Otherwise we'll just iterate the
 	// Ensure the object name actually ends with a dir suffix. Otherwise we'll just iterate the
@@ -488,7 +488,7 @@ func (s3 *S3Storage) List(path string) ([]*StorageInfo, error) {
 func (s3 *S3Storage) ListDirectories(path string) ([]*StorageInfo, error) {
 func (s3 *S3Storage) ListDirectories(path string) ([]*StorageInfo, error) {
 	path = trimLeading(path)
 	path = trimLeading(path)
 
 
-	log.Debugf("S3Storage::List(%s)", path)
+	log.Tracef("S3Storage::List(%s)", path)
 	ctx := context.Background()
 	ctx := context.Background()
 
 
 	if path != "" {
 	if path != "" {

+ 3 - 0
pkg/storage/storage.go

@@ -4,6 +4,7 @@ import (
 	"os"
 	"os"
 	"time"
 	"time"
 
 
+	"github.com/opencost/opencost/core/pkg/log"
 	"github.com/pkg/errors"
 	"github.com/pkg/errors"
 )
 )
 
 
@@ -63,6 +64,8 @@ func Validate(storage Storage) error {
 	const testPath = "tmp/test.txt"
 	const testPath = "tmp/test.txt"
 	const testContent = "test"
 	const testContent = "test"
 
 
+	log.Debug("validating storage")
+
 	// attempt to read a path
 	// attempt to read a path
 	_, err := storage.Exists(testPath)
 	_, err := storage.Exists(testPath)
 	if err != nil {
 	if err != nil {

+ 0 - 3
ui/.babelrc

@@ -1,3 +0,0 @@
-{
-  "plugins": ["@babel/plugin-transform-runtime", "@babel/plugin-proposal-class-properties"]
-}

+ 0 - 2
ui/.dockerignore

@@ -1,2 +0,0 @@
-.parcel-cache/
-node_modules/

+ 0 - 1
ui/.nvmrc

@@ -1 +0,0 @@
-18.3.0

+ 0 - 48
ui/Dockerfile

@@ -1,48 +0,0 @@
-FROM node:18.3.0 as builder
-ADD package*.json /opt/ui/
-WORKDIR /opt/ui
-RUN npm install
-ADD src /opt/ui/src
-RUN npx parcel build src/index.html
-
-FROM nginx:alpine
-
-LABEL org.opencontainers.image.description="Cross-cloud cost allocation models for Kubernetes workloads"
-LABEL org.opencontainers.image.documentation=https://opencost.io/docs/
-LABEL org.opencontainers.image.licenses=Apache-2.0
-LABEL org.opencontainers.image.source=https://github.com/opencost/opencost
-LABEL org.opencontainers.image.title=opencost-ui
-LABEL org.opencontainers.image.url=https://opencost.io
-
-ARG version=dev
-ARG	commit=HEAD
-ENV VERSION=${version}
-ENV HEAD=${commit}
-
-ENV API_PORT=9003
-ENV API_SERVER=0.0.0.0
-ENV UI_PORT=9090
-
-COPY --from=builder /opt/ui/dist /opt/ui/dist
-RUN mkdir -p /var/www
-
-COPY THIRD_PARTY_LICENSES.txt /THIRD_PARTY_LICENSES.txt
-COPY --from=builder /opt/ui/dist /var/www
-
-COPY default.nginx.conf.template /etc/nginx/conf.d/default.nginx.conf.template
-COPY nginx.conf /etc/nginx/
-COPY ./docker-entrypoint.sh /usr/local/bin/
-
-RUN rm -rf /etc/nginx/conf.d/default.conf
-
-RUN adduser 1001 -g 1000 -D
-RUN chown 1001:1000 -R /var/www
-RUN chown 1001:1000 -R /etc/nginx
-RUN chown 1001:1000 -R /usr/local/bin/docker-entrypoint.sh
-
-ENV BASE_URL=/model
-
-USER 1001
-
-ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
-CMD ["nginx", "-g", "daemon off;"]

+ 0 - 38
ui/Dockerfile.cross

@@ -1,38 +0,0 @@
-FROM nginx:alpine
-
-LABEL org.opencontainers.image.description="Cross-cloud cost allocation models for Kubernetes workloads"
-LABEL org.opencontainers.image.documentation=https://opencost.io/docs/
-LABEL org.opencontainers.image.licenses=Apache-2.0
-LABEL org.opencontainers.image.source=https://github.com/opencost/opencost
-LABEL org.opencontainers.image.title=opencost-ui
-LABEL org.opencontainers.image.url=https://opencost.io
-
-ARG version=dev
-ARG	commit=HEAD
-ENV VERSION=${version}
-ENV HEAD=${commit}
-
-ENV API_PORT=9003
-ENV API_SERVER=0.0.0.0
-ENV UI_PORT=9090
-
-COPY ./dist /opt/ui/dist
-COPY THIRD_PARTY_LICENSES.txt /THIRD_PARTY_LICENSES.txt
-COPY default.nginx.conf.template /etc/nginx/conf.d/default.nginx.conf.template
-COPY nginx.conf /etc/nginx/
-COPY ./docker-entrypoint.sh /usr/local/bin/
-RUN mkdir -p /var/www
-
-RUN rm -rf /etc/nginx/conf.d/default.conf
-
-RUN adduser 1001 -g 1000 -D
-RUN chown 1001:1000 -R /var/www
-RUN chown 1001:1000 -R /etc/nginx
-RUN chown 1001:1000 -R /usr/local/bin/docker-entrypoint.sh
-
-ENV BASE_URL=/model
-
-USER 1001
-
-ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
-CMD ["nginx", "-g", "daemon off;"]

+ 0 - 40
ui/Dockerfile.debug

@@ -1,40 +0,0 @@
-# This dockerfile is for development purposes only; do not use this for production deployments
-# This file exists due to changes introduced in https://github.com/opencost/opencost/pull/2502
-# Tilt cannot reference files that exist outside of this ./ui folder so the reference to THIRD_PARTY_LICENSES.txt is removed
-FROM nginx:alpine
-
-LABEL org.opencontainers.image.description="Cross-cloud cost allocation models for Kubernetes workloads"
-LABEL org.opencontainers.image.documentation=https://opencost.io/docs/
-LABEL org.opencontainers.image.licenses=Apache-2.0
-LABEL org.opencontainers.image.source=https://github.com/opencost/opencost
-LABEL org.opencontainers.image.title=opencost-ui
-LABEL org.opencontainers.image.url=https://opencost.io
-
-ARG version=dev
-ARG	commit=HEAD
-ENV VERSION=${version}
-ENV HEAD=${commit}
-
-ENV API_PORT=9003
-ENV API_SERVER=0.0.0.0
-ENV UI_PORT=9090
-
-COPY ./dist /opt/ui/dist
-COPY default.nginx.conf.template /etc/nginx/conf.d/default.nginx.conf.template
-COPY nginx.conf /etc/nginx/
-COPY ./docker-entrypoint.sh /usr/local/bin/
-RUN mkdir -p /var/www
-
-RUN rm -rf /etc/nginx/conf.d/default.conf
-
-RUN adduser 1001 -g 1000 -D
-RUN chown 1001:1000 -R /var/www
-RUN chown 1001:1000 -R /etc/nginx
-RUN chown 1001:1000 -R /usr/local/bin/docker-entrypoint.sh
-
-ENV BASE_URL=/model
-
-USER 1001
-
-ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
-CMD ["nginx", "-g", "daemon off;"]

+ 0 - 65
ui/README.md

@@ -1,65 +0,0 @@
-# OpenCost UI
-
-## Installing
-
-See https://www.opencost.io/docs/install for the full instructions.
-
-```
-helm install prometheus --repo https://prometheus-community.github.io/helm-charts prometheus \
-  --namespace prometheus-system --create-namespace \
-  --set pushgateway.enabled=false \
-  --set alertmanager.enabled=false \
-  -f https://raw.githubusercontent.com/opencost/opencost/develop/kubernetes/prometheus/extraScrapeConfigs.yaml
-
-kubectl apply --namespace opencost -f https://raw.githubusercontent.com/opencost/opencost/develop/kubernetes/opencost.yaml
-```
-
-## Using
-
-After following the installation instructions, access the UI by port forwarding:
-```
-kubectl port-forward --namespace opencost service/opencost 9090
-```
-
-## Running Locally
-
-The UI can be run locally using the `npm run serve` command.
-
-```sh
-$ npm install
-...
-$ npm run serve
-> opencost-ui@0.1.0 serve
-> npx parcel serve src/index.html
-
-Server running at http://localhost:1234
-✨ Built in 1.96s
-```
-
-And can have a custom URL backend prefix.
-
-```sh
-BASE_URL=http://localhost:9090/test npm run serve
-
-> opencost-ui@0.1.0 serve
-> npx parcel serve src/index.html
-
-Server running at http://localhost:1234
-✨ Built in 772ms
-```
-
-In addition, similar behavior can be replicated with the docker container:
-
-```sh
-$ docker run -e BASE_URL_OVERRIDE=test -p 9091:9090 -d opencost-ui:latest
-$ curl localhost:9091
-<html gibberish>
-```
-
-## Overriding the Base API URL
-
-For some use cases such as the case of [OpenCost deployed behind an ingress controller](https://github.com/opencost/opencost/issues/1677), it is useful to override the `BASE_URL` variable responsible for requests sent from the UI to the API.  This means that instead of sending requests to `<domain>/model/allocation/compute/etc`, requests can be sent to `<domain>/{BASE_URL_OVERRIDE}/allocation/compute/etc`.  To do this, supply the environment variable `BASE_URL_OVERRIDE` to the docker image.
-
-```sh
-$ docker run -p 9091:9090 -e BASE_URL_OVERRIDE=anything -d opencost-ui:latest
-```

+ 0 - 79
ui/default.nginx.conf.template

@@ -1,79 +0,0 @@
-gzip_static  on;
-gzip on;
-gzip_min_length 50000;
-gzip_proxied expired no-cache no-store private auth;
-gzip_types
-    application/atom+xml
-    application/geo+json
-    application/javascript
-    application/x-javascript
-    application/json
-    application/ld+json
-    application/manifest+json
-    application/rdf+xml
-    application/rss+xml
-    application/vnd.ms-fontobject
-    application/wasm
-    application/x-web-app-manifest+json
-    application/xhtml+xml
-    application/xml
-    font/eot
-    font/otf
-    font/ttf
-    image/bmp
-    image/svg+xml
-    text/cache-manifest
-    text/calendar
-    text/css
-    text/javascript
-    text/markdown
-    text/plain
-    text/xml
-    text/x-component
-    text/x-cross-domain-policy;
-
-upstream model {
-    # Update to the cost model endpoint
-    # Example: host.docker.internal:9003;
-    server ${API_SERVER}:${API_PORT};
-}
-
-server {
-    server_name _;
-    root /var/www;
-    index index.html;
-    large_client_header_buffers 4 32k;
-    add_header Cache-Control "must-revalidate";
-
-    error_page 504 /custom_504.html;
-    location = /custom_504.html {
-        internal;
-    }
-
-    add_header Cache-Control "max-age=300";
-    location / {
-        root /var/www;
-        index index.html index.htm;
-        try_files $uri /index.html;
-    }
-
-    add_header ETag "1.96.0";
-    listen ${UI_PORT};
-    listen [::]:${UI_PORT};
-    resolver 127.0.0.1 valid=5s;
-    location /healthz {
-        access_log /dev/null;
-        return 200 'OK';
-    }
-    location /model/ {
-        proxy_connect_timeout       180;
-        proxy_send_timeout          180;
-        proxy_read_timeout          180;
-        proxy_pass http://model/;
-        proxy_redirect off;
-        proxy_http_version 1.1;
-        proxy_set_header Connection "";
-        proxy_set_header  X-Real-IP  $remote_addr;
-        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
-    }
-}

+ 0 - 26
ui/docker-entrypoint.sh

@@ -1,26 +0,0 @@
-#!/bin/sh
-set -e
-
-cp -rv /opt/ui/dist/* /var/www
-
-if [[ ! -z "$BASE_URL_OVERRIDE" ]]; then
-    echo "running with BASE_URL=${BASE_URL_OVERRIDE}"
-    sed -i "s^{PLACEHOLDER_BASE_URL}^$BASE_URL_OVERRIDE^g" /var/www/*.js
-else
-    echo "running with BASE_URL=${BASE_URL}"
-    sed -i "s^{PLACEHOLDER_BASE_URL}^$BASE_URL^g" /var/www/*.js
-fi
-
-# export your OPENCOST_FOOTER_CONTENT='<a href="https://opencost.io">OpenCost</a>' in your Dockerfile to set
-if [[ ! -z "$OPENCOST_FOOTER_CONTENT" ]]; then
-    sed -i "s^PLACEHOLDER_FOOTER_CONTENT^$OPENCOST_FOOTER_CONTENT^g" /var/www/*.js
-else
-    sed -i "s^PLACEHOLDER_FOOTER_CONTENT^OpenCost version: $VERSION ($HEAD)^g" /var/www/*.js
-fi
-
-envsubst '$API_PORT $API_SERVER $UI_PORT' < /etc/nginx/conf.d/default.nginx.conf.template > /etc/nginx/conf.d/default.nginx.conf
-
-echo "Starting OpenCost UI version $VERSION ($HEAD)"
-
-# Run the parent (nginx) container's entrypoint script
-exec /docker-entrypoint.sh "$@"

+ 0 - 41
ui/justfile

@@ -1,41 +0,0 @@
-commit := `git rev-parse --short HEAD`
-thirdPartyLicenseFile := "THIRD_PARTY_LICENSES.txt"
-
-default:
-    just --list
-
-build-local:
-    npm install
-
-    npx parcel build src/index.html
-
-build IMAGE_TAG RELEASE_VERSION: build-local
-    cp ../{{thirdPartyLicenseFile}} .
-    docker buildx build \
-        --rm \
-        --platform "linux/amd64" \
-        -f 'Dockerfile.cross' \
-        --provenance=false \
-        -t {{IMAGE_TAG}}-amd64 \
-        --build-arg version={{RELEASE_VERSION}} \
-        --build-arg commit={{commit}} \
-        --push \
-        .
-
-    docker buildx build \
-        --rm \
-        --platform "linux/arm64" \
-        -f 'Dockerfile.cross' \
-        --provenance=false \
-        -t {{IMAGE_TAG}}-arm64 \
-        --build-arg version={{RELEASE_VERSION}} \
-        --build-arg commit={{commit}} \
-        --push \
-        .
-
-    manifest-tool push from-args \
-        --platforms "linux/amd64,linux/arm64" \
-        --template {{IMAGE_TAG}}-ARCH \
-        --target {{IMAGE_TAG}}
-
-    rm -f {{thirdPartyLicenseFile}}

+ 0 - 36
ui/nginx.conf

@@ -1,36 +0,0 @@
-worker_processes  auto;
-
-error_log  /var/log/nginx/error.log notice;
-pid        /tmp/nginx.pid;
-
-
-events {
-    worker_connections  1024;
-}
-
-
-http {
-    proxy_temp_path /tmp/proxy_temp;
-    client_body_temp_path /tmp/client_temp;
-    fastcgi_temp_path /tmp/fastcgi_temp;
-    uwsgi_temp_path /tmp/uwsgi_temp;
-    scgi_temp_path /tmp/scgi_temp;
-
-    include       /etc/nginx/mime.types;
-    default_type  application/octet-stream;
-
-    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
-                      '$status $body_bytes_sent "$http_referer" '
-                      '"$http_user_agent" "$http_x_forwarded_for"';
-
-    access_log  /var/log/nginx/access.log  main;
-
-    sendfile        on;
-    #tcp_nopush     on;
-
-    keepalive_timeout  65;
-
-    #gzip  on;
-
-    include /etc/nginx/conf.d/*.conf;
-}

+ 0 - 6093
ui/package-lock.json

@@ -1,6093 +0,0 @@
-{
-  "name": "opencost-ui",
-  "version": "0.1.0",
-  "lockfileVersion": 3,
-  "requires": true,
-  "packages": {
-    "": {
-      "name": "opencost-ui",
-      "version": "0.1.0",
-      "hasInstallScript": true,
-      "license": "Apache-2.0",
-      "dependencies": {
-        "material-design-icons-iconfont": "^6.1.0",
-        "axios": "^1.6.0",
-        "@material-ui/icons": "^4.11.2",
-        "@material-ui/pickers": "^3.3.10",
-        "html-to-react": "^1.7.0",
-        "@babel/runtime": "^7.23.9",
-        "date-fns": "^2.30.0",
-        "react-dom": "^17.0.1",
-        "@material-ui/core": "^4.11.3",
-        "recharts": "^2.2.0",
-        "react-router-dom": "^5.2.0",
-        "@date-io/core": "^1.3.13",
-        "@material-ui/styles": "^4.11.5",
-        "react": "^17.0.1",
-        "@date-io/date-fns": "^1.3.13",
-        "prop-types": "^15.7.2"
-      },
-      "devDependencies": {
-        "@babel/core": "^7.13.10",
-        "@babel/plugin-proposal-class-properties": "^7.13.0",
-        "@babel/plugin-transform-runtime": "^7.23.9",
-        "@babel/preset-react": "^7.12.13",
-        "buffer": "^6.0.3",
-        "parcel": "^2.11.0",
-        "process": "^0.11.10",
-        "set-value": "4.1.0"
-      }
-    },
-    "node_modules/argparse": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
-      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
-      "dev": true
-    },
-    "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/@emotion/hash": {
-      "version": "0.8.0",
-      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
-      "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="
-    },
-    "node_modules/convert-source-map": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
-      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
-      "dev": true
-    },
-    "node_modules/detect-libc": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
-      "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
-      "dev": true,
-      "bin": {
-        "detect-libc": "bin/detect-libc.js"
-      },
-      "engines": {
-        "node": ">=0.10"
-      }
-    },
-    "node_modules/hyphenate-style-name": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
-      "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ=="
-    },
-    "node_modules/react-router-dom": {
-      "version": "5.3.4",
-      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
-      "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
-      "dependencies": {
-        "@babel/runtime": "^7.12.13",
-        "history": "^4.9.0",
-        "loose-envify": "^1.3.1",
-        "prop-types": "^15.6.2",
-        "react-router": "5.3.4",
-        "tiny-invariant": "^1.0.2",
-        "tiny-warning": "^1.0.0"
-      },
-      "peerDependencies": {
-        "react": ">=15"
-      }
-    },
-    "node_modules/@parcel/transformer-react-refresh-wrap": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.11.0.tgz",
-      "integrity": "sha512-6pY0CdIgIpXC6XpsDWizf+zLgiuEsJ106HjWLwF7/R72BrvDhLPZ6jRu4UTrnd6bM89KahPw9fZZzjKoA5Efcw==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/plugin": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "react-refresh": "^0.9.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "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/parse-json": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
-      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
-      "dev": true,
-      "dependencies": {
-        "@babel/code-frame": "^7.0.0",
-        "error-ex": "^1.3.1",
-        "json-parse-even-better-errors": "^2.3.0",
-        "lines-and-columns": "^1.1.6"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/@parcel/codeframe/node_modules/color-name": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-      "dev": true
-    },
-    "node_modules/srcset": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz",
-      "integrity": "sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/@parcel/watcher-linux-arm64-glibc": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.0.tgz",
-      "integrity": "sha512-QuJTAQdsd7PFW9jNGaV9Pw+ZMWV9wKThEzzlY3Lhnnwy7iW23qtQFPql8iEaSFMCVI5StNNmONUopk+MFKpiKg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">= 10.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/get-port": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz",
-      "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/@parcel/runtime-js": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/runtime-js/-/runtime-js-2.11.0.tgz",
-      "integrity": "sha512-fH3nJoexINz7s4cDzp0Vjsx0k1pMYSa5ch38LbbNqCKTermy0pS0zZuvgfLfHFFP+AMRpFQenrF7h7N3bgDmHw==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/plugin": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "nullthrows": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@date-io/date-fns": {
-      "version": "1.3.13",
-      "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-1.3.13.tgz",
-      "integrity": "sha512-yXxGzcRUPcogiMj58wVgFjc9qUYrCnnU9eLcyNbsQCmae4jPuZCDoIBR21j8ZURsM7GRtU62VOw5yNd4dDHunA==",
-      "dependencies": {
-        "@date-io/core": "^1.3.13"
-      },
-      "peerDependencies": {
-        "date-fns": "^2.0.0"
-      }
-    },
-    "node_modules/resolve-from": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
-      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/@lmdb/lmdb-linux-arm64": {
-      "version": "2.8.5",
-      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-2.8.5.tgz",
-      "integrity": "sha512-vtbZRHH5UDlL01TT5jB576Zox3+hdyogvpcbvVJlmU5PdL3c5V7cj1EODdh1CHPksRl+cws/58ugEHi8bcj4Ww==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@lezer/lr": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.0.tgz",
-      "integrity": "sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==",
-      "dev": true,
-      "dependencies": {
-        "@lezer/common": "^1.0.0"
-      }
-    },
-    "node_modules/@parcel/utils": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/utils/-/utils-2.11.0.tgz",
-      "integrity": "sha512-AcL70cXlIyE7eQdvjQbYxegN5l+skqvlJllxTWg4YkIZe9p8Gmv74jLAeLWh5F+IGl5WRn0TSy9JhNJjIMQGwQ==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/codeframe": "2.11.0",
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/logger": "2.11.0",
-        "@parcel/markdown-ansi": "2.11.0",
-        "@parcel/rust": "2.11.0",
-        "@parcel/source-map": "^2.1.1",
-        "chalk": "^4.1.0",
-        "nullthrows": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@parcel/bundler-default": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/bundler-default/-/bundler-default-2.11.0.tgz",
-      "integrity": "sha512-ZIs0865Lp871ZK83k5I9L4DeeE26muNMrHa7j8bvls6fKBJKAn8djrhfU4XOLyziU4aAOobcPwXU0+npWqs52g==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/graph": "3.1.0",
-        "@parcel/plugin": "2.11.0",
-        "@parcel/rust": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "nullthrows": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/react-resize-detector": {
-      "version": "8.1.0",
-      "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-8.1.0.tgz",
-      "integrity": "sha512-S7szxlaIuiy5UqLhLL1KY3aoyGHbZzsTpYal9eYMwCyKqoqoVLCmIgAgNyIM1FhnP2KyBygASJxdhejrzjMb+w==",
-      "dependencies": {
-        "lodash": "^4.17.21"
-      },
-      "peerDependencies": {
-        "react": "^16.0.0 || ^17.0.0 || ^18.0.0",
-        "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
-      }
-    },
-    "node_modules/@material-ui/utils": {
-      "version": "4.11.3",
-      "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.3.tgz",
-      "integrity": "sha512-ZuQPV4rBK/V1j2dIkSSEcH5uT6AaHuKWFfotADHsC0wVL1NLd2WkFCm4ZZbX33iO4ydl6V0GPngKm8HZQ2oujg==",
-      "dependencies": {
-        "@babel/runtime": "^7.4.4",
-        "prop-types": "^15.7.2",
-        "react-is": "^16.8.0 || ^17.0.0"
-      },
-      "engines": {
-        "node": ">=8.0.0"
-      },
-      "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0"
-      }
-    },
-    "node_modules/@parcel/package-manager/node_modules/semver": {
-      "version": "7.5.4",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
-      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
-      "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/@jridgewell/set-array": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
-      "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
-      "dev": true,
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
-    "node_modules/jss-plugin-camel-case": {
-      "version": "10.10.0",
-      "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz",
-      "integrity": "sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==",
-      "dependencies": {
-        "@babel/runtime": "^7.3.1",
-        "hyphenate-style-name": "^1.0.3",
-        "jss": "10.10.0"
-      }
-    },
-    "node_modules/css-what": {
-      "version": "6.1.0",
-      "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
-      "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
-      "dev": true,
-      "engines": {
-        "node": ">= 6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/fb55"
-      }
-    },
-    "node_modules/@parcel/utils/node_modules/color-name": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-      "dev": true
-    },
-    "node_modules/jss": {
-      "version": "10.10.0",
-      "resolved": "https://registry.npmjs.org/jss/-/jss-10.10.0.tgz",
-      "integrity": "sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==",
-      "dependencies": {
-        "@babel/runtime": "^7.3.1",
-        "csstype": "^3.0.2",
-        "is-in-browser": "^1.1.3",
-        "tiny-warning": "^1.0.2"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/jss"
-      }
-    },
-    "node_modules/@material-ui/types": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz",
-      "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==",
-      "peerDependencies": {
-        "@types/react": "*"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@parcel/watcher-darwin-arm64": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.0.tgz",
-      "integrity": "sha512-T/At5pansFuQ8VJLRx0C6C87cgfqIYhW2N/kBfLCUvDhCah0EnLLwaD/6MW3ux+rpgkpQAnMELOCTKlbwncwiA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">= 10.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "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==",
-      "dev": true,
-      "optional": true,
-      "peer": true
-    },
-    "node_modules/@types/prop-types": {
-      "version": "15.7.9",
-      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz",
-      "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g=="
-    },
-    "node_modules/nullthrows": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
-      "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==",
-      "dev": true
-    },
-    "node_modules/dotenv": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-7.0.0.tgz",
-      "integrity": "sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/msgpackr-extract/node_modules/node-gyp-build-optional-packages": {
-      "version": "5.0.7",
-      "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz",
-      "integrity": "sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==",
-      "dev": true,
-      "optional": true,
-      "bin": {
-        "node-gyp-build-optional-packages": "bin.js",
-        "node-gyp-build-optional-packages-optional": "optional.js",
-        "node-gyp-build-optional-packages-test": "build-test.js"
-      }
-    },
-    "node_modules/caniuse-lite": {
-      "version": "1.0.30001581",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz",
-      "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "opencollective",
-          "url": "https://opencollective.com/browserslist"
-        },
-        {
-          "type": "tidelift",
-          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
-        },
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/ai"
-        }
-      ]
-    },
-    "node_modules/babel-plugin-polyfill-corejs3": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz",
-      "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-define-polyfill-provider": "^0.5.0",
-        "core-js-compat": "^3.34.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
-      }
-    },
-    "node_modules/dotenv-expand": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz",
-      "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==",
-      "dev": true
-    },
-    "node_modules/@swc/helpers": {
-      "version": "0.5.3",
-      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz",
-      "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==",
-      "dev": true,
-      "dependencies": {
-        "tslib": "^2.4.0"
-      }
-    },
-    "node_modules/@parcel/packager-js/node_modules/globals": {
-      "version": "13.24.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
-      "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
-      "dev": true,
-      "dependencies": {
-        "type-fest": "^0.20.2"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/@parcel/codeframe/node_modules/color-convert": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "dev": true,
-      "dependencies": {
-        "color-name": "~1.1.4"
-      },
-      "engines": {
-        "node": ">=7.0.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",
-      "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
-      "dev": true,
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/json-parse-even-better-errors": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
-      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
-      "dev": true
-    },
-    "node_modules/@parcel/node-resolver-core": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/@parcel/node-resolver-core/-/node-resolver-core-3.2.0.tgz",
-      "integrity": "sha512-XJRSxCkNbGFWjfmwFdcQZ/qlzWZd35qLtvLz2va8euGL7M5OMEQOv7dsvEhl0R+CC2zcnfFzZwxk78q6ezs8AQ==",
-      "dev": true,
-      "dependencies": {
-        "@mischnic/json-sourcemap": "^0.1.0",
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/fs": "2.11.0",
-        "@parcel/rust": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "nullthrows": "^1.1.1",
-        "semver": "^7.5.2"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@parcel/transformer-css": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/transformer-css/-/transformer-css-2.11.0.tgz",
-      "integrity": "sha512-nFmBulF/ErNoafO87JbVrBavjBMNwE/kahbCRVxc2Mvlphz4F4lBW4eDRS5l4xBqFJaNkHr9R55ehLBBilF4Jw==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/plugin": "2.11.0",
-        "@parcel/source-map": "^2.1.1",
-        "@parcel/utils": "2.11.0",
-        "browserslist": "^4.6.6",
-        "lightningcss": "^1.22.1",
-        "nullthrows": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/buffer": {
-      "version": "6.0.3",
-      "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
-      "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "dependencies": {
-        "base64-js": "^1.3.1",
-        "ieee754": "^1.2.1"
-      }
-    },
-    "node_modules/@parcel/rust": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/rust/-/rust-2.11.0.tgz",
-      "integrity": "sha512-UkLWdHOD8Md2YmJDPsqd3yIs9chhdl/ATfV/B/xdPKGmqtNouYpDCRlq+WxMt3mLoYgHEg9UwrWLTebo2rr2iQ==",
-      "dev": true,
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/callsites": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
-      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/@parcel/package-manager/node_modules/lru-cache": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-      "dev": true,
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/@parcel/utils/node_modules/chalk": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-      "dev": true,
-      "dependencies": {
-        "ansi-styles": "^4.1.0",
-        "supports-color": "^7.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
-      }
-    },
-    "node_modules/@parcel/packager-wasm": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/packager-wasm/-/packager-wasm-2.11.0.tgz",
-      "integrity": "sha512-tTy4EbDXeeiZ0oB7L2FWaHSD1mbmYZP6R5HXqkvc5dECGUKPU5Jz6ek2C5AM+HfQdQLKXPQ/Xw3eJnI/AmctVg==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/plugin": "2.11.0"
-      },
-      "engines": {
-        "node": ">=12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/entities": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
-      "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.12"
-      },
-      "funding": {
-        "url": "https://github.com/fb55/entities?sponsor=1"
-      }
-    },
-    "node_modules/@types/scheduler": {
-      "version": "0.16.5",
-      "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.5.tgz",
-      "integrity": "sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw=="
-    },
-    "node_modules/@parcel/watcher-win32-x64": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.0.tgz",
-      "integrity": "sha512-pAUyUVjfFjWaf/pShmJpJmNxZhbMvJASUpdes9jL6bTEJ+gDxPRSpXTIemNyNsb9AtbiGXs9XduP1reThmd+dA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">= 10.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/domutils": {
-      "version": "2.8.0",
-      "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
-      "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
-      "dev": true,
-      "dependencies": {
-        "dom-serializer": "^1.0.1",
-        "domelementtype": "^2.2.0",
-        "domhandler": "^4.2.0"
-      },
-      "funding": {
-        "url": "https://github.com/fb55/domutils?sponsor=1"
-      }
-    },
-    "node_modules/@swc/core-darwin-arm64": {
-      "version": "1.3.107",
-      "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.107.tgz",
-      "integrity": "sha512-47tD/5vSXWxPd0j/ZllyQUg4bqalbQTsmqSw0J4dDdS82MWqCAwUErUrAZPRjBkjNQ6Kmrf5rpCWaGTtPw+ngw==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/@swc/core-linux-arm64-gnu": {
-      "version": "1.3.107",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.107.tgz",
-      "integrity": "sha512-HWgnn7JORYlOYnGsdunpSF8A+BCZKPLzLtEUA27/M/ZuANcMZabKL9Zurt7XQXq888uJFAt98Gy+59PU90aHKg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/@parcel/codeframe/node_modules/ansi-styles": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "dev": true,
-      "dependencies": {
-        "color-convert": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-      }
-    },
-    "node_modules/@parcel/node-resolver-core/node_modules/lru-cache": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-      "dev": true,
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/domhandler": {
-      "version": "4.3.1",
-      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
-      "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
-      "dev": true,
-      "dependencies": {
-        "domelementtype": "^2.2.0"
-      },
-      "engines": {
-        "node": ">= 4"
-      },
-      "funding": {
-        "url": "https://github.com/fb55/domhandler?sponsor=1"
-      }
-    },
-    "node_modules/tiny-warning": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
-      "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
-    },
-    "node_modules/dom-helpers/node_modules/csstype": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
-      "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
-    },
-    "node_modules/@parcel/package-manager": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/package-manager/-/package-manager-2.11.0.tgz",
-      "integrity": "sha512-QzdsrUYlAwIzb8by7WJjqYnbR1MoMKWbtE1MXUeYsZbFusV8B6pOH+lwqNJKS/BFtddZMRPYFueZS2N2fwzjig==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/fs": "2.11.0",
-        "@parcel/logger": "2.11.0",
-        "@parcel/node-resolver-core": "3.2.0",
-        "@parcel/types": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "@parcel/workers": "2.11.0",
-        "semver": "^7.5.2"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      },
-      "peerDependencies": {
-        "@parcel/core": "^2.11.0"
-      }
-    },
-    "node_modules/parcel/node_modules/color-convert": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "dev": true,
-      "dependencies": {
-        "color-name": "~1.1.4"
-      },
-      "engines": {
-        "node": ">=7.0.0"
-      }
-    },
-    "node_modules/babel-plugin-polyfill-regenerator": {
-      "version": "0.5.5",
-      "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz",
-      "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-define-polyfill-provider": "^0.5.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
-      }
-    },
-    "node_modules/@parcel/core/node_modules/semver": {
-      "version": "7.5.4",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
-      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
-      "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/@parcel/graph": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/@parcel/graph/-/graph-3.1.0.tgz",
-      "integrity": "sha512-d1dTW5C7A52HgDtoXlyvlET1ypSlmIxSIZOJ1xp3R9L9hgo3h1u3jHNyaoTe/WPkGVe2QnFxh0h+UibVJhu9vg==",
-      "dev": true,
-      "dependencies": {
-        "nullthrows": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/mime-types": {
-      "version": "2.1.35",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
-      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
-      "dependencies": {
-        "mime-db": "1.52.0"
-      },
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/date-fns": {
-      "version": "2.30.0",
-      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
-      "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
-      "dependencies": {
-        "@babel/runtime": "^7.21.0"
-      },
-      "engines": {
-        "node": ">=0.11"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/date-fns"
-      }
-    },
-    "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz",
-      "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==",
-      "dev": true,
-      "dependencies": {
-        "@babel/types": "^7.22.5"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/lodash.debounce": {
-      "version": "4.0.8",
-      "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
-      "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
-      "dev": true
-    },
-    "node_modules/react-smooth/node_modules/dom-helpers": {
-      "version": "3.4.0",
-      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
-      "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
-      "dependencies": {
-        "@babel/runtime": "^7.1.2"
-      }
-    },
-    "node_modules/axios": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
-      "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
-      "dependencies": {
-        "follow-redirects": "^1.15.0",
-        "form-data": "^4.0.0",
-        "proxy-from-env": "^1.1.0"
-      }
-    },
-    "node_modules/abortcontroller-polyfill": {
-      "version": "1.7.5",
-      "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz",
-      "integrity": "sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==",
-      "dev": true
-    },
-    "node_modules/@jridgewell/gen-mapping": {
-      "version": "0.3.3",
-      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
-      "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
-      "dev": true,
-      "dependencies": {
-        "@jridgewell/set-array": "^1.0.1",
-        "@jridgewell/sourcemap-codec": "^1.4.10",
-        "@jridgewell/trace-mapping": "^0.3.9"
-      },
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
-    "node_modules/posthtml/node_modules/posthtml-parser": {
-      "version": "0.11.0",
-      "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.11.0.tgz",
-      "integrity": "sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==",
-      "dev": true,
-      "dependencies": {
-        "htmlparser2": "^7.1.1"
-      },
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/postcss-value-parser": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
-      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
-      "dev": true
-    },
-    "node_modules/@swc/core-linux-arm-gnueabihf": {
-      "version": "1.3.107",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.107.tgz",
-      "integrity": "sha512-I2wzcC0KXqh0OwymCmYwNRgZ9nxX7DWnOOStJXV3pS0uB83TXAkmqd7wvMBuIl9qu4Hfomi9aDM7IlEEn9tumQ==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/html-to-react/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==",
-      "dependencies": {
-        "domelementtype": "^2.3.0",
-        "domhandler": "^5.0.2",
-        "entities": "^4.2.0"
-      },
-      "funding": {
-        "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
-      }
-    },
-    "node_modules/@parcel/reporter-cli": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/reporter-cli/-/reporter-cli-2.11.0.tgz",
-      "integrity": "sha512-hY0iO0f+LifgJHDUIjGQJnxLFSkk2jlbfy+kIaft5oI3/IM+UljecfGO+14XH8mYlqRXXPsT09TJe8ZKQzp4ZQ==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/plugin": "2.11.0",
-        "@parcel/types": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "chalk": "^4.1.0",
-        "cli-progress": "^3.12.0",
-        "term-size": "^2.2.1"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/react-refresh": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.9.0.tgz",
-      "integrity": "sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/@parcel/reporter-cli/node_modules/has-flag": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/@babel/types": {
-      "version": "7.23.0",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
-      "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-string-parser": "^7.22.5",
-        "@babel/helper-validator-identifier": "^7.22.20",
-        "to-fast-properties": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/template": {
-      "version": "7.22.15",
-      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
-      "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
-      "dev": true,
-      "dependencies": {
-        "@babel/code-frame": "^7.22.13",
-        "@babel/parser": "^7.22.15",
-        "@babel/types": "^7.22.15"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/compat-data": {
-      "version": "7.23.2",
-      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz",
-      "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/html-to-react/node_modules/domhandler": {
-      "version": "5.0.3",
-      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
-      "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
-      "dependencies": {
-        "domelementtype": "^2.3.0"
-      },
-      "engines": {
-        "node": ">= 4"
-      },
-      "funding": {
-        "url": "https://github.com/fb55/domhandler?sponsor=1"
-      }
-    },
-    "node_modules/@parcel/workers": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/workers/-/workers-2.11.0.tgz",
-      "integrity": "sha512-wjybqdSy6Nk0N9iBGsFcp7739W2zvx0WGfVxPVShqhz46pIkPOiFF/iSn+kFu5EmMKTRWeUif42+a6rRZ7pCnQ==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/logger": "2.11.0",
-        "@parcel/profiler": "2.11.0",
-        "@parcel/types": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "nullthrows": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      },
-      "peerDependencies": {
-        "@parcel/core": "^2.11.0"
-      }
-    },
-    "node_modules/@parcel/transformer-babel/node_modules/lru-cache": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-      "dev": true,
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/@parcel/optimizer-htmlnano": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/optimizer-htmlnano/-/optimizer-htmlnano-2.11.0.tgz",
-      "integrity": "sha512-c20pz4EFF5DNFmqYgptlIj49eT6xjGLkDTdHH3RRzxKovuSXWfYSPs3GED3ZsjVuQyjNQif+/MAk9547F7hrdQ==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/plugin": "2.11.0",
-        "htmlnano": "^2.0.0",
-        "nullthrows": "^1.1.1",
-        "posthtml": "^0.16.5",
-        "svgo": "^2.4.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/lightningcss-freebsd-x64": {
-      "version": "1.23.0",
-      "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.23.0.tgz",
-      "integrity": "sha512-xhnhf0bWPuZxcqknvMDRFFo2TInrmQRWZGB0f6YoAsZX8Y+epfjHeeOIGCfAmgF0DgZxHwYc8mIR5tQU9/+ROA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/update-browserslist-db": {
-      "version": "1.0.13",
-      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
-      "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "opencollective",
-          "url": "https://opencollective.com/browserslist"
-        },
-        {
-          "type": "tidelift",
-          "url": "https://tidelift.com/funding/github/npm/browserslist"
-        },
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/ai"
-        }
-      ],
-      "dependencies": {
-        "escalade": "^3.1.1",
-        "picocolors": "^1.0.0"
-      },
-      "bin": {
-        "update-browserslist-db": "cli.js"
-      },
-      "peerDependencies": {
-        "browserslist": ">= 4.21.0"
-      }
-    },
-    "node_modules/@trysound/sax": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
-      "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
-      "dev": true,
-      "engines": {
-        "node": ">=10.13.0"
-      }
-    },
-    "node_modules/@parcel/optimizer-css": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/optimizer-css/-/optimizer-css-2.11.0.tgz",
-      "integrity": "sha512-bV97PRxshHV3dMwOpLRgcP1QNhrVWh6VVDfm2gmWULpvsjoykcPS6vrCFksY5CpQsSvNHqJBzQjWS8FubUI76w==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/plugin": "2.11.0",
-        "@parcel/source-map": "^2.1.1",
-        "@parcel/utils": "2.11.0",
-        "browserslist": "^4.6.6",
-        "lightningcss": "^1.22.1",
-        "nullthrows": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@lmdb/lmdb-linux-x64": {
-      "version": "2.8.5",
-      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-2.8.5.tgz",
-      "integrity": "sha512-Xkc8IUx9aEhP0zvgeKy7IQ3ReX2N8N1L0WPcQwnZweWmOuKfwpS3GRIYqLtK5za/w3E60zhFfNdS+3pBZPytqQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/babel-plugin-polyfill-corejs2": {
-      "version": "0.4.8",
-      "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz",
-      "integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==",
-      "dev": true,
-      "dependencies": {
-        "@babel/compat-data": "^7.22.6",
-        "@babel/helper-define-polyfill-provider": "^0.5.0",
-        "semver": "^6.3.1"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
-      }
-    },
-    "node_modules/electron-to-chromium": {
-      "version": "1.4.650",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.650.tgz",
-      "integrity": "sha512-sYSQhJCJa4aGA1wYol5cMQgekDBlbVfTRavlGZVr3WZpDdOPcp6a6xUnFfrt8TqZhsBYYbDxJZCjGfHuGupCRQ==",
-      "dev": true
-    },
-    "node_modules/@babel/parser": {
-      "version": "7.23.0",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
-      "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
-      "dev": true,
-      "bin": {
-        "parser": "bin/babel-parser.js"
-      },
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
-    "node_modules/lines-and-columns": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
-      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
-      "dev": true
-    },
-    "node_modules/lmdb": {
-      "version": "2.8.5",
-      "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-2.8.5.tgz",
-      "integrity": "sha512-9bMdFfc80S+vSldBmG3HOuLVHnxRdNTlpzR6QDnzqCQtCzGUEAGTzBKYMeIM+I/sU4oZfgbcbS7X7F65/z/oxQ==",
-      "dev": true,
-      "hasInstallScript": true,
-      "dependencies": {
-        "msgpackr": "^1.9.5",
-        "node-addon-api": "^6.1.0",
-        "node-gyp-build-optional-packages": "5.1.1",
-        "ordered-binary": "^1.4.1",
-        "weak-lru-cache": "^1.2.2"
-      },
-      "bin": {
-        "download-lmdb-prebuilds": "bin/download-prebuilds.js"
-      },
-      "optionalDependencies": {
-        "@lmdb/lmdb-darwin-arm64": "2.8.5",
-        "@lmdb/lmdb-darwin-x64": "2.8.5",
-        "@lmdb/lmdb-linux-arm": "2.8.5",
-        "@lmdb/lmdb-linux-arm64": "2.8.5",
-        "@lmdb/lmdb-linux-x64": "2.8.5",
-        "@lmdb/lmdb-win32-x64": "2.8.5"
-      }
-    },
-    "node_modules/@babel/helper-replace-supers": {
-      "version": "7.22.20",
-      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz",
-      "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-environment-visitor": "^7.22.20",
-        "@babel/helper-member-expression-to-functions": "^7.22.15",
-        "@babel/helper-optimise-call-expression": "^7.22.5"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0"
-      }
-    },
-    "node_modules/is-fullwidth-code-point": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz",
-      "integrity": "sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ]
-    },
-    "node_modules/@material-ui/core": {
-      "version": "4.12.4",
-      "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.4.tgz",
-      "integrity": "sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ==",
-      "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.",
-      "dependencies": {
-        "@babel/runtime": "^7.4.4",
-        "@material-ui/styles": "^4.11.5",
-        "@material-ui/system": "^4.12.2",
-        "@material-ui/types": "5.1.0",
-        "@material-ui/utils": "^4.11.3",
-        "@types/react-transition-group": "^4.2.0",
-        "clsx": "^1.0.4",
-        "hoist-non-react-statics": "^3.3.2",
-        "popper.js": "1.16.1-lts",
-        "prop-types": "^15.7.2",
-        "react-is": "^16.8.0 || ^17.0.0",
-        "react-transition-group": "^4.4.0"
-      },
-      "engines": {
-        "node": ">=8.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/material-ui"
-      },
-      "peerDependencies": {
-        "@types/react": "^16.8.6 || ^17.0.0",
-        "react": "^16.8.0 || ^17.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@parcel/reporter-dev-server": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/reporter-dev-server/-/reporter-dev-server-2.11.0.tgz",
-      "integrity": "sha512-T4ue1+oLFNdcd9maw8QWQuxzOS2kX2jOrSvYKwYd9oGnqiAr1rpiHYYKJhHng+PF5ybwWkj8dUJfGh2NoQysJA==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/plugin": "2.11.0",
-        "@parcel/utils": "2.11.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "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/@msgpackr-extract/msgpackr-extract-linux-x64": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz",
-      "integrity": "sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@parcel/markdown-ansi/node_modules/chalk": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-      "dev": true,
-      "dependencies": {
-        "ansi-styles": "^4.1.0",
-        "supports-color": "^7.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
-      }
-    },
-    "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/@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==",
-      "dev": true
-    },
-    "node_modules/posthtml-parser": {
-      "version": "0.10.2",
-      "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.10.2.tgz",
-      "integrity": "sha512-PId6zZ/2lyJi9LiKfe+i2xv57oEjJgWbsHGGANwos5AvdQp98i6AtamAl8gzSVFGfQ43Glb5D614cvZf012VKg==",
-      "dev": true,
-      "dependencies": {
-        "htmlparser2": "^7.1.1"
-      },
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/debug": {
-      "version": "4.3.4",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
-      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
-      "dev": true,
-      "dependencies": {
-        "ms": "2.1.2"
-      },
-      "engines": {
-        "node": ">=6.0"
-      },
-      "peerDependenciesMeta": {
-        "supports-color": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@babel/code-frame": {
-      "version": "7.22.13",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
-      "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
-      "dev": true,
-      "dependencies": {
-        "@babel/highlight": "^7.22.13",
-        "chalk": "^2.4.2"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/highlight": {
-      "version": "7.22.20",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
-      "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-validator-identifier": "^7.22.20",
-        "chalk": "^2.4.2",
-        "js-tokens": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/jss/node_modules/csstype": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
-      "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
-    },
-    "node_modules/commander": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
-      "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
-      "dev": true,
-      "engines": {
-        "node": ">= 10"
-      }
-    },
-    "node_modules/@types/d3-ease": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.1.tgz",
-      "integrity": "sha512-VZofjpEt8HWv3nxUAosj5o/+4JflnJ7Bbv07k17VO3T2WRuzGdZeookfaF60iVh5RdhVG49LE5w6LIshVUC6rg=="
-    },
-    "node_modules/@parcel/watcher-linux-arm64-musl": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.0.tgz",
-      "integrity": "sha512-oyN+uA9xcTDo/45bwsd6TFHa7Lc7hKujyMlvwrCLvSckvWogndCEoVYFNfZ6JJ2KNL/6fFiGPcbjp8jJmEh5Ng==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">= 10.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/react-smooth": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.5.tgz",
-      "integrity": "sha512-BMP2Ad42tD60h0JW6BFaib+RJuV5dsXJK9Baxiv/HlNFjvRLqA9xrNKxVWnUIZPQfzUwGXIlU/dSYLU+54YGQA==",
-      "dependencies": {
-        "fast-equals": "^5.0.0",
-        "react-transition-group": "2.9.0"
-      },
-      "peerDependencies": {
-        "prop-types": "^15.6.0",
-        "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
-        "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
-      }
-    },
-    "node_modules/is-plain-object": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
-      "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
-      "dev": true,
-      "dependencies": {
-        "isobject": "^3.0.1"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/@babel/helper-define-polyfill-provider": {
-      "version": "0.5.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz",
-      "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-compilation-targets": "^7.22.6",
-        "@babel/helper-plugin-utils": "^7.22.5",
-        "debug": "^4.1.1",
-        "lodash.debounce": "^4.0.8",
-        "resolve": "^1.14.2"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
-      }
-    },
-    "node_modules/@swc/types": {
-      "version": "0.1.5",
-      "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz",
-      "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==",
-      "dev": true
-    },
-    "node_modules/isobject": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-      "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/@babel/helper-annotate-as-pure": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz",
-      "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==",
-      "dev": true,
-      "dependencies": {
-        "@babel/types": "^7.22.5"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/lightningcss-darwin-arm64": {
-      "version": "1.23.0",
-      "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.23.0.tgz",
-      "integrity": "sha512-kl4Pk3Q2lnE6AJ7Qaij47KNEfY2/UXRZBT/zqGA24B8qwkgllr/j7rclKOf1axcslNXvvUdztjo4Xqh39Yq1aA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/svgo": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz",
-      "integrity": "sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==",
-      "dev": true,
-      "optional": true,
-      "peer": true,
-      "dependencies": {
-        "@trysound/sax": "0.2.0",
-        "commander": "^7.2.0",
-        "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": ">=14.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/svgo"
-      }
-    },
-    "node_modules/node-gyp-build-optional-packages": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz",
-      "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==",
-      "dev": true,
-      "dependencies": {
-        "detect-libc": "^2.0.1"
-      },
-      "bin": {
-        "node-gyp-build-optional-packages": "bin.js",
-        "node-gyp-build-optional-packages-optional": "optional.js",
-        "node-gyp-build-optional-packages-test": "build-test.js"
-      }
-    },
-    "node_modules/parent-module": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
-      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
-      "dev": true,
-      "dependencies": {
-        "callsites": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/color-name": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-      "dev": true
-    },
-    "node_modules/@lmdb/lmdb-linux-arm": {
-      "version": "2.8.5",
-      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-2.8.5.tgz",
-      "integrity": "sha512-c0TGMbm2M55pwTDIfkDLB6BpIsgxV4PjYck2HiOX+cy/JWiBXz32lYbarPqejKs9Flm7YVAKSILUducU9g2RVg==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/posthtml": {
-      "version": "0.16.6",
-      "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.6.tgz",
-      "integrity": "sha512-JcEmHlyLK/o0uGAlj65vgg+7LIms0xKXe60lcDOTU7oVX/3LuEuLwrQpW3VJ7de5TaFKiW4kWkaIpJL42FEgxQ==",
-      "dev": true,
-      "dependencies": {
-        "posthtml-parser": "^0.11.0",
-        "posthtml-render": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=12.0.0"
-      }
-    },
-    "node_modules/@parcel/transformer-raw": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/transformer-raw/-/transformer-raw-2.11.0.tgz",
-      "integrity": "sha512-2ltp3TgS+cxEqSM1vk5gDtJrYx4KMuRRtbSgSvkdldyOgPhflnLU3/HRz72hXSNGqYOV0/JN0+ocsfPnqR00ug==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/plugin": "2.11.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/parcel/node_modules/ansi-styles": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "dev": true,
-      "dependencies": {
-        "color-convert": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-      }
-    },
-    "node_modules/@ampproject/remapping": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
-      "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
-      "dev": true,
-      "dependencies": {
-        "@jridgewell/gen-mapping": "^0.3.0",
-        "@jridgewell/trace-mapping": "^0.3.9"
-      },
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
-    "node_modules/@babel/helper-simple-access": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz",
-      "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==",
-      "dev": true,
-      "dependencies": {
-        "@babel/types": "^7.22.5"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/jss-plugin-rule-value-function": {
-      "version": "10.10.0",
-      "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.10.0.tgz",
-      "integrity": "sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==",
-      "dependencies": {
-        "@babel/runtime": "^7.3.1",
-        "jss": "10.10.0",
-        "tiny-warning": "^1.0.2"
-      }
-    },
-    "node_modules/strip-ansi": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
-      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-      "dev": true,
-      "dependencies": {
-        "ansi-regex": "^5.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/chalk": {
-      "version": "2.4.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-      "dev": true,
-      "dependencies": {
-        "ansi-styles": "^3.2.1",
-        "escape-string-regexp": "^1.0.5",
-        "supports-color": "^5.3.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/@babel/helper-module-transforms": {
-      "version": "7.23.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz",
-      "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-environment-visitor": "^7.22.20",
-        "@babel/helper-module-imports": "^7.22.15",
-        "@babel/helper-simple-access": "^7.22.5",
-        "@babel/helper-split-export-declaration": "^7.22.6",
-        "@babel/helper-validator-identifier": "^7.22.20"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0"
-      }
-    },
-    "node_modules/@babel/plugin-transform-runtime": {
-      "version": "7.23.9",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.9.tgz",
-      "integrity": "sha512-A7clW3a0aSjm3ONU9o2HAILSegJCYlEZmOhmBRReVtIpY/Z/p7yIZ+wR41Z+UipwdGuqwtID/V/dOdZXjwi9gQ==",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-module-imports": "^7.22.15",
-        "@babel/helper-plugin-utils": "^7.22.5",
-        "babel-plugin-polyfill-corejs2": "^0.4.8",
-        "babel-plugin-polyfill-corejs3": "^0.9.0",
-        "babel-plugin-polyfill-regenerator": "^0.5.5",
-        "semver": "^6.3.1"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/value-equal": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
-      "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
-    },
-    "node_modules/@parcel/watcher-darwin-x64": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.0.tgz",
-      "integrity": "sha512-vZMv9jl+szz5YLsSqEGCMSllBl1gU1snfbRL5ysJU03MEa6gkVy9OMcvXV1j4g0++jHEcvzhs3Z3LpeEbVmY6Q==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">= 10.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/msgpackr-extract": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz",
-      "integrity": "sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==",
-      "dev": true,
-      "hasInstallScript": true,
-      "optional": true,
-      "dependencies": {
-        "node-gyp-build-optional-packages": "5.0.7"
-      },
-      "bin": {
-        "download-msgpackr-prebuilds": "bin/download-prebuilds.js"
-      },
-      "optionalDependencies": {
-        "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.2",
-        "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.2",
-        "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.2",
-        "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.2",
-        "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.2",
-        "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2"
-      }
-    },
-    "node_modules/stable": {
-      "version": "0.1.8",
-      "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
-      "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
-      "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility",
-      "dev": true
-    },
-    "node_modules/@parcel/transformer-babel/node_modules/semver": {
-      "version": "7.5.4",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
-      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
-      "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz",
-      "integrity": "sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ]
-    },
-    "node_modules/@swc/core-win32-x64-msvc": {
-      "version": "1.3.107",
-      "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.107.tgz",
-      "integrity": "sha512-Eyzo2XRqWOxqhE1gk9h7LWmUf4Bp4Xn2Ttb0ayAXFp6YSTxQIThXcT9kipXZqcpxcmDwoq8iWbbf2P8XL743EA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/@material-ui/system": {
-      "version": "4.12.2",
-      "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.2.tgz",
-      "integrity": "sha512-6CSKu2MtmiJgcCGf6nBQpM8fLkuB9F55EKfbdTC80NND5wpTmKzwdhLYLH3zL4cLlK0gVaaltW7/wMuyTnN0Lw==",
-      "dependencies": {
-        "@babel/runtime": "^7.4.4",
-        "@material-ui/utils": "^4.11.3",
-        "csstype": "^2.5.2",
-        "prop-types": "^15.7.2"
-      },
-      "engines": {
-        "node": ">=8.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/material-ui"
-      },
-      "peerDependencies": {
-        "@types/react": "^16.8.6 || ^17.0.0",
-        "react": "^16.8.0 || ^17.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@parcel/packager-js": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/packager-js/-/packager-js-2.11.0.tgz",
-      "integrity": "sha512-SxjCsd0xQfg5H73YtVJj9VOpr9s0rwMsSoeykjkatbkEla9NsZajsUkd/bfYf+/0WvEKOrB8oUBo15HkGOgKug==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/plugin": "2.11.0",
-        "@parcel/rust": "2.11.0",
-        "@parcel/source-map": "^2.1.1",
-        "@parcel/types": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "globals": "^13.2.0",
-        "nullthrows": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@types/d3-scale": {
-      "version": "4.0.6",
-      "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.6.tgz",
-      "integrity": "sha512-lo3oMLSiqsQUovv8j15X4BNEDOsnHuGjeVg7GRbAuB2PUa1prK5BNSOu6xixgNf3nqxPl4I1BqJWrPvFGlQoGQ==",
-      "dependencies": {
-        "@types/d3-time": "*"
-      }
-    },
-    "node_modules/@lmdb/lmdb-win32-x64": {
-      "version": "2.8.5",
-      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-2.8.5.tgz",
-      "integrity": "sha512-4wvrf5BgnR8RpogHhtpCPJMKBmvyZPhhUtEwMJbXh0ni2BucpfF07jlmyM11zRqQ2XIq6PbC2j7W7UCCcm1rRQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ]
-    },
-    "node_modules/node-releases": {
-      "version": "2.0.14",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
-      "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
-      "dev": true
-    },
-    "node_modules/@jridgewell/trace-mapping": {
-      "version": "0.3.20",
-      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
-      "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
-      "dev": true,
-      "dependencies": {
-        "@jridgewell/resolve-uri": "^3.1.0",
-        "@jridgewell/sourcemap-codec": "^1.4.14"
-      }
-    },
-    "node_modules/@parcel/transformer-postcss/node_modules/semver": {
-      "version": "7.5.4",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
-      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
-      "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/react-dom": {
-      "version": "17.0.2",
-      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
-      "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
-      "dependencies": {
-        "loose-envify": "^1.1.0",
-        "object-assign": "^4.1.1",
-        "scheduler": "^0.20.2"
-      },
-      "peerDependencies": {
-        "react": "17.0.2"
-      }
-    },
-    "node_modules/picomatch": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
-      "dev": true,
-      "engines": {
-        "node": ">=8.6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/jonschlinkert"
-      }
-    },
-    "node_modules/parcel/node_modules/color-name": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-      "dev": true
-    },
-    "node_modules/lodash.camelcase": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
-      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
-    },
-    "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==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/base64-js": {
-      "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
-      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ]
-    },
-    "node_modules/react": {
-      "version": "17.0.2",
-      "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
-      "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
-      "dependencies": {
-        "loose-envify": "^1.1.0",
-        "object-assign": "^4.1.1"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/@lmdb/lmdb-darwin-x64": {
-      "version": "2.8.5",
-      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-2.8.5.tgz",
-      "integrity": "sha512-w/sLhN4T7MW1nB3R/U8WK5BgQLz904wh+/SmA2jD8NnF7BLLoUgflCNxOeSPOWp8geP6nP/+VjWzZVip7rZ1ug==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ]
-    },
-    "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz",
-      "integrity": "sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/weak-lru-cache": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz",
-      "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==",
-      "dev": true
-    },
-    "node_modules/@babel/helper-compilation-targets": {
-      "version": "7.22.15",
-      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz",
-      "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==",
-      "dev": true,
-      "dependencies": {
-        "@babel/compat-data": "^7.22.9",
-        "@babel/helper-validator-option": "^7.22.15",
-        "browserslist": "^4.21.9",
-        "lru-cache": "^5.1.1",
-        "semver": "^6.3.1"
-      },
-      "engines": {
-        "node": ">=6.9.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==",
-      "dev": true,
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/chrome-trace-event": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
-      "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==",
-      "dev": true,
-      "engines": {
-        "node": ">=6.0"
-      }
-    },
-    "node_modules/@parcel/transformer-babel": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/transformer-babel/-/transformer-babel-2.11.0.tgz",
-      "integrity": "sha512-WKGblnp7r426VG+cpeQzc6dj/30EoUaYwyl4OEaigQSJizyuPWTBWTz6FUw+ih1/sg37h+D1BIh9C2FsVzpzbw==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/plugin": "2.11.0",
-        "@parcel/source-map": "^2.1.1",
-        "@parcel/utils": "2.11.0",
-        "browserslist": "^4.6.6",
-        "json5": "^2.2.0",
-        "nullthrows": "^1.1.1",
-        "semver": "^7.5.2"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@parcel/transformer-html": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/transformer-html/-/transformer-html-2.11.0.tgz",
-      "integrity": "sha512-90vp7mbvvfqPr9XIINpMcELtywj56f1bxfOkLQgWU1bm22H0FT3i5dqdac++2My0IGDvMwhAEjQfbn4pA579NQ==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/plugin": "2.11.0",
-        "@parcel/rust": "2.11.0",
-        "nullthrows": "^1.1.1",
-        "posthtml": "^0.16.5",
-        "posthtml-parser": "^0.10.1",
-        "posthtml-render": "^3.0.0",
-        "semver": "^7.5.2",
-        "srcset": "4"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@babel/preset-react": {
-      "version": "7.22.15",
-      "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.22.15.tgz",
-      "integrity": "sha512-Csy1IJ2uEh/PecCBXXoZGAZBeCATTuePzCSB7dLYWS0vOEj6CNpjxIhW4duWwZodBNueH7QO14WbGn8YyeuN9w==",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.22.5",
-        "@babel/helper-validator-option": "^7.22.15",
-        "@babel/plugin-transform-react-display-name": "^7.22.5",
-        "@babel/plugin-transform-react-jsx": "^7.22.15",
-        "@babel/plugin-transform-react-jsx-development": "^7.22.5",
-        "@babel/plugin-transform-react-pure-annotations": "^7.22.5"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/@parcel/reporter-tracer": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/reporter-tracer/-/reporter-tracer-2.11.0.tgz",
-      "integrity": "sha512-33q4ftO26OPWHkUpEm0bzzSjW2kHEh6q/JFePwf8W6APTQVruj4mV46+Fh6rxX42ixs92K/QoiE0gYgWZQVDHA==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/plugin": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "chrome-trace-event": "^1.0.3",
-        "nullthrows": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "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==",
-      "dev": true,
-      "optional": true,
-      "peer": true,
-      "dependencies": {
-        "domelementtype": "^2.3.0",
-        "domhandler": "^5.0.2",
-        "entities": "^4.2.0"
-      },
-      "funding": {
-        "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
-      }
-    },
-    "node_modules/@parcel/profiler": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/profiler/-/profiler-2.11.0.tgz",
-      "integrity": "sha512-s10SS09prOdwnaAcjK8M5zO8o+zPJJW5oOqXPNdf6KH4NGD/ue7iOk2xM8QLw6ulSwxE7NDt++lyfW3AXgCZwg==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/events": "2.11.0",
-        "chrome-trace-event": "^1.0.2"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/css-select": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
-      "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
-      "dev": true,
-      "optional": true,
-      "peer": true,
-      "dependencies": {
-        "boolbase": "^1.0.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/source-map": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/is-arrayish": {
-      "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
-      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
-      "dev": true
-    },
-    "node_modules/react-smooth/node_modules/react-transition-group": {
-      "version": "2.9.0",
-      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz",
-      "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==",
-      "dependencies": {
-        "dom-helpers": "^3.4.0",
-        "loose-envify": "^1.4.0",
-        "prop-types": "^15.6.2",
-        "react-lifecycles-compat": "^3.0.4"
-      },
-      "peerDependencies": {
-        "react": ">=15.0.0",
-        "react-dom": ">=15.0.0"
-      }
-    },
-    "node_modules/error-ex": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
-      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
-      "dev": true,
-      "dependencies": {
-        "is-arrayish": "^0.2.1"
-      }
-    },
-    "node_modules/utility-types": {
-      "version": "3.11.0",
-      "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
-      "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==",
-      "dev": true,
-      "engines": {
-        "node": ">= 4"
-      }
-    },
-    "node_modules/@swc/core": {
-      "version": "1.3.107",
-      "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.107.tgz",
-      "integrity": "sha512-zKhqDyFcTsyLIYK1iEmavljZnf4CCor5pF52UzLAz4B6Nu/4GLU+2LQVAf+oRHjusG39PTPjd2AlRT3f3QWfsQ==",
-      "dev": true,
-      "hasInstallScript": true,
-      "dependencies": {
-        "@swc/counter": "^0.1.1",
-        "@swc/types": "^0.1.5"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/swc"
-      },
-      "optionalDependencies": {
-        "@swc/core-darwin-arm64": "1.3.107",
-        "@swc/core-darwin-x64": "1.3.107",
-        "@swc/core-linux-arm-gnueabihf": "1.3.107",
-        "@swc/core-linux-arm64-gnu": "1.3.107",
-        "@swc/core-linux-arm64-musl": "1.3.107",
-        "@swc/core-linux-x64-gnu": "1.3.107",
-        "@swc/core-linux-x64-musl": "1.3.107",
-        "@swc/core-win32-arm64-msvc": "1.3.107",
-        "@swc/core-win32-ia32-msvc": "1.3.107",
-        "@swc/core-win32-x64-msvc": "1.3.107"
-      },
-      "peerDependencies": {
-        "@swc/helpers": "^0.5.0"
-      },
-      "peerDependenciesMeta": {
-        "@swc/helpers": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/msgpackr": {
-      "version": "1.10.1",
-      "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.10.1.tgz",
-      "integrity": "sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ==",
-      "dev": true,
-      "optionalDependencies": {
-        "msgpackr-extract": "^3.0.2"
-      }
-    },
-    "node_modules/@parcel/transformer-js/node_modules/semver": {
-      "version": "7.5.4",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
-      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
-      "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/mime-db": {
-      "version": "1.52.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
-      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/jss-plugin-vendor-prefixer": {
-      "version": "10.10.0",
-      "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.10.0.tgz",
-      "integrity": "sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==",
-      "dependencies": {
-        "@babel/runtime": "^7.3.1",
-        "css-vendor": "^2.0.8",
-        "jss": "10.10.0"
-      }
-    },
-    "node_modules/htmlnano": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-2.1.0.tgz",
-      "integrity": "sha512-jVGRE0Ep9byMBKEu0Vxgl8dhXYOUk0iNQ2pjsG+BcRB0u0oDF5A9p/iBGMg/PGKYUyMD0OAGu8dVT5Lzj8S58g==",
-      "dev": true,
-      "dependencies": {
-        "cosmiconfig": "^8.0.0",
-        "posthtml": "^0.16.5",
-        "timsort": "^0.3.0"
-      },
-      "peerDependencies": {
-        "cssnano": "^6.0.0",
-        "postcss": "^8.3.11",
-        "purgecss": "^5.0.0",
-        "relateurl": "^0.2.7",
-        "srcset": "4.0.0",
-        "svgo": "^3.0.2",
-        "terser": "^5.10.0",
-        "uncss": "^0.17.3"
-      },
-      "peerDependenciesMeta": {
-        "cssnano": {
-          "optional": true
-        },
-        "postcss": {
-          "optional": true
-        },
-        "purgecss": {
-          "optional": true
-        },
-        "relateurl": {
-          "optional": true
-        },
-        "srcset": {
-          "optional": true
-        },
-        "svgo": {
-          "optional": true
-        },
-        "terser": {
-          "optional": true
-        },
-        "uncss": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/recharts-scale": {
-      "version": "0.4.5",
-      "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
-      "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
-      "dependencies": {
-        "decimal.js-light": "^2.4.1"
-      }
-    },
-    "node_modules/@parcel/codeframe/node_modules/chalk": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-      "dev": true,
-      "dependencies": {
-        "ansi-styles": "^4.1.0",
-        "supports-color": "^7.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
-      }
-    },
-    "node_modules/@parcel/reporter-cli/node_modules/color-name": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-      "dev": true
-    },
-    "node_modules/resolve": {
-      "version": "1.22.8",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
-      "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
-      "dev": true,
-      "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/@parcel/fs": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/fs/-/fs-2.11.0.tgz",
-      "integrity": "sha512-zWckdnnovdrgdFX4QYuQV4bbKCsh6IYCkmwaB4yp47rhw1MP0lkBINLt4yFPHBxWXOpElCfxjL+z69c9xJQRBQ==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/rust": "2.11.0",
-        "@parcel/types": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "@parcel/watcher": "^2.0.7",
-        "@parcel/workers": "2.11.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      },
-      "peerDependencies": {
-        "@parcel/core": "^2.11.0"
-      }
-    },
-    "node_modules/@lezer/common": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz",
-      "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==",
-      "dev": true
-    },
-    "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==",
-      "dev": true
-    },
-    "node_modules/@parcel/runtime-browser-hmr": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.11.0.tgz",
-      "integrity": "sha512-uVwNBtoLMrlPHLvRS05BVhLseduMOpZT36yiIjS0YSBJcC6/otI9AY7ZiDPYmrB5xTqM0R+D554JhPaJHCuocw==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/plugin": "2.11.0",
-        "@parcel/utils": "2.11.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/is-in-browser": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz",
-      "integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g=="
-    },
-    "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==",
-      "dev": true,
-      "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/@parcel/watcher": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.0.tgz",
-      "integrity": "sha512-XJLGVL0DEclX5pcWa2N9SX1jCGTDd8l972biNooLFtjneuGqodupPQh6XseXIBBeVIMaaJ7bTcs3qGvXwsp4vg==",
-      "dev": true,
-      "hasInstallScript": true,
-      "dependencies": {
-        "detect-libc": "^1.0.3",
-        "is-glob": "^4.0.3",
-        "micromatch": "^4.0.5",
-        "node-addon-api": "^7.0.0"
-      },
-      "engines": {
-        "node": ">= 10.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      },
-      "optionalDependencies": {
-        "@parcel/watcher-android-arm64": "2.4.0",
-        "@parcel/watcher-darwin-arm64": "2.4.0",
-        "@parcel/watcher-darwin-x64": "2.4.0",
-        "@parcel/watcher-freebsd-x64": "2.4.0",
-        "@parcel/watcher-linux-arm-glibc": "2.4.0",
-        "@parcel/watcher-linux-arm64-glibc": "2.4.0",
-        "@parcel/watcher-linux-arm64-musl": "2.4.0",
-        "@parcel/watcher-linux-x64-glibc": "2.4.0",
-        "@parcel/watcher-linux-x64-musl": "2.4.0",
-        "@parcel/watcher-win32-arm64": "2.4.0",
-        "@parcel/watcher-win32-ia32": "2.4.0",
-        "@parcel/watcher-win32-x64": "2.4.0"
-      }
-    },
-    "node_modules/string-width": {
-      "version": "4.2.3",
-      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
-      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-      "dev": true,
-      "dependencies": {
-        "emoji-regex": "^8.0.0",
-        "is-fullwidth-code-point": "^3.0.0",
-        "strip-ansi": "^6.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/@babel/generator": {
-      "version": "7.23.0",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
-      "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
-      "dev": true,
-      "dependencies": {
-        "@babel/types": "^7.23.0",
-        "@jridgewell/gen-mapping": "^0.3.2",
-        "@jridgewell/trace-mapping": "^0.3.17",
-        "jsesc": "^2.5.1"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "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/@parcel/reporter-cli/node_modules/chalk": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-      "dev": true,
-      "dependencies": {
-        "ansi-styles": "^4.1.0",
-        "supports-color": "^7.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
-      }
-    },
-    "node_modules/@babel/helpers": {
-      "version": "7.23.2",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz",
-      "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==",
-      "dev": true,
-      "dependencies": {
-        "@babel/template": "^7.22.15",
-        "@babel/traverse": "^7.23.2",
-        "@babel/types": "^7.23.0"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/jss-plugin-props-sort": {
-      "version": "10.10.0",
-      "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.10.0.tgz",
-      "integrity": "sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==",
-      "dependencies": {
-        "@babel/runtime": "^7.3.1",
-        "jss": "10.10.0"
-      }
-    },
-    "node_modules/@parcel/watcher-linux-x64-glibc": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.0.tgz",
-      "integrity": "sha512-KphV8awJmxU3q52JQvJot0QMu07CIyEjV+2Tb2ZtbucEgqyRcxOBDMsqp1JNq5nuDXtcCC0uHQICeiEz38dPBQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">= 10.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@parcel/reporter-cli/node_modules/color-convert": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "dev": true,
-      "dependencies": {
-        "color-name": "~1.1.4"
-      },
-      "engines": {
-        "node": ">=7.0.0"
-      }
-    },
-    "node_modules/@parcel/cache": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/cache/-/cache-2.11.0.tgz",
-      "integrity": "sha512-RSSkGNjO00lJPyftzaC9eaNVs4jMjPSAm0VJNWQ9JSm2n4A9BzQtTFAt1vhJOzzW1UsQvvBge9DdfkB7a2gIOw==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/fs": "2.11.0",
-        "@parcel/logger": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "lmdb": "2.8.5"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      },
-      "peerDependencies": {
-        "@parcel/core": "^2.11.0"
-      }
-    },
-    "node_modules/@parcel/transformer-html/node_modules/lru-cache": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-      "dev": true,
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/to-regex-range": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
-      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-      "dev": true,
-      "dependencies": {
-        "is-number": "^7.0.0"
-      },
-      "engines": {
-        "node": ">=8.0"
-      }
-    },
-    "node_modules/ieee754": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
-      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ]
-    },
-    "node_modules/tiny-invariant": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
-      "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw=="
-    },
-    "node_modules/node-addon-api": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz",
-      "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==",
-      "dev": true,
-      "engines": {
-        "node": "^16 || ^18 || >= 20"
-      }
-    },
-    "node_modules/escalade": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
-      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/popper.js": {
-      "version": "1.16.1-lts",
-      "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz",
-      "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA=="
-    },
-    "node_modules/gensync": {
-      "version": "1.0.0-beta.2",
-      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
-      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
-      "dev": true,
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "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==",
-      "dev": true,
-      "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/is-primitive": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz",
-      "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/ms": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-      "dev": true
-    },
-    "node_modules/material-design-icons-iconfont": {
-      "version": "6.7.0",
-      "resolved": "https://registry.npmjs.org/material-design-icons-iconfont/-/material-design-icons-iconfont-6.7.0.tgz",
-      "integrity": "sha512-lSj71DgVv20kO0kGbs42icDzbRot61gEDBLQACzkUuznRQBUYmbxzEkGU6dNBb5fRWHMaScYlAXX96HQ4/cJWA=="
-    },
-    "node_modules/@types/d3-shape": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.4.tgz",
-      "integrity": "sha512-M2/xsWPsjaZc5ifMKp1EBp0gqJG0eO/zlldJNOC85Y/5DGsBQ49gDkRJ2h5GY7ZVD6KUumvZWsylSbvTaJTqKg==",
-      "dependencies": {
-        "@types/d3-path": "*"
-      }
-    },
-    "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz",
-      "integrity": "sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ]
-    },
-    "node_modules/lightningcss-linux-arm64-musl": {
-      "version": "1.23.0",
-      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.23.0.tgz",
-      "integrity": "sha512-cU00LGb6GUXCwof6ACgSMKo3q7XYbsyTj0WsKHLi1nw7pV0NCq8nFTn6ZRBYLoKiV8t+jWl0Hv8KkgymmK5L5g==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@parcel/watcher-win32-ia32": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.0.tgz",
-      "integrity": "sha512-IO/nM+K2YD/iwjWAfHFMBPz4Zqn6qBDqZxY4j2n9s+4+OuTSRM/y/irksnuqcspom5DjkSeF9d0YbO+qpys+JA==",
-      "cpu": [
-        "ia32"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">= 10.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@parcel/utils/node_modules/color-convert": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "dev": true,
-      "dependencies": {
-        "color-name": "~1.1.4"
-      },
-      "engines": {
-        "node": ">=7.0.0"
-      }
-    },
-    "node_modules/color-convert": {
-      "version": "1.9.3",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "dev": true,
-      "dependencies": {
-        "color-name": "1.1.3"
-      }
-    },
-    "node_modules/posthtml-render": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/posthtml-render/-/posthtml-render-3.0.0.tgz",
-      "integrity": "sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==",
-      "dev": true,
-      "dependencies": {
-        "is-json": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/@parcel/watcher-freebsd-x64": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.0.tgz",
-      "integrity": "sha512-dHTRMIplPDT1M0+BkXjtMN+qLtqq24sLDUhmU+UxxLP2TEY2k8GIoqIJiVrGWGomdWsy5IO27aDV1vWyQ6gfHA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">= 10.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@parcel/transformer-svg/node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-      "dev": true
-    },
-    "node_modules/lodash": {
-      "version": "4.17.21",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
-      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
-    },
-    "node_modules/@parcel/transformer-posthtml": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/transformer-posthtml/-/transformer-posthtml-2.11.0.tgz",
-      "integrity": "sha512-dMK4p1RRAoIJEjK/Wz9GOLqwHqdD/VQDhMPk+6sUKp5zf2MhSohUstpp5gKsSZivCM3PS2f8k9rgroacJ/ReuA==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/plugin": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "nullthrows": "^1.1.1",
-        "posthtml": "^0.16.5",
-        "posthtml-parser": "^0.10.1",
-        "posthtml-render": "^3.0.0",
-        "semver": "^7.5.2"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@parcel/transformer-postcss/node_modules/lru-cache": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-      "dev": true,
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/recharts": {
-      "version": "2.9.0",
-      "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.9.0.tgz",
-      "integrity": "sha512-cVgiAU3W5UrA8nRRV/N0JrudgZzY/vjkzrlShbH+EFo1vs4nMlXgshZWLI0DfDLmn4/p4pF7Lq7F5PU+K94Ipg==",
-      "dependencies": {
-        "classnames": "^2.2.5",
-        "eventemitter3": "^4.0.1",
-        "lodash": "^4.17.19",
-        "react-is": "^16.10.2",
-        "react-resize-detector": "^8.0.4",
-        "react-smooth": "^2.0.4",
-        "recharts-scale": "^0.4.4",
-        "tiny-invariant": "^1.3.1",
-        "victory-vendor": "^36.6.8"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "peerDependencies": {
-        "prop-types": "^15.6.0",
-        "react": "^16.0.0 || ^17.0.0 || ^18.0.0",
-        "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
-      }
-    },
-    "node_modules/@material-ui/styles": {
-      "version": "4.11.5",
-      "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.5.tgz",
-      "integrity": "sha512-o/41ot5JJiUsIETME9wVLAJrmIWL3j0R0Bj2kCOLbSfqEkKf0fmaPt+5vtblUh5eXr2S+J/8J3DaCb10+CzPGA==",
-      "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.",
-      "dependencies": {
-        "@material-ui/types": "5.1.0",
-        "jss-plugin-props-sort": "^10.5.1",
-        "jss-plugin-default-unit": "^10.5.1",
-        "@babel/runtime": "^7.4.4",
-        "jss-plugin-nested": "^10.5.1",
-        "jss-plugin-rule-value-function": "^10.5.1",
-        "@material-ui/utils": "^4.11.3",
-        "jss-plugin-camel-case": "^10.5.1",
-        "clsx": "^1.0.4",
-        "@emotion/hash": "^0.8.0",
-        "csstype": "^2.5.2",
-        "jss-plugin-vendor-prefixer": "^10.5.1",
-        "jss": "^10.5.1",
-        "jss-plugin-global": "^10.5.1",
-        "hoist-non-react-statics": "^3.3.2",
-        "prop-types": "^15.7.2"
-      },
-      "engines": {
-        "node": ">=8.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/material-ui"
-      },
-      "peerDependencies": {
-        "@types/react": "^16.8.6 || ^17.0.0",
-        "react": "^16.8.0 || ^17.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/js-tokens": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
-      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
-    },
-    "node_modules/@parcel/transformer-html/node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-      "dev": true
-    },
-    "node_modules/@types/d3-timer": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.1.tgz",
-      "integrity": "sha512-GGTvzKccVEhxmRfJEB6zhY9ieT4UhGVUIQaBzFpUO9OXy2ycAlnPCSJLzmGGgqt3KVjqN3QCQB4g1rsZnHsWhg=="
-    },
-    "node_modules/asynckit": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
-    },
-    "node_modules/@parcel/transformer-postcss": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/transformer-postcss/-/transformer-postcss-2.11.0.tgz",
-      "integrity": "sha512-Ugy8XHBaUptGotsvwzq7gPCvkCopTIqqZ0JZ40Jmy9slGms8wnx06pNHA1Be/RcJwkJ2TbSu+7ncZdgmP5x5GQ==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/plugin": "2.11.0",
-        "@parcel/rust": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "clone": "^2.1.1",
-        "nullthrows": "^1.1.1",
-        "postcss-value-parser": "^4.2.0",
-        "semver": "^7.5.2"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@parcel/logger": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/logger/-/logger-2.11.0.tgz",
-      "integrity": "sha512-HtMEdCq3LKnvv4T2CIskcqlf2gpBvHMm3pkeUFB/hc/7hW/hE1k6/HA2VOQvc0tBsaMpmEx7PCrfrH56usQSyA==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/events": "2.11.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@parcel/plugin": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/plugin/-/plugin-2.11.0.tgz",
-      "integrity": "sha512-9npuKBlhnPn7oeUpLJGecceg16GkXbvzbr6MNSZiHhkx3IBeITHQXlZnp2zAjUOFreNsYOfifwEF2S4KsARfBQ==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/types": "2.11.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/rifm": {
-      "version": "0.7.0",
-      "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz",
-      "integrity": "sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ==",
-      "dependencies": {
-        "@babel/runtime": "^7.3.1"
-      },
-      "peerDependencies": {
-        "react": ">=16.8"
-      }
-    },
-    "node_modules/json5": {
-      "version": "2.2.3",
-      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
-      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
-      "dev": true,
-      "bin": {
-        "json5": "lib/cli.js"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/decimal.js-light": {
-      "version": "2.5.1",
-      "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
-      "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
-    },
-    "node_modules/@parcel/transformer-posthtml/node_modules/semver": {
-      "version": "7.5.4",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
-      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
-      "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/eventemitter3": {
-      "version": "4.0.7",
-      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
-      "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
-    },
-    "node_modules/@types/react/node_modules/csstype": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
-      "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
-    },
-    "node_modules/@parcel/runtime-react-refresh": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/runtime-react-refresh/-/runtime-react-refresh-2.11.0.tgz",
-      "integrity": "sha512-Kfnc7gLjhoephLMnjABrkIkzVfzPrpJlxiJFIleY2Fm57YhmCfKsEYxm3lHOutNaYl1VArW0LKClPH/VHG9vfQ==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/plugin": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "react-error-overlay": "6.0.9",
-        "react-refresh": "^0.9.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@parcel/transformer-js": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/transformer-js/-/transformer-js-2.11.0.tgz",
-      "integrity": "sha512-G1sv0n8/fJqHqwUs0iVnVdmRY0Kh8kWaDkuWcU/GJBHMGhUnLXKdNwxX2Av9UdBL14bU1nTINfr9qOfnQotXWg==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/plugin": "2.11.0",
-        "@parcel/rust": "2.11.0",
-        "@parcel/source-map": "^2.1.1",
-        "@parcel/utils": "2.11.0",
-        "@parcel/workers": "2.11.0",
-        "@swc/helpers": "^0.5.0",
-        "browserslist": "^4.6.6",
-        "nullthrows": "^1.1.1",
-        "regenerator-runtime": "^0.13.7",
-        "semver": "^7.5.2"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      },
-      "peerDependencies": {
-        "@parcel/core": "^2.11.0"
-      }
-    },
-    "node_modules/@parcel/core/node_modules/lru-cache": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-      "dev": true,
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/node-gyp-build-optional-packages/node_modules/detect-libc": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
-      "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/@babel/helper-environment-visitor": {
-      "version": "7.22.20",
-      "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
-      "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
-      "dev": true,
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "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==",
-      "dev": 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"
-      },
-      "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",
-      "integrity": "sha512-ftf42F3JyZxJb6nnLlgNGyNQ273YOla4dFGH/tWC8iTwObHUpWe7cMbCGcrSJBvAlsLkZfLpFNAXFxUgxdKyHQ==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/plugin": "2.11.0",
-        "@parcel/source-map": "^2.1.1",
-        "@parcel/utils": "2.11.0",
-        "@swc/core": "^1.3.36",
-        "nullthrows": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@babel/traverse": {
-      "version": "7.23.2",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
-      "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
-      "dev": true,
-      "dependencies": {
-        "@babel/code-frame": "^7.22.13",
-        "@babel/generator": "^7.23.0",
-        "@babel/helper-environment-visitor": "^7.22.20",
-        "@babel/helper-function-name": "^7.23.0",
-        "@babel/helper-hoist-variables": "^7.22.5",
-        "@babel/helper-split-export-declaration": "^7.22.6",
-        "@babel/parser": "^7.23.0",
-        "@babel/types": "^7.23.0",
-        "debug": "^4.1.0",
-        "globals": "^11.1.0"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/lightningcss-linux-x64-gnu": {
-      "version": "1.23.0",
-      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.23.0.tgz",
-      "integrity": "sha512-q4jdx5+5NfB0/qMbXbOmuC6oo7caPnFghJbIAV90cXZqgV8Am3miZhC4p+sQVdacqxfd+3nrle4C8icR3p1AYw==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@parcel/markdown-ansi/node_modules/color-convert": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "dev": true,
-      "dependencies": {
-        "color-name": "~1.1.4"
-      },
-      "engines": {
-        "node": ">=7.0.0"
-      }
-    },
-    "node_modules/@parcel/transformer-posthtml/node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-      "dev": true
-    },
-    "node_modules/csstype": {
-      "version": "2.6.21",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz",
-      "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w=="
-    },
-    "node_modules/lru-cache": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
-      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
-      "dev": true,
-      "dependencies": {
-        "yallist": "^3.0.2"
-      }
-    },
-    "node_modules/globals": {
-      "version": "11.12.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
-      "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "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==",
-      "dev": true,
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/classnames": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
-      "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
-    },
-    "node_modules/cosmiconfig": {
-      "version": "8.3.6",
-      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
-      "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
-      "dev": true,
-      "dependencies": {
-        "import-fresh": "^3.3.0",
-        "js-yaml": "^4.1.0",
-        "parse-json": "^5.2.0",
-        "path-type": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/d-fischer"
-      },
-      "peerDependencies": {
-        "typescript": ">=4.9.5"
-      },
-      "peerDependenciesMeta": {
-        "typescript": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/react-is": {
-      "version": "17.0.2",
-      "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
-      "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
-    },
-    "node_modules/@parcel/utils/node_modules/ansi-styles": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "dev": true,
-      "dependencies": {
-        "color-convert": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-      }
-    },
-    "node_modules/regenerator-runtime": {
-      "version": "0.14.0",
-      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
-      "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
-    },
-    "node_modules/@material-ui/icons": {
-      "version": "4.11.3",
-      "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.3.tgz",
-      "integrity": "sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA==",
-      "dependencies": {
-        "@babel/runtime": "^7.4.4"
-      },
-      "engines": {
-        "node": ">=8.0.0"
-      },
-      "peerDependencies": {
-        "@material-ui/core": "^4.0.0",
-        "@types/react": "^16.8.6 || ^17.0.0",
-        "react": "^16.8.0 || ^17.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": 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/combined-stream": {
-      "version": "1.0.8",
-      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
-      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
-      "dependencies": {
-        "delayed-stream": "~1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/@types/d3-array": {
-      "version": "3.0.9",
-      "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.9.tgz",
-      "integrity": "sha512-mZowFN3p64ajCJJ4riVYlOjNlBJv3hctgAY01pjw3qTnJePD8s9DZmYDzhHKvzfCYvdjwylkU38+Vdt7Cu2FDA=="
-    },
-    "node_modules/history": {
-      "version": "4.10.1",
-      "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
-      "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
-      "dependencies": {
-        "@babel/runtime": "^7.1.2",
-        "loose-envify": "^1.2.0",
-        "resolve-pathname": "^3.0.0",
-        "tiny-invariant": "^1.0.2",
-        "tiny-warning": "^1.0.0",
-        "value-equal": "^1.0.1"
-      }
-    },
-    "node_modules/@parcel/codeframe/node_modules/supports-color": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-      "dev": true,
-      "dependencies": {
-        "has-flag": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/@parcel/packager-svg": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/packager-svg/-/packager-svg-2.11.0.tgz",
-      "integrity": "sha512-2wQBkzLwcaWFGWz8TP+bgsXgiueWPzrjKsWugWdDfq0FbXh8XVeR/599qnus3RFHZy4cH6L6yq/7zxcljtxK8A==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/plugin": "2.11.0",
-        "@parcel/types": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "posthtml": "^0.16.4"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@jridgewell/sourcemap-codec": {
-      "version": "1.4.15",
-      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
-      "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
-      "dev": true
-    },
-    "node_modules/hasown": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
-      "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
-      "dev": true,
-      "dependencies": {
-        "function-bind": "^1.1.2"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/parcel/node_modules/has-flag": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/semver": {
-      "version": "6.3.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
-      "dev": true,
-      "bin": {
-        "semver": "bin/semver.js"
-      }
-    },
-    "node_modules/parcel": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/parcel/-/parcel-2.11.0.tgz",
-      "integrity": "sha512-H/RI1/DmuOkL8RuG/EpNPvtzrbF+7jA/R56ydEEm+lqFbYktKB4COR7JXdHkZXRgbSJyimrFB8d0r9+SaRnj0Q==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/config-default": "2.11.0",
-        "@parcel/core": "2.11.0",
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/events": "2.11.0",
-        "@parcel/fs": "2.11.0",
-        "@parcel/logger": "2.11.0",
-        "@parcel/package-manager": "2.11.0",
-        "@parcel/reporter-cli": "2.11.0",
-        "@parcel/reporter-dev-server": "2.11.0",
-        "@parcel/reporter-tracer": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "chalk": "^4.1.0",
-        "commander": "^7.0.0",
-        "get-port": "^4.2.0"
-      },
-      "bin": {
-        "parcel": "lib/bin.js"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/react-error-overlay": {
-      "version": "6.0.9",
-      "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz",
-      "integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==",
-      "dev": true
-    },
-    "node_modules/htmlparser2": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz",
-      "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==",
-      "dev": true,
-      "funding": [
-        "https://github.com/fb55/htmlparser2?sponsor=1",
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/fb55"
-        }
-      ],
-      "dependencies": {
-        "domelementtype": "^2.0.1",
-        "domhandler": "^4.2.2",
-        "domutils": "^2.8.0",
-        "entities": "^3.0.1"
-      }
-    },
-    "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/js-yaml": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
-      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
-      "dev": true,
-      "dependencies": {
-        "argparse": "^2.0.1"
-      },
-      "bin": {
-        "js-yaml": "bin/js-yaml.js"
-      }
-    },
-    "node_modules/react-lifecycles-compat": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
-      "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
-    },
-    "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/@swc/core-linux-x64-gnu": {
-      "version": "1.3.107",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.107.tgz",
-      "integrity": "sha512-uBVNhIg0ip8rH9OnOsCARUFZ3Mq3tbPHxtmWk9uAa5u8jQwGWeBx5+nTHpDOVd3YxKb6+5xDEI/edeeLpha/9g==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/path-to-regexp": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
-      "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
-      "dependencies": {
-        "isarray": "0.0.1"
-      }
-    },
-    "node_modules/delayed-stream": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
-      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
-      "engines": {
-        "node": ">=0.4.0"
-      }
-    },
-    "node_modules/@babel/runtime": {
-      "version": "7.23.9",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
-      "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
-      "dependencies": {
-        "regenerator-runtime": "^0.14.0"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/lightningcss-linux-arm-gnueabihf": {
-      "version": "1.23.0",
-      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.23.0.tgz",
-      "integrity": "sha512-fBamf/bULvmWft9uuX+bZske236pUZEoUlaHNBjnueaCTJ/xd8eXgb0cEc7S5o0Nn6kxlauMBnqJpF70Bgq3zg==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/browserslist": {
-      "version": "4.22.3",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz",
-      "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "opencollective",
-          "url": "https://opencollective.com/browserslist"
-        },
-        {
-          "type": "tidelift",
-          "url": "https://tidelift.com/funding/github/npm/browserslist"
-        },
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/ai"
-        }
-      ],
-      "dependencies": {
-        "caniuse-lite": "^1.0.30001580",
-        "electron-to-chromium": "^1.4.648",
-        "node-releases": "^2.0.14",
-        "update-browserslist-db": "^1.0.13"
-      },
-      "bin": {
-        "browserslist": "cli.js"
-      },
-      "engines": {
-        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
-      }
-    },
-    "node_modules/loose-envify": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
-      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
-      "dependencies": {
-        "js-tokens": "^3.0.0 || ^4.0.0"
-      },
-      "bin": {
-        "loose-envify": "cli.js"
-      }
-    },
-    "node_modules/@parcel/transformer-js/node_modules/lru-cache": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-      "dev": true,
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/css-vendor": {
-      "version": "2.0.8",
-      "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz",
-      "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==",
-      "dependencies": {
-        "@babel/runtime": "^7.8.3",
-        "is-in-browser": "^1.0.2"
-      }
-    },
-    "node_modules/jsesc": {
-      "version": "2.5.2",
-      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
-      "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
-      "dev": true,
-      "bin": {
-        "jsesc": "bin/jsesc"
-      },
-      "engines": {
-        "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==",
-      "dev": true,
-      "dependencies": {
-        "@babel/types": "^7.22.15"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/react-router": {
-      "version": "5.3.4",
-      "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
-      "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
-      "dependencies": {
-        "@babel/runtime": "^7.12.13",
-        "history": "^4.9.0",
-        "hoist-non-react-statics": "^3.1.0",
-        "loose-envify": "^1.3.1",
-        "path-to-regexp": "^1.7.0",
-        "prop-types": "^15.6.2",
-        "react-is": "^16.6.0",
-        "tiny-invariant": "^1.0.2",
-        "tiny-warning": "^1.0.0"
-      },
-      "peerDependencies": {
-        "react": ">=15"
-      }
-    },
-    "node_modules/html-to-react/node_modules/domutils": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
-      "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
-      "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/@babel/helper-hoist-variables": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
-      "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
-      "dev": true,
-      "dependencies": {
-        "@babel/types": "^7.22.5"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@types/d3-path": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.1.tgz",
-      "integrity": "sha512-blRhp7ki7pVznM8k6lk5iUU9paDbVRVq+/xpf0RRgSJn5gr6SE7RcFtxooYGMBOc1RZiGyqRpVdu5AD0z0ooMA=="
-    },
-    "node_modules/is-number": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
-      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.12.0"
-      }
-    },
-    "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==",
-      "dev": true,
-      "dependencies": {
-        "mdn-data": "2.0.14",
-        "source-map": "^0.6.1"
-      },
-      "engines": {
-        "node": ">=8.0.0"
-      }
-    },
-    "node_modules/is-json": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/is-json/-/is-json-2.0.1.tgz",
-      "integrity": "sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==",
-      "dev": true
-    },
-    "node_modules/@lmdb/lmdb-darwin-arm64": {
-      "version": "2.8.5",
-      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-2.8.5.tgz",
-      "integrity": "sha512-KPDeVScZgA1oq0CiPBcOa3kHIqU+pTOwRFDIhxvmf8CTNvqdZQYp5cCKW0bUk69VygB2PuTiINFWbY78aR2pQw==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ]
-    },
-    "node_modules/@parcel/core/node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-      "dev": true
-    },
-    "node_modules/jss-plugin-nested": {
-      "version": "10.10.0",
-      "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.10.0.tgz",
-      "integrity": "sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==",
-      "dependencies": {
-        "@babel/runtime": "^7.3.1",
-        "jss": "10.10.0",
-        "tiny-warning": "^1.0.2"
-      }
-    },
-    "node_modules/parcel/node_modules/supports-color": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-      "dev": true,
-      "dependencies": {
-        "has-flag": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/nth-check": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
-      "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
-      "dev": true,
-      "dependencies": {
-        "boolbase": "^1.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/fb55/nth-check?sponsor=1"
-      }
-    },
-    "node_modules/csso": {
-      "version": "5.0.5",
-      "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
-      "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
-      "dev": true,
-      "optional": true,
-      "peer": true,
-      "dependencies": {
-        "css-tree": "~2.2.0"
-      },
-      "engines": {
-        "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
-        "npm": ">=7.0.0"
-      }
-    },
-    "node_modules/lightningcss-linux-arm64-gnu": {
-      "version": "1.23.0",
-      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.23.0.tgz",
-      "integrity": "sha512-RS7sY77yVLOmZD6xW2uEHByYHhQi5JYWmgVumYY85BfNoVI3DupXSlzbw+b45A9NnVKq45+oXkiN6ouMMtTwfg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@parcel/source-map": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/@parcel/source-map/-/source-map-2.1.1.tgz",
-      "integrity": "sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==",
-      "dev": true,
-      "dependencies": {
-        "detect-libc": "^1.0.3"
-      },
-      "engines": {
-        "node": "^12.18.3 || >=14"
-      }
-    },
-    "node_modules/@parcel/watcher-win32-arm64": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.0.tgz",
-      "integrity": "sha512-NOej2lqlq8bQNYhUMnOD0nwvNql8ToQF+1Zhi9ULZoG+XTtJ9hNnCFfyICxoZLXor4bBPTOnzs/aVVoefYnjIg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">= 10.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/react-router/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/@swc/core-win32-ia32-msvc": {
-      "version": "1.3.107",
-      "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.107.tgz",
-      "integrity": "sha512-ZBUtgyjTHlz8TPJh7kfwwwFma+ktr6OccB1oXC8fMSopD0AxVnQasgun3l3099wIsAB9eEsJDQ/3lDkOLs1gBA==",
-      "cpu": [
-        "ia32"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/@parcel/markdown-ansi/node_modules/color-name": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-      "dev": true
-    },
-    "node_modules/proxy-from-env": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
-      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
-    },
-    "node_modules/@parcel/resolver-default": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/resolver-default/-/resolver-default-2.11.0.tgz",
-      "integrity": "sha512-suZNN2lE5W48LPTwAbG7gnj1IeubkCVEm0XspWXcXUtCzglimNJ8PVVBGx171o5CqDpdbGF3AqHjG9N3uOwXag==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/node-resolver-core": "3.2.0",
-        "@parcel/plugin": "2.11.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/parcel/node_modules/chalk": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-      "dev": true,
-      "dependencies": {
-        "ansi-styles": "^4.1.0",
-        "supports-color": "^7.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
-      }
-    },
-    "node_modules/resolve-pathname": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
-      "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
-    },
-    "node_modules/@parcel/transformer-js/node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-      "dev": true
-    },
-    "node_modules/@parcel/diagnostic": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/diagnostic/-/diagnostic-2.11.0.tgz",
-      "integrity": "sha512-4dJmOXVL5YGGQRRsQosQbSRONBcboB71mSwaeaEgz3pPdq9QXVPLACkGe/jTXSqa3OnAHu3g5vQLpE1g5xqBqw==",
-      "dev": true,
-      "dependencies": {
-        "@mischnic/json-sourcemap": "^0.1.0",
-        "nullthrows": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@parcel/transformer-image": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/transformer-image/-/transformer-image-2.11.0.tgz",
-      "integrity": "sha512-QiZj18UHf3lVFsi65Vz8YbS3ydx9Pe9x8ktMxE1oh9qpznN8lD7gE/Z9DxuTZB84EZ9pKytKwcv5WGXP25xIFg==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/plugin": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "@parcel/workers": "2.11.0",
-        "nullthrows": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "peerDependencies": {
-        "@parcel/core": "^2.11.0"
-      }
-    },
-    "node_modules/@parcel/transformer-html/node_modules/semver": {
-      "version": "7.5.4",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
-      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
-      "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/ordered-binary": {
-      "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.1.tgz",
-      "integrity": "sha512-5VyHfHY3cd0iza71JepYG50My+YUbrFtGoUz2ooEydPyPM7Aai/JW098juLr+RG6+rDJuzNNTsEQu2DZa1A41A==",
-      "dev": true
-    },
-    "node_modules/@parcel/node-resolver-core/node_modules/semver": {
-      "version": "7.5.4",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
-      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
-      "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/@types/d3-time": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.2.tgz",
-      "integrity": "sha512-kbdRXTmUgNfw5OTE3KZnFQn6XdIc4QGroN5UixgdrXATmYsdlPQS6pEut9tVlIojtzuFD4txs/L+Rq41AHtLpg=="
-    },
-    "node_modules/@types/react-transition-group": {
-      "version": "4.4.8",
-      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.8.tgz",
-      "integrity": "sha512-QmQ22q+Pb+HQSn04NL3HtrqHwYMf4h3QKArOy5F8U5nEVMaihBs3SR10WiOM1iwPz5jIo8x/u11al+iEGZZrvg==",
-      "dependencies": {
-        "@types/react": "*"
-      }
-    },
-    "node_modules/term-size": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz",
-      "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/@parcel/optimizer-svgo": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/optimizer-svgo/-/optimizer-svgo-2.11.0.tgz",
-      "integrity": "sha512-TQpvfBhjV2IsuFHXUolbDS6XWB3DDR2rYTlqlA8LMmuOY7jQd9Bnkl4JnapzWm/bRuzRlzdGjjVCPGL8iShFvA==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/plugin": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "svgo": "^2.4.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "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/@babel/plugin-syntax-jsx": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz",
-      "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.22.5"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/fill-range": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
-      "dev": true,
-      "dependencies": {
-        "to-regex-range": "^5.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/ansi-styles": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "dev": true,
-      "dependencies": {
-        "color-convert": "^1.9.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/core-js-compat": {
-      "version": "3.35.1",
-      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz",
-      "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==",
-      "dev": true,
-      "dependencies": {
-        "browserslist": "^4.22.2"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/core-js"
-      }
-    },
-    "node_modules/@babel/helper-string-parser": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
-      "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
-      "dev": true,
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/jss-plugin-default-unit": {
-      "version": "10.10.0",
-      "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.10.0.tgz",
-      "integrity": "sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==",
-      "dependencies": {
-        "@babel/runtime": "^7.3.1",
-        "jss": "10.10.0"
-      }
-    },
-    "node_modules/clone": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
-      "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.8"
-      }
-    },
-    "node_modules/fast-equals": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
-      "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==",
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
-    "node_modules/process": {
-      "version": "0.11.10",
-      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
-      "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
-      "dev": true,
-      "engines": {
-        "node": ">= 0.6.0"
-      }
-    },
-    "node_modules/@parcel/reporter-cli/node_modules/supports-color": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-      "dev": true,
-      "dependencies": {
-        "has-flag": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/lightningcss-linux-x64-musl": {
-      "version": "1.23.0",
-      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.23.0.tgz",
-      "integrity": "sha512-G9Ri3qpmF4qef2CV/80dADHKXRAQeQXpQTLx7AiQrBYQHqBjB75oxqj06FCIe5g4hNCqLPnM9fsO4CyiT1sFSQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@parcel/transformer-svg/node_modules/lru-cache": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-      "dev": true,
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "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==",
-      "dev": 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"
-      },
-      "bin": {
-        "svgo": "bin/svgo"
-      },
-      "engines": {
-        "node": ">=10.13.0"
-      }
-    },
-    "node_modules/source-map-js": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
-      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
-      "dev": true,
-      "optional": true,
-      "peer": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "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==",
-      "dev": true,
-      "dependencies": {
-        "mdn-data": "2.0.14",
-        "source-map": "^0.6.1"
-      },
-      "engines": {
-        "node": ">=8.0.0"
-      }
-    },
-    "node_modules/d3-timer": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
-      "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/is-extglob": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/@swc/core-win32-arm64-msvc": {
-      "version": "1.3.107",
-      "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.107.tgz",
-      "integrity": "sha512-J3P14Ngy/1qtapzbguEH41kY109t6DFxfbK4Ntz9dOWNuVY3o9/RTB841ctnJk0ZHEG+BjfCJjsD2n8H5HcaOA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "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==",
-      "dev": true,
-      "dependencies": {
-        "css-tree": "^1.1.2"
-      },
-      "engines": {
-        "node": ">=8.0.0"
-      }
-    },
-    "node_modules/html-to-react": {
-      "version": "1.7.0",
-      "resolved": "https://registry.npmjs.org/html-to-react/-/html-to-react-1.7.0.tgz",
-      "integrity": "sha512-b5HTNaTGyOj5GGIMiWVr1k57egAZ/vGy0GGefnCQ1VW5hu9+eku8AXHtf2/DeD95cj/FKBKYa1J7SWBOX41yUQ==",
-      "dependencies": {
-        "domhandler": "^5.0",
-        "htmlparser2": "^9.0",
-        "lodash.camelcase": "^4.3.0"
-      },
-      "peerDependencies": {
-        "react": "^0.13.0 || ^0.14.0 || >=15"
-      }
-    },
-    "node_modules/@swc/core-darwin-x64": {
-      "version": "1.3.107",
-      "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.107.tgz",
-      "integrity": "sha512-hwiLJ2ulNkBGAh1m1eTfeY1417OAYbRGcb/iGsJ+LuVLvKAhU/itzsl535CvcwAlt2LayeCFfcI8gdeOLeZa9A==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/@parcel/codeframe/node_modules/has-flag": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/@types/d3-interpolate": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.3.tgz",
-      "integrity": "sha512-6OZ2EIB4lLj+8cUY7I/Cgn9Q+hLdA4DjJHYOQDiHL0SzqS1K9DL5xIOVBSIHgF+tiuO9MU1D36qvdIvRDRPh+Q==",
-      "dependencies": {
-        "@types/d3-color": "*"
-      }
-    },
-    "node_modules/@types/styled-jsx": {
-      "version": "2.2.9",
-      "resolved": "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.9.tgz",
-      "integrity": "sha512-W/iTlIkGEyTBGTEvZCey8EgQlQ5l0DwMqi3iOXlLs2kyBwYTXHKEiU6IZ5EwoRwngL8/dGYuzezSup89ttVHLw==",
-      "dependencies": {
-        "@types/react": "*"
-      }
-    },
-    "node_modules/@swc/counter": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz",
-      "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==",
-      "dev": true
-    },
-    "node_modules/emoji-regex": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-      "dev": true
-    },
-    "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==",
-      "dependencies": {
-        "react-is": "^16.7.0"
-      }
-    },
-    "node_modules/@babel/helper-validator-option": {
-      "version": "7.22.15",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz",
-      "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==",
-      "dev": true,
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@parcel/node-resolver-core/node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-      "dev": true
-    },
-    "node_modules/@babel/helper-optimise-call-expression": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz",
-      "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==",
-      "dev": true,
-      "dependencies": {
-        "@babel/types": "^7.22.5"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/set-value": {
-      "version": "4.1.0",
-      "funding": [
-        "https://github.com/sponsors/jonschlinkert",
-        "https://paypal.me/jonathanschlinkert",
-        "https://jonschlinkert.dev/sponsor"
-      ],
-      "resolved": "https://registry.npmjs.org/set-value/-/set-value-4.1.0.tgz",
-      "integrity": "sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==",
-      "dev": true,
-      "dependencies": {
-        "is-plain-object": "^2.0.4",
-        "is-primitive": "^3.0.1"
-      },
-      "engines": {
-        "node": ">=11.0"
-      }
-    },
-    "node_modules/@material-ui/pickers": {
-      "version": "3.3.11",
-      "resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.3.11.tgz",
-      "integrity": "sha512-pDYjbjUeabapijS2FpSwK/ruJdk7IGeAshpLbKDa3PRRKRy7Nv6sXxAvUg2F+lID/NwUKgBmCYS5bzrl7Xxqzw==",
-      "deprecated": "This package no longer supported. It has been relaced by @mui/x-date-pickers",
-      "dependencies": {
-        "@babel/runtime": "^7.6.0",
-        "@date-io/core": "1.x",
-        "@types/styled-jsx": "^2.2.8",
-        "clsx": "^1.0.2",
-        "react-transition-group": "^4.0.0",
-        "rifm": "^0.7.0"
-      },
-      "peerDependencies": {
-        "@date-io/core": "^1.3.6",
-        "@material-ui/core": "^4.0.0",
-        "prop-types": "^15.6.0",
-        "react": "^16.8.0 || ^17.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0"
-      }
-    },
-    "node_modules/follow-redirects": {
-      "version": "1.15.4",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
-      "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
-      "funding": [
-        {
-          "type": "individual",
-          "url": "https://github.com/sponsors/RubenVerborgh"
-        }
-      ],
-      "engines": {
-        "node": ">=4.0"
-      },
-      "peerDependenciesMeta": {
-        "debug": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@babel/plugin-transform-react-jsx": {
-      "version": "7.22.15",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.15.tgz",
-      "integrity": "sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-annotate-as-pure": "^7.22.5",
-        "@babel/helper-module-imports": "^7.22.15",
-        "@babel/helper-plugin-utils": "^7.22.5",
-        "@babel/plugin-syntax-jsx": "^7.22.5",
-        "@babel/types": "^7.22.15"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/domelementtype": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
-      "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/fb55"
-        }
-      ]
-    },
-    "node_modules/@parcel/markdown-ansi/node_modules/ansi-styles": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "dev": true,
-      "dependencies": {
-        "color-convert": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-      }
-    },
-    "node_modules/@parcel/transformer-js/node_modules/regenerator-runtime": {
-      "version": "0.13.11",
-      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
-      "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
-      "dev": true
-    },
-    "node_modules/@parcel/codeframe": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/codeframe/-/codeframe-2.11.0.tgz",
-      "integrity": "sha512-YHs9g/i5af/sd/JrWAojU9YFbKffcJ3Tx2EJaK0ME8OJsye91UaI/3lxSUYLmJG9e4WLNJtqci8V5FBMz//ZPg==",
-      "dev": true,
-      "dependencies": {
-        "chalk": "^4.1.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@babel/helper-create-class-features-plugin": {
-      "version": "7.22.15",
-      "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz",
-      "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-annotate-as-pure": "^7.22.5",
-        "@babel/helper-environment-visitor": "^7.22.5",
-        "@babel/helper-function-name": "^7.22.5",
-        "@babel/helper-member-expression-to-functions": "^7.22.15",
-        "@babel/helper-optimise-call-expression": "^7.22.5",
-        "@babel/helper-replace-supers": "^7.22.9",
-        "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
-        "@babel/helper-split-export-declaration": "^7.22.6",
-        "semver": "^6.3.1"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0"
-      }
-    },
-    "node_modules/micromatch": {
-      "version": "4.0.5",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
-      "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
-      "dev": true,
-      "dependencies": {
-        "braces": "^3.0.2",
-        "picomatch": "^2.3.1"
-      },
-      "engines": {
-        "node": ">=8.6"
-      }
-    },
-    "node_modules/@parcel/package-manager/node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-      "dev": true
-    },
-    "node_modules/lightningcss-win32-x64-msvc": {
-      "version": "1.23.0",
-      "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.23.0.tgz",
-      "integrity": "sha512-1rcBDJLU+obPPJM6qR5fgBUiCdZwZLafZM5f9kwjFLkb/UBNIzmae39uCSmh71nzPCTXZqHbvwu23OWnWEz+eg==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/type-fest": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
-      "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/d3-ease": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
-      "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/@jridgewell/resolve-uri": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
-      "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
-      "dev": true,
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
-    "node_modules/@parcel/utils/node_modules/supports-color": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-      "dev": true,
-      "dependencies": {
-        "has-flag": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz",
-      "integrity": "sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@parcel/namer-default": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/namer-default/-/namer-default-2.11.0.tgz",
-      "integrity": "sha512-DEwBSKSClg4DA2xAWimYkw9bFi7MFb9TdT7/TYZStMTsfYHPWOyyjGR7aVr3Ra4wNb+XX6g4rR41yp3HD6KO7A==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/plugin": "2.11.0",
-        "nullthrows": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "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/@parcel/transformer-svg/node_modules/semver": {
-      "version": "7.5.4",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
-      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
-      "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/lightningcss-darwin-x64": {
-      "version": "1.23.0",
-      "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.23.0.tgz",
-      "integrity": "sha512-KeRFCNoYfDdcolcFXvokVw+PXCapd2yHS1Diko1z1BhRz/nQuD5XyZmxjWdhmhN/zj5sH8YvWsp0/lPLVzqKpg==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/clsx": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
-      "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/dom-serializer/node_modules/entities": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
-      "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
-      "dev": true,
-      "funding": {
-        "url": "https://github.com/fb55/entities?sponsor=1"
-      }
-    },
-    "node_modules/@types/d3-color": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.2.tgz",
-      "integrity": "sha512-At+Ski7dL8Bs58E8g8vPcFJc8tGcaC12Z4m07+p41+DRqnZQcAlp3NfYjLrhNYv+zEyQitU1CUxXNjqUyf+c0g=="
-    },
-    "node_modules/@parcel/packager-html": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/packager-html/-/packager-html-2.11.0.tgz",
-      "integrity": "sha512-ho5AQ70naTV8IqkKIbKtK+jsXQ5TJfFgtBvmJlyB3YydRMbIc+3g4G0xgIvf15V4uCMw9Md0Sv1W65nQXHPQoA==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/plugin": "2.11.0",
-        "@parcel/types": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "nullthrows": "^1.1.1",
-        "posthtml": "^0.16.5"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/supports-color": {
-      "version": "5.5.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-      "dev": true,
-      "dependencies": {
-        "has-flag": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/lmdb/node_modules/node-addon-api": {
-      "version": "6.1.0",
-      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
-      "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
-      "dev": true
-    },
-    "node_modules/yallist": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
-      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
-      "dev": true
-    },
-    "node_modules/safe-buffer": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
-      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ]
-    },
-    "node_modules/@parcel/transformer-json": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/transformer-json/-/transformer-json-2.11.0.tgz",
-      "integrity": "sha512-Wt/wgSBaRWmPL4gpvjkV0bCBRxFOtsuLNzsm8vYA5poxTFhuLY+AoyQ8S2+xXU4VxwBfdppfIr2Ny3SwGs8xbQ==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/plugin": "2.11.0",
-        "json5": "^2.2.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "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/@parcel/transformer-posthtml/node_modules/lru-cache": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-      "dev": true,
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/import-fresh": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
-      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
-      "dev": true,
-      "dependencies": {
-        "parent-module": "^1.0.0",
-        "resolve-from": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "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==",
-      "dev": true,
-      "dependencies": {
-        "css-tree": "^1.1.2"
-      },
-      "engines": {
-        "node": ">=8.0.0"
-      }
-    },
-    "node_modules/@mischnic/json-sourcemap": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/@mischnic/json-sourcemap/-/json-sourcemap-0.1.1.tgz",
-      "integrity": "sha512-iA7+tyVqfrATAIsIRWQG+a7ZLLD0VaOCKV2Wd/v4mqIU3J9c4jx9p7S0nw1XH3gJCKNBOOwACOPYYSUu9pgT+w==",
-      "dev": true,
-      "dependencies": {
-        "@lezer/common": "^1.0.0",
-        "@lezer/lr": "^1.0.0",
-        "json5": "^2.2.1"
-      },
-      "engines": {
-        "node": ">=12.0.0"
-      }
-    },
-    "node_modules/@babel/helper-plugin-utils": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz",
-      "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==",
-      "dev": true,
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/escape-string-regexp": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.8.0"
-      }
-    },
-    "node_modules/@date-io/core": {
-      "version": "1.3.13",
-      "resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz",
-      "integrity": "sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA=="
-    },
-    "node_modules/@parcel/compressor-raw": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/compressor-raw/-/compressor-raw-2.11.0.tgz",
-      "integrity": "sha512-RArhBPRTCfz77soX2IECH09NUd76UBWujXiPRcXGPIHK+C3L1cRuzsNcA39QeSb3thz3b99JcozMJ1nkC2Bsgw==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/plugin": "2.11.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/timsort": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
-      "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==",
-      "dev": true
-    },
-    "node_modules/base-x": {
-      "version": "3.0.9",
-      "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz",
-      "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==",
-      "dev": true,
-      "dependencies": {
-        "safe-buffer": "^5.0.1"
-      }
-    },
-    "node_modules/@parcel/types": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/types/-/types-2.11.0.tgz",
-      "integrity": "sha512-lN5XlfV9b1s2rli8q1LqsLtu+D4ZwNI3sKmNcL/3tohSfQcF2EgF+MaiANGo9VzXOzoWFHt4dqWjO4OcdyC5tg==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/cache": "2.11.0",
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/fs": "2.11.0",
-        "@parcel/package-manager": "2.11.0",
-        "@parcel/source-map": "^2.1.1",
-        "@parcel/workers": "2.11.0",
-        "utility-types": "^3.10.0"
-      }
-    },
-    "node_modules/braces": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
-      "dev": true,
-      "dependencies": {
-        "fill-range": "^7.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/@parcel/packager-css": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/packager-css/-/packager-css-2.11.0.tgz",
-      "integrity": "sha512-AyIxsp4eL8c22vp2oO2hSRnr3hSVNkARNZc9DG6uXxCc2Is5tUEX0I4PwxWnAx0EI44l+3zX/o414zT8yV9wwQ==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/plugin": "2.11.0",
-        "@parcel/source-map": "^2.1.1",
-        "@parcel/utils": "2.11.0",
-        "nullthrows": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "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==",
-      "dev": true,
-      "optional": true,
-      "peer": true,
-      "engines": {
-        "node": ">=0.12"
-      },
-      "funding": {
-        "url": "https://github.com/fb55/entities?sponsor=1"
-      }
-    },
-    "node_modules/@parcel/utils/node_modules/has-flag": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "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/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/ansi-regex": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
-      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-      "dev": true,
-      "engines": {
-        "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==",
-      "dev": true,
-      "dependencies": {
-        "hasown": "^2.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/@parcel/packager-raw": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/packager-raw/-/packager-raw-2.11.0.tgz",
-      "integrity": "sha512-2/0JQ8DZrz7cVNXwD6OYoUUtSSnlr4dsz8ZkpFDKsBJhvMHtC78Sq+1EDixDGOMiUcalSEjNsoHtkpq9uNh+Xw==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/plugin": "2.11.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@babel/plugin-transform-react-display-name": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.22.5.tgz",
-      "integrity": "sha512-PVk3WPYudRF5z4GKMEYUrLjPl38fJSKNaEOkFuoprioowGuWN6w2RKznuFNSlJx7pzzXXStPUnNSOEO0jL5EVw==",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.22.5"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "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==",
-      "dev": true
-    },
-    "node_modules/@babel/helper-split-export-declaration": {
-      "version": "7.22.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
-      "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
-      "dev": true,
-      "dependencies": {
-        "@babel/types": "^7.22.5"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@parcel/optimizer-image": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/optimizer-image/-/optimizer-image-2.11.0.tgz",
-      "integrity": "sha512-jCaJww5QFG2GuNzYW8nlSW+Ea+Cv47TRnOPJNquFIajgfTLJ5ddsWbaNal0GQsL8yNiCBKWd1AV4W0RH9tG0Jg==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/plugin": "2.11.0",
-        "@parcel/rust": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "@parcel/workers": "2.11.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      },
-      "peerDependencies": {
-        "@parcel/core": "^2.11.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==",
-      "dev": true,
-      "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/is-glob": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
-      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
-      "dev": true,
-      "dependencies": {
-        "is-extglob": "^2.1.1"
-      },
-      "engines": {
-        "node": ">=0.10.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=="
-    },
-    "node_modules/picocolors": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
-      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
-      "dev": true
-    },
-    "node_modules/@babel/plugin-transform-react-jsx-development": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz",
-      "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==",
-      "dev": true,
-      "dependencies": {
-        "@babel/plugin-transform-react-jsx": "^7.22.5"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/tslib": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
-      "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
-      "dev": true
-    },
-    "node_modules/form-data": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
-      "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
-      "dependencies": {
-        "asynckit": "^0.4.0",
-        "combined-stream": "^1.0.8",
-        "mime-types": "^2.1.12"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
-    "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==",
-      "dev": true,
-      "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/@parcel/watcher-linux-x64-musl": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.0.tgz",
-      "integrity": "sha512-7jzcOonpXNWcSijPpKD5IbC6xC7yTibjJw9jviVzZostYLGxbz8LDJLUnLzLzhASPlPGgpeKLtFUMjAAzM+gSA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">= 10.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/isarray": {
-      "version": "0.0.1",
-      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
-      "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
-    },
-    "node_modules/@babel/plugin-proposal-class-properties": {
-      "version": "7.18.6",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz",
-      "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==",
-      "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-create-class-features-plugin": "^7.18.6",
-        "@babel/helper-plugin-utils": "^7.18.6"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/scheduler": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
-      "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
-      "dependencies": {
-        "loose-envify": "^1.1.0",
-        "object-assign": "^4.1.1"
-      }
-    },
-    "node_modules/@babel/core": {
-      "version": "7.23.2",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz",
-      "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==",
-      "dev": true,
-      "dependencies": {
-        "@ampproject/remapping": "^2.2.0",
-        "@babel/code-frame": "^7.22.13",
-        "@babel/generator": "^7.23.0",
-        "@babel/helper-compilation-targets": "^7.22.15",
-        "@babel/helper-module-transforms": "^7.23.0",
-        "@babel/helpers": "^7.23.2",
-        "@babel/parser": "^7.23.0",
-        "@babel/template": "^7.22.15",
-        "@babel/traverse": "^7.23.2",
-        "@babel/types": "^7.23.0",
-        "convert-source-map": "^2.0.0",
-        "debug": "^4.1.0",
-        "gensync": "^1.0.0-beta.2",
-        "json5": "^2.2.3",
-        "semver": "^6.3.1"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/babel"
-      }
-    },
-    "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/@parcel/watcher-linux-arm-glibc": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.0.tgz",
-      "integrity": "sha512-9NQXD+qk46RwATNC3/UB7HWurscY18CnAPMTFcI9Y8CTbtm63/eex1SNt+BHFinEQuLBjaZwR2Lp+n7pmEJPpQ==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">= 10.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/html-to-react/node_modules/htmlparser2": {
-      "version": "9.1.0",
-      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
-      "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
-      "funding": [
-        "https://github.com/fb55/htmlparser2?sponsor=1",
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/fb55"
-        }
-      ],
-      "dependencies": {
-        "domelementtype": "^2.3.0",
-        "domhandler": "^5.0.3",
-        "domutils": "^3.1.0",
-        "entities": "^4.5.0"
-      }
-    },
-    "node_modules/@parcel/transformer-svg": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/transformer-svg/-/transformer-svg-2.11.0.tgz",
-      "integrity": "sha512-GrTNi04OoQSXsyrB7FqQPeYREscEXFhIBPkyQ0q7WDG/yYynWljiA0kwITCtMjPfv2EDVks292dvM3EcnERRIA==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/diagnostic": "2.11.0",
-        "@parcel/plugin": "2.11.0",
-        "@parcel/rust": "2.11.0",
-        "nullthrows": "^1.1.1",
-        "posthtml": "^0.16.5",
-        "posthtml-parser": "^0.10.1",
-        "posthtml-render": "^3.0.0",
-        "semver": "^7.5.2"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/path-type": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
-      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/victory-vendor": {
-      "version": "36.6.11",
-      "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.6.11.tgz",
-      "integrity": "sha512-nT8kCiJp8dQh8g991J/R5w5eE2KnO8EAIP0xocWlh9l2okngMWglOPoMZzJvek8Q1KUc4XE/mJxTZnvOB1sTYg==",
-      "dependencies": {
-        "@types/d3-array": "^3.0.3",
-        "@types/d3-ease": "^3.0.0",
-        "@types/d3-interpolate": "^3.0.1",
-        "@types/d3-scale": "^4.0.2",
-        "@types/d3-shape": "^3.1.0",
-        "@types/d3-time": "^3.0.0",
-        "@types/d3-timer": "^3.0.0",
-        "d3-array": "^3.1.6",
-        "d3-ease": "^3.0.1",
-        "d3-interpolate": "^3.0.1",
-        "d3-scale": "^4.0.2",
-        "d3-shape": "^3.1.0",
-        "d3-time": "^3.0.0",
-        "d3-timer": "^3.0.1"
-      }
-    },
-    "node_modules/boolbase": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
-      "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
-      "dev": true
-    },
-    "node_modules/lightningcss": {
-      "version": "1.23.0",
-      "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.23.0.tgz",
-      "integrity": "sha512-SEArWKMHhqn/0QzOtclIwH5pXIYQOUEkF8DgICd/105O+GCgd7jxjNod/QPnBCSWvpRHQBGVz5fQ9uScby03zA==",
-      "dev": true,
-      "dependencies": {
-        "detect-libc": "^1.0.3"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      },
-      "optionalDependencies": {
-        "lightningcss-darwin-arm64": "1.23.0",
-        "lightningcss-darwin-x64": "1.23.0",
-        "lightningcss-freebsd-x64": "1.23.0",
-        "lightningcss-linux-arm-gnueabihf": "1.23.0",
-        "lightningcss-linux-arm64-gnu": "1.23.0",
-        "lightningcss-linux-arm64-musl": "1.23.0",
-        "lightningcss-linux-x64-gnu": "1.23.0",
-        "lightningcss-linux-x64-musl": "1.23.0",
-        "lightningcss-win32-x64-msvc": "1.23.0"
-      }
-    },
-    "node_modules/has-flag": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/@parcel/watcher-android-arm64": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.0.tgz",
-      "integrity": "sha512-+fPtO/GsbYX1LJnCYCaDVT3EOBjvSFdQN9Mrzh9zWAOOfvidPWyScTrHIZHHfJBvlHzNA0Gy0U3NXFA/M7PHUA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">= 10.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "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/@swc/core-linux-x64-musl": {
-      "version": "1.3.107",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.107.tgz",
-      "integrity": "sha512-mvACkUvzSIB12q1H5JtabWATbk3AG+pQgXEN95AmEX2ZA5gbP9+B+mijsg7Sd/3tboHr7ZHLz/q3SHTvdFJrEw==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/cli-progress": {
-      "version": "3.12.0",
-      "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
-      "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
-      "dev": true,
-      "dependencies": {
-        "string-width": "^4.2.3"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/@types/react": {
-      "version": "17.0.69",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.69.tgz",
-      "integrity": "sha512-klEeru//GhiQvXUBayz0Q4l3rKHWsBR/EUOhOeow6hK2jV7MlO44+8yEk6+OtPeOlRfnpUnrLXzGK+iGph5aeg==",
-      "dependencies": {
-        "@types/prop-types": "*",
-        "@types/scheduler": "*",
-        "csstype": "^3.0.2"
-      }
-    },
-    "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==",
-      "dev": true,
-      "optional": true,
-      "peer": true,
-      "dependencies": {
-        "domelementtype": "^2.3.0"
-      },
-      "engines": {
-        "node": ">= 4"
-      },
-      "funding": {
-        "url": "https://github.com/fb55/domhandler?sponsor=1"
-      }
-    },
-    "node_modules/@parcel/markdown-ansi": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/markdown-ansi/-/markdown-ansi-2.11.0.tgz",
-      "integrity": "sha512-YA60EWbXi6cLOIzcwRC2wijotPauOGQbUi0vSbu0O6/mjQ68kWCMGz0hwZjDRQcPypQVJEIvTgMymLbvumxwhg==",
-      "dev": true,
-      "dependencies": {
-        "chalk": "^4.1.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@babel/helper-function-name": {
-      "version": "7.23.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
-      "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
-      "dev": true,
-      "dependencies": {
-        "@babel/template": "^7.22.15",
-        "@babel/types": "^7.23.0"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@parcel/transformer-postcss/node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-      "dev": true
-    },
-    "node_modules/@swc/core-linux-arm64-musl": {
-      "version": "1.3.107",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.107.tgz",
-      "integrity": "sha512-vfPF74cWfAm8hyhS8yvYI94ucMHIo8xIYU+oFOW9uvDlGQRgnUf/6DEVbLyt/3yfX5723Ln57U8uiMALbX5Pyw==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/html-to-react/node_modules/entities": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
-      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
-      "engines": {
-        "node": ">=0.12"
-      },
-      "funding": {
-        "url": "https://github.com/fb55/entities?sponsor=1"
-      }
-    },
-    "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==",
-      "dev": true,
-      "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/core": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/core/-/core-2.11.0.tgz",
-      "integrity": "sha512-Npe0S6hVaqWEwRL+HI7gtOYOaoE5bJQZTgUDhsDoppWbau51jOlRYOZTXuvRK/jxXnze4/S1sdM24xBYAQ5qkw==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/rust": "2.11.0",
-        "@mischnic/json-sourcemap": "^0.1.0",
-        "@parcel/utils": "2.11.0",
-        "@parcel/package-manager": "2.11.0",
-        "@parcel/fs": "2.11.0",
-        "@parcel/diagnostic": "2.11.0",
-        "semver": "^7.5.2",
-        "@parcel/graph": "3.1.0",
-        "@parcel/source-map": "^2.1.1",
-        "@parcel/types": "2.11.0",
-        "abortcontroller-polyfill": "^1.1.9",
-        "dotenv": "^7.0.0",
-        "@parcel/events": "2.11.0",
-        "msgpackr": "^1.9.9",
-        "@parcel/profiler": "2.11.0",
-        "base-x": "^3.0.8",
-        "@parcel/plugin": "2.11.0",
-        "@parcel/cache": "2.11.0",
-        "@parcel/logger": "2.11.0",
-        "@parcel/workers": "2.11.0",
-        "dotenv-expand": "^5.1.0",
-        "json5": "^2.2.0",
-        "browserslist": "^4.6.6",
-        "clone": "^2.1.1",
-        "nullthrows": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/recharts/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/@babel/helper-member-expression-to-functions": {
-      "version": "7.23.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz",
-      "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==",
-      "dev": true,
-      "dependencies": {
-        "@babel/types": "^7.23.0"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/plugin-transform-react-pure-annotations": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.22.5.tgz",
-      "integrity": "sha512-gP4k85wx09q+brArVinTXhWiyzLl9UpmGva0+mWyKxk6JZequ05x3eUcIUE+FyttPKJFRRVtAvQaJ6YF9h1ZpA==",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-annotate-as-pure": "^7.22.5",
-        "@babel/helper-plugin-utils": "^7.22.5"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/dom-serializer": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
-      "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
-      "dev": true,
-      "dependencies": {
-        "domelementtype": "^2.0.1",
-        "domhandler": "^4.2.0",
-        "entities": "^2.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
-      }
-    },
-    "node_modules/@parcel/reporter-cli/node_modules/ansi-styles": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "dev": true,
-      "dependencies": {
-        "color-convert": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-      }
-    },
-    "node_modules/@parcel/transformer-babel/node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-      "dev": true
-    },
-    "node_modules/jss-plugin-global": {
-      "version": "10.10.0",
-      "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.10.0.tgz",
-      "integrity": "sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==",
-      "dependencies": {
-        "@babel/runtime": "^7.3.1",
-        "jss": "10.10.0"
-      }
-    },
-    "node_modules/mdn-data": {
-      "version": "2.0.30",
-      "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
-      "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
-      "dev": true,
-      "optional": true,
-      "peer": true
-    },
-    "node_modules/@parcel/events": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/events/-/events-2.11.0.tgz",
-      "integrity": "sha512-K6SOjOrQsz1GdNl2qKBktq7KJ3Q3yxK8WXdmQYo10wG39dr051xtMb38aqieTp4eVhL8Yaq2iJgGkdr11fuBnA==",
-      "dev": true,
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@parcel/runtime-service-worker": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/runtime-service-worker/-/runtime-service-worker-2.11.0.tgz",
-      "integrity": "sha512-c8MaSpSbXIKuN5sA/g4UsrsH1BtBZ6Em+eSxt9AYbdPtWrW+qwCioNVZj9lugBRUzDMjVfJz0yK59nS42hABvw==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/plugin": "2.11.0",
-        "@parcel/utils": "2.11.0",
-        "nullthrows": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 12.0.0",
-        "parcel": "^2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      }
-    },
-    "node_modules/@parcel/config-default": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/@parcel/config-default/-/config-default-2.11.0.tgz",
-      "integrity": "sha512-1e2+qcZkm5/0f4eI20p/DemcYiSxq9d/eyjpTXA7PulJaHbL1wonwUAuy3mvnAvDnLOJmAk/obDVgX1ZfxMGtg==",
-      "dev": true,
-      "dependencies": {
-        "@parcel/transformer-react-refresh-wrap": "2.11.0",
-        "@parcel/transformer-css": "2.11.0",
-        "@parcel/transformer-svg": "2.11.0",
-        "@parcel/transformer-babel": "2.11.0",
-        "@parcel/transformer-image": "2.11.0",
-        "@parcel/transformer-js": "2.11.0",
-        "@parcel/bundler-default": "2.11.0",
-        "@parcel/packager-html": "2.11.0",
-        "@parcel/transformer-json": "2.11.0",
-        "@parcel/packager-wasm": "2.11.0",
-        "@parcel/optimizer-svgo": "2.11.0",
-        "@parcel/optimizer-css": "2.11.0",
-        "@parcel/packager-js": "2.11.0",
-        "@parcel/packager-svg": "2.11.0",
-        "@parcel/runtime-react-refresh": "2.11.0",
-        "@parcel/compressor-raw": "2.11.0",
-        "@parcel/reporter-dev-server": "2.11.0",
-        "@parcel/packager-css": "2.11.0",
-        "@parcel/transformer-html": "2.11.0",
-        "@parcel/namer-default": "2.11.0",
-        "@parcel/resolver-default": "2.11.0",
-        "@parcel/transformer-raw": "2.11.0",
-        "@parcel/transformer-posthtml": "2.11.0",
-        "@parcel/optimizer-htmlnano": "2.11.0",
-        "@parcel/runtime-service-worker": "2.11.0",
-        "@parcel/runtime-js": "2.11.0",
-        "@parcel/optimizer-swc": "2.11.0",
-        "@parcel/packager-raw": "2.11.0",
-        "@parcel/runtime-browser-hmr": "2.11.0",
-        "@parcel/transformer-postcss": "2.11.0",
-        "@parcel/optimizer-image": "2.11.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel"
-      },
-      "peerDependencies": {
-        "@parcel/core": "^2.11.0"
-      }
-    }
-  }
-}

+ 0 - 47
ui/package.json

@@ -1,47 +0,0 @@
-{
-  "name": "opencost-ui",
-  "description": "Open source UI for OpenCost",
-  "version": "0.1.0",
-  "license": "Apache-2.0",
-  "scripts": {
-    "build": "npx parcel build src/index.html",
-    "serve": "npx parcel serve src/index.html --no-cache",
-    "clean": "rm -rf dist/*",
-    "test": "echo \"Error: no test specified\" && exit 1",
-    "preinstall": "npx npm-force-resolutions"
-  },
-  "browserslist": [
-    "defaults"
-  ],
-  "dependencies": {
-    "@babel/runtime": "^7.23.9",
-    "@date-io/core": "^1.3.13",
-    "@date-io/date-fns": "^1.3.13",
-    "@material-ui/core": "^4.11.3",
-    "@material-ui/icons": "^4.11.2",
-    "@material-ui/pickers": "^3.3.10",
-    "@material-ui/styles": "^4.11.5",
-    "axios": "^1.6.0",
-    "date-fns": "^2.30.0",
-    "html-to-react": "^1.7.0",
-    "material-design-icons-iconfont": "^6.1.0",
-    "prop-types": "^15.7.2",
-    "react": "^17.0.1",
-    "react-dom": "^17.0.1",
-    "react-router-dom": "^5.2.0",
-    "recharts": "^2.2.0"
-  },
-  "devDependencies": {
-    "@babel/core": "^7.13.10",
-    "@babel/plugin-proposal-class-properties": "^7.13.0",
-    "@babel/plugin-transform-runtime": "^7.23.9",
-    "@babel/preset-react": "^7.12.13",
-    "buffer": "^6.0.3",
-    "parcel": "^2.11.0",
-    "process": "^0.11.10",
-    "set-value": "4.1.0"
-  },
-  "resolutions": {
-    "set-value": "4.1.0"
-  }
-}

+ 0 - 314
ui/src/Reports.js

@@ -1,314 +0,0 @@
-import CircularProgress from "@material-ui/core/CircularProgress";
-import IconButton from "@material-ui/core/IconButton";
-import Paper from "@material-ui/core/Paper";
-import Typography from "@material-ui/core/Typography";
-import RefreshIcon from "@material-ui/icons/Refresh";
-import { makeStyles } from "@material-ui/styles";
-import {
-  filter,
-  find,
-  forEach,
-  get,
-  isArray,
-  sortBy,
-  toArray,
-  trim,
-} from "lodash";
-import React, { useEffect, useState } from "react";
-import ReactDOM from "react-dom";
-import { useLocation, useHistory } from "react-router";
-
-import AllocationReport from "./components/allocationReport";
-import Controls from "./components/Controls";
-import Header from "./components/Header";
-import Page from "./components/Page";
-import Footer from "./components/Footer";
-import Subtitle from "./components/Subtitle";
-import Warnings from "./components/Warnings";
-import AllocationService from "./services/allocation";
-import {
-  checkCustomWindow,
-  cumulativeToTotals,
-  rangeToCumulative,
-  toVerboseTimeRange,
-} from "./util";
-import { currencyCodes } from "./constants/currencyCodes";
-
-const windowOptions = [
-  { name: "Today", value: "today" },
-  { name: "Yesterday", value: "yesterday" },
-  { name: "Last 24h", value: "24h" },
-  { name: "Last 48h", value: "48h" },
-  { name: "Week-to-date", value: "week" },
-  { name: "Last week", value: "lastweek" },
-  { name: "Last 7 days", value: "7d" },
-  { name: "Last 14 days", value: "14d" },
-];
-
-const aggregationOptions = [
-  { name: "Cluster", value: "cluster" },
-  { name: "Node", value: "node" },
-  { name: "Namespace", value: "namespace" },
-  { name: "Controller Kind", value: "controllerKind" },
-  { name: "Controller", value: "controller" },
-  { name: "DaemonSet", value: "daemonset" },
-  { name: "Deployment", value: "deployment" },
-  { name: "Job", value: "job" },
-  { name: "Service", value: "service" },
-  { name: "StatefulSet", value: "statefulset" },
-  { name: "Pod", value: "pod" },
-  { name: "Container", value: "container" },
-];
-
-const accumulateOptions = [
-  { name: "Entire window", value: true },
-  { name: "Daily", value: false },
-];
-
-const useStyles = makeStyles({
-  reportHeader: {
-    display: "flex",
-    flexFlow: "row",
-    padding: 24,
-  },
-  titles: {
-    flexGrow: 1,
-  },
-});
-
-// generateTitle generates a string title from a report object
-function generateTitle({ window, aggregateBy, accumulate }) {
-  let windowName = get(find(windowOptions, { value: window }), "name", "");
-  if (windowName === "") {
-    if (checkCustomWindow(window)) {
-      windowName = toVerboseTimeRange(window);
-    } else {
-      console.warn(`unknown window: ${window}`);
-    }
-  }
-
-  let aggregationName = get(
-    find(aggregationOptions, { value: aggregateBy }),
-    "name",
-    ""
-  ).toLowerCase();
-  if (aggregationName === "") {
-    console.warn(`unknown aggregation: ${aggregateBy}`);
-  }
-
-  let str = `${windowName} by ${aggregationName}`;
-
-  if (!accumulate) {
-    str = `${str} daily`;
-  }
-
-  return str;
-}
-
-const ReportsPage = () => {
-  const classes = useStyles();
-
-  // Allocation data state
-  const [allocationData, setAllocationData] = useState([]);
-  const [cumulativeData, setCumulativeData] = useState({});
-  const [totalData, setTotalData] = useState({});
-
-  // When allocation data changes, create a cumulative version of it
-  useEffect(() => {
-    const cumulative = rangeToCumulative(allocationData, aggregateBy);
-    setCumulativeData(toArray(cumulative));
-    setTotalData(cumulativeToTotals(cumulative));
-  }, [allocationData]);
-
-  // Form state, which controls form elements, but not the report itself. On
-  // certain actions, the form state may flow into the report state.
-  const [window, setWindow] = useState(windowOptions[0].value);
-  const [aggregateBy, setAggregateBy] = useState(aggregationOptions[0].value);
-  const [accumulate, setAccumulate] = useState(accumulateOptions[0].value);
-  const [currency, setCurrency] = useState("USD");
-
-  // Report state, including current report and saved options
-  const [title, setTitle] = useState("Last 7 days by namespace daily");
-
-  // When parameters changes, fetch data. This should be the
-  // only mechanism used to fetch data. Also generate a sensible title from the paramters.
-  useEffect(() => {
-    setFetch(true);
-    setTitle(generateTitle({ window, aggregateBy, accumulate }));
-  }, [window, aggregateBy, accumulate]);
-
-  // page and settings state
-  const [init, setInit] = useState(false);
-  const [fetch, setFetch] = useState(false);
-  const [loading, setLoading] = useState(true);
-  const [errors, setErrors] = useState([]);
-
-  // Initialize once, then fetch report each time setFetch(true) is called
-  useEffect(() => {
-    if (!init) {
-      initialize();
-    }
-    if (init || fetch) {
-      fetchData();
-    }
-  }, [init, fetch]);
-
-  // parse any context information from the URL
-  const routerLocation = useLocation();
-  const searchParams = new URLSearchParams(routerLocation.search);
-  const routerHistory = useHistory();
-  useEffect(() => {
-    setWindow(searchParams.get("window") || "7d");
-    setAggregateBy(searchParams.get("agg") || "namespace");
-    setAccumulate(searchParams.get("acc") === "true" || false);
-    setCurrency(searchParams.get("currency") || "USD");
-  }, [routerLocation]);
-
-  async function initialize() {
-    setInit(true);
-  }
-
-  async function fetchData() {
-    setLoading(true);
-    setErrors([]);
-
-    try {
-      const resp = await AllocationService.fetchAllocation(
-        window,
-        aggregateBy,
-        { accumulate }
-      );
-      if (resp.data && resp.data.length > 0) {
-        const allocationRange = resp.data;
-        for (const i in allocationRange) {
-          // update cluster aggregations to use clusterName/clusterId names
-          allocationRange[i] = sortBy(allocationRange[i], (a) => a.totalCost);
-        }
-        setAllocationData(allocationRange);
-      } else {
-        if (resp.message && resp.message.indexOf("boundary error") >= 0) {
-          let match = resp.message.match(/(ETL is \d+\.\d+% complete)/);
-          let secondary = "Try again after ETL build is complete";
-          if (match.length > 0) {
-            secondary = `${match[1]}. ${secondary}`;
-          }
-          setErrors([
-            {
-              primary: "Data unavailable while ETL is building",
-              secondary: secondary,
-            },
-          ]);
-        }
-        setAllocationData([]);
-      }
-    } catch (err) {
-      if (err.message.indexOf("404") === 0) {
-        setErrors([
-          {
-            primary: "Failed to load report data",
-            secondary:
-              "Please update OpenCost to the latest version, then open an Issue on GitHub if problems persist.",
-          },
-        ]);
-      } else {
-        let secondary =
-          "Please open an Issue on GitHub if problems persist.";
-        if (err.message.length > 0) {
-          secondary = err.message;
-        }
-        setErrors([
-          {
-            primary: "Failed to load report data",
-            secondary: secondary,
-          },
-        ]);
-      }
-      setAllocationData([]);
-    }
-
-    setLoading(false);
-    setFetch(false);
-  }
-  return (
-    <Page active="reports.html">
-      <Header>
-        <IconButton aria-label="refresh" onClick={() => setFetch(true)}>
-          <RefreshIcon />
-        </IconButton>
-      </Header>
-
-      {!loading && errors.length > 0 && (
-        <div style={{ marginBottom: 20 }}>
-          <Warnings warnings={errors} />
-        </div>
-      )}
-
-      {init && (
-        <Paper id="report">
-          <div className={classes.reportHeader}>
-            <div className={classes.titles}>
-              <Typography variant="h5">{title}</Typography>
-              <Subtitle report={{ window, aggregateBy, accumulate }} />
-            </div>
-
-            <Controls
-              windowOptions={windowOptions}
-              window={window}
-              setWindow={(win) => {
-                searchParams.set("window", win);
-                routerHistory.push({
-                  search: `?${searchParams.toString()}`,
-                });
-              }}
-              aggregationOptions={aggregationOptions}
-              aggregateBy={aggregateBy}
-              setAggregateBy={(agg) => {
-                searchParams.set("agg", agg);
-                routerHistory.push({
-                  search: `?${searchParams.toString()}`,
-                });
-              }}
-              accumulateOptions={accumulateOptions}
-              accumulate={accumulate}
-              setAccumulate={(acc) => {
-                searchParams.set("acc", acc);
-                routerHistory.push({
-                  search: `?${searchParams.toString()}`,
-                });
-              }}
-              title={title}
-              cumulativeData={cumulativeData}
-              currency={currency}
-              currencyOptions={currencyCodes}
-              setCurrency={(curr) => {
-                searchParams.set("currency", curr);
-                routerHistory.push({
-                  search: `?${searchParams.toString()}`,
-                });
-              }}
-            />
-          </div>
-
-          {loading && (
-            <div style={{ display: "flex", justifyContent: "center" }}>
-              <div style={{ paddingTop: 100, paddingBottom: 100 }}>
-                <CircularProgress />
-              </div>
-            </div>
-          )}
-          {!loading && (
-            <AllocationReport
-              allocationData={allocationData}
-              cumulativeData={cumulativeData}
-              totalData={totalData}
-              currency={currency}
-            />
-          )}
-        </Paper>
-      )}
-      <Footer/>
-    </Page>
-  );
-};
-
-export default React.memo(ReportsPage);

+ 0 - 5
ui/src/app.js

@@ -1,5 +0,0 @@
-import * as React from "react";
-import ReactDOM from "react-dom";
-import Routes from "./route";
-
-ReactDOM.render(<Routes />, document.getElementById("app"));

+ 0 - 217
ui/src/cloudCost/cloudCost.js

@@ -1,217 +0,0 @@
-import * as React from "react";
-import { get } from "lodash";
-import { makeStyles } from "@material-ui/styles";
-import {
-  Typography,
-  TableContainer,
-  TableCell,
-  TableHead,
-  TablePagination,
-  TableRow,
-  TableSortLabel,
-  Table,
-  TableBody,
-} from "@material-ui/core";
-
-import { toCurrency } from "../util";
-import CloudCostChart from "./cloudCostChart";
-import { CloudCostRow } from "./cloudCostRow";
-
-const CloudCost = ({
-  cumulativeData = [],
-  totalData: totalsRow = {},
-  graphData = [],
-  currency = "USD",
-  drilldown,
-  sampleData = false,
-}) => {
-  const useStyles = makeStyles({
-    noResults: {
-      padding: 24,
-    },
-  });
-
- 
-
-  const classes = useStyles();
-
-  function descendingComparator(a, b, orderBy) {
-    if (get(b, orderBy) < get(a, orderBy)) {
-      return -1;
-    }
-    if (get(b, orderBy) > get(a, orderBy)) {
-      return 1;
-    }
-    return 0;
-  }
-
-  function getComparator(order, orderBy) {
-    return order === "desc"
-      ? (a, b) => descendingComparator(a, b, orderBy)
-      : (a, b) => -descendingComparator(a, b, orderBy);
-  }
-
-  function stableSort(array, comparator) {
-    const stabilizedThis = array.map((el, index) => [el, index]);
-    stabilizedThis.sort((a, b) => {
-      const order = comparator(a[0], b[0]);
-      if (order !== 0) return order;
-      return a[1] - b[1];
-    });
-    return stabilizedThis.map((el) => el[0]);
-  }
-
-  const headCells = [
-    {
-      id: "name",
-      numeric: false,
-      label: "Name",
-      width: "auto",
-    },
-    {
-      id: "kubernetesPercent",
-      numeric: true,
-      label: "K8s Utilization",
-      width: 160,
-    },
-    sampleData
-      ? {
-          id: "cost",
-          numeric: true,
-          label: "Sum of Sample Data",
-          width: 200,
-        }
-      : {
-          id: "cost",
-          numeric: true,
-          label: "Total cost",
-          width: 155,
-        },
-  ];
-
-  const [order, setOrder] = React.useState("desc");
-  const [orderBy, setOrderBy] = React.useState("totalCost");
-  const [page, setPage] = React.useState(0);
-  const [rowsPerPage, setRowsPerPage] = React.useState(25);
-  const numData = cumulativeData?.length;
-
-  const lastPage = Math.floor(numData / rowsPerPage);
-
-  const handleChangePage = (event, newPage) => setPage(newPage);
-
-  const handleChangeRowsPerPage = (event) => {
-    setRowsPerPage(parseInt(event.target.value, 10));
-    setPage(0);
-  };
-
-  const orderedRows = stableSort(cumulativeData, getComparator(order, orderBy));
-  const pageRows = orderedRows.slice(
-    page * rowsPerPage,
-    page * rowsPerPage + rowsPerPage
-  );
-
-  React.useEffect(() => {
-    setPage(0);
-  }, [numData]);
-
-  if (cumulativeData.length === 0) {
-    return (
-      <Typography variant="body2" className={classes.noResults}>
-        No results
-      </Typography>
-    );
-  }
-
-  function dataToCloudCostRow(row) {
-    const suffix =
-      { hourly: "/hr", monthly: "/mo", daily: "/day" }["cumulative"] || "";
-    return (
-      <CloudCostRow
-        costSuffix={suffix}
-        cost={row.cost}
-        drilldown={drilldown}
-        key={row.name}
-        kubernetesPercent={row.kubernetesPercent}
-        name={
-          sampleData && row.labelName ? row.labelName ?? "" : row.name ?? ""
-        }
-        row={row}
-        sampleData={sampleData}
-      />
-    );
-  }
-
-  return (
-    <div id="cloud-cost">
-      <div id="cloud-graph-">
-        <CloudCostChart
-          currency={currency}
-          graphData={graphData}
-          height={300}
-          n={10}
-        />
-      </div>
-      <div id="cloud-cost-table">
-        <TableContainer>
-          <Table>
-            <TableHead>
-              <TableRow>
-                {headCells.map((cell) => (
-                  <TableCell
-                    key={cell.id}
-                    colSpan={cell.colspan}
-                    align={cell.numeric ? "right" : "left"}
-                    sortDirection={orderBy === cell.id ? order : false}
-                    style={{ width: cell.width }}
-                  >
-                    <TableSortLabel
-                      active={orderBy === cell.id}
-                      direction={orderBy === cell.id ? order : "asc"}
-                      onClick={() => {
-                        const isDesc = orderBy === cell.id && order === "desc";
-                        setOrder(isDesc ? "asc" : "desc");
-                        setOrderBy(cell.id);
-                      }}
-                    >
-                      {cell.label}
-                    </TableSortLabel>
-                  </TableCell>
-                ))}
-              </TableRow>
-            </TableHead>
-            <TableBody>
-              <TableRow>
-                <TableCell align={"left"} style={{ fontWeight: 500 }}>
-                  {totalsRow?.name || "Totals"}
-                </TableCell>
-
-                <TableCell align={"right"} style={{ fontWeight: 500 }}>
-                  {Math.round(totalsRow?.kubernetesPercent * 100)}%
-                </TableCell>
-
-                <TableCell
-                  align={"right"}
-                  style={{ fontWeight: 500, paddingRight: "2em" }}
-                >
-                  {toCurrency(totalsRow?.cost || 0, currency)}
-                </TableCell>
-              </TableRow>
-              {pageRows.map(dataToCloudCostRow)}
-            </TableBody>
-          </Table>
-        </TableContainer>
-        <TablePagination
-          component="div"
-          count={numData}
-          rowsPerPage={rowsPerPage}
-          rowsPerPageOptions={[10, 25, 50]}
-          page={Math.min(page, lastPage)}
-          onChangePage={handleChangePage}
-          onChangeRowsPerPage={handleChangeRowsPerPage}
-        />
-      </div>
-    </div>
-  );
-};
-
-export default React.memo(CloudCost);

+ 0 - 14
ui/src/cloudCost/cloudCostChart/index.js

@@ -1,14 +0,0 @@
-import * as React from "react";
-
-import Typography from "@material-ui/core/Typography";
-
-import RangeChart from "./rangeChart";
-
-const CloudCostChart = ({ graphData, currency, n, height }) => {
-  if (graphData.length === 0) {
-    return <Typography variant="body2">No data</Typography>;
-  }
-  return <RangeChart data={graphData} currency={currency} height={height} />;
-};
-
-export default React.memo(CloudCostChart);

+ 0 - 275
ui/src/cloudCost/cloudCostChart/rangeChart.js

@@ -1,275 +0,0 @@
-import * as React from "react";
-import { makeStyles } from "@material-ui/styles";
-import {
-  BarChart,
-  Bar,
-  XAxis,
-  YAxis,
-  CartesianGrid,
-  Tooltip,
-  ResponsiveContainer,
-  Cell,
-} from "recharts";
-import { primary, greyscale, browns } from "../../constants/colors";
-import { toCurrency } from "../../util";
-
-const RangeChart = ({ data, currency, height }) => {
-  const useStyles = makeStyles({
-    tooltip: {
-      borderRadius: 2,
-      background: "rgba(255, 255, 255, 0.95)",
-      padding: 12,
-    },
-    tooltipLineItem: {
-      fontSize: "1rem",
-      margin: 0,
-      marginBottom: 4,
-      padding: 0,
-    },
-  });
-
-  const accents = [...primary, ...greyscale, ...browns];
-
-  const _IDLE_ = "__idle__";
-  const _OTHER_ = "others";
-
-  const getItemCost = (item) => {
-    return item.value;
-  };
-
-  function toBar({ end, graph, start }) {
-    const points = graph.map((item) => ({
-      ...item,
-      window: { end, start },
-    }));
-
-    const dateFormatter = Intl.DateTimeFormat(navigator.language, {
-      year: "numeric",
-      month: "numeric",
-      day: "numeric",
-      timeZone: "UTC",
-    });
-
-    const timeFormatter = Intl.DateTimeFormat(navigator.language, {
-      hour: "numeric",
-      minute: "numeric",
-      timeZone: "UTC",
-    });
-
-    const s = new Date(start);
-    const e = new Date(end);
-    const interval = (e.valueOf() - s.valueOf()) / 1000 / 60 / 60;
-
-    const bar = {
-      end: new Date(end),
-      key: interval >= 24 ? dateFormatter.format(s) : timeFormatter.format(s),
-      items: {},
-      start: new Date(start),
-    };
-
-    points.forEach((item) => {
-      const windowStart = new Date(item.window.start);
-      const windowEnd = new Date(item.window.end);
-      const windowHours =
-        (windowEnd.valueOf() - windowStart.valueOf()) / 1000 / 60 / 60;
-
-      if (windowHours >= 24) {
-        bar.key = dateFormatter.format(bar.start);
-      } else {
-        bar.key = timeFormatter.format(bar.start);
-      }
-
-      bar.items[item.name] = getItemCost(item);
-    });
-
-    return bar;
-  }
-
-  const getDataForCloudDay = (dayData) => {
-    const { end, start } = dayData;
-    const copy = [...dayData.items];
-
-    // find items for idle and other
-    const idleIndex = copy.findIndex((item) => item.name === _IDLE_);
-    let idle = undefined;
-    if (idleIndex > -1) {
-      idle = copy[idleIndex];
-      copy.splice(idleIndex, 1);
-    }
-    const otherIndex = copy.findIndex(
-      (i) => i.name === _OTHER_ || i.name === "other"
-    );
-    let other = undefined;
-    if (otherIndex > -1) {
-      other = { ...copy[otherIndex], name: "other" };
-      copy.splice(otherIndex, 1);
-    }
-
-    // sort and remove any items < top 8
-    const sortedItems = copy.slice().sort((a, b) => {
-      return a.value > b.value ? -1 : 1;
-    });
-
-    const top8 = sortedItems.slice(0, 8);
-    // get items that didn't make the cut and shove into other
-    const lefovers = sortedItems.slice(8);
-    if (lefovers.length > 0) {
-      const othersTotal = lefovers.reduce((a, b) => a.value + b.value);
-      if (other) {
-        other.value += othersTotal;
-      } else if (othersTotal) {
-        other = {
-          name: "other",
-          value: othersTotal,
-        };
-      }
-    }
-    // add in idle and other
-    if (idle) {
-      top8.unshift(idle);
-    }
-    if (other) {
-      top8.unshift(other);
-    }
-
-    return { end, start, graph: top8 };
-  };
-
-  const getDataForGraph = (dataPoints) => {
-    // for each day, we want top 8 + Idle and Other
-    const orderedDataPoints = dataPoints.map(getDataForCloudDay);
-    const bars = orderedDataPoints.map(toBar);
-
-    const keyToFill = {};
-    // we want to keep track of the order of fill assignment
-    const assignmentOrder = [];
-    let p = 0;
-
-    orderedDataPoints.forEach(({ graph, start, end }) => {
-      graph.forEach(({ name }) => {
-        const key = name;
-        if (keyToFill[key] === undefined) {
-          assignmentOrder.push(key);
-          if (key === _IDLE_) {
-            keyToFill[key] = browns;
-          } else if (key === _OTHER_ || key === "other") {
-            keyToFill[key] = greyscale;
-          } else {
-            // non-idle/other allocations get the next available color
-            keyToFill[key] = accents[p];
-            p = (p + 1) % accents.length;
-          }
-        }
-      });
-    });
-    // list of dataKeys and fillColors in order of importance (price w/ 'others' last)
-    const labels = assignmentOrder.map((dataKey) => ({
-      dataKey,
-      fill: keyToFill[dataKey],
-    }));
-
-    return { bars, labels, keyToFill };
-  };
-
-  const { bars: barData, labels: barLabels, keyToFill } = getDataForGraph(data);
-
-  const classes = useStyles();
-
-  const CustomTooltip = (params) => {
-    const { active, payload } = params;
-
-    if (!payload || payload.length == 0) {
-      return null;
-    }
-
-    const total = payload.reduce((sum, item) => sum + item.value, 0.0);
-    if (active) {
-      return (
-        <div className={classes.tooltip}>
-          <p
-            className={classes.tooltipLineItem}
-            style={{ color: "#000000" }}
-          >{`Total: ${toCurrency(total, currency)}`}</p>
-
-          {payload
-            .slice()
-            .map((item, i) => (
-              <div
-                key={item.name}
-                style={{
-                  display: "grid",
-                  gridTemplateColumns: "20px 1fr",
-                  gap: ".5em",
-                  margin: ".25em",
-                }}
-              >
-                <div>
-                  <div
-                    style={{
-                      backgroundColor: keyToFill[item.payload.items[i][0]],
-                      width: 18,
-                      height: 18,
-                    }}
-                  />
-                </div>
-                <div>
-                  <p className={classes.tooltipLineItem}>{`${
-                    item.payload.items[i][0]
-                  }: ${toCurrency(item.value, currency)}`}</p>
-                </div>
-              </div>
-            ))
-            .reverse()}
-        </div>
-      );
-    }
-
-    return null;
-  };
-
-  const orderedBars = barData.map((bar) => {
-    return {
-      ...bar,
-      items: Object.entries(bar.items).sort((a, b) => {
-        if (a[0] === "other") {
-          return -1;
-        }
-        if (b[0] === "other") {
-          return 1;
-        }
-        return a[1] > b[1] ? -1 : 1;
-      }),
-    };
-  });
-
-  return (
-    <ResponsiveContainer height={height} width={"100%"}>
-      <BarChart
-        data={orderedBars}
-        margin={{ top: 30, right: 35, left: 30, bottom: 45 }}
-      >
-        <CartesianGrid strokeDasharray={"3 3"} vertical={false} />
-        <XAxis dataKey={"key"} />
-        <YAxis tickFormatter={(val) => toCurrency(val, currency, 2, true)} />
-        <Tooltip content={<CustomTooltip />} wrapperStyle={{ zIndex: 1000 }} />
-
-        {new Array(10).fill(0).map((item, idx) => (
-          <Bar
-            dataKey={(entry) => (entry.items[idx] ? entry.items[idx][1] : null)}
-            stackId="x"
-          >
-            {orderedBars.map((bar) =>
-              bar.items[idx] ? (
-                <Cell fill={keyToFill[bar.items[idx][0]]} />
-              ) : (
-                <Cell />
-              )
-            )}
-          </Bar>
-        ))}
-      </BarChart>
-    </ResponsiveContainer>
-  );
-};
-
-export default RangeChart;

+ 0 - 178
ui/src/cloudCost/cloudCostDetails.js

@@ -1,178 +0,0 @@
-import * as React from "react";
-import { Modal, Paper, Typography } from "@material-ui/core";
-import Warnings from "../components/Warnings";
-import CircularProgress from "@material-ui/core/CircularProgress";
-
-import {
-  ResponsiveContainer,
-  CartesianGrid,
-  Legend,
-  XAxis,
-  YAxis,
-  Tooltip,
-  BarChart,
-  Bar,
-} from "recharts";
-import { toCurrency } from "../util";
-import cloudCostDayTotals from "../services/cloudCostDayTotals";
-
-const CloudCostDetails = ({
-  onClose,
-  selectedProviderId,
-  selectedItem,
-  agg,
-  filters,
-  costMetric,
-  window,
-  currency,
-}) => {
-  const [data, setData] = React.useState([]);
-  const [loading, setLoading] = React.useState(false);
-  const [errors, setErrors] = React.useState([]);
-  const [fetch, setFetch] = React.useState(true);
-
-  const nextFilters = [
-    ...(filters ?? []),
-    { property: "providerID", value: selectedProviderId },
-  ];
-
-  async function fetchData() {
-    setLoading(true);
-    setErrors([]);
-
-    try {
-      const resp = await cloudCostDayTotals.fetchCloudCostData(
-        window,
-        agg,
-        costMetric,
-        nextFilters
-      );
-
-      if (resp.data) {
-        setData(resp.data);
-      } else {
-        if (resp.message && resp.message.indexOf("boundary error") >= 0) {
-          let match = resp.message.match(/(ETL is \d+\.\d+% complete)/);
-          let secondary = "Try again after ETL build is complete";
-          if (match.length > 0) {
-            secondary = `${match[1]}. ${secondary}`;
-          }
-          setErrors([
-            {
-              primary: "Data unavailable while ETL is building",
-              secondary: secondary,
-            },
-          ]);
-        }
-        setData([]);
-      }
-    } catch (err) {
-      console.log(err);
-      if (err.message.indexOf("404") === 0) {
-        setErrors([
-          {
-            primary: "Failed to load report data",
-            secondary:
-              "Please update OpenCost to the latest version, then open an Issue on GitHub if problems persist.",
-          },
-        ]);
-      } else {
-        let secondary =
-          "Please open an Issue on GitHub if problems persist.";
-        if (err.message.length > 0) {
-          secondary = err.message;
-        }
-        setErrors([
-          {
-            primary: "Failed to load report data",
-            secondary: secondary,
-          },
-        ]);
-      }
-      setData([]);
-    }
-    setLoading(false);
-    setFetch(false);
-  }
-
-  React.useEffect(() => {
-    if (fetch) {
-      fetchData();
-    }
-  }, [fetch]);
-
-  const drilldownData = data.sort(
-    (a, b) =>
-      new Date(a.date ?? "").getTime() - new Date(b.date ?? "").getTime()
-  );
-
-  const itemData = drilldownData.map((items) => {
-    const dataPoint = {
-      time: new Date(items.date),
-      cost: items.cost,
-    };
-    return dataPoint;
-  });
-
-  return (
-    <div>
-      <Modal
-        open={true}
-        onClose={onClose}
-        title={`Costs over the last ${window}`}
-        style={{ margin: "10%" }}
-      >
-        <Paper style={{ padding: 20 }}>
-          <Typography style={{ marginTop: "1rem" }} variant="body1">
-            {selectedItem}
-          </Typography>
-
-          {loading && (
-            <div style={{ display: "flex", justifyContent: "center" }}>
-              <div style={{ paddingTop: 100, paddingBottom: 100 }}>
-                <CircularProgress />
-              </div>
-            </div>
-          )}
-          {!loading && errors.length > 0 && (
-            <div style={{ marginBottom: 20 }}>
-              <Warnings warnings={errors} />
-            </div>
-          )}
-          {data && (
-            <div style={{ display: "flex", marginTop: "2.5rem" }}>
-              <ResponsiveContainer
-                height={250}
-                id={"cloud-cost-drilldown"}
-                width={"100%"}
-              >
-                <BarChart
-                  data={itemData}
-                  margin={{
-                    top: 0,
-                    bottom: 10,
-                    left: 20,
-                    right: 0,
-                  }}
-                >
-                  <CartesianGrid vertical={false} />
-                  <Legend verticalAlign={"bottom"} />
-                  <XAxis dataKey={"time"} />
-                  <YAxis tickFormatter={(tick) => `${toCurrency(tick)}`} />
-                  <Bar dataKey={"cost"} fill={"#2196f3"} name={"Item Cost"} />
-                  <Tooltip
-                    formatter={(value) =>
-                      `${toCurrency(value ?? 0, currency, 4, true)}`
-                    }
-                  />
-                </BarChart>
-              </ResponsiveContainer>
-            </div>
-          )}
-        </Paper>
-      </Modal>
-    </div>
-  );
-};
-
-export { CloudCostDetails };

+ 0 - 48
ui/src/cloudCost/cloudCostRow.js

@@ -1,48 +0,0 @@
-import * as React from "react";
-
-import { TableCell, TableRow } from "@material-ui/core";
-
-import { toCurrency } from "../util";
-import { primary } from "../constants/colors";
-
-const displayCurrencyAsLessThanPenny = (amount, currency) =>
-  amount > 0 && amount < 0.01
-    ? `<${toCurrency(0.01, currency)}`
-    : toCurrency(amount, currency);
-
-const CloudCostRow = ({
-  cost,
-  costSuffix,
-  currency,
-  drilldown,
-  kubernetesPercent,
-  name,
-  row,
-  sampleData,
-}) => {
-  function calculatePercent() {
-    const totalPercent = (kubernetesPercent * 100).toFixed();
-    return `${totalPercent}%`;
-  }
-
-  const whichPercent = sampleData
-    ? `${(kubernetesPercent * 100).toFixed(1)}%`
-    : calculatePercent();
-  return (
-    <TableRow onClick={() => drilldown(row)}>
-      <TableCell
-        align={"left"}
-        style={{ cursor: "pointer", color: "#346ef2", padding: "1rem" }}
-      >
-        {name}
-      </TableCell>
-      <TableCell align={"right"}>{whichPercent}</TableCell>
-      {/* total cost */}
-      <TableCell align={"right"} style={{ paddingRight: "2em" }}>
-        {`${displayCurrencyAsLessThanPenny(cost, currency)}${costSuffix}`}
-      </TableCell>
-    </TableRow>
-  );
-};
-
-export { CloudCostRow };

+ 0 - 91
ui/src/cloudCost/controls/cloudCostEditControls.js

@@ -1,91 +0,0 @@
-import { makeStyles } from "@material-ui/styles";
-import FormControl from "@material-ui/core/FormControl";
-import InputLabel from "@material-ui/core/InputLabel";
-import MenuItem from "@material-ui/core/MenuItem";
-import Select from "@material-ui/core/Select";
-
-import * as React from "react";
-
-import SelectWindow from "../../components/SelectWindow";
-
-const useStyles = makeStyles({
-  wrapper: {
-    display: "inline-flex",
-  },
-  formControl: {
-    margin: 8,
-    minWidth: 120,
-  },
-});
-
-function EditCloudCostControls({
-  windowOptions,
-  window,
-  setWindow,
-  aggregationOptions,
-  aggregateBy,
-  setAggregateBy,
-  costMetricOptions,
-  costMetric,
-  setCostMetric,
-  currencyOptions,
-  currency,
-  setCurrency,
-}) {
-  const classes = useStyles();
-  return (
-    <div className={classes.wrapper}>
-      <SelectWindow
-        windowOptions={windowOptions}
-        window={window}
-        setWindow={setWindow}
-      />
-      <FormControl className={classes.formControl}>
-        <InputLabel id="aggregation-select-label">Breakdown</InputLabel>
-        <Select
-          id="aggregation-select"
-          value={aggregateBy}
-          onChange={(e) => {
-            setAggregateBy(e.target.value);
-          }}
-        >
-          {aggregationOptions.map((opt) => (
-            <MenuItem key={opt.value} value={opt.value}>
-              {opt.name}
-            </MenuItem>
-          ))}
-        </Select>
-      </FormControl>
-      <FormControl className={classes.formControl}>
-        <InputLabel id="costMetric-label">Cost Metric</InputLabel>
-        <Select
-          id="costMetric"
-          value={costMetric}
-          onChange={(e) => setCostMetric(e.target.value)}
-        >
-          {costMetricOptions.map((opt) => (
-            <MenuItem key={opt.value} value={opt.value}>
-              {opt.name}
-            </MenuItem>
-          ))}
-        </Select>
-      </FormControl>
-      <FormControl className={classes.formControl}>
-        <InputLabel id="currency-label">Currency</InputLabel>
-        <Select
-          id="currency"
-          value={currency}
-          onChange={(e) => setCurrency(e.target.value)}
-        >
-          {currencyOptions?.map((currency) => (
-            <MenuItem key={currency} value={currency}>
-              {currency}
-            </MenuItem>
-          ))}
-        </Select>
-      </FormControl>
-    </div>
-  );
-}
-
-export default React.memo(EditCloudCostControls);

+ 0 - 49
ui/src/cloudCost/tokens.js

@@ -1,49 +0,0 @@
-const windowOptions = [
-  { name: "Today", value: "today" },
-  { name: "Yesterday", value: "yesterday" },
-  { name: "Last 24h", value: "24h" },
-  { name: "Last 48h", value: "48h" },
-  { name: "Week-to-date", value: "week" },
-  { name: "Last week", value: "lastweek" },
-  { name: "Last 7 days", value: "7d" },
-  { name: "Last 14 days", value: "14d" },
-];
-
-const aggregationOptions = [
-  { name: "Account", value: "accountID" },
-  { name: "Invoice Entity", value: "invoiceEntityID" },
-  { name: "Provider", value: "provider" },
-  { name: "Service ", value: "service" },
-  { name: "Category", value: "category" },
-  { name: "Item", value: "item" },
-];
-
-const costMetricOptions = [
-  { name: "Amortized Net Cost", value: "AmortizedNetCost" },
-  { name: "List Cost", value: "ListCost" },
-  { name: "Invoiced Cost", value: "InvoicedCost" },
-  { name: "Amortized Cost", value: "AmortizedCost" },
-];
-
-const aggMap = {
-  invoiceEntityID: "Invoice Entity",
-  provider: "Provider",
-  service: "Service",
-  accountID: "Account",
-};
-
-const costMetricToPropName = {
-  AmortizedNetCost: "amortizedNetCost",
-  AmortizedCost: "amortizedCost",
-  ListCost: "listCost",
-  NetCost: "netCost",
-  InvoicedCost: "invoicedCost",
-};
-
-export {
-  windowOptions,
-  aggregationOptions,
-  costMetricOptions,
-  aggMap,
-  costMetricToPropName,
-};

+ 0 - 337
ui/src/cloudCostReports.js

@@ -1,337 +0,0 @@
-import * as React from "react";
-import Page from "./components/Page";
-import Header from "./components/Header";
-import Footer from "./components/Footer";
-import IconButton from "@material-ui/core/IconButton";
-import RefreshIcon from "@material-ui/icons/Refresh";
-import { makeStyles } from "@material-ui/styles";
-import { Box, Link, Paper, Typography } from "@material-ui/core";
-import CircularProgress from "@material-ui/core/CircularProgress";
-import { get, find } from "lodash";
-import { useLocation, useHistory } from "react-router";
-
-import { checkCustomWindow, toVerboseTimeRange } from "./util";
-import CloudCostEditControls from "./cloudCost/controls/cloudCostEditControls";
-import Subtitle from "./components/Subtitle";
-import Warnings from "./components/Warnings";
-import CloudCostTopService from "./services/cloudCostTop";
-
-import {
-  windowOptions,
-  costMetricOptions,
-  aggregationOptions,
-  aggMap,
-} from "./cloudCost/tokens";
-import { currencyCodes } from "./constants/currencyCodes";
-import CloudCost from "./cloudCost/cloudCost";
-import { CloudCostDetails } from "./cloudCost/cloudCostDetails";
-
-const CloudCostReports = () => {
-  const useStyles = makeStyles({
-    reportHeader: {
-      display: "flex",
-      flexFlow: "row",
-      padding: 24,
-    },
-    titles: {
-      flexGrow: 1,
-    },
-  });
-  const classes = useStyles();
-
-  // Form state, which controls form elements, but not the report itself. On
-  // certain actions, the form state may flow into the report state.
-  const [title, setTitle] = React.useState(
-    "Cumulative cost for last 7 days by account"
-  );
-  const [window, setWindow] = React.useState(windowOptions[0].value);
-  const [aggregateBy, setAggregateBy] = React.useState(
-    aggregationOptions[0].value
-  );
-  const [costMetric, setCostMetric] = React.useState(
-    costMetricOptions[0].value
-  );
-  const [filters, setFilters] = React.useState([]);
-  const [currency, setCurrency] = React.useState("USD");
-  const [selectedProviderId, setSelectedProviderId] = React.useState("");
-  const [selectedItemName, setselectedItemName] = React.useState("");
-  const sampleData = aggregateBy.includes("item");
-  // page and settings state
-  const [init, setInit] = React.useState(false);
-  const [fetch, setFetch] = React.useState(false);
-  const [loading, setLoading] = React.useState(true);
-  const [errors, setErrors] = React.useState([]);
-
-  // data
-  const [cloudCostData, setCloudCostData] = React.useState([]);
-
-  function generateTitle({ window, aggregateBy, costMetric }) {
-    let windowName = get(find(windowOptions, { value: window }), "name", "");
-    if (windowName === "") {
-      if (checkCustomWindow(window)) {
-        windowName = toVerboseTimeRange(window);
-      } else {
-        console.warn(`unknown window: ${window}`);
-      }
-    }
-
-    let aggregationName = get(
-      find(aggregationOptions, { value: aggregateBy }),
-      "name",
-      ""
-    ).toLowerCase();
-    if (aggregationName === "") {
-      console.warn(`unknown aggregation: ${aggregateBy}`);
-    }
-
-    let str = `Cumulative cost for ${windowName} by ${aggregationName}`;
-
-    if (!costMetric) {
-      str = `${str} amoritizedNetCost`;
-    }
-
-    return str;
-  }
-
-  // parse any context information from the URL
-  const routerLocation = useLocation();
-  const searchParams = new URLSearchParams(routerLocation.search);
-  const routerHistory = useHistory();
-
-  async function initialize() {
-    setInit(true);
-  }
-
-  async function fetchData() {
-    setLoading(true);
-    setErrors([]);
-    try {
-      const resp = await CloudCostTopService.fetchCloudCostData(
-        window,
-        aggregateBy,
-        costMetric,
-        filters
-      );
-      if (resp) {
-        setCloudCostData(resp);
-      } else {
-        if (resp.message && resp.message.indexOf("boundary error") >= 0) {
-          let match = resp.message.match(/(ETL is \d+\.\d+% complete)/);
-          let secondary = "Try again after ETL build is complete";
-          if (match.length > 0) {
-            secondary = `${match[1]}. ${secondary}`;
-          }
-          setErrors([
-            {
-              primary: "Data unavailable while ETL is building",
-              secondary: secondary,
-            },
-          ]);
-        }
-        setCloudCostData([]);
-      }
-    } catch (err) {
-      if (err.message.indexOf("404") === 0) {
-        setErrors([
-          {
-            primary: "Failed to load report data",
-            secondary:
-            "Please update OpenCost to the latest version, and open an Issue if problems persist.",
-          },
-        ]);
-      } else {
-        let secondary =
-          "Please open an Issue with OpenCost if problems persist.";
-        if (err.message.length > 0) {
-          secondary = err.message;
-        }
-        setErrors([
-          {
-            primary: "Failed to load report data",
-            secondary: secondary,
-          },
-        ]);
-      }
-      setCloudCostData([]);
-    }
-    setLoading(false);
-  }
-
-  function drilldown(row) {
-    if (aggregateBy.includes("item")) {
-      try {
-        setSelectedProviderId(row.providerID);
-        setselectedItemName(row.labelName ?? row.name);
-      } catch (e) {
-        logger.error(e);
-      }
-
-      return;
-    }
-    const nameParts = row.name.split("/");
-    const nextAgg = aggregateBy.includes("service") ? "item" : "service";
-    const aggToString = [aggregateBy];
-    const newFilters = aggToString.map((property, i) => {
-      const value = nameParts[i];
-      return {
-        property,
-        value,
-      };
-    });
-    setFilters(newFilters);
-    setAggregateBy(nextAgg);
-  }
-
-  React.useEffect(() => {
-    setWindow(searchParams.get("window") || "7d");
-    setAggregateBy(searchParams.get("agg") || "provider");
-    setCostMetric(searchParams.get("costMetric") || "AmortizedNetCost");
-    setCurrency(searchParams.get("currency") || "USD");
-  }, [routerLocation]);
-
-  // Initialize once, then fetch report each time setFetch(true) is called
-  React.useEffect(() => {
-    if (!init) {
-      initialize();
-    }
-    if (init || fetch) {
-      fetchData();
-    }
-  }, [init, fetch]);
-
-  React.useEffect(() => {
-    setFetch(!fetch);
-    setTitle(generateTitle({ window, aggregateBy, costMetric }));
-  }, [window, aggregateBy, costMetric, filters]);
-
-  const hasCloudCostEnabled = aggregateBy.includes("item")
-    ? true // this is kind of hacky but something weird is happening
-    : // when drilling down will address in a later PR - @jjarrett21
-      !!cloudCostData.cloudCostStatus?.length;
-
-  const enabledWarnings = [
-    {
-      primary: "There are no Cloud Cost integrations currently configured.",
-      secondary: (
-        <>
-          Learn more about setting up Cloud Costs{" "}
-          <Link
-            href={
-              "https://www.opencost.io/docs/configuration/#cloud-costs"
-            }
-            target="_blank"
-          >
-            here
-          </Link>
-        </>
-      ),
-    },
-  ];
-
-  return (
-    <Page active="cloud.html">
-      <Header>
-        <IconButton aria-label="refresh" onClick={() => setFetch(true)}>
-          <RefreshIcon />
-        </IconButton>
-      </Header>
-
-      {!loading && !hasCloudCostEnabled && (
-        <div style={{ marginBottom: 20 }}>
-          <Warnings warnings={enabledWarnings} />
-        </div>
-      )}
-
-      {!loading && errors.length > 0 && hasCloudCostEnabled && (
-        <div style={{ marginBottom: 20 }}>
-          <Warnings warnings={errors} />
-        </div>
-      )}
-
-      {init && hasCloudCostEnabled && (
-        <Paper id="cloud-cost">
-          <div className={classes.reportHeader}>
-            <div className={classes.titles}>
-              <Typography variant="h5">{title}</Typography>
-              <Subtitle report={{ window, aggregateBy }} />
-            </div>
-            <CloudCostEditControls
-              windowOptions={windowOptions}
-              window={window}
-              setWindow={(win) => {
-                searchParams.set("window", win);
-                routerHistory.push({
-                  search: `?${searchParams.toString()}`,
-                });
-              }}
-              aggregationOptions={aggregationOptions}
-              aggregateBy={aggregateBy}
-              setAggregateBy={(agg) => {
-                setFilters([])
-                searchParams.set("agg", agg);
-                routerHistory.push({
-                  search: `?${searchParams.toString()}`,
-                });
-              }}
-              costMetricOptions={costMetricOptions}
-              costMetric={costMetric}
-              setCostMetric={(c) => {
-                searchParams.set("costMetric", c);
-                routerHistory.push({
-                  search: `?${searchParams.toString()}`,
-                });
-              }}
-              title={title}
-              // cumulativeData={cumulativeData}
-              currency={currency}
-              currencyOptions={currencyCodes}
-              setCurrency={(curr) => {
-                searchParams.set("currency", curr);
-                routerHistory.push({
-                  search: `?${searchParams.toString()}`,
-                });
-              }}
-            />
-          </div>
-
-          {loading && (
-            <div style={{ display: "flex", justifyContent: "center" }}>
-              <div style={{ paddingTop: 100, paddingBottom: 100 }}>
-                <CircularProgress />
-              </div>
-            </div>
-          )}
-
-          {!loading && (
-            <CloudCost
-              cumulativeData={cloudCostData.tableRows}
-              currency={currency}
-              graphData={cloudCostData.graphData}
-              totalData={cloudCostData.tableTotal}
-              drilldown={drilldown}
-              sampleData={sampleData}
-            />
-          )}
-          {selectedProviderId && selectedItemName && (
-            <CloudCostDetails
-              onClose={() => {
-                setSelectedProviderId("");
-                setselectedItemName("");
-              }}
-              selectedProviderId={selectedProviderId}
-              selectedItem={selectedItemName}
-              agg={aggregateBy}
-              filters={filters}
-              costMetric={costMetric}
-              window={window}
-              currency={currency}
-            />
-          )}
-        </Paper>
-      )}
-      <Footer/>
-    </Page>
-  );
-};
-
-export default React.memo(CloudCostReports);

+ 0 - 149
ui/src/components/AllocationChart/RangeChart.js

@@ -1,149 +0,0 @@
-import React from 'react'
-import { reverse } from 'lodash'
-import { makeStyles } from '@material-ui/styles'
-import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
-import { primary, greyscale, browns } from '../../constants/colors';
-import { toCurrency } from '../../util';
-
-const useStyles = makeStyles({
-  tooltip: {
-    borderRadius: 2,
-    background: 'rgba(255, 255, 255, 0.95)',
-    padding: 12,
-  },
-  tooltipLineItem: {
-    fontSize: '1rem',
-    margin: 0,
-    marginBottom: 4,
-    padding: 0,
-  },
-})
-
-function toBarLabels(allocationRange) {
-  let keyToFill = {}
-  let p = 0
-  let g = 0
-  let b = 0
-
-  for (const { idle } of allocationRange) {
-    for (const allocation of idle) {
-      const key = allocation.name
-      if (keyToFill[key] === undefined) {
-        // idle allocations are assigned grey
-        keyToFill[key] = greyscale[g]
-        g = (g+1) % greyscale.length
-      }
-    }
-  }
-
-  for (const { top } of allocationRange) {
-    for (const allocation of top) {
-      const key = allocation.name
-      if (keyToFill[key] === undefined) {
-        if (key === "__unallocated__") {
-          // unallocated gets black (clean up)
-          keyToFill[key] = "#212121"
-        } else {
-          // non-idle allocations get the next available color
-          keyToFill[key] = primary[p]
-          p = (p+1) % primary.length
-        }
-      }
-    }
-  }
-
-  for (const { other } of allocationRange) {
-    for (const allocation of other) {
-      const key = allocation.name
-      if (keyToFill[key] === undefined) {
-        // idle allocations are assigned grey
-        keyToFill[key] = browns[b]
-        b = (b+1) % browns.length
-      }
-    }
-  }
-
-  let labels = []
-  for (const key in keyToFill) {
-    labels.push({
-      dataKey: key,
-      fill: keyToFill[key],
-    })
-  }
-
-  return reverse(labels)
-}
-
-function toBar(datum) {
-  const { top, other, idle } = datum
-  const bar = {}
-
-  for (const key in top) {
-    const allocation = top[key]
-    const start = new Date(allocation.start)
-    bar.start = `${start.getUTCFullYear()}-${start.getUTCMonth()+1}-${start.getUTCDate()}`
-    bar[allocation.name] = allocation.totalCost
-  }
-
-  for (const key in other) {
-    const allocation = other[key]
-    const start = new Date(allocation.start)
-    bar.start = `${start.getUTCFullYear()}-${start.getUTCMonth()+1}-${start.getUTCDate()}`
-    bar[allocation.name] = allocation.totalCost
-  }
-
-  for (const key in idle) {
-    const allocation = idle[key]
-    const start = new Date(allocation.start)
-    bar.start = `${start.getUTCFullYear()}-${start.getUTCMonth()+1}-${start.getUTCDate()}`
-    bar[allocation.name] = allocation.totalCost
-  }
-
-  return bar
-}
-
-const RangeChart = ({ data, currency, height }) => {
-  const classes = useStyles()
-
-  const barData = data.map(toBar)
-  const barLabels = toBarLabels(data)
-
-  const CustomTooltip = (params) => {
-    const { active, payload } = params
-
-    if (!payload || payload.length == 0) {
-      return null
-    }
-
-    const total = payload.reduce((sum, item) => sum + item.value, 0.0)
-    if (active) {
-      return (
-        <div className={classes.tooltip}>
-          <p className={classes.tooltipLineItem} style={{ color: '#000000' }}>{`Total: ${toCurrency(total, currency)}`}</p>
-          {reverse(payload).map((item, i) => (
-            <p key={i} className={classes.tooltipLineItem} style={{ color: item.fill }}>{`${item.name}: ${toCurrency(item.value, currency)}`}</p>
-          ))}
-        </div>
-      )
-    }
-
-    return null
-  }
-
-  return (
-    <ResponsiveContainer width="100%" height={height}>
-      <BarChart
-        data={barData}
-        margin={{ top: 30, right: 30, left: 30, bottom: 12 }}
-      >
-        <CartesianGrid strokeDasharray="3 3" />
-        <XAxis dataKey="start" />
-        <YAxis />
-        <Tooltip content={<CustomTooltip />} />
-        {barLabels.map((barLabel, i) => <Bar key={i} dataKey={barLabel.dataKey} stackId="a" fill={barLabel.fill} />)}
-      </BarChart>
-    </ResponsiveContainer>
-  )
-}
-
-export default RangeChart

+ 0 - 96
ui/src/components/AllocationChart/SummaryChart.js

@@ -1,96 +0,0 @@
-import React from 'react'
-import { ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'
-import { primary, greyscale, browns } from '../../constants/colors';
-import { toCurrency } from '../../util';
-
-function toPieData(top, other, idle) {
-  let slices = []
-
-  for (const i in top) {
-    const allocation = top[i]
-    const fill = allocation.name === "__unallocated__"
-      ? "#212121"
-      : primary[i % primary.length]
-
-    slices.push({
-      name: allocation.name,
-      value: allocation.totalCost,
-      fill: fill,
-    })
-  }
-
-  for (const i in other) {
-    const allocation = other[i]
-    const fill = browns[i % browns.length]
-    slices.push({
-      name: allocation.name,
-      value: allocation.totalCost,
-      fill: fill,
-    })
-  }
-
-  for (const i in idle) {
-    const allocation = idle[i]
-    const fill = greyscale[i % greyscale.length]
-    slices.push({
-      name: allocation.name,
-      value: allocation.totalCost,
-      fill: fill,
-    })
-  }
-
-  return slices
-}
-
-const SummaryChart = ({ top, other, idle, currency, height }) => {
-  const pieData = toPieData(top, other, idle)
-
-  const renderLabel = (params) => {
-    const {
-      cx, cy, midAngle, outerRadius, percent, name, fill, value
-    } = params
-
-    const RADIAN = Math.PI / 180
-    const radius = outerRadius * 1.1
-    let x = cx + radius * Math.cos(-midAngle * RADIAN)
-    x += x > cx ? 2 : -2
-    let y = cy + radius * Math.sin(-midAngle * RADIAN)
-    // y -= Math.min(Math.abs(2 / Math.cos(-midAngle * RADIAN)), 8)
-
-    if (percent < 0.02) {
-      return
-    }
-
-    return (
-      <text x={x} y={y} fill={fill} textAnchor={x > cx ? 'start' : 'end'} dominantBaseline="central">
-        {`${name}: ${toCurrency(value, currency)} (${(percent * 100).toFixed(1)}%)`}
-      </text>
-    )
-  }
-
-  return (
-    <ResponsiveContainer width="100%" height={height}>
-      <PieChart>
-        <Pie
-          data={pieData}
-          dataKey="value"
-          nameKey="name"
-          label={renderLabel}
-          labelLine
-          // niko: if tooltips error, try disabling animation
-          // isAnimationActive={false}
-          animationDuration={400}
-          cy="90%"
-          outerRadius="140%"
-          innerRadius="60%"
-          startAngle={180}
-          endAngle={0}
-        >
-          {pieData.map((datum, i) => <Cell key={i} fill={datum.fill} />)}
-        </Pie>
-      </PieChart>
-    </ResponsiveContainer>
-  )
-}
-
-export default SummaryChart

+ 0 - 81
ui/src/components/AllocationChart/index.js

@@ -1,81 +0,0 @@
-import React from 'react'
-import { isArray, filter, map, reduce, reverse, sortBy } from 'lodash'
-
-import Typography from '@material-ui/core/Typography'
-
-import RangeChart from './RangeChart'
-import SummaryChart from './SummaryChart'
-
-// TODO niko/etl
-// sum allocationSet to single allocation
-function agg(allocationSet, name) {
-  if (allocationSet.length === 0) {
-    return null
-  }
-
-  return reduce(allocationSet, (agg, cur) => ({
-    name: agg.name,
-    aggregatedBy: cur.aggregatedBy,
-    properties: agg.properties,
-    start: cur.start,
-    end: cur.end,
-    cpuCost: agg.cpuCost + cur.cpuCost,
-    gpuCost: agg.gpuCost + cur.gpuCost,
-    ramCost: agg.ramCost + cur.ramCost,
-    pvCost: agg.pvCost + cur.pvCost,
-    totalCost: agg.totalCost + cur.totalCost,
-    count: agg.count + 1
-  }), {
-    name: name,
-    properties: null,
-    cpuCost: 0.0,
-    gpuCost: 0.0,
-    ramCost: 0.0,
-    pvCost: 0.0,
-    totalCost: 0.0,
-    count: 0,
-  })
-}
-
-function isIdle(allocation) {
-  return allocation.name.indexOf('__idle__') >= 0
-}
-
-function top(n, by) {
-  return (allocations) => {
-    if (isArray(allocations[0])) {
-      return map(allocations, top(n, by))
-    }
-
-    const sorted = reverse(sortBy(allocations, by))
-    const active = filter(sorted, a => !isIdle(a))
-    const idle = filter(sorted, a => isIdle(a))
-    const topn = active.slice(0, n)
-    const other = []
-    if (active.length > n) {
-      other.push(agg(active.slice(n), 'other'))
-    }
-
-    return {
-      top: topn,
-      other: other,
-      idle: idle,
-    }
-  }
-}
-
-const AllocationChart = ({ allocationRange, currency, n, height }) => {
-  if (allocationRange.length === 0) {
-    return <Typography variant="body2">No data</Typography>
-  }
-
-  if (allocationRange.length === 1) {
-    const datum = top(n, alloc => alloc.totalCost)(allocationRange[0])
-    return <SummaryChart top={datum.top} other={datum.other} idle={datum.idle} currency={currency} height={height} />
-  }
-
-  const data = top(n, alloc => alloc.totalCost)(allocationRange)
-  return <RangeChart data={data} currency={currency} height={height} />
-}
-
-export default React.memo(AllocationChart)

+ 0 - 89
ui/src/components/Controls/Download.js

@@ -1,89 +0,0 @@
-import React from 'react'
-import { get, forEach, reverse, round, sortBy } from 'lodash'
-import ExportIcon from '@material-ui/icons/GetApp'
-import IconButton from '@material-ui/core/IconButton'
-import Tooltip from '@material-ui/core/Tooltip'
-
-const columns = [
-  {
-    head: "Name",
-    prop: "name",
-    currency: false,
-  }, {
-    head: "CPU",
-    prop: "cpuCost",
-    currency: true,
-  }, {
-    head: "GPU",
-    prop: "gpuCost",
-    currency: true,
-  }, {
-    head: "RAM",
-    prop: "ramCost",
-    currency: true,
-  }, {
-    head: "PV",
-    prop: "pvCost",
-    currency: true,
-  }, {
-    head: "Network",
-    prop: "networkCost",
-    currency: true,
-  }, {
-    head: "Shared",
-    prop: "sharedCost",
-    currency: true,
-  }, {
-    head: "Total",
-    prop: "totalCost",
-    currency: true,
-  }
-]
-
-const toCSVLine = (datum) => {
-  let cols = []
-
-  forEach(columns, c => {
-    if (c.currency) {
-      cols.push(round(get(datum, c.prop, 0.0), 2))
-    } else {
-      cols.push(`"${get(datum, c.prop, "")}"`)
-    }
-  })
-
-  return cols.join(',')
-}
-
-const DownloadControl = ({
-  cumulativeData,
-  title,
-}) => {
-  // downloadReport downloads a CSV of the cumulative allocation data
-  function downloadReport() {
-    // Build CSV
-    const head = columns.map(c => c.head).join(',')
-    const body = reverse(sortBy(cumulativeData, 'totalCost')).map(toCSVLine).join('\r\n')
-    const csv = `${head}\r\n${body}`
-
-    // Create download link
-    const a = document.createElement("a")
-    a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }))
-    const filename = title.toLowerCase().replace(/\s/gi, '-')
-    a.setAttribute("download", `${filename}-${Date.now()}.csv`)
-
-    // Click the link
-    document.body.appendChild(a)
-    a.click()
-    document.body.removeChild(a)
-  }
-
-  return (
-    <Tooltip title="Download CSV">
-      <IconButton onClick={downloadReport}>
-        <ExportIcon />
-      </IconButton>
-    </Tooltip>
-  )
-}
-
-export default React.memo(DownloadControl)

+ 0 - 74
ui/src/components/Controls/Edit.js

@@ -1,74 +0,0 @@
-import { makeStyles } from '@material-ui/styles';
-import FormControl from '@material-ui/core/FormControl'
-import InputLabel from '@material-ui/core/InputLabel'
-import MenuItem from '@material-ui/core/MenuItem'
-import Select from '@material-ui/core/Select'
-
-import React from 'react';
-
-import SelectWindow from '../SelectWindow';
-
-const useStyles = makeStyles({
-  wrapper: {
-    display: 'inline-flex',
-  },
-  formControl: {
-    margin: 8,
-    minWidth: 120,
-  },
-});
-
-function EditControl({
-  windowOptions, window, setWindow,
-  aggregationOptions, aggregateBy, setAggregateBy,
-  accumulateOptions, accumulate, setAccumulate,
-  currencyOptions, currency, setCurrency,
-}) {
-  const classes = useStyles();
-  return (
-    <div className={classes.wrapper}>
-      <SelectWindow
-        windowOptions={windowOptions}
-        window={window}
-        setWindow={setWindow} />
-      <FormControl className={classes.formControl}>
-        <InputLabel id="aggregation-select-label">Breakdown</InputLabel>
-        <Select
-          id="aggregation-select"
-          value={aggregateBy}
-          onChange={e => {
-            setAggregateBy(e.target.value)
-          }}
-        >
-          {aggregationOptions.map((opt) => <MenuItem key={opt.value} value={opt.value}>{opt.name}</MenuItem>)}
-        </Select>
-      </FormControl>
-      <FormControl className={classes.formControl}>
-        <InputLabel id="accumulate-label">Resolution</InputLabel>
-        <Select
-          id="accumulate"
-          value={accumulate}
-          onChange={e => setAccumulate(e.target.value)}
-        >
-          {accumulateOptions.map((opt) => <MenuItem key={opt.value} value={opt.value}>{opt.name}</MenuItem>)}
-        </Select>
-      </FormControl>
-      <FormControl className={classes.formControl}>
-        <InputLabel id="currency-label">Currency</InputLabel>
-        <Select
-          id="currency"
-          value={currency}
-          onChange={e => setCurrency(e.target.value)}
-        >
-          {currencyOptions?.map((currency) => (
-            <MenuItem key={currency} value={currency}>
-              {currency}
-            </MenuItem>
-          ))}
-        </Select>
-      </FormControl>
-    </div>
-  );
-}
-
-export default React.memo(EditControl);

+ 0 - 48
ui/src/components/Controls/index.js

@@ -1,48 +0,0 @@
-import React from 'react'
-import { makeStyles } from '@material-ui/styles'
-import EditControl from './Edit'
-import DownloadControl from './Download'
-
-const Controls = ({
-  windowOptions,
-  window,
-  setWindow,
-  aggregationOptions,
-  aggregateBy,
-  setAggregateBy,
-  accumulateOptions,
-  accumulate,
-  setAccumulate,
-  title,
-  cumulativeData,
-  currency,
-  currencyOptions,
-  setCurrency,
-}) => {
-
-  return (
-    <div>
-      <EditControl
-        windowOptions={windowOptions}
-        window={window}
-        setWindow={setWindow}
-        aggregationOptions={aggregationOptions}
-        aggregateBy={aggregateBy}
-        setAggregateBy={setAggregateBy}
-        accumulateOptions={accumulateOptions}
-        accumulate={accumulate}
-        setAccumulate={setAccumulate}
-        currency={currency}
-        currencyOptions={currencyOptions}
-        setCurrency={setCurrency}
-      />
-
-      <DownloadControl
-        cumulativeData={cumulativeData}
-        title={title}
-      />
-    </div>
-  )
-}
-
-export default React.memo(Controls)

+ 0 - 227
ui/src/components/Details.js

@@ -1,227 +0,0 @@
-import React, { memo, useEffect, useState } from 'react';
-import { forEach, get, reverse, round, sortBy } from 'lodash';
-import CircularProgress from '@material-ui/core/CircularProgress';
-import ClusterIcon from '@material-ui/icons/GroupWork';
-import NodeIcon from '@material-ui/icons/Memory';
-import List from '@material-ui/core/List';
-import ListItem from '@material-ui/core/ListItem';
-import ListItemIcon from '@material-ui/core/ListItemIcon';
-import ListItemText from '@material-ui/core/ListItemText';
-import Table from '@material-ui/core/Table';
-import TableBody from '@material-ui/core/TableBody';
-import TableCell from '@material-ui/core/TableCell';
-import TableContainer from '@material-ui/core/TableContainer';
-import TableHead from '@material-ui/core/TableHead';
-import TableRow from '@material-ui/core/TableRow';
-import Warnings from './Warnings';
-import AllocationService from '../services/allocation';
-import { bytesToString, toCurrency } from '../util';
-
-const Details = ({
-  window,
-  namespace,
-  controllerKind,
-  controller,
-  pod,
-  currency,
-}) => {
-  const [cluster, setCluster] = useState('')
-  const [node, setNode] = useState('')
-
-  const [fetch, setFetch] = useState(true)
-  const [loading, setLoading] = useState(false)
-  const [errors, setErrors] = useState([])
-  const [rows, setRows] = useState([])
-
-  useEffect(() => {
-    if (fetch) {
-      setCluster('')
-      setNode('')
-      fetchData()
-    }
-  }, [fetch])
-
-  async function fetchData() {
-    setLoading(true)
-    setErrors([])
-
-    try {
-      const filters = []
-
-      if (cluster) {
-        filters.push({
-          property: "cluster",
-          value: cluster,
-        })
-      }
-
-      if (node) {
-        filters.push({
-          property: "node",
-          value: node,
-        })
-      }
-
-      if (namespace) {
-        filters.push({
-          property: "namespace",
-          value: namespace,
-        })
-      }
-
-      if (controllerKind) {
-        filters.push({
-          property: "controllerKind",
-          value: controllerKind,
-        })
-      }
-
-      if (controller) {
-        filters.push({
-          property: "controller",
-          value: controller,
-        })
-      }
-
-      if (pod) {
-        filters.push({
-          property: "pod",
-          value: pod,
-        })
-      }
-
-      const resp = await AllocationService.fetchAllocation(window, '', { accumulate: true })
-
-      let data = []
-      forEach(resp.data[0], (datum) => {
-        if (datum.name === "__idle__") {
-          return
-        }
-
-        if (!cluster) {
-          setCluster(get(datum, 'properties.cluster', ''))
-        }
-
-        if (!node) {
-          setNode(get(datum, 'properties.node', ''))
-        }
-
-        // TODO can we get pod, container back in properties?
-        const names = datum.name.split("/")
-        datum.pod = names[names.length-2]
-        datum.container = names[names.length-1]
-
-        datum.hours = round(get(datum, 'minutes', 0.0) / 60.0, 2)
-
-        if (datum.hours > 0) {
-          datum.cpu = round(get(datum, 'cpuCoreHours', 0.0) / datum.hours, 2)
-          datum.cpuCostPerCoreHr = datum.cpuCost / (datum.cpu * datum.hours)
-          if (datum.cpu === 0) {
-            datum.cpuCostPerCoreHr = 0.0
-          }
-
-          datum.ram = round(get(datum, 'ramByteHours', 0.0) / datum.hours, 2)
-          const ramGiB = datum.ram / 1024 / 1024 / 1024
-          datum.ramCostPerGiBHr = datum.ramCost / (ramGiB * datum.hours)
-          if (ramGiB === 0) {
-            datum.ramCostPerGiBHr = 0.0
-          }
-        } else {
-          datum.cpu = 0.0
-          datum.cpuCostPerCoreHr = 0.0
-          datum.ram = 0.0
-          datum.ramCostPerGiBHr = 0.0
-        }
-
-        data.push(datum)
-      })
-
-      data = reverse(sortBy(data, 'totalCost'))
-
-      setRows(data)
-    } catch (e) {
-      console.warn(`Error fetching details for (${controllerKind}, ${controller}):`, e)
-      setErrors([{
-        primary: "Error fetching details",
-        secondary: `Tried fetching details for: ${namespace}, ${controllerKind}, ${controller}, ${pod}`,
-      }])
-    }
-
-    setLoading(false)
-    setFetch(false)
-  }
-
-  if (loading) {
-    return (
-      <div style={{ display: 'flex', justifyContent: 'center' }}>
-        <div style={{ paddingTop: 100, paddingBottom: 100 }}>
-          <CircularProgress />
-        </div>
-      </div>
-    )
-  }
-
-  return (
-    <div>
-
-      {!loading && errors.length > 0 && (
-        <div style={{ marginBottom: 20 }}>
-          <Warnings warnings={errors} />
-        </div>
-      )}
-
-      <List>
-        {cluster && (
-          <ListItem>
-            <ListItemIcon>
-              <ClusterIcon />
-            </ListItemIcon>
-            <ListItemText primary={cluster} />
-          </ListItem>
-        )}
-        {node && (
-        <ListItem>
-          <ListItemIcon>
-            <NodeIcon />
-          </ListItemIcon>
-          <ListItemText primary={node} />
-        </ListItem>
-        )}
-      </List>
-      <TableContainer>
-        <Table>
-          <TableHead>
-            <TableRow>
-              <TableCell align="left" component="th" scope="row" width={200}>Container</TableCell>
-              <TableCell align="right" component="th" scope="row">Hours</TableCell>
-              <TableCell align="right" component="th" scope="row">CPU</TableCell>
-              <TableCell align="right" component="th" scope="row">$/(CPU*Hr)</TableCell>
-              <TableCell align="right" component="th" scope="row">CPU cost</TableCell>
-              <TableCell align="right" component="th" scope="row">RAM</TableCell>
-              <TableCell align="right" component="th" scope="row">$/(GiB*Hr)</TableCell>
-              <TableCell align="right" component="th" scope="row">RAM cost</TableCell>
-              <TableCell align="right" component="th" scope="row">Total cost</TableCell>
-            </TableRow>
-          </TableHead>
-          <TableBody>
-            {rows.map((row, i) => (
-              <TableRow key={i} hover>
-                <TableCell align="left" component="th" scope="row" width={200}>{row.container}</TableCell>
-                <TableCell align="right" component="th" scope="row">{row.hours}</TableCell>
-                <TableCell align="right" component="th" scope="row">{row.cpu}</TableCell>
-                <TableCell align="right" component="th" scope="row">{toCurrency(row.cpuCostPerCoreHr, currency, 5)}</TableCell>
-                <TableCell align="right" component="th" scope="row">{toCurrency(row.cpuCost, currency, 3)}</TableCell>
-                <TableCell align="right" component="th" scope="row">{bytesToString(row.ram)}</TableCell>
-                <TableCell align="right" component="th" scope="row">{toCurrency(row.ramCostPerGiBHr, currency, 5)}</TableCell>
-                <TableCell align="right" component="th" scope="row">{toCurrency(row.ramCost, currency, 3)}</TableCell>
-                <TableCell align="right" component="th" scope="row">{toCurrency(row.totalCost, currency, 3)}</TableCell>
-              </TableRow>
-            ))}
-          </TableBody>
-        </Table>
-      </TableContainer>
-    </div>
-  )
-}
-
-export default memo(Details)

+ 0 - 13
ui/src/components/Footer.js

@@ -1,13 +0,0 @@
-import {Parser as HtmlToReactParser} from 'html-to-react'
-
-// Footer could be HTML, so we need to parse it.
-const Footer = () => {
-  const content = '<div align="right"><br/>PLACEHOLDER_FOOTER_CONTENT</div>';
-  const htmlToReactParser = new HtmlToReactParser();
-  const parsedContent = htmlToReactParser.parse(content);
-  return (
-    parsedContent
-    )
-  }
-
-export default Footer;

+ 0 - 60
ui/src/components/Header.js

@@ -1,60 +0,0 @@
-import * as React from "react";
-import { makeStyles } from "@material-ui/styles";
-import Breadcrumbs from "@material-ui/core/Breadcrumbs";
-import Link from "@material-ui/core/Link";
-import Typography from "@material-ui/core/Typography";
-import { useLocation } from "react-router-dom";
-
-const useStyles = makeStyles({
-  root: {
-    alignItems: "center",
-    display: "flex",
-    flexFlow: "row",
-    width: "100%",
-    marginTop: "10px",
-  },
-  context: {
-    flex: "1 0 auto",
-  },
-  actions: {
-    flex: "0 0 auto",
-  },
-});
-
-const Header = (props) => {
-  const classes = useStyles();
-  const { title, breadcrumbs } = props;
-  const { pathname } = useLocation();
-
-  const headerTitle = pathname === "/cloud" ? "Cloud Costs" : "Cost Allocation";
-
-  return (
-    <div className={classes.root}>
-      <Typography variant="h3" style={{ marginBottom: "10px" }}>
-        {headerTitle}
-      </Typography>
-      <div className={classes.context}>
-        {title && (
-          <Typography variant="h4" className={classes.title}>
-            {props.title}
-          </Typography>
-        )}
-        {breadcrumbs && breadcrumbs.length > 0 && (
-          <Breadcrumbs aria-label="breadcrumb">
-            {breadcrumbs.slice(0, breadcrumbs.length - 1).map((b) => (
-              <Link color="inherit" href={b.href} key={b.name}>
-                {b.name}
-              </Link>
-            ))}
-            <Typography color="textPrimary">
-              {breadcrumbs[breadcrumbs.length - 1].name}
-            </Typography>
-          </Breadcrumbs>
-        )}
-      </div>
-      <div className={classes.actions}>{props.children}</div>
-    </div>
-  );
-};
-
-export default Header;

+ 0 - 78
ui/src/components/Nav/NavItem.js

@@ -1,78 +0,0 @@
-import * as React from "react";
-import { ListItem, ListItemIcon, ListItemText } from "@material-ui/core";
-import { Link } from "react-router-dom";
-import { makeStyles } from "@material-ui/styles";
-
-const NavItem = ({ active, href, name, onClick, secondary, title, icon }) => {
-  const useStyles = makeStyles({
-    root: {
-      cursor: "pointer",
-      "&:hover": {
-        backgroundColor: "#ebebeb",
-      },
-      "&:selected": {
-        backgroundColor: "#e1e1e1",
-      },
-    },
-    text: {
-      maxWidth: 200,
-      overflow: "hidden",
-      textOverflow: "ellipsis",
-      whiteSpace: "nowrap",
-    },
-    activeIcon: {
-      color: "#346ef2",
-      minWidth: 36,
-    },
-    activeText: {
-      color: "#346ef2",
-    },
-    icon: {
-      color: "#4e4e4e",
-      minWidth: 36,
-    },
-  });
-  const classes = useStyles();
-
-  const listItemIconClasses = { root: classes.icon };
-  const listItemTextClasses = {
-    secondary: classes.text,
-  };
-
-  if (active) {
-    listItemIconClasses.root = classes.activeIcon;
-    listItemTextClasses.primary = classes.activeText;
-  }
-
-  const renderListItemCore = () => (
-    <ListItem
-      className={active ? "active" : ""}
-      classes={{ root: classes.root }}
-      onClick={(e) => {
-        if (onClick) {
-          onClick();
-          e.stopPropagation();
-        }
-      }}
-      selected={active}
-      title={title}
-    >
-      <ListItemIcon classes={listItemIconClasses}>{icon}</ListItemIcon>
-      <ListItemText
-        classes={listItemTextClasses}
-        primary={name}
-        secondary={secondary}
-      />
-    </ListItem>
-  );
-
-  return href && !active ? (
-    <Link style={{ textDecoration: "none", color: "inherit" }} to={`${href}`}>
-      {renderListItemCore()}
-    </Link>
-  ) : (
-    renderListItemCore()
-  );
-};
-
-export { NavItem };

+ 0 - 70
ui/src/components/Nav/SidebarNav.js

@@ -1,70 +0,0 @@
-import * as React from "react";
-import { Drawer, List } from "@material-ui/core";
-
-import { NavItem } from "./NavItem";
-import { BarChart } from "@material-ui/icons";
-import { Cloud } from "@material-ui/icons";
-import { makeStyles } from "@material-ui/styles";
-
-const DRAWER_WIDTH = 200;
-
-const SidebarNav = ({ active }) => {
-  const useStyles = makeStyles({
-    drawer: {
-      width: DRAWER_WIDTH,
-      flexShrink: 0,
-    },
-    drawerPaper: {
-      backgroundColor: "inherit",
-      border: 0,
-      width: DRAWER_WIDTH,
-      paddingTop: "2.5rem",
-    },
-    text: {
-      overflow: "hidden",
-      textOverflow: "ellipsis",
-      whiteSpace: "nowrap",
-    },
-  });
-
-  const classes = useStyles();
-
-  const [init, setInit] = React.useState(false);
-
-  React.useEffect(() => {
-    if (!init) {
-      setInit(true);
-    }
-  }, [init]);
-
-  const top = [
-    {
-      name: "Cost Allocation",
-      href: "allocation",
-      icon: <BarChart />,
-    },
-    { name: "Cloud Costs", href: "cloud", icon: <Cloud /> },
-  ];
-
-  return (
-    <Drawer
-      anchor={"left"}
-      className={classes.drawer}
-      classes={{ paper: classes.drawerPaper }}
-      variant={"permanent"}
-    >
-      <img
-        src={require("../../images/logo.png")}
-        alt="OpenCost"
-        style={{ flexShrink: 1, padding: "1rem" }}
-      />
-      <List style={{ flexGrow: 1 }}>
-        {top.map((l) => (
-          <NavItem active={active === `/${l.href}`} key={l.name} {...l} />
-        ))}
-      </List>
-    </Drawer>
-  );
-};
-
-export { SidebarNav };

+ 0 - 3
ui/src/components/Nav/index.js

@@ -1,3 +0,0 @@
-import { SidebarNav } from "./SidebarNav";
-
-export default SidebarNav;

+ 0 - 46
ui/src/components/Page.js

@@ -1,46 +0,0 @@
-import { makeStyles } from "@material-ui/styles";
-import * as React from "react";
-import { useLocation } from "react-router-dom";
-import { SidebarNav } from "./Nav/SidebarNav";
-
-const useStyles = makeStyles({
-  wrapper: {
-    position: "relative",
-    height: "100vh",
-    flexGrow: 1,
-    overflowX: "auto",
-    paddingLeft: "2rem",
-    paddingRight: "rem",
-    paddingTop: "2.5rem",
-  },
-  flexGrow: {
-    display: "flex",
-    flexFlow: "column",
-    flexGrow: 1,
-  },
-  body: {
-    display: "flex",
-    overflowY: "scroll",
-    margin: "0px",
-    backgroundColor: "f3f3f3",
-  },
-});
-
-const Page = (props) => {
-  const classes = useStyles();
-
-  const { pathname } = useLocation();
-
-  return (
-    <div className={classes.body}>
-      <SidebarNav active={pathname} />
-      <div className={classes.flexGrow}>
-        <div className={classes.wrapper}>
-          <div className={classes.flexGrow}>{props.children}</div>
-        </div>
-      </div>
-    </div>
-  );
-};
-
-export default Page;

+ 0 - 190
ui/src/components/SelectWindow.js

@@ -1,190 +0,0 @@
-import React, { useEffect, useState } from 'react'
-import { makeStyles } from '@material-ui/styles'
-import { endOfDay, startOfDay } from 'date-fns'
-import { MuiPickersUtilsProvider, KeyboardDatePicker } from '@material-ui/pickers'
-import Button from '@material-ui/core/Button'
-import DateFnsUtils from '@date-io/date-fns'
-import FormControl from '@material-ui/core/FormControl'
-import Link from '@material-ui/core/Link'
-import Popover from '@material-ui/core/Popover'
-import TextField from '@material-ui/core/TextField'
-import Typography from '@material-ui/core/Typography'
-import { isValid } from 'date-fns'
-import { find, get } from 'lodash'
-
-const useStyles = makeStyles({
-  dateContainer: {
-    paddingLeft: 18,
-    paddingRight: 18,
-    paddingTop: 6,
-    paddingBottom: 18,
-    display: 'flex',
-    flexFlow: 'row',
-  },
-  dateContainerColumn: {
-    display: 'flex',
-    flexFlow: 'column',
-  },
-  formControl: {
-    margin: 8,
-    width: 120,
-  },
-})
-
-const SelectWindow = ({ windowOptions, window, setWindow }) => {
-    const classes = useStyles()
-    const [anchorEl, setAnchorEl] = useState(null)
-  
-    const [startDate, setStartDate] = useState(null)
-    const [endDate, setEndDate] = useState(null)
-    const [intervalString, setIntervalString] = useState(null)
-  
-    const handleClick = (event) => {
-      setAnchorEl(event.currentTarget)
-    }
-  
-    const handleClose = () => {
-      setAnchorEl(null)
-    }
-  
-    const handleStartDateChange = (date) => {
-      if (isValid(date)) {
-        setStartDate(startOfDay(date))
-      }
-    }
-  
-    const handleEndDateChange = (date) => {
-      if (isValid(date)) {
-        setEndDate(endOfDay(date))
-      }
-    }
-  
-    const handleSubmitPresetDates = (dateString) => {
-      setWindow(dateString)
-      setStartDate(null)
-      setEndDate(null)
-      handleClose()
-    }
-  
-    const handleSubmitCustomDates = () => {
-      if (intervalString !== null) {
-        setWindow(intervalString)
-        handleClose()
-      }
-    }
-  
-    useEffect(() => {
-      if (startDate !== null && endDate !== null) {
-        // Note: getTimezoneOffset() is calculated based on current system locale, NOT date object
-        let adjustedStartDate = new Date(startDate - startDate.getTimezoneOffset() * 60000)
-        let adjustedEndDate = new Date(endDate - endDate.getTimezoneOffset() * 60000)
-        setIntervalString(
-          adjustedStartDate.toISOString().split('.')[0] + "Z" 
-          + "," 
-          + adjustedEndDate.toISOString().split('.')[0] + "Z"
-        )
-      }
-    }, [startDate, endDate])
-  
-    const open = Boolean(anchorEl)
-    const id = open ? 'date-range-popover' : undefined
-  
-    return (
-      <>
-        <FormControl className={classes.formControl}>
-          <TextField
-          id="filled-read-only-input"
-          label="Date Range"
-          value={get(find(windowOptions, { value: window }), "name", "Custom")}
-          onClick={e => handleClick(e)}
-          inputProps={{
-            readOnly: true,
-            style: { cursor: 'pointer' },
-          }}
-          />
-        </FormControl>
-        <Popover
-          id={id}
-          open={open}
-          anchorEl={anchorEl}
-          onClose={handleClose}
-          anchorOrigin={{
-            vertical: 'bottom',
-            horizontal: 'left',
-          }}
-          transformOrigin={{
-            vertical: 'top',
-            horizontal: 'center',
-          }}
-        >
-          <div className={classes.dateContainer}>
-            <div className={classes.dateContainerColumn}>
-            <MuiPickersUtilsProvider utils={DateFnsUtils}>
-              <KeyboardDatePicker
-                style={{ width: '144px' }}
-                autoOk={true}
-                disableToolbar
-                variant="inline"
-                format="MM/dd/yyyy"
-                margin="normal"
-                id="date-picker-start"
-                label="Start Date"
-                value={startDate}
-                maxDate={new Date()}
-                maxDateMessage="Date should not be after today."
-                onChange={handleStartDateChange}
-                KeyboardButtonProps={{
-                  'aria-label': 'change date',
-                }}
-              />
-              <KeyboardDatePicker
-                style={{ width: '144px' }}
-                autoOk={true}
-                disableToolbar
-                variant="inline"
-                format="MM/dd/yyyy"
-                margin="normal"
-                id="date-picker-end"
-                label="End Date"
-                value={endDate}
-                maxDate={new Date()}
-                maxDateMessage="Date should not be after today."
-                onChange={handleEndDateChange}
-                KeyboardButtonProps={{
-                  'aria-label': 'change date',
-                }}
-              />
-            </MuiPickersUtilsProvider>
-              <div>
-                <Button 
-                  style={{ marginTop: 16 }} 
-                  variant="contained" 
-                  color="default"
-                  onClick={handleSubmitCustomDates}
-                >
-                  Apply
-                </Button>
-              </div>
-            </div>
-            <div className={classes.dateContainerColumn} style={{ paddingTop: 12, marginLeft: 18 }}>
-              {windowOptions.map(opt => 
-              <Typography key={opt.value}
-              >
-                <Link
-                  style={{ cursor: "pointer" }}
-                  key={opt.value}
-                  value={opt.value}
-                  onClick={() => handleSubmitPresetDates(opt.value)}
-                >
-                  {opt.name}
-                </Link>
-              </Typography>
-              )}
-            </div>
-          </div>
-        </Popover>
-      </>
-    )
-  }
-
-  export default React.memo(SelectWindow)

+ 0 - 44
ui/src/components/Subtitle.js

@@ -1,44 +0,0 @@
-import * as React from "react";
-import { makeStyles } from "@material-ui/styles";
-import { upperFirst } from "lodash";
-import Breadcrumbs from "@material-ui/core/Breadcrumbs";
-import NavigateNextIcon from "@material-ui/icons/NavigateNext";
-import Typography from "@material-ui/core/Typography";
-import { toVerboseTimeRange } from "../util";
-
-const useStyles = makeStyles({
-  root: {
-    "& > * + *": {
-      marginTop: 2,
-    },
-  },
-  link: {
-    cursor: "pointer",
-  },
-});
-
-const Subtitle = ({ report, onClick }) => {
-  const classes = useStyles();
-
-  const { aggregateBy, window } = report;
-
-  return (
-    <div className={classes.root}>
-      <Breadcrumbs
-        separator={<NavigateNextIcon fontSize="small" />}
-        aria-label="breadcrumb"
-        onClick={onClick}
-      >
-        {aggregateBy && aggregateBy.length > 0 ? (
-          <Typography>
-            {toVerboseTimeRange(window)} by {upperFirst(aggregateBy)}
-          </Typography>
-        ) : (
-          <Typography>{toVerboseTimeRange(window)}</Typography>
-        )}
-      </Breadcrumbs>
-    </div>
-  );
-};
-
-export default React.memo(Subtitle);

+ 0 - 37
ui/src/components/Warnings.js

@@ -1,37 +0,0 @@
-import React from "react";
-import { makeStyles } from "@material-ui/styles";
-import List from "@material-ui/core/List";
-import ListItem from "@material-ui/core/ListItem";
-import ListItemIcon from "@material-ui/core/ListItemIcon";
-import ListItemText from "@material-ui/core/ListItemText";
-import Paper from "@material-ui/core/Paper";
-import WarningIcon from "@material-ui/icons/Warning";
-
-const useStyles = makeStyles({
-  root: {},
-});
-
-const Warnings = ({ warnings }) => {
-  const classes = useStyles();
-
-  if (!warnings || warnings.length === 0) {
-    return null;
-  }
-
-  return (
-    <Paper className={classes.root}>
-      <List>
-        {warnings.map((warn, i) => (
-          <ListItem key={i}>
-            <ListItemIcon>
-              <WarningIcon />
-            </ListItemIcon>
-            <ListItemText primary={warn.primary} secondary={warn.secondary} />
-          </ListItem>
-        ))}
-      </List>
-    </Paper>
-  );
-};
-
-export default Warnings;

+ 0 - 241
ui/src/components/allocationReport.js

@@ -1,241 +0,0 @@
-import React, { useEffect, useState } from "react";
-import { get, round } from "lodash";
-import { makeStyles } from "@material-ui/styles";
-import Table from "@material-ui/core/Table";
-import TableBody from "@material-ui/core/TableBody";
-import TableCell from "@material-ui/core/TableCell";
-import TableContainer from "@material-ui/core/TableContainer";
-import TableHead from "@material-ui/core/TableHead";
-import TablePagination from "@material-ui/core/TablePagination";
-import TableRow from "@material-ui/core/TableRow";
-import TableSortLabel from "@material-ui/core/TableSortLabel";
-import Typography from "@material-ui/core/Typography";
-import AllocationChart from "./AllocationChart";
-import { toCurrency } from "../util";
-
-const useStyles = makeStyles({
-  noResults: {
-    padding: 24,
-  },
-});
-
-function descendingComparator(a, b, orderBy) {
-  if (get(b, orderBy) < get(a, orderBy)) {
-    return -1;
-  }
-  if (get(b, orderBy) > get(a, orderBy)) {
-    return 1;
-  }
-  return 0;
-}
-
-function getComparator(order, orderBy) {
-  return order === "desc"
-    ? (a, b) => descendingComparator(a, b, orderBy)
-    : (a, b) => -descendingComparator(a, b, orderBy);
-}
-
-function stableSort(array, comparator) {
-  const stabilizedThis = array.map((el, index) => [el, index]);
-  stabilizedThis.sort((a, b) => {
-    const order = comparator(a[0], b[0]);
-    if (order !== 0) return order;
-    return a[1] - b[1];
-  });
-  return stabilizedThis.map((el) => el[0]);
-}
-
-const headCells = [
-  { id: "name", numeric: false, label: "Name", width: "auto" },
-  { id: "cpuCost", numeric: true, label: "CPU", width: 90 },
-  { id: "ramCost", numeric: true, label: "RAM", width: 90 },
-  { id: "pvCost", numeric: true, label: "PV", width: 90 },
-  { id: "totalEfficiency", numeric: true, label: "Efficiency", width: 90 },
-  { id: "totalCost", numeric: true, label: "Total cost", width: 90 },
-];
-
-const AllocationReport = ({
-  allocationData,
-  cumulativeData,
-  totalData,
-  currency,
-}) => {
-  const classes = useStyles();
-
-  if (allocationData.length === 0) {
-    return (
-      <Typography variant="body2" className={classes.noResults}>
-        No results
-      </Typography>
-    );
-  }
-
-  const [order, setOrder] = React.useState("desc");
-  const [orderBy, setOrderBy] = React.useState("totalCost");
-  const [page, setPage] = useState(0);
-  const [rowsPerPage, setRowsPerPage] = useState(25);
-  const numData = cumulativeData.length;
-
-  useEffect(() => {
-    setPage(0);
-  }, [numData]);
-
-  const lastPage = Math.floor(numData / rowsPerPage);
-
-  const handleChangePage = (event, newPage) => setPage(newPage);
-
-  const handleChangeRowsPerPage = (event) => {
-    setRowsPerPage(parseInt(event.target.value, 10));
-    setPage(0);
-  };
-
-  const createSortHandler = (property) => (event) =>
-    handleRequestSort(event, property);
-
-  const handleRequestSort = (event, property) => {
-    const isDesc = orderBy === property && order === "desc";
-    setOrder(isDesc ? "asc" : "desc");
-    setOrderBy(property);
-  };
-
-  const orderedRows = stableSort(cumulativeData, getComparator(order, orderBy));
-  const pageRows = orderedRows.slice(
-    page * rowsPerPage,
-    page * rowsPerPage + rowsPerPage
-  );
-
-  return (
-    <div id="report">
-      <AllocationChart
-        allocationRange={allocationData}
-        currency={currency}
-        n={10}
-        height={300}
-      />
-      <TableContainer>
-        <Table>
-          <TableHead>
-            <TableRow>
-              {headCells.map((cell) => (
-                <TableCell
-                  key={cell.id}
-                  colSpan={cell.colspan}
-                  align={cell.numeric ? "right" : "left"}
-                  sortDirection={orderBy === cell.id ? order : false}
-                  style={{ width: cell.width }}
-                >
-                  <TableSortLabel
-                    active={orderBy === cell.id}
-                    direction={orderBy === cell.id ? order : "asc"}
-                    onClick={createSortHandler(cell.id)}
-                  >
-                    {cell.label}
-                  </TableSortLabel>
-                </TableCell>
-              ))}
-            </TableRow>
-          </TableHead>
-          <TableBody>
-            <TableRow>
-              {headCells.map((cell) => {
-                return (
-                  <TableCell
-                    key={cell.id}
-                    colSpan={cell.colspan}
-                    align={cell.numeric ? "right" : "left"}
-                    style={{ fontWeight: 500 }}
-                  >
-                    {cell.numeric
-                      ? cell.label === "Efficiency"
-                        ? totalData.totalEfficiency == 1.0 &&
-                          totalData.cpuReqCoreHrs == 0 &&
-                          totalData.ramReqByteHrs == 0
-                          ? "Inf%"
-                          : `${round(totalData.totalEfficiency * 100, 1)}%`
-                        : toCurrency(totalData[cell.id], currency)
-                      : totalData[cell.id]}
-                  </TableCell>
-                );
-              })}
-            </TableRow>
-            {pageRows.map((row, key) => {
-              if (row.name === "__unmounted__") {
-                row.name = "Unmounted PVs";
-              }
-
-              let isIdle = row.name.indexOf("__idle__") >= 0;
-              let isUnallocated = row.name.indexOf("__unallocated__") >= 0;
-              let isUnmounted = row.name.indexOf("Unmounted PVs") >= 0;
-
-              // Replace "efficiency" with Inf if there is usage w/o request
-              let efficiency = round(row.totalEfficiency * 100, 1);
-              if (
-                row.totalEfficiency == 1.0 &&
-                row.cpuReqCoreHrs == 0 &&
-                row.ramReqByteHrs == 0
-              ) {
-                efficiency = "Inf";
-              }
-
-              // Do not allow drill-down for idle and unallocated rows
-              if (isIdle || isUnallocated || isUnmounted) {
-                return (
-                  <TableRow key={key}>
-                    <TableCell align="left">{row.name}</TableCell>
-                    <TableCell align="right">
-                      {toCurrency(row.cpuCost, currency)}
-                    </TableCell>
-                    <TableCell align="right">
-                      {toCurrency(row.ramCost, currency)}
-                    </TableCell>
-                    <TableCell align="right">
-                      {toCurrency(row.pvCost, currency)}
-                    </TableCell>
-                    {isIdle ? (
-                      <TableCell align="right">&mdash;</TableCell>
-                    ) : (
-                      <TableCell align="right">{efficiency}%</TableCell>
-                    )}
-                    <TableCell align="right">
-                      {toCurrency(row.totalCost, currency)}
-                    </TableCell>
-                  </TableRow>
-                );
-              }
-
-              return (
-                <TableRow key={key}>
-                  <TableCell align="left">{row.name}</TableCell>
-                  <TableCell align="right">
-                    {toCurrency(row.cpuCost, currency)}
-                  </TableCell>
-                  <TableCell align="right">
-                    {toCurrency(row.ramCost, currency)}
-                  </TableCell>
-                  <TableCell align="right">
-                    {toCurrency(row.pvCost, currency)}
-                  </TableCell>
-                  <TableCell align="right">{efficiency}%</TableCell>
-                  <TableCell align="right">
-                    {toCurrency(row.totalCost, currency)}
-                  </TableCell>
-                </TableRow>
-              );
-            })}
-          </TableBody>
-        </Table>
-      </TableContainer>
-      <TablePagination
-        component="div"
-        count={numData}
-        rowsPerPage={rowsPerPage}
-        rowsPerPageOptions={[10, 25, 50]}
-        page={Math.min(page, lastPage)}
-        onChangePage={handleChangePage}
-        onChangeRowsPerPage={handleChangeRowsPerPage}
-      />
-    </div>
-  );
-};
-
-export default React.memo(AllocationReport);

+ 0 - 38
ui/src/constants/colors.js

@@ -1,38 +0,0 @@
-import blue from '@material-ui/core/colors/blue'
-import brown from '@material-ui/core/colors/brown'
-import cyan from '@material-ui/core/colors/cyan'
-import deepOrange from '@material-ui/core/colors/deepOrange'
-import deepPurple from '@material-ui/core/colors/deepPurple'
-import green from '@material-ui/core/colors/green'
-import grey from '@material-ui/core/colors/grey'
-import indigo from '@material-ui/core/colors/indigo'
-import orange from '@material-ui/core/colors/orange'
-import red from '@material-ui/core/colors/red'
-import teal from '@material-ui/core/colors/teal'
-import yellow from '@material-ui/core/colors/yellow'
-
-export const primary = [
-  blue[500],
-  red[500],
-  green[500],
-  yellow[500],
-  cyan[500],
-  orange[500],
-  teal[500],
-  indigo[500],
-  deepOrange[500],
-  deepPurple[500],
-]
-
-export const greyscale = [
-  grey[300],
-  grey[400],
-  grey[200],
-  grey[500],
-  grey[100],
-  grey[600],
-]
-
-export const browns = [
-  brown[500],
-]

+ 0 - 1
ui/src/constants/currencyCodes.js

@@ -1 +0,0 @@
-export const currencyCodes = ["AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BOV", "BRL", "BSD", "BTN", "BWP", "BYR", "BZD", "CAD", "CDF", "CHE", "CHF", "CHW", "CLF", "CLP", "CNY", "COP", "COU", "CRC", "CUC", "CUP", "CVE", "CZK", "DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HRK", "HTG", "HUF", "IDR", "ILS", "INR", "IQD", "IRR", "ISK", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LTL", "LVL", "LYD", "MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRO", "MUR", "MVR", "MWK", "MXN", "MXV", "MYR", "MZN", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG", "QAR", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLL", "SOS", "SRD", "SSP", "STD", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "USN", "USS", "UYI", "UYU", "UZS", "VEF", "VND", "VUV", "WST", "XAF", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XCD", "XDR", "XFU", "XOF", "XPD", "XPF", "XPT", "XTS", "XXX", "YER", "ZAR", "ZMW"];

+ 0 - 20
ui/src/css/index.css

@@ -1,20 +0,0 @@
-@import '../../node_modules/material-design-icons-iconfont/dist/material-design-icons.css';
-
-body {
-    background-color: #F3F3F3;
-    display: flex;
-    flex-flow: column;
-    font-family: 'Roboto', sans-serif;
-    margin: 0px;
-    overflow-y: scroll;
-}
-
-body .page-container {
-    display: flex;
-    flex-flow: column;
-    flex-grow: 1;
-}
-
-.recharts-tooltip-wrapper {
-    z-index: 1000;
-}

BIN
ui/src/images/favicon.ico


BIN
ui/src/images/logo.png


+ 0 - 16
ui/src/index.html

@@ -1,16 +0,0 @@
-<!DOCTYPE html>
-<html>
-
-<head>
-	<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
-	<meta content="utf-8" http-equiv="encoding" />
-	<link rel="icon" href="./images/favicon.ico" />
-	<link rel="stylesheet" href="./css/index.css" />
-</head>
-
-<body>
-	<div id="app" class="page-container"></div>
-	<script src="./app.js" type="module"></script>
-</body>
-
-</html>

BIN
ui/src/opencost-ui.png


+ 0 - 25
ui/src/route.js

@@ -1,25 +0,0 @@
-import * as React from "react";
-import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
-
-import Reports from "./Reports.js";
-import CloudCostReports from "./cloudCostReports.js";
-
-const Routes = () => {
-  return (
-    <Router>
-      <Switch>
-        <Route exact path="/">
-          <Reports />
-        </Route>
-        <Route exact path="/allocation">
-          <Reports />
-        </Route>
-        <Route exact path="/cloud">
-          <CloudCostReports />
-        </Route>
-      </Switch>
-    </Router>
-  );
-};
-
-export default Routes;

+ 0 - 28
ui/src/services/allocation.js

@@ -1,28 +0,0 @@
-import axios from "axios";
-
-class AllocationService {
-  BASE_URL = process.env.BASE_URL || "{PLACEHOLDER_BASE_URL}";
-
-  async fetchAllocation(win, aggregate, options) {
-    if (this.BASE_URL.includes("PLACEHOLDER_BASE_URL")) {
-      this.BASE_URL = `http://localhost:9090/model`;
-    }
-
-    const { accumulate, filters } = options;
-    const params = {
-      window: win,
-      aggregate: aggregate,
-      includeIdle: true,
-      step: "1d",
-    };
-    if (typeof accumulate === "boolean") {
-      params.accumulate = accumulate;
-    }
-    const result = await axios.get(`${this.BASE_URL}/allocation/compute`, {
-      params,
-    });
-    return result.data;
-  }
-}
-
-export default new AllocationService();

+ 0 - 42
ui/src/services/cloudCostDayTotals.js

@@ -1,42 +0,0 @@
-import axios from "axios";
-import { parseFilters } from "../util";
-import { costMetricToPropName } from "../cloudCost/tokens";
-
-function formatItemsForCost({ data, costType }) {
-  return data.sets.map(({ cloudCosts, window }) => {
-    return {
-      date: window.start,
-      cost: Object.values(cloudCosts).reduce(
-        (acc, costs) => acc + costs[costType || "amortizedNetCost"].cost,
-        0
-      ),
-    };
-  });
-}
-
-class CloudCostDayTotalsService {
-  BASE_URL = process.env.BASE_URL || "{PLACEHOLDER_BASE_URL}";
-
-  async fetchCloudCostData(window, aggregate, costMetric, filters) {
-    if (this.BASE_URL.includes("PLACEHOLDER_BASE_URL")) {
-      this.BASE_URL = `http://localhost:9090/model`;
-    }
-    if (aggregate.includes("item")) {
-      const resp = await axios.get(
-        `${
-          this.BASE_URL
-        }/cloudCost?window=${window}&costMetric=${costMetric}&filter=${parseFilters(
-          filters
-        )}`
-      );
-      const costMetricProp = costMetricToPropName[costMetric];
-
-      const result_2 = await resp.data;
-      return { data: formatItemsForCost(result_2, costMetricProp) };
-    }
-
-    return [];
-  }
-}
-
-export default new CloudCostDayTotalsService();

+ 0 - 57
ui/src/services/cloudCostTop.js

@@ -1,57 +0,0 @@
-import axios from "axios";
-import { formatSampleItemsForGraph, parseFilters } from "../util";
-
-class CloudCostTopService {
-  BASE_URL = process.env.BASE_URL || "{PLACEHOLDER_BASE_URL}";
-
-  async fetchCloudCostData(window, aggregate, costMetric, filters) {
-    if (this.BASE_URL.includes("PLACEHOLDER_BASE_URL")) {
-      this.BASE_URL = `http://localhost:9090/model`;
-    }
-
-    const params = {
-      window,
-      aggregate,
-      costMetric,
-      filter: parseFilters(filters ?? []),
-      limit: 1000,
-    };
-
-    if (aggregate.includes("item")) {
-      const resp = await axios.get(
-        `${
-          this.BASE_URL
-        }/cloudCost?window=${window}&costMetric=${costMetric}&filter=${parseFilters(
-          filters
-        )}`
-      );
-      const result_2 = await resp.data;
-
-      return formatSampleItemsForGraph(result_2, costMetric);
-    }
-
-    const tableView = await axios.get(`${this.BASE_URL}/cloudCost/view/table`, {
-      params,
-    });
-    const totalsView = await axios.get(
-      `${this.BASE_URL}/cloudCost/view/totals`,
-      {
-        params,
-      }
-    );
-    const graphView = await axios.get(`${this.BASE_URL}/cloudCost/view/graph`, {
-      params,
-    });
-
-    const status = await axios.get(`${this.BASE_URL}/cloudCost/status`);
-
-    return {
-      tableRows: tableView.data.data,
-      graphData: graphView.data.data,
-      tableTotal: totalsView.data.data.combined,
-      cloudCostStatus: status.data.data,
-    };
-  }
-}
-
-export default new CloudCostTopService();

BIN
ui/src/thumbnail.png


+ 0 - 452
ui/src/util.js

@@ -1,452 +0,0 @@
-import { forEach, get, round } from "lodash";
-import { costMetricToPropName } from "./cloudCost/tokens";
-
-// rangeToCumulative takes an AllocationSetRange (type: array[AllocationSet])
-// and accumulates the values into a single AllocationSet (type: object)
-export function rangeToCumulative(allocationSetRange, aggregateBy) {
-  if (allocationSetRange.length === 0) {
-    return null;
-  }
-
-  const result = {};
-
-  forEach(allocationSetRange, (allocSet) => {
-    forEach(allocSet, (alloc) => {
-      if (result[alloc.name] === undefined) {
-        const hrs = get(alloc, "minutes", 0) / 60.0;
-
-        result[alloc.name] = {
-          name: alloc.name,
-          [aggregateBy]: alloc.name,
-          cpuCost: get(alloc, "cpuCost", 0),
-          gpuCost: get(alloc, "gpuCost", 0),
-          ramCost: get(alloc, "ramCost", 0),
-          pvCost: get(alloc, "pvCost", 0),
-          networkCost: get(alloc, "networkCost", 0),
-          sharedCost: get(alloc, "sharedCost", 0),
-          externalCost: get(alloc, "externalCost", 0),
-          totalCost: get(alloc, "totalCost", 0),
-          cpuUseCoreHrs: get(alloc, "cpuCoreUsageAverage", 0) * hrs,
-          cpuReqCoreHrs: get(alloc, "cpuCoreRequestAverage", 0) * hrs,
-          ramUseByteHrs: get(alloc, "ramByteUsageAverage", 0) * hrs,
-          ramReqByteHrs: get(alloc, "ramByteRequestAverage", 0) * hrs,
-          cpuEfficiency: get(alloc, "cpuEfficiency", 0),
-          ramEfficiency: get(alloc, "ramEfficiency", 0),
-          totalEfficiency: get(alloc, "totalEfficiency", 0),
-        };
-      } else {
-        const hrs = get(alloc, "minutes", 0) / 60.0;
-
-        result[alloc.name].cpuCost += get(alloc, "cpuCost", 0);
-        result[alloc.name].gpuCost += get(alloc, "gpuCost", 0);
-        result[alloc.name].ramCost += get(alloc, "ramCost", 0);
-        result[alloc.name].pvCost += get(alloc, "pvCost", 0);
-        result[alloc.name].networkCost += get(alloc, "networkCost", 0);
-        result[alloc.name].sharedCost += get(alloc, "sharedCost", 0);
-        result[alloc.name].externalCost += get(alloc, "externalCost", 0);
-        result[alloc.name].totalCost += get(alloc, "totalCost", 0);
-        result[alloc.name].cpuUseCoreHrs +=
-          get(alloc, "cpuCoreUsageAverage", 0) * hrs;
-        result[alloc.name].cpuReqCoreHrs +=
-          get(alloc, "cpuCoreRequestAverage", 0) * hrs;
-        result[alloc.name].ramUseByteHrs +=
-          get(alloc, "ramByteUsageAverage", 0) * hrs;
-        result[alloc.name].ramReqByteHrs +=
-          get(alloc, "ramByteRequestAverage", 0) * hrs;
-      }
-    });
-  });
-
-  // If the range is of length > 1 (i.e. it is not just a single set) then
-  // compute efficiency for each result after accumulating.
-  if (allocationSetRange.length > 1) {
-    forEach(result, (alloc, name) => {
-      // If we can't compute total efficiency, it defaults to 0.0
-      let totalEfficiency = 0.0;
-
-      // CPU efficiency is defined as (usage/request). If request == 0.0 but
-      // usage > 0, then efficiency gets set to 1.0.
-      let cpuEfficiency = 0.0;
-      if (alloc.cpuReqCoreHrs > 0) {
-        cpuEfficiency = alloc.cpuUseCoreHrs / alloc.cpuReqCoreHrs;
-      } else if (alloc.cpuUseCoreHrs > 0) {
-        cpuEfficiency = 1.0;
-      }
-
-      // RAM efficiency is defined as (usage/request). If request == 0.0 but
-      // usage > 0, then efficiency gets set to 1.0.
-      let ramEfficiency = 0.0;
-      if (alloc.ramReqByteHrs > 0) {
-        ramEfficiency = alloc.ramUseByteHrs / alloc.ramReqByteHrs;
-      } else if (alloc.ramUseByteHrs > 0) {
-        ramEfficiency = 1.0;
-      }
-
-      // Compute efficiency as the cost-weighted average of CPU and RAM
-      // efficiency
-      if (alloc.cpuCost + alloc.ramCost > 0.0) {
-        totalEfficiency =
-          (alloc.cpuCost * cpuEfficiency + alloc.ramCost * ramEfficiency) /
-          (alloc.cpuCost + alloc.ramCost);
-      }
-
-      result[name].cpuEfficiency = cpuEfficiency;
-      result[name].ramEfficiency = ramEfficiency;
-      result[name].totalEfficiency = totalEfficiency;
-    });
-  }
-
-  return result;
-}
-
-// cumulativeToTotals adds each entry in the given AllocationSet (type: object)
-// and returns a single Allocation (type: object) representing the totals
-export function cumulativeToTotals(allocationSet) {
-  let totals = {
-    name: "Totals",
-    cpuCost: 0,
-    gpuCost: 0,
-    ramCost: 0,
-    pvCost: 0,
-    networkCost: 0,
-    sharedCost: 0,
-    externalCost: 0,
-    totalCost: 0,
-    cpuEfficiency: 0,
-    ramEfficiency: 0,
-    totalEfficiency: 0,
-  };
-
-  // Use these for computing efficiency. As such, idle will not factor into
-  // these numbers, including CPU and RAM cost.
-  let cpuReqCoreHrs = 0;
-  let cpuUseCoreHrs = 0;
-  let ramReqByteHrs = 0;
-  let ramUseByteHrs = 0;
-  let cpuCost = 0;
-  let ramCost = 0;
-
-  forEach(allocationSet, (alloc, name) => {
-    // Accumulate efficiency-related fields
-    if (name !== "__idle__") {
-      cpuReqCoreHrs += get(alloc, "cpuReqCoreHrs", 0.0);
-      cpuUseCoreHrs += get(alloc, "cpuUseCoreHrs", 0.0);
-      ramReqByteHrs += get(alloc, "ramReqByteHrs", 0.0);
-      ramUseByteHrs += get(alloc, "ramUseByteHrs", 0.0);
-      cpuCost += get(alloc, "cpuCost", 0.0);
-      ramCost += get(alloc, "ramCost", 0.0);
-    }
-
-    // Sum cumulative fields
-    totals.cpuCost += get(alloc, "cpuCost", 0);
-    totals.gpuCost += get(alloc, "gpuCost", 0);
-    totals.ramCost += get(alloc, "ramCost", 0);
-    totals.pvCost += get(alloc, "pvCost", 0);
-    totals.networkCost += get(alloc, "networkCost", 0);
-    totals.sharedCost += get(alloc, "sharedCost", 0);
-    totals.externalCost += get(alloc, "externalCost", 0);
-    totals.totalCost += get(alloc, "totalCost", 0);
-  });
-
-  // Compute efficiency
-  if (cpuReqCoreHrs > 0) {
-    totals.cpuEfficiency = cpuUseCoreHrs / cpuReqCoreHrs;
-  } else if (cpuUseCoreHrs > 0) {
-    totals.cpuEfficiency = 1.0;
-  }
-
-  if (ramReqByteHrs > 0) {
-    totals.ramEfficiency = ramUseByteHrs / ramReqByteHrs;
-  } else if (ramUseByteHrs > 0) {
-    totals.ramEfficiency = 1.0;
-  }
-
-  if (cpuCost + ramCost > 0) {
-    totals.totalEfficiency =
-      (cpuCost * totals.cpuEfficiency + ramCost * totals.ramEfficiency) /
-      (cpuCost + ramCost);
-  }
-
-  totals.cpuReqCoreHrs = cpuReqCoreHrs;
-  totals.cpuUseCoreHrs = cpuUseCoreHrs;
-  totals.ramReqByteHrs = ramReqByteHrs;
-  totals.ramUseByteHrs = ramUseByteHrs;
-
-  return totals;
-}
-
-export function toVerboseTimeRange(window) {
-  const months = [
-    "January",
-    "February",
-    "March",
-    "April",
-    "May",
-    "June",
-    "July",
-    "August",
-    "September",
-    "October",
-    "November",
-    "December",
-  ];
-
-  const start = new Date();
-  start.setUTCHours(0, 0, 0, 0);
-
-  const end = new Date();
-  end.setUTCHours(0, 0, 0, 0);
-
-  switch (window) {
-    case "today":
-      return `${start.getUTCDate()} ${
-        months[start.getUTCMonth()]
-      } ${start.getUTCFullYear()}`;
-    case "yesterday":
-      start.setUTCDate(start.getUTCDate() - 1);
-      return `${start.getUTCDate()} ${
-        months[start.getUTCMonth()]
-      } ${start.getUTCFullYear()}`;
-    case "week":
-      start.setUTCDate(start.getUTCDate() - start.getUTCDay());
-      return `${start.getUTCDate()} ${
-        months[start.getUTCMonth()]
-      } ${start.getUTCFullYear()} until now`;
-    case "month":
-      start.setUTCDate(1);
-      return `${start.getUTCDate()} ${
-        months[start.getUTCMonth()]
-      } ${start.getUTCFullYear()} until now`;
-    case "lastweek":
-      start.setUTCDate(start.getUTCDate() - (start.getUTCDay() + 7));
-      end.setUTCDate(end.getUTCDate() - (end.getUTCDay() + 1));
-      return `${start.getUTCDate()} ${
-        months[start.getUTCMonth()]
-      } ${start.getUTCFullYear()} through ${end.getUTCDate()} ${
-        months[end.getUTCMonth()]
-      } ${end.getUTCFullYear()}`;
-    case "lastmonth":
-      end.setUTCDate(1);
-      end.setUTCDate(end.getUTCDate() - 1);
-      start.setUTCDate(1);
-      start.setUTCDate(start.getUTCDate() - 1);
-      start.setUTCDate(1);
-      return `${start.getUTCDate()} ${
-        months[start.getUTCMonth()]
-      } ${start.getUTCFullYear()} through ${end.getUTCDate()} ${
-        months[end.getUTCMonth()]
-      } ${end.getUTCFullYear()}`;
-    case "6d":
-      start.setUTCDate(start.getUTCDate() - 6);
-      return `${start.getUTCDate()} ${
-        months[start.getUTCMonth()]
-      } ${start.getUTCFullYear()} through now`;
-    case "29d":
-      start.setUTCDate(start.getUTCDate() - 29);
-      return `${start.getUTCDate()} ${
-        months[start.getUTCMonth()]
-      } ${start.getUTCFullYear()} through now`;
-    case "59d":
-      start.setUTCDate(start.getUTCDate() - 59);
-      return `${start.getUTCDate()} ${
-        months[start.getUTCMonth()]
-      } ${start.getUTCFullYear()} through now`;
-    case "89d":
-      start.setUTCDate(start.getUTCDate() - 89);
-      return `${start.getUTCDate()} ${
-        months[start.getUTCMonth()]
-      } ${start.getUTCFullYear()} through now`;
-  }
-
-  const splitDates = window.split(",");
-  if (checkCustomWindow(window) && splitDates.length > 1) {
-    let s = splitDates[0].split(/\D+/).slice(0, 3);
-    let e = splitDates[1].split(/\D+/).slice(0, 3);
-    if (s.length === 3 && e.length === 3) {
-      start.setUTCFullYear(s[0], s[1] - 1, s[2]);
-      end.setUTCFullYear(e[0], e[1] - 1, e[2]);
-      if (start === end) {
-        return `${start.getUTCDate()} ${
-          months[start.getUTCMonth()]
-        } ${start.getUTCFullYear()}`;
-      } else {
-        return `${start.getUTCDate()} ${
-          months[start.getUTCMonth()]
-        } ${start.getUTCFullYear()} through ${end.getUTCDate()} ${
-          months[end.getUTCMonth()]
-        } ${end.getUTCFullYear()}`;
-      }
-    }
-  }
-  return null;
-}
-
-export function bytesToString(bytes) {
-  const ei = Math.pow(1024, 6);
-  if (bytes >= ei) {
-    return `${round(bytes / ei, 1)} EiB`;
-  }
-  const pi = Math.pow(1024, 5);
-  if (bytes >= pi) {
-    return `${round(bytes / pi, 1)} PiB`;
-  }
-  const ti = Math.pow(1024, 4);
-  if (bytes >= ti) {
-    return `${round(bytes / ti, 1)} TiB`;
-  }
-  const gi = Math.pow(1024, 3);
-  if (bytes >= gi) {
-    return `${round(bytes / gi, 1)} GiB`;
-  }
-  const mi = Math.pow(1024, 2);
-  if (bytes >= mi) {
-    return `${round(bytes / mi, 1)} MiB`;
-  }
-  const ki = Math.pow(1024, 1);
-  if (bytes >= ki) {
-    return `${round(bytes / ki, 1)} KiB`;
-  }
-
-  return `${round(bytes, 1)} B`;
-}
-
-const currencyLocale = "en-US";
-
-export function toCurrency(amount, currency, precision) {
-  if (typeof amount !== "number") {
-    console.warn(
-      `Tried to convert "${amount}" to currency, but it is not a number`
-    );
-    return "";
-  }
-
-  if (currency === undefined || currency === "") {
-    currency = "USD";
-  }
-
-  const opts = {
-    style: "currency",
-    currency: currency,
-  };
-
-  if (typeof precision === "number") {
-    opts.minimumFractionDigits = precision;
-    opts.maximumFractionDigits = precision;
-  }
-
-  return amount.toLocaleString(currencyLocale, opts);
-}
-
-export function checkCustomWindow(window) {
-  // Example ISO interval string: 2020-12-02T00:00:00Z,2020-12-03T23:59:59Z
-  const customDateRegex =
-    /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z,\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/;
-  return customDateRegex.test(window);
-}
-
-export function formatSampleItemsForGraph({ data, costMetric }) {
-  const costMetricPropName = costMetric
-    ? costMetricToPropName[costMetric]
-    : "amortizedNetCost";
-  const graphData = data.sets.map(({ cloudCosts, window: { end, start } }) => {
-    return {
-      end,
-      items: Object.entries(cloudCosts).map(([name, item]) => ({
-        name,
-        value: item.netCost.cost,
-      })),
-      start,
-    };
-  });
-  const accumulator = {};
-  data.sets.forEach(({ cloudCosts, window }) => {
-    Object.entries(cloudCosts).forEach(([name, cloudCostItem]) => {
-      const { properties } = cloudCostItem;
-      accumulator[name] ||= {
-        cost: 0,
-        start: "",
-        end: "",
-        providerID: "",
-        labelName: "",
-        kubernetesCost: 0,
-        kubernetesPercent: 0,
-      };
-      accumulator[name].cost += cloudCostItem[costMetricPropName].cost;
-      accumulator[name].kubernetesCost +=
-        cloudCostItem[costMetricPropName].cost *
-        cloudCostItem[costMetricPropName].kubernetesPercent;
-      accumulator[name].start = window.start;
-      accumulator[name].end = window.end;
-      accumulator[name].providerID = properties.providerID;
-      accumulator[name].labelName = properties.labels?.name;
-      accumulator[name].kubernetesPercent =
-        cloudCostItem[costMetricPropName].kubernetesPercent;
-    });
-  });
-  const tableRows = Object.entries(accumulator)
-    .map(
-      ([
-        name,
-        {
-          cost,
-          start,
-          end,
-          providerID,
-          kubernetesCost,
-          kubernetesPercent,
-          labelName,
-        },
-      ]) => ({
-        cost,
-        name,
-        kubernetesCost,
-        kubernetesPercent,
-        start,
-        end,
-        providerID,
-        labelName,
-      })
-    )
-    .sort((a, b) => (a.cost > b.cost ? -1 : 1));
-
-  const tableTotal = tableRows.reduce(
-    (tr1, tr2) => ({
-      ...tr1,
-      cost: tr1.cost + tr2.cost,
-      kubernetesCost: tr1.kubernetesCost + tr2.kubernetesCost,
-    }),
-    {
-      cost: 0,
-      name: "",
-      kubernetesCost: 0,
-      kubernetesPercent: 0,
-      end: "",
-      start: "",
-      labelName: "",
-      providerID: "",
-    }
-  );
-
-  return { graphData, tableRows, tableTotal };
-}
-
-export function parseFilters(filters) {
-  if (typeof filters === "string") {
-    return filters;
-  }
-  // remove dups (via context ) and format
-  return (
-    [...new Set(filters.map((f) => `${f.property}:"${f.value}"`))].join(
-      encodeURIComponent("+")
-    ) || ""
-  );
-}
-
-export default {
-  rangeToCumulative,
-  cumulativeToTotals,
-  toVerboseTimeRange,
-  bytesToString,
-  toCurrency,
-  checkCustomWindow,
-};