diff --git a/.tekton/koku-frontend-pull-request.yaml b/.tekton/koku-frontend-pull-request.yaml index 0d1164f6e..b55962e88 100644 --- a/.tekton/koku-frontend-pull-request.yaml +++ b/.tekton/koku-frontend-pull-request.yaml @@ -299,7 +299,7 @@ spec: - name: name value: buildah - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.2@sha256:e107cfdf4ee68741ad366b2768cd33e2d5f99569b639f95f50df8b9835c2d144 + value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.2@sha256:2d6e09f356059ccfd8aada165d5b020e1eb025aeac4717a1ea2de239bed2a0d7 - name: kind value: task resolver: bundles @@ -402,7 +402,7 @@ spec: - name: name value: clair-scan - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.2@sha256:37b9187c1d5f6672bbc9c61d88fc71a3ee688076cb16edef42d1ff92a59027fb + value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.2@sha256:28fee4bf5da87f2388c973d9336086749cad8436003f9a514e22ac99735e056b - name: kind value: task resolver: bundles @@ -444,7 +444,7 @@ spec: - name: name value: sast-snyk-check - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check:0.2@sha256:69ae591831f0f96d31c85d360273c1ce436ae1dbbfa3d0b22a083cb228c9e82c + value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check:0.2@sha256:c1ea706405f9ae146e31baef4abfea49b1e855a75bfc44c33eb0eb29516831b3 - name: kind value: task resolver: bundles @@ -469,7 +469,7 @@ spec: - name: name value: clamav-scan - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.1@sha256:5ac9b24cff7cfb391bc54cd5135536892090354862327d1028fa08872d759c03 + value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.1@sha256:1e29eebe916b81b7100138d62db0e03e22d03657274d37041c59cbaca5fdbf7d - name: kind value: task resolver: bundles @@ -510,7 +510,7 @@ spec: - name: name value: push-dockerfile - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:0d2b6d31dc8bc02c5493d7d28a163bb6c867be5f86c3a82388b0d5c69e18d352 + value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:674e70f7d724aaf1dd631ba9be2998ab0305fb3e0d9ec361351cc5e57bcdd3ec - name: kind value: task resolver: bundles diff --git a/.tekton/koku-frontend-push.yaml b/.tekton/koku-frontend-push.yaml index f37a7be7c..84d07898d 100644 --- a/.tekton/koku-frontend-push.yaml +++ b/.tekton/koku-frontend-push.yaml @@ -296,7 +296,7 @@ spec: - name: name value: buildah - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.2@sha256:e107cfdf4ee68741ad366b2768cd33e2d5f99569b639f95f50df8b9835c2d144 + value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.2@sha256:2d6e09f356059ccfd8aada165d5b020e1eb025aeac4717a1ea2de239bed2a0d7 - name: kind value: task resolver: bundles @@ -399,7 +399,7 @@ spec: - name: name value: clair-scan - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.2@sha256:37b9187c1d5f6672bbc9c61d88fc71a3ee688076cb16edef42d1ff92a59027fb + value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.2@sha256:28fee4bf5da87f2388c973d9336086749cad8436003f9a514e22ac99735e056b - name: kind value: task resolver: bundles @@ -441,7 +441,7 @@ spec: - name: name value: sast-snyk-check - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check:0.2@sha256:69ae591831f0f96d31c85d360273c1ce436ae1dbbfa3d0b22a083cb228c9e82c + value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check:0.2@sha256:c1ea706405f9ae146e31baef4abfea49b1e855a75bfc44c33eb0eb29516831b3 - name: kind value: task resolver: bundles @@ -466,7 +466,7 @@ spec: - name: name value: clamav-scan - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.1@sha256:5ac9b24cff7cfb391bc54cd5135536892090354862327d1028fa08872d759c03 + value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.1@sha256:1e29eebe916b81b7100138d62db0e03e22d03657274d37041c59cbaca5fdbf7d - name: kind value: task resolver: bundles @@ -507,7 +507,7 @@ spec: - name: name value: push-dockerfile - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:0d2b6d31dc8bc02c5493d7d28a163bb6c867be5f86c3a82388b0d5c69e18d352 + value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:674e70f7d724aaf1dd631ba9be2998ab0305fb3e0d9ec361351cc5e57bcdd3ec - name: kind value: task resolver: bundles diff --git a/locales/data.json b/locales/data.json index fbf9ac6ee..bf3cae29b 100644 --- a/locales/data.json +++ b/locales/data.json @@ -152,6 +152,12 @@ "value": "Back" } ], + "backToIntegrations": [ + { + "type": 0, + "value": "Back to integrations status" + } + ], "breakdownBackToDetails": [ { "options": { @@ -7746,6 +7752,14 @@ } ] }, + "instance": { + "value": [ + { + "type": 0, + "value": "Filter by instance" + } + ] + }, "name": { "value": [ { @@ -7762,6 +7776,14 @@ } ] }, + "operating_system": { + "value": [ + { + "type": 0, + "value": "Filter by operating system" + } + ] + }, "org_unit_id": { "value": [ { @@ -10378,6 +10400,18 @@ "value": "Integration" } ], + "integrationsDetails": [ + { + "type": 0, + "value": "Integrations details" + } + ], + "integrationsStatus": [ + { + "type": 0, + "value": "Integrations status" + } + ], "lastProcessed": [ { "type": 0, @@ -10880,6 +10914,20 @@ "value": "No" } ], + "noCurrentData": [ + { + "type": 0, + "value": "No data available for " + }, + { + "type": 1, + "value": "dateRange" + }, + { + "type": 0, + "value": ". You are viewing data for the previous month." + } + ], "noDataForDate": [ { "type": 0, @@ -11807,12 +11855,6 @@ "value": "Rate must be a positive number" } ], - "providerDetails": [ - { - "type": 0, - "value": "Integrations details" - } - ], "pvcTitle": [ { "type": 0, @@ -13559,6 +13601,12 @@ "value": "vCPU" } ], + "viewAll": [ + { + "type": 0, + "value": "View all" + } + ], "volumeTitle": [ { "type": 0, diff --git a/locales/translations.json b/locales/translations.json index c6cb32396..4a951916d 100644 --- a/locales/translations.json +++ b/locales/translations.json @@ -19,6 +19,7 @@ "azureDetailsTitle": "Microsoft Azure Details", "azureOcpDashboardCostTitle": "Microsoft Azure filtered by OpenShift cost", "back": "Back", + "backToIntegrations": "Back to integrations status", "breakdownBackToDetails": "{groupBy, select, account {Back to {value} account details} aws_category {Back to {value} cost category details} cluster {Back to {value} cluster details} gcp_project {Back to {value} GCP project details} node {Back to {value} node details} org_unit_id {Back to {value} organizational unit details} payer_tenant_id {Back to {value} account details} product_service {Back to {value} service details} project {Back to {value} project details} region {Back to {value} region details} resource_location {Back to {value} region details} service {Back to {value} service details} service_name {Back to {value} service details} subscription_guid {Back to {value} account details} tag {Back to {value} tag details} other {}}", "breakdownBackToDetailsAriaLabel": "Back to details", "breakdownBackToOptimizations": "Back to optimizations", @@ -327,7 +328,7 @@ "filterByInputAriaLabel": "{value, select, account {Input for account name} aws_category {Input for cost category name} cluster {Input for cluster name} gcp_project {Input for GCP project name} name {Input for name} node {Input for node name} org_unit_id {Input for organizational unit name} payer_tenant_id {Input for account name} product_service {Input for service_name} project {Input for project name} region {Input for region name} resource_location {Input for region name} service {Input for service name} service_name {Input for service_name} subscription_guid {Input for account name} status {Input for status value} tag {Input for tag name} tag_key {Input for tag key} tag_key_child {Input for child tag key} tag_key_parent {Input for parent tag key} other {}}", "filterByOrgUnitAriaLabel": "Organizational units", "filterByOrgUnitPlaceholder": "Choose unit", - "filterByPlaceholder": "{value, select, account {Filter by account} aws_category {Filter by cost category} cluster {Filter by cluster} container {Filter by container} description {Filter by description} gcp_project {Filter by GCP project} group {Filter by group} name {Filter by name} node {Filter by node} org_unit_id {Filter by organizational unit} payer_tenant_id {Filter by account} persistent_volume_claim {Filter by persistent volume claim} product_service {Filter by service} project {Filter by project} region {Filter by region} resource_location {Filter by region} service {Filter by service} service_name {Filter by service} source_type {Filter by integration} status {Filter by status} storage_class {Filter by StorageClass} subscription_guid {Filter by account} workload {Filter by workload name} workload_type {Filter by workload type} tag {Filter by tag} tag_key {Filter by tag key} tag_key_child {Filter by child tag key} tag_key_parent {Filter by parent tag key} other {}}", + "filterByPlaceholder": "{value, select, account {Filter by account} aws_category {Filter by cost category} cluster {Filter by cluster} container {Filter by container} description {Filter by description} gcp_project {Filter by GCP project} group {Filter by group} instance {Filter by instance} name {Filter by name} node {Filter by node} operating_system {Filter by operating system} org_unit_id {Filter by organizational unit} payer_tenant_id {Filter by account} persistent_volume_claim {Filter by persistent volume claim} product_service {Filter by service} project {Filter by project} region {Filter by region} resource_location {Filter by region} service {Filter by service} service_name {Filter by service} source_type {Filter by integration} status {Filter by status} storage_class {Filter by StorageClass} subscription_guid {Filter by account} workload {Filter by workload name} workload_type {Filter by workload type} tag {Filter by tag} tag_key {Filter by tag key} tag_key_child {Filter by child tag key} tag_key_parent {Filter by parent tag key} other {}}", "filterByTagKeyAriaLabel": "Tag keys", "filterByTagValueAriaLabel": "Tag values", "filterByTagValueButtonAriaLabel": "Filter button for tag value", @@ -368,6 +369,8 @@ "infrastructure": "Infrastructure", "instances": "Instances", "integration": "Integration", + "integrationsDetails": "Integrations details", + "integrationsStatus": "Integrations status", "lastProcessed": "Last processed", "lastUpdated": "Last updated", "learnMore": "Learn more", @@ -403,6 +406,7 @@ "networkUnattributedDistributedDesc": "Costs associated with ingress and egress network traffic for individual nodes.", "next": "next", "no": "No", + "noCurrentData": "No data available for {dateRange}. You are viewing data for the previous month.", "noDataForDate": "No data available for {dateRange}", "noDataStateDesc": "We have detected an integration, but we are not done processing the incoming data. {status}The time to process could take up to 24 hours. Try refreshing the page at a later time.", "noDataStateRefresh": "Refresh this page", @@ -507,7 +511,6 @@ "priceListEmptyRateDesc": "To add rates to the price list, click on the \"Add\" rate button above.", "priceListNumberRate": "Rate must be a number", "priceListPosNumberRate": "Rate must be a positive number", - "providerDetails": "Integrations details", "pvcTitle": "Persistent Volume Claims", "rate": "Rate", "rawCostDesc": "The costs reported by a cloud provider without any cost model calculations applied.", @@ -634,6 +637,7 @@ "valueUnits": "{value} {units}", "various": "Various", "vcpuTitle": "vCPU", + "viewAll": "View all", "volumeTitle": "Volume", "workerUnallocated": "Worker unallocated", "workerUnallocatedDesc": "Distribute unused and non-reserved resource costs to projects", diff --git a/package-lock.json b/package-lock.json index cc2f88719..c3473b480 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,12 +17,12 @@ "@patternfly/react-icons": "5.4.0", "@patternfly/react-table": "5.4.1", "@patternfly/react-tokens": "5.4.0", - "@redhat-cloud-services/frontend-components": "^4.2.15", + "@redhat-cloud-services/frontend-components": "^4.2.16", "@redhat-cloud-services/frontend-components-notifications": "^4.1.0", "@redhat-cloud-services/frontend-components-translations": "^3.2.8", "@redhat-cloud-services/frontend-components-utilities": "^4.0.17", "@redhat-cloud-services/rbac-client": "^2.2.5", - "@reduxjs/toolkit": "^2.2.7", + "@reduxjs/toolkit": "^2.3.0", "@unleash/proxy-client-react": "^4.3.1", "axios": "^1.7.7", "date-fns": "^4.1.0", @@ -31,23 +31,23 @@ "qs": "^6.13.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-intl": "6.7.0", + "react-intl": "6.8.0", "react-redux": "^9.1.2", - "react-router-dom": "^6.26.2", + "react-router-dom": "^6.27.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "typesafe-actions": "^5.1.0", - "victory": "^37.1.1" + "victory": "^37.1.2" }, "devDependencies": { "@eslint/compat": "^1.2.0", "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.12.0", - "@formatjs/cli": "^6.2.12", - "@formatjs/ecma402-abstract": "^2.0.0", - "@formatjs/icu-messageformat-parser": "^2.7.8", + "@formatjs/ecma402-abstract": "^2.2.0", + "@formatjs/fast-memoize": "^2.2.1", + "@formatjs/intl-localematcher": "^0.5.5", "@redhat-cloud-services/eslint-config-redhat-cloud-services": "^2.0.4", - "@redhat-cloud-services/frontend-components-config": "^6.3.0", + "@redhat-cloud-services/frontend-components-config": "^6.3.1", "@redhat-cloud-services/tsc-transform-imports": "^1.0.16", "@swc/core": "^1.7.26", "@swc/jest": "^0.2.36", @@ -57,17 +57,16 @@ "@types/jest": "^29.5.13", "@types/qs": "^6.9.16", "@types/react": "^18.3.11", - "@types/react-dom": "^18.3.0", + "@types/react-dom": "^18.3.1", "@types/react-redux": "^7.1.34", "@types/react-router-dom": "^5.3.3", - "@typescript-eslint/eslint-plugin": "^8.8.0", - "@typescript-eslint/parser": "^8.8.0", - "aphrodite": "^2.4.0", + "@typescript-eslint/eslint-plugin": "^8.9.0", + "@typescript-eslint/parser": "^8.9.0", "copy-webpack-plugin": "^12.0.2", "eslint": "^9.12.0", - "eslint-plugin-formatjs": "^5.0.0", + "eslint-plugin-formatjs": "^5.1.0", "eslint-plugin-jest-dom": "^5.4.0", - "eslint-plugin-jsdoc": "^50.3.1", + "eslint-plugin-jsdoc": "^50.4.1", "eslint-plugin-markdown": "^5.1.0", "eslint-plugin-patternfly-react": "^5.4.0", "eslint-plugin-prettier": "^5.2.1", @@ -76,21 +75,19 @@ "eslint-plugin-sort-keys-fix": "^1.1.2", "eslint-plugin-testing-library": "^6.3.0", "git-revision-webpack-plugin": "^5.0.0", - "globals": "^15.10.0", + "globals": "^15.11.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-mock-axios": "^4.7.3", "jest-transform-stub": "^2.0.0", - "jws": "^4.0.0", "npm-run-all": "^4.1.5", "prettier": "^3.3.3", "rimraf": "^6.0.1", "swc_mut_cjs_exports": "^0.99.0", "ts-jest": "^29.2.5", "ts-patch": "^3.2.1", - "typescript": "^5.6.2", - "webpack-bundle-analyzer": "^4.10.2" + "typescript": "^5.6.3" }, "engines": { "node": ">=20.15.0", @@ -658,9 +655,9 @@ "integrity": "sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg==" }, "node_modules/@es-joy/jsdoccomment": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.48.0.tgz", - "integrity": "sha512-G6QUWIcC+KvSwXNsJyDTHvqUdNoAVJPPgkc3+Uk4WBKqZvoXhlvazOgm9aL0HwihJLQf0l+tOE2UFzXBqCqgDw==", + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.49.0.tgz", + "integrity": "sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==", "dev": true, "dependencies": { "comment-parser": "1.4.1", @@ -865,104 +862,56 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@formatjs/cli": { - "version": "6.2.12", - "resolved": "https://registry.npmjs.org/@formatjs/cli/-/cli-6.2.12.tgz", - "integrity": "sha512-bt1NEgkeYN8N9zWcpsPu3fZ57vv+biA+NtIQBlyOZnCp1bcvh+vNTXvmwF4C5qxqDtCylpOIb3yi3Ktgp4v0JQ==", - "dev": true, - "bin": { - "formatjs": "bin/formatjs" - }, - "engines": { - "node": ">= 16" - }, - "peerDependencies": { - "@glimmer/env": "^0.1.7", - "@glimmer/reference": "^0.91.1 || ^0.92.0", - "@glimmer/syntax": "^0.92.0", - "@glimmer/validator": "^0.92.0", - "@vue/compiler-core": "^3.4.0", - "content-tag": "^2.0.1", - "ember-template-recast": "^6.1.4", - "vue": "^3.4.0" - }, - "peerDependenciesMeta": { - "@glimmer/env": { - "optional": true - }, - "@glimmer/reference": { - "optional": true - }, - "@glimmer/syntax": { - "optional": true - }, - "@glimmer/validator": { - "optional": true - }, - "@vue/compiler-core": { - "optional": true - }, - "content-tag": { - "optional": true - }, - "ember-template-recast": { - "optional": true - }, - "vue": { - "optional": true - } - } - }, "node_modules/@formatjs/ecma402-abstract": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz", - "integrity": "sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.0.tgz", + "integrity": "sha512-IpM+ev1E4QLtstniOE29W1rqH9eTdx5hQdNL8pzrflMj/gogfaoONZqL83LUeQScHAvyMbpqP5C9MzNf+fFwhQ==", "dependencies": { - "@formatjs/intl-localematcher": "0.5.4", - "tslib": "^2.4.0" + "@formatjs/fast-memoize": "2.2.1", + "@formatjs/intl-localematcher": "0.5.5", + "tslib": "^2.7.0" } }, "node_modules/@formatjs/fast-memoize": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz", - "integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.1.tgz", + "integrity": "sha512-XS2RcOSyWxmUB7BUjj3mlPH0exsUzlf6QfhhijgI941WaJhVxXQ6mEWkdUFIdnKi3TuTYxRdelsgv3mjieIGIA==", "license": "MIT", "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.7.0" } }, "node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.7.8", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.8.tgz", - "integrity": "sha512-nBZJYmhpcSX0WeJ5SDYUkZ42AgR3xiyhNCsQweFx3cz/ULJjym8bHAzWKvG5e2+1XO98dBYC0fWeeAECAVSwLA==", + "version": "2.7.10", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.10.tgz", + "integrity": "sha512-wlQfqCZ7PURkUNL2+8VTEFavPovtADU/isSKLFvDbdFmV7QPZIYqFMkhklaDYgMyLSBJa/h2MVQ2aFvoEJhxgg==", "dependencies": { - "@formatjs/ecma402-abstract": "2.0.0", - "@formatjs/icu-skeleton-parser": "1.8.2", - "tslib": "^2.4.0" + "@formatjs/ecma402-abstract": "2.2.0", + "@formatjs/icu-skeleton-parser": "1.8.4", + "tslib": "^2.7.0" } }, "node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.2.tgz", - "integrity": "sha512-k4ERKgw7aKGWJZgTarIcNEmvyTVD9FYh0mTrrBMHZ1b8hUu6iOJ4SzsZlo3UNAvHYa+PnvntIwRPt1/vy4nA9Q==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.4.tgz", + "integrity": "sha512-LMQ1+Wk1QSzU4zpd5aSu7+w5oeYhupRwZnMQckLPRYhSjf2/8JWQ882BauY9NyHxs5igpuQIXZDgfkaH3PoATg==", "dependencies": { - "@formatjs/ecma402-abstract": "2.0.0", - "tslib": "^2.4.0" + "@formatjs/ecma402-abstract": "2.2.0", + "tslib": "^2.7.0" } }, "node_modules/@formatjs/intl": { - "version": "2.10.5", - "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.10.5.tgz", - "integrity": "sha512-f9qPNNgLrh2KvoFvHGIfcPTmNGbyy7lyyV4/P6JioDqtTE7Akdmgt+ZzVndr+yMLZnssUShyTMXxM/6aV9eVuQ==", - "license": "MIT", - "dependencies": { - "@formatjs/ecma402-abstract": "2.0.0", - "@formatjs/fast-memoize": "2.2.0", - "@formatjs/icu-messageformat-parser": "2.7.8", - "@formatjs/intl-displaynames": "6.6.8", - "@formatjs/intl-listformat": "7.5.7", - "intl-messageformat": "10.5.14", - "tslib": "^2.4.0" + "version": "2.10.8", + "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.10.8.tgz", + "integrity": "sha512-eY8r8RMmrRTTkLdbNBOZLFGXN3OnrEmInaNt8s4msIVfo+xuLqAqvB3W1jevj0I9QjU6ueIP7tEk+1rj6Xbv5A==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.2.0", + "@formatjs/fast-memoize": "2.2.1", + "@formatjs/icu-messageformat-parser": "2.7.10", + "@formatjs/intl-displaynames": "6.6.10", + "@formatjs/intl-listformat": "7.5.9", + "intl-messageformat": "10.7.0", + "tslib": "^2.7.0" }, "peerDependencies": { "typescript": "^4.7 || 5" @@ -974,47 +923,46 @@ } }, "node_modules/@formatjs/intl-displaynames": { - "version": "6.6.8", - "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.6.8.tgz", - "integrity": "sha512-Lgx6n5KxN16B3Pb05z3NLEBQkGoXnGjkTBNCZI+Cn17YjHJ3fhCeEJJUqRlIZmJdmaXQhjcQVDp6WIiNeRYT5g==", - "license": "MIT", + "version": "6.6.10", + "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.6.10.tgz", + "integrity": "sha512-tUz5qT61og1WwMM0K1/p46J69gLl1YJbty8xhtbigDA9LhbBmW2ShDg4ld+aqJTwCq4WK3Sj0VlFCKvFYeY3rQ==", "dependencies": { - "@formatjs/ecma402-abstract": "2.0.0", - "@formatjs/intl-localematcher": "0.5.4", - "tslib": "^2.4.0" + "@formatjs/ecma402-abstract": "2.2.0", + "@formatjs/intl-localematcher": "0.5.5", + "tslib": "^2.7.0" } }, "node_modules/@formatjs/intl-listformat": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.5.7.tgz", - "integrity": "sha512-MG2TSChQJQT9f7Rlv+eXwUFiG24mKSzmF144PLb8m8OixyXqn4+YWU+5wZracZGCgVTVmx8viCf7IH3QXoiB2g==", - "license": "MIT", + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.5.9.tgz", + "integrity": "sha512-HqtGkxUh2Uz0oGVTxHAvPZ3EGxc8+ol5+Bx7S9xB97d4PEJJd9oOgHrerIghHA0gtIjsNKBFUae3P0My+F6YUA==", "dependencies": { - "@formatjs/ecma402-abstract": "2.0.0", - "@formatjs/intl-localematcher": "0.5.4", - "tslib": "^2.4.0" + "@formatjs/ecma402-abstract": "2.2.0", + "@formatjs/intl-localematcher": "0.5.5", + "tslib": "^2.7.0" } }, "node_modules/@formatjs/intl-localematcher": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz", - "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.5.tgz", + "integrity": "sha512-t5tOGMgZ/i5+ALl2/offNqAQq/lfUnKLEw0mXQI4N4bqpedhrSE+fyKLpwnd22sK0dif6AV+ufQcTsKShB9J1g==", + "license": "MIT", "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.7.0" } }, "node_modules/@formatjs/ts-transformer": { - "version": "3.13.14", - "resolved": "https://registry.npmjs.org/@formatjs/ts-transformer/-/ts-transformer-3.13.14.tgz", - "integrity": "sha512-TP/R54lxQ9Drzzimxrrt6yBT/xBofTgYl5wSTpyKe3Aq9vIBVcFmS6EOqycj0X34KGu3EpDPGO0ng8ZQZGLIFg==", + "version": "3.13.16", + "resolved": "https://registry.npmjs.org/@formatjs/ts-transformer/-/ts-transformer-3.13.16.tgz", + "integrity": "sha512-ZIV7KB2EQ5w9k7yrwSsdGdoOgqlXNd2sfG317pbJPHDgIo04sxoRzZPayCiNo7VWaRyqkVYUpME94rd54FDvuw==", "dev": true, "dependencies": { - "@formatjs/icu-messageformat-parser": "2.7.8", + "@formatjs/icu-messageformat-parser": "2.7.10", "@types/json-stable-stringify": "^1.0.32", - "@types/node": "14 || 16 || 17", + "@types/node": "14 || 16 || 17 || 18", "chalk": "^4.0.0", "json-stable-stringify": "^1.0.1", - "tslib": "^2.4.0", + "tslib": "^2.7.0", "typescript": "5" }, "peerDependencies": { @@ -1027,10 +975,13 @@ } }, "node_modules/@formatjs/ts-transformer/node_modules/@types/node": { - "version": "17.0.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", - "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", - "dev": true + "version": "18.19.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.55.tgz", + "integrity": "sha512-zzw5Vw52205Zr/nmErSEkN5FLqXPuKX/k5d1D7RKHATGqU7y6YfX9QxZraUzUrFGqH6XzOzG196BC35ltJC4Cw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@formatjs/ts-transformer/node_modules/ansi-styles": { "version": "4.3.0", @@ -2309,12 +2260,6 @@ "react-dom": "^17 || ^18" } }, - "node_modules/@patternfly/react-charts/node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "license": "0BSD" - }, "node_modules/@patternfly/react-component-groups": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/@patternfly/react-component-groups/-/react-component-groups-5.4.0.tgz", @@ -2350,12 +2295,6 @@ "react-dom": "^17 || ^18" } }, - "node_modules/@patternfly/react-core/node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "license": "0BSD" - }, "node_modules/@patternfly/react-icons": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-5.4.0.tgz", @@ -2390,12 +2329,6 @@ "react-dom": "^17 || ^18" } }, - "node_modules/@patternfly/react-table/node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "license": "0BSD" - }, "node_modules/@patternfly/react-tokens": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-5.4.0.tgz", @@ -2530,12 +2463,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/@polka/url": { - "version": "1.0.0-next.25", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", - "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==", - "dev": true - }, "node_modules/@redhat-cloud-services/eslint-config-redhat-cloud-services": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@redhat-cloud-services/eslint-config-redhat-cloud-services/-/eslint-config-redhat-cloud-services-2.0.4.tgz", @@ -2591,10 +2518,9 @@ } }, "node_modules/@redhat-cloud-services/frontend-components": { - "version": "4.2.15", - "resolved": "https://registry.npmjs.org/@redhat-cloud-services/frontend-components/-/frontend-components-4.2.15.tgz", - "integrity": "sha512-52bu9XFZCosQ7QUzLiA4NNYWWas14Gir3/OBgxWlKQ8UayMfiBP9nuwvelDfC/jU8jLdYVbAznkNzbxQdhTKiw==", - "license": "Apache-2.0", + "version": "4.2.16", + "resolved": "https://registry.npmjs.org/@redhat-cloud-services/frontend-components/-/frontend-components-4.2.16.tgz", + "integrity": "sha512-xtCSn4dCeyooG8czEZThoA9YWjghaXAqWt2JZVLmGdk9Gejhfco0l5Of/O3hfXgnPzXqRfOZJDA0jNVo1qKrTw==", "dependencies": { "@patternfly/react-component-groups": "^5.0.0", "@redhat-cloud-services/frontend-components-utilities": "^4.0.0", @@ -2602,7 +2528,7 @@ "@scalprum/core": "^0.8.1", "@scalprum/react-core": "^0.9.1", "classnames": "^2.2.5", - "sanitize-html": "^2.12.1" + "sanitize-html": "^2.13.1" }, "peerDependencies": { "@patternfly/react-core": "^5.0.0", @@ -2618,9 +2544,9 @@ } }, "node_modules/@redhat-cloud-services/frontend-components-config": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@redhat-cloud-services/frontend-components-config/-/frontend-components-config-6.3.0.tgz", - "integrity": "sha512-BXoTUI5k3WUOVIc7VS9HadTr83tvudKxslSpgsiQuG6wgSM7LtbHdO5qmI9fD0W7kFyh2acwvHhj7oZlEdmL5w==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@redhat-cloud-services/frontend-components-config/-/frontend-components-config-6.3.1.tgz", + "integrity": "sha512-WLItTdGoIrc0s7QsrjGpcxELQljHSZXrPCTyf9y9fv1QopOtQRoWSIAMolDzABquWQ98HPeMyz4XsJRF7PCX1Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2954,9 +2880,10 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.7.tgz", - "integrity": "sha512-faI3cZbSdFb8yv9dhDTmGwclW0vk0z5o1cia+kf7gCbaCwHI5e+7tP57mJUv22pNcNbeA62GSrPpfrUfdXcQ6g==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.3.0.tgz", + "integrity": "sha512-WC7Yd6cNGfHx8zf+iu+Q1UPTfEcXhQ+ATi7CV1hlrSAaQBdlPzg7Ww/wJHNQem7qG9rxmWoFCDCPubSvFObGzA==", + "license": "MIT", "dependencies": { "immer": "^10.0.3", "redux": "^5.0.1", @@ -2977,9 +2904,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz", - "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", + "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -4107,9 +4034,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", "dev": true, "dependencies": { "@types/react": "*" @@ -4253,17 +4180,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz", - "integrity": "sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.9.0.tgz", + "integrity": "sha512-Y1n621OCy4m7/vTXNlCbMVp87zSd7NH0L9cXD8aIpOaNlzeWxIK4+Q19A68gSmTNRZn92UjocVUWDthGxtqHFg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.8.0", - "@typescript-eslint/type-utils": "8.8.0", - "@typescript-eslint/utils": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0", + "@typescript-eslint/scope-manager": "8.9.0", + "@typescript-eslint/type-utils": "8.9.0", + "@typescript-eslint/utils": "8.9.0", + "@typescript-eslint/visitor-keys": "8.9.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -4287,16 +4214,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.0.tgz", - "integrity": "sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.9.0.tgz", + "integrity": "sha512-U+BLn2rqTTHnc4FL3FJjxaXptTxmf9sNftJK62XLz4+GxG3hLHm/SUNaaXP5Y4uTiuYoL5YLy4JBCJe3+t8awQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.8.0", - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/typescript-estree": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0", + "@typescript-eslint/scope-manager": "8.9.0", + "@typescript-eslint/types": "8.9.0", + "@typescript-eslint/typescript-estree": "8.9.0", + "@typescript-eslint/visitor-keys": "8.9.0", "debug": "^4.3.4" }, "engines": { @@ -4316,14 +4243,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz", - "integrity": "sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.9.0.tgz", + "integrity": "sha512-bZu9bUud9ym1cabmOYH9S6TnbWRzpklVmwqICeOulTCZ9ue2/pczWzQvt/cGj2r2o1RdKoZbuEMalJJSYw3pHQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0" + "@typescript-eslint/types": "8.9.0", + "@typescript-eslint/visitor-keys": "8.9.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4334,14 +4261,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz", - "integrity": "sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.9.0.tgz", + "integrity": "sha512-JD+/pCqlKqAk5961vxCluK+clkppHY07IbV3vett97KOV+8C6l+CPEPwpUuiMwgbOz/qrN3Ke4zzjqbT+ls+1Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.8.0", - "@typescript-eslint/utils": "8.8.0", + "@typescript-eslint/typescript-estree": "8.9.0", + "@typescript-eslint/utils": "8.9.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -4359,9 +4286,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz", - "integrity": "sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.9.0.tgz", + "integrity": "sha512-SjgkvdYyt1FAPhU9c6FiYCXrldwYYlIQLkuc+LfAhCna6ggp96ACncdtlbn8FmnG72tUkXclrDExOpEYf1nfJQ==", "dev": true, "license": "MIT", "engines": { @@ -4373,14 +4300,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz", - "integrity": "sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.9.0.tgz", + "integrity": "sha512-9iJYTgKLDG6+iqegehc5+EqE6sqaee7kb8vWpmHZ86EqwDjmlqNNHeqDVqb9duh+BY6WCNHfIGvuVU3Tf9Db0g==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0", + "@typescript-eslint/types": "8.9.0", + "@typescript-eslint/visitor-keys": "8.9.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -4415,16 +4342,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz", - "integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.9.0.tgz", + "integrity": "sha512-PKgMmaSo/Yg/F7kIZvrgrWa1+Vwn036CdNUvYFEkYbPwOH4i8xvkaRlu148W3vtheWK9ckKRIz7PBP5oUlkrvQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.8.0", - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/typescript-estree": "8.8.0" + "@typescript-eslint/scope-manager": "8.9.0", + "@typescript-eslint/types": "8.9.0", + "@typescript-eslint/typescript-estree": "8.9.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4438,13 +4365,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.0.tgz", - "integrity": "sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.9.0.tgz", + "integrity": "sha512-Ht4y38ubk4L5/U8xKUBfKNYGmvKvA1CANoxiTRMM+tOLk3lbF3DvzZCxJCRSE+2GdCMSh6zq9VZJc3asc1XuAA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/types": "8.9.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -4903,17 +4830,6 @@ "node": ">= 8" } }, - "node_modules/aphrodite": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/aphrodite/-/aphrodite-2.4.0.tgz", - "integrity": "sha512-1rVRlLco+j1YAT5aKEE8Wuw5zWV+tI41/quEheJAG0vNaGHE64iJ/a2SiVMz8Uc80VdP2/Hjlfd2bPJOWsqJuQ==", - "dev": true, - "dependencies": { - "asap": "^2.0.3", - "inline-style-prefixer": "^5.1.0", - "string-hash": "^1.1.3" - } - }, "node_modules/are-docs-informative": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", @@ -5132,12 +5048,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true - }, "node_modules/assert": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", @@ -6488,11 +6398,10 @@ "dev": true }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -6786,16 +6695,6 @@ "node": ">= 8" } }, - "node_modules/css-in-js-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz", - "integrity": "sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==", - "dev": true, - "dependencies": { - "hyphenate-style-name": "^1.0.2", - "isobject": "^3.0.1" - } - }, "node_modules/css-jss": { "version": "10.10.0", "resolved": "https://registry.npmjs.org/css-jss/-/css-jss-10.10.0.tgz", @@ -7159,12 +7058,6 @@ "url": "https://github.com/sponsors/kossnocorp" } }, - "node_modules/debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", - "dev": true - }, "node_modules/debounce-promise": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/debounce-promise/-/debounce-promise-3.1.2.tgz", @@ -7621,12 +7514,6 @@ "tslib": "^2.0.3" } }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -8279,20 +8166,20 @@ } }, "node_modules/eslint-plugin-formatjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-formatjs/-/eslint-plugin-formatjs-5.0.0.tgz", - "integrity": "sha512-ZIQGwa2mF6MU2AWzRi1aigaBzFMRESJfp+KRC7Tm0qm13UTG0GsDkDsxFruf2y/acrbusAvUwkuSwHmyTtVNmw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-formatjs/-/eslint-plugin-formatjs-5.1.0.tgz", + "integrity": "sha512-IN3/MKq5XumXqgta7OnvixcEoQCyxLAsNVwzsOQIqQqughcHwkvv7JHiIurt+7V6IjBYDyfTiH7opZggR+t+7w==", "dev": true, "dependencies": { - "@formatjs/icu-messageformat-parser": "2.7.8", - "@formatjs/ts-transformer": "3.13.14", + "@formatjs/icu-messageformat-parser": "2.7.10", + "@formatjs/ts-transformer": "3.13.16", "@types/eslint": "9", "@types/picomatch": "^2.3.0", "@typescript-eslint/utils": "8.5.0", "emoji-regex": "^10.2.1", "magic-string": "^0.30.0", "picomatch": "^2.3.1", - "tslib": "2.6.2", + "tslib": "^2.7.0", "typescript": "5", "unicode-emoji-utils": "^1.2.0" }, @@ -8690,13 +8577,13 @@ "dev": true }, "node_modules/eslint-plugin-jsdoc": { - "version": "50.3.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.3.1.tgz", - "integrity": "sha512-SY9oUuTMr6aWoJggUS40LtMjsRzJPB5ZT7F432xZIHK3EfHF+8i48GbUBpwanrtlL9l1gILNTHK9o8gEhYLcKA==", + "version": "50.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.4.1.tgz", + "integrity": "sha512-OXIq+JJQPCLAKL473/esioFOwbXyRE5MAQ4HbZjcp3e+K3zdxt2uDpGs3FR+WezUXNStzEtTfgx15T+JFrVwBA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@es-joy/jsdoccomment": "~0.48.0", + "@es-joy/jsdoccomment": "~0.49.0", "are-docs-informative": "^0.0.2", "comment-parser": "1.4.1", "debug": "^4.3.6", @@ -9704,18 +9591,17 @@ } }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, - "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -10541,9 +10427,9 @@ } }, "node_modules/globals": { - "version": "15.10.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.10.0.tgz", - "integrity": "sha512-tqFIbz83w4Y5TCbtgjZjApohbuh7K9BxGYFm7ifwDR240tvdb7P9x+/9VvUKlmkPoiknoJtanI8UOrqxS3a7lQ==", + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz", + "integrity": "sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==", "dev": true, "license": "MIT", "engines": { @@ -10611,21 +10497,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "dev": true, - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -11288,15 +11159,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "node_modules/inline-style-prefixer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-5.1.2.tgz", - "integrity": "sha512-PYUF+94gDfhy+LsQxM0g3d6Hge4l1pAqOSOiZuHWzMvQEGsbRQ/ck2WioLqrY2ZkHyPgVUXxn+hrkF7D6QUGbA==", - "dev": true, - "dependencies": { - "css-in-js-utils": "^2.0.0" - } - }, "node_modules/inquirer": { "version": "8.2.6", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", @@ -11426,15 +11288,14 @@ } }, "node_modules/intl-messageformat": { - "version": "10.5.14", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.14.tgz", - "integrity": "sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w==", - "license": "BSD-3-Clause", + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.0.tgz", + "integrity": "sha512-2P06M9jFTqJnEQzE072VGPjbAx6ZG1YysgopAwc8ui0ajSjtwX1MeQ6bXFXIzKcNENJTizKkcJIcZ0zlpl1zSg==", "dependencies": { - "@formatjs/ecma402-abstract": "2.0.0", - "@formatjs/fast-memoize": "2.2.0", - "@formatjs/icu-messageformat-parser": "2.7.8", - "tslib": "^2.4.0" + "@formatjs/ecma402-abstract": "2.2.0", + "@formatjs/fast-memoize": "2.2.1", + "@formatjs/icu-messageformat-parser": "2.7.10", + "tslib": "^2.7.0" } }, "node_modules/ipaddr.js": { @@ -15409,15 +15270,6 @@ "node": ">=10" } }, - "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -16985,21 +16837,20 @@ "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" }, "node_modules/react-intl": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.7.0.tgz", - "integrity": "sha512-f5QhjuKb+WEqiAbL5hDqUs2+sSRkF0vxkTbJ4A8ompt55XTyOHcrDlCXGq4o73ywFFrpgz+78C9IXegSLlya2A==", - "license": "BSD-3-Clause", - "dependencies": { - "@formatjs/ecma402-abstract": "2.0.0", - "@formatjs/icu-messageformat-parser": "2.7.8", - "@formatjs/intl": "2.10.5", - "@formatjs/intl-displaynames": "6.6.8", - "@formatjs/intl-listformat": "7.5.7", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.8.0.tgz", + "integrity": "sha512-rx/UcAtlmrYWaPfrgAIDu7VsJoyUPFPdftIFUnOSOj/LHR6ACTU3tunfk69c4LGygQ592YxilBXDWH6rKlTu6Q==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.2.0", + "@formatjs/icu-messageformat-parser": "2.7.10", + "@formatjs/intl": "2.10.8", + "@formatjs/intl-displaynames": "6.6.10", + "@formatjs/intl-listformat": "7.5.9", "@types/hoist-non-react-statics": "^3.3.1", - "@types/react": "16 || 17 || 18", + "@types/react": "^18.3.11", "hoist-non-react-statics": "^3.3.2", - "intl-messageformat": "10.5.14", - "tslib": "^2.4.0" + "intl-messageformat": "10.7.0", + "tslib": "^2.7.0" }, "peerDependencies": { "react": "^16.6.0 || 17 || 18", @@ -17070,12 +16921,12 @@ } }, "node_modules/react-router": { - "version": "6.26.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz", - "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", + "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.19.2" + "@remix-run/router": "1.20.0" }, "engines": { "node": ">=14.0.0" @@ -17085,13 +16936,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.26.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz", - "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz", + "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.19.2", - "react-router": "6.26.2" + "@remix-run/router": "1.20.0", + "react-router": "6.27.0" }, "engines": { "node": ">=14.0.0" @@ -17627,9 +17478,9 @@ "dev": true }, "node_modules/sanitize-html": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.12.1.tgz", - "integrity": "sha512-Plh+JAn0UVDpBRP/xEjsk+xDCoOvMBwQUf/K+/cBAVuTbtX8bj2VB7S1sL1dssVpykqp0/KPSesHrqXtokVBpA==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.1.tgz", + "integrity": "sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==", "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", @@ -18112,20 +17963,6 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", - "dev": true, - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -18384,12 +18221,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", - "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==", - "dev": true - }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -18973,15 +18804,6 @@ "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/tough-cookie": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", @@ -19394,9 +19216,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -19549,9 +19371,9 @@ } }, "node_modules/typescript": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", - "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -19842,386 +19664,386 @@ } }, "node_modules/victory": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory/-/victory-37.1.1.tgz", - "integrity": "sha512-3tyIZ79YVd9bxS3KocGa6UuQdCA4Kenqzh3Th7QBB7Am96MHXVyePsYwhg0KorOmKqocQxYgLShGIjEHT1Qv+w==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory/-/victory-37.1.2.tgz", + "integrity": "sha512-V1YkJiWQ/vu5MSZ/Yf9/AJQeE+N1p1eUW6r5xJgOjbhioIbnL7FBTyJW1AXqqDZN9WdyECI3TkFQ1l/QbgztoA==", "license": "MIT", "dependencies": { - "victory-area": "37.1.1", - "victory-axis": "37.1.1", - "victory-bar": "37.1.1", - "victory-box-plot": "37.1.1", - "victory-brush-container": "37.1.1", - "victory-brush-line": "37.1.1", - "victory-candlestick": "37.1.1", - "victory-canvas": "37.1.1", - "victory-chart": "37.1.1", - "victory-core": "37.1.1", - "victory-create-container": "37.1.1", - "victory-cursor-container": "37.1.1", - "victory-errorbar": "37.1.1", - "victory-group": "37.1.1", - "victory-histogram": "37.1.1", - "victory-legend": "37.1.1", - "victory-line": "37.1.1", - "victory-pie": "37.1.1", - "victory-polar-axis": "37.1.1", - "victory-scatter": "37.1.1", - "victory-selection-container": "37.1.1", - "victory-shared-events": "37.1.1", - "victory-stack": "37.1.1", - "victory-tooltip": "37.1.1", - "victory-voronoi": "37.1.1", - "victory-voronoi-container": "37.1.1", - "victory-zoom-container": "37.1.1" + "victory-area": "37.1.2", + "victory-axis": "37.1.2", + "victory-bar": "37.1.2", + "victory-box-plot": "37.1.2", + "victory-brush-container": "37.1.2", + "victory-brush-line": "37.1.2", + "victory-candlestick": "37.1.2", + "victory-canvas": "37.1.2", + "victory-chart": "37.1.2", + "victory-core": "37.1.2", + "victory-create-container": "37.1.2", + "victory-cursor-container": "37.1.2", + "victory-errorbar": "37.1.2", + "victory-group": "37.1.2", + "victory-histogram": "37.1.2", + "victory-legend": "37.1.2", + "victory-line": "37.1.2", + "victory-pie": "37.1.2", + "victory-polar-axis": "37.1.2", + "victory-scatter": "37.1.2", + "victory-selection-container": "37.1.2", + "victory-shared-events": "37.1.2", + "victory-stack": "37.1.2", + "victory-tooltip": "37.1.2", + "victory-voronoi": "37.1.2", + "victory-voronoi-container": "37.1.2", + "victory-zoom-container": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-area": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-area/-/victory-area-37.1.1.tgz", - "integrity": "sha512-9OVILTIT5DW/BsMksZ1xCjmNrT0iIhsHnumeNJDvvfzWUeqLyYPwmqp8e2wRraj1VRhRAAgZGXAHi7XA3rJkgQ==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-area/-/victory-area-37.1.2.tgz", + "integrity": "sha512-72i02xTD47i7P+X02AHhZ32yO16VcM1h/7gulgAioLEx+8m3zShBKu46Md/vqmbyS2Bypr3xpUvd+8mCDIvCbw==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", - "victory-core": "37.1.1", - "victory-vendor": "37.1.1" + "victory-core": "37.1.2", + "victory-vendor": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-axis": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-axis/-/victory-axis-37.1.1.tgz", - "integrity": "sha512-LqlXoAHNxvS/GdAKR6YSHZf0I9egMZf84kqUb7dG3NNLE8M1XnaEkYlfIOJsL+vsZJqm4kqoe67yI56eqIY5Hw==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-axis/-/victory-axis-37.1.2.tgz", + "integrity": "sha512-TuivC84cHrFoDetWDhU2VXQ34froIXBrtjYYPdmwBrMEFSu+FfrakYWUr3r25XNEPyOyk4z3a8lL/sqrxWYSRQ==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", - "victory-core": "37.1.1" + "victory-core": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-bar": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-bar/-/victory-bar-37.1.1.tgz", - "integrity": "sha512-1e1QtVDMgFRwXZDrt9nT1Fqv57yHL9Z9ssA2mgyzV/wi/HRneuUXE958Q/t59z4cTEkRYwNrUE3dODBCpxXMKw==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-bar/-/victory-bar-37.1.2.tgz", + "integrity": "sha512-VJDE+TGSgyIchvln189cPMuG3LYqa8zCjHa8kYValP3bFTa5c+D1Y8R/FjVger40uEL3aQz1teHJODJCOxuXGA==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", - "victory-core": "37.1.1", - "victory-vendor": "37.1.1" + "victory-core": "37.1.2", + "victory-vendor": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-box-plot": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-box-plot/-/victory-box-plot-37.1.1.tgz", - "integrity": "sha512-cdmAxg1Sqt/c2lbPJdD8+4qBNj8UMav8fLtsGd/uCNHWYzv52+0g9B8ToE6ImsKyBFRGnW+c0BD5vKbtyW6tJw==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-box-plot/-/victory-box-plot-37.1.2.tgz", + "integrity": "sha512-i7JIjpaPTr3uaoW6ibfX4PrH1QcUeLXNxeCbmPRb+Hs+ug0d16J4RELPCaeNo/ZNg4rEzfueNTvExsYFIpHaWw==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", - "victory-core": "37.1.1", - "victory-vendor": "37.1.1" + "victory-core": "37.1.2", + "victory-vendor": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-brush-container": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-brush-container/-/victory-brush-container-37.1.1.tgz", - "integrity": "sha512-iZkp/r7uzkc7UN3EgAWe4aDDEFHe7BQs0nv/mmyFeFYIXG5e2uiKs28OsZnfgp6CDIHDqUoV8DAGOccotUbUaQ==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-brush-container/-/victory-brush-container-37.1.2.tgz", + "integrity": "sha512-pJrMSo815UJxOT5OTXnq1tI5qQxQLnrlgDRNF8pxVF9dSxm7BhETjZSQfZgcLmCe3N931U19j8oCxw8sMSpJJw==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", "react-fast-compare": "^3.2.0", - "victory-core": "37.1.1" + "victory-core": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-brush-line": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-brush-line/-/victory-brush-line-37.1.1.tgz", - "integrity": "sha512-nsuJW7VFYFO2R+i0wveC4nizOhLj/UcTHwv98J6PYt3c0LQXa04YMFOfrRuKV/+Qsrj4DOVO3/GU6/PSUwozlQ==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-brush-line/-/victory-brush-line-37.1.2.tgz", + "integrity": "sha512-Bq9JGu/o4p/NQ/ZOASUm6MmomS+2b0EvAHjULa06z7nsElNePpedTYPk2aAb7mr4sJZe6u/AsDMthG+C8Zc32Q==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", "react-fast-compare": "^3.2.0", - "victory-core": "37.1.1" + "victory-core": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-candlestick": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-candlestick/-/victory-candlestick-37.1.1.tgz", - "integrity": "sha512-M5ftMbFi8HM9QYLrPb1DfrHOYKCwnDkxe8ct8MjE2ibsnKNCxUrwjJbkh0QXPa4ndk5y4jl98T9FmJS1Q14nPg==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-candlestick/-/victory-candlestick-37.1.2.tgz", + "integrity": "sha512-X+pLwvdIj/+nrvk1bZxhdJ9UBj7QLN4jdkIPDl6ekjfZ9Ylhi8/I/ttAkBu+7w7ilpGudIK6fr7PVHyZyYU6TA==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", - "victory-core": "37.1.1" + "victory-core": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-canvas": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-canvas/-/victory-canvas-37.1.1.tgz", - "integrity": "sha512-nq+du3x2D8sdRfNNg2idieElJbwq7vI2DO5FoFyFyowX6plXjOXoJZAOX/+7GTBQ4FP7tktNka5AQ9z8u5Sxbw==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-canvas/-/victory-canvas-37.1.2.tgz", + "integrity": "sha512-4Qmz7YpFBj2KaBSe+j5zLVrKAJLG3HtXVVaKI3oUzw4GzHlYXf77dJLYe2EqJVEFCMgVsmASqE3xVTklioMV7g==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", - "victory-bar": "37.1.1", - "victory-core": "37.1.1" + "victory-bar": "37.1.2", + "victory-core": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-chart": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-chart/-/victory-chart-37.1.1.tgz", - "integrity": "sha512-p//04lKzUX1ocXmp9RWmQMOsQUcP7m1CsrYkBOvqzD1sjgMhDzTqZdn38rMUzW0bpbCs0Tl6wbOzxMN+/PA8fQ==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-chart/-/victory-chart-37.1.2.tgz", + "integrity": "sha512-efV7lnqwu4+zLzB6aY3jjYbbfYJ9+1VC6uwx+8AGjbb8vGkbByUOKC6Fhdcuca2mLqNQHM0Ynvevs3+4XrBz1g==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", "react-fast-compare": "^3.2.0", - "victory-axis": "37.1.1", - "victory-core": "37.1.1", - "victory-polar-axis": "37.1.1", - "victory-shared-events": "37.1.1" + "victory-axis": "37.1.2", + "victory-core": "37.1.2", + "victory-polar-axis": "37.1.2", + "victory-shared-events": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-core": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-core/-/victory-core-37.1.1.tgz", - "integrity": "sha512-4UK1S1+9CFBn1Nwu18JsOf2EtaTI/DOE4Eoi5byLd6kFO8/luSbaLvc7BDPxiLpSj0BGiX/Hbqs12T2gPaEnAA==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-core/-/victory-core-37.1.2.tgz", + "integrity": "sha512-9fskAQw9MvYEBL+0cDk2lihKyECdrh+8HGicDfSKGOWtsSLk+x7R6PKCpOzhmSgc9fG+HjWYfs2uTWSPNTjF9A==", "license": "MIT", "dependencies": { "lodash": "^4.17.21", "react-fast-compare": "^3.2.0", - "victory-vendor": "37.1.1" + "victory-vendor": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-create-container": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-create-container/-/victory-create-container-37.1.1.tgz", - "integrity": "sha512-t/soXK97TcP4yxHYwvfCWJW9jGlRyYS4zdhjLe9Q2iETY0ngiVk+bpETZVPMgubPxq3JPaogMQKgd+1hDWjBMg==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-create-container/-/victory-create-container-37.1.2.tgz", + "integrity": "sha512-GvWA+N3SXf6he+hW1IQqaRjKG7XSV9WBr06mZixRVyOeHJGGZ5g7Vsse1WrwUz5/DM8HcqF34PTfhTs39v6zaw==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", - "victory-brush-container": "37.1.1", - "victory-core": "37.1.1", - "victory-cursor-container": "37.1.1", - "victory-selection-container": "37.1.1", - "victory-voronoi-container": "37.1.1", - "victory-zoom-container": "37.1.1" + "victory-brush-container": "37.1.2", + "victory-core": "37.1.2", + "victory-cursor-container": "37.1.2", + "victory-selection-container": "37.1.2", + "victory-voronoi-container": "37.1.2", + "victory-zoom-container": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-cursor-container": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-cursor-container/-/victory-cursor-container-37.1.1.tgz", - "integrity": "sha512-m2YS7nmAcGHatVhuqjuJW7jXRXutI0e1pBz9PbHm692HNAJbMfFTJAKtgPXUj5wYVae4OAr6f0551/ekkcL7xQ==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-cursor-container/-/victory-cursor-container-37.1.2.tgz", + "integrity": "sha512-GqOVB/Emas/ODw7Sb7FX1FmUyH3jb5eNF+2sR+DdYfDMTFmjVUyqGkSpi1bIgHoSWTrdG9C2tkxA69gI9JDtLA==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", - "victory-core": "37.1.1" + "victory-core": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-errorbar": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-errorbar/-/victory-errorbar-37.1.1.tgz", - "integrity": "sha512-1nDaa6zT/OaA99DYwznwEwbD3lHfsnBV0UbUlQn91Hv99sg0Rvyk9cZinQWTZ0nNf8cNBYOzZlpFxY35XbQVSA==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-errorbar/-/victory-errorbar-37.1.2.tgz", + "integrity": "sha512-sgs1nla57Ctt9slG5WXWdxqTXtTdKcZM+u83C5j1ceKKmMjCiqiNYmMQpF7yz7Nj2ewJTrOzZON9h2zgurr2Cg==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", - "victory-core": "37.1.1" + "victory-core": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-group": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-group/-/victory-group-37.1.1.tgz", - "integrity": "sha512-170CnQ6+doT8VUPZzcq6IIluSMSYqactT9J0ANSDEwHsO/+r0tFwez44FtA4/DgdDh5ObWQ6VfQx330urMG5bA==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-group/-/victory-group-37.1.2.tgz", + "integrity": "sha512-Zwdvs6pSfF02xax8rQbahSqRP7eua4mS0so0gFYr/M2sNiKN4hxnM72j3dLo9nQ63kQpYhcUZe8U/hEjlhHxYQ==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", "react-fast-compare": "^3.2.0", - "victory-core": "37.1.1", - "victory-shared-events": "37.1.1" + "victory-core": "37.1.2", + "victory-shared-events": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-histogram": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-histogram/-/victory-histogram-37.1.1.tgz", - "integrity": "sha512-2KnfdQYaO+MELM/PB3saPHcUf+tHg0SwbaLHKRk+Im7+aQRUlprlHH7sHZJM/TYXCkJdqbQQNoW6R9VK/kQiGg==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-histogram/-/victory-histogram-37.1.2.tgz", + "integrity": "sha512-IGeQZ2HGuvmMyYxoKOczIILNH6ARDJaWcDG3h5BX4jP4JH2+eWeEukCVHGT3b1VM1OFxuvPijJrePXYzKgQ+AQ==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", "react-fast-compare": "^3.2.0", - "victory-bar": "37.1.1", - "victory-core": "37.1.1", - "victory-vendor": "37.1.1" + "victory-bar": "37.1.2", + "victory-core": "37.1.2", + "victory-vendor": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-legend": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-legend/-/victory-legend-37.1.1.tgz", - "integrity": "sha512-8F51DbYzG+jkMJoGp2Ulqqxgoq00TWgvQcBTZptdrN2PFlc2b1Ug7z3lbK1ziUCunrVbHQpAhge0onDoRyn1Vg==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-legend/-/victory-legend-37.1.2.tgz", + "integrity": "sha512-dmwwHtFpEXPIelY9iH1a2Q/V2Ji8DaF0a2g+hLH4SM/xbA9YwjP2/9DIQcwS7/OVl4l1AnSbLFcu5RyDPJ0kww==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", - "victory-core": "37.1.1" + "victory-core": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-line": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-line/-/victory-line-37.1.1.tgz", - "integrity": "sha512-YLR9/i7BwN3taBvHCfmc5hA0po16QFQuFnO61NPNCBZtv8kNf39m3BpDTDYMeuEgEBCnMw0znR0C1NASZcJDWQ==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-line/-/victory-line-37.1.2.tgz", + "integrity": "sha512-DjttWkQG0iZtQ9SM/KphN168dQUgw1xwyr0qR1aN12VtVb1jspI1LkBH8XqUeYXgfuI1vKQJWcV/zMOK2Q1n8g==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", - "victory-core": "37.1.1", - "victory-vendor": "37.1.1" + "victory-core": "37.1.2", + "victory-vendor": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-pie": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-pie/-/victory-pie-37.1.1.tgz", - "integrity": "sha512-GWHR4prUq6ZNeMd0IEywHvvWn3dkn7vS3fkLMVTKitpbMIRPGlFxo5gLTkAQv3nnA/762GLSyELbcFgFQXOQUA==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-pie/-/victory-pie-37.1.2.tgz", + "integrity": "sha512-lwPMAtkcGDJ4gdpKFmR7hRnowJZIGQ6XIvyPj7Ir+QfL6ew64kl7YiIsQpDnC4zqwAjDPIbIW/kRROiSKRjXjQ==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", - "victory-core": "37.1.1", - "victory-vendor": "37.1.1" + "victory-core": "37.1.2", + "victory-vendor": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-polar-axis": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-polar-axis/-/victory-polar-axis-37.1.1.tgz", - "integrity": "sha512-I9okmw1MauiucV6WxylHDOZtW5mgrozYmfglOSR6fnQ9gcxPoXSgBNxo801kyV2/pu8BP6dD07Uz1QLbCh3KSA==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-polar-axis/-/victory-polar-axis-37.1.2.tgz", + "integrity": "sha512-cvELVQ5MwDjDfC/n/g8QVfUhexLNKcp7kXxbjp6IGbzQMCfNtROHaVaHaISNH7/EV5zinwBhNj0+ISWatROtrQ==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", - "victory-core": "37.1.1" + "victory-core": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-scatter": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-scatter/-/victory-scatter-37.1.1.tgz", - "integrity": "sha512-2jt0HgYnLngw8oVAY5Tcq2MEHVc3FDo47gMQf7LysFvsuCtBLvgkaDuRPnF+8Ty3hP/7qwjV9tgM7Ui2cSfZSg==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-scatter/-/victory-scatter-37.1.2.tgz", + "integrity": "sha512-6orfcqdfZCuTHqf/wE+B+sQbpzf2/TyEvLZhvYIXFr5GzdVu39psNl74K3GQ2Ky0db+e6oLEHV8nZYO2IvWoWg==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", - "victory-core": "37.1.1" + "victory-core": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-selection-container": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-selection-container/-/victory-selection-container-37.1.1.tgz", - "integrity": "sha512-5FYlMQNt7uV+EfndtCTYkE5/yjnHo243ZnBiUzXmvXU+IBCjzXmcOeyqyn7IY7+p1fvA2Hc698mDLGydd8QJrA==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-selection-container/-/victory-selection-container-37.1.2.tgz", + "integrity": "sha512-1sp1CV9LrBADnsBcFgVQuYUNCLeANuybtOS9/5TvPPELBGWQQ55nBN3mH/laVPDy9gGyPARh1lmdPgREHmSkmQ==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", - "victory-core": "37.1.1" + "victory-core": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-shared-events": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-shared-events/-/victory-shared-events-37.1.1.tgz", - "integrity": "sha512-hMZI4GMLNWoIQ/Yso/tiTKpx5wUgNi2iwozrxWDesr11I5uqwutkBeHpIBMBwsGRWy6plkMyBp9lCf2Etkxm4A==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-shared-events/-/victory-shared-events-37.1.2.tgz", + "integrity": "sha512-fpgpe6eI0A9dD39ZsFaid3sXdrCf1WIzFnpkNFT6hBYrDDD5Fd2/2SgqOxuul64PlYJAk6NOY+F1agmEtmB+/Q==", "license": "MIT", "dependencies": { "json-stringify-safe": "^5.0.1", "lodash": "^4.17.19", "react-fast-compare": "^3.2.0", - "victory-core": "37.1.1" + "victory-core": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-stack": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-stack/-/victory-stack-37.1.1.tgz", - "integrity": "sha512-jIHV7xRZW8jEuOGjrEreIh/u1mddDix98NmIJnd2+qMk1EuWIHngC2neCKQ0iF3wc8eAMuaK8gGr6ksSkpsqPA==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-stack/-/victory-stack-37.1.2.tgz", + "integrity": "sha512-H3FWiv3c6s/++PB3pBZ/9r8mcry1FHg8JK+03DZhRKHtJIti/38iIYUUiFOoQKmjVUQ7wrLdftYiemy3st77Dg==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", "react-fast-compare": "^3.2.0", - "victory-core": "37.1.1", - "victory-shared-events": "37.1.1" + "victory-core": "37.1.2", + "victory-shared-events": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-tooltip": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-tooltip/-/victory-tooltip-37.1.1.tgz", - "integrity": "sha512-n5TTR92jIDaeXSADV+edevcMcNLz1iPwzQr7CNX38vWU6RWf/FRcdiBlBNg3v4rNh41+sO8jjMQhjOpDti6Rvw==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-tooltip/-/victory-tooltip-37.1.2.tgz", + "integrity": "sha512-j1r1t83X0epSwivhf4eYSD2DoWRVy5fkINbLk4sVnnV2EUT4Lt4yH3uelIhYQuT4Y+Ez9KFLoQvR6bfwmHyfZw==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", - "victory-core": "37.1.1" + "victory-core": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-vendor": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.1.1.tgz", - "integrity": "sha512-WDnoGOSqmgyFgY/+7v4i40Vc/I/iOqc9JpUniWO9TvLCWAVEmwAjKxrorBlxEv+vQxQuhxGKOf3PcJqfjZqA9g==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.1.2.tgz", + "integrity": "sha512-kZ2UVcoINrisEW7JDaxws2v17D4n4ShRzsPUcYnF37/avByNbjzybhvs8JrqO6+vUmoP2W1DrTEI2L/86PEQjw==", "license": "MIT AND ISC", "dependencies": { "@types/d3-array": "^3.0.3", @@ -20241,43 +20063,43 @@ } }, "node_modules/victory-voronoi": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-voronoi/-/victory-voronoi-37.1.1.tgz", - "integrity": "sha512-LIGT4JLP+9GxzvA1rka3W8iHXx8TXvGDzcgDhj3E14dSjkDkYaX0/tyBBirHo7T3IFHThAO6GNPsfMrCzz8Z9w==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-voronoi/-/victory-voronoi-37.1.2.tgz", + "integrity": "sha512-rbihVJMDLmrMKfm6mbzTft9BbaJWZkymFkYxZZT0ZdHjsyaFm7t3jjrtvG1cq6HsTI10AfCh7iWmD9aky69eMQ==", "license": "MIT", "dependencies": { "d3-voronoi": "^1.1.4", "lodash": "^4.17.19", - "victory-core": "37.1.1" + "victory-core": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-voronoi-container": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-voronoi-container/-/victory-voronoi-container-37.1.1.tgz", - "integrity": "sha512-OIiT/KroQCvPaITEGcZfPd7B5Byw2vjo52RiUfzdg5WfCvqxuOURnvXsv6lh8nTNS/VI9uWaxHYdATXqXtNgfA==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-voronoi-container/-/victory-voronoi-container-37.1.2.tgz", + "integrity": "sha512-uFnZmRWp+QP7mH9jqetmoSR/KYhnFr4sFGR9+HrQkUbOzBQpT7Q2SNrDcr5l29Hm7Lb+3iUuF/l0E//EzuS+Ig==", "license": "MIT", "dependencies": { "delaunay-find": "0.0.6", "lodash": "^4.17.19", "react-fast-compare": "^3.2.0", - "victory-core": "37.1.1", - "victory-tooltip": "37.1.1" + "victory-core": "37.1.2", + "victory-tooltip": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" } }, "node_modules/victory-zoom-container": { - "version": "37.1.1", - "resolved": "https://registry.npmjs.org/victory-zoom-container/-/victory-zoom-container-37.1.1.tgz", - "integrity": "sha512-pBW64iT9zlFqmo468+MXkqNwJuuM+Q/+5/llFCKBoMA6wE1SwpkgHQ8RITWQUDCY9dR3y/bJFLEQg2aqoFB8/g==", + "version": "37.1.2", + "resolved": "https://registry.npmjs.org/victory-zoom-container/-/victory-zoom-container-37.1.2.tgz", + "integrity": "sha512-OI0AgskIpruWaFWF1BkJWi4UZGyEJ+ol3uzlIMk3tPmYkuw5Gh4pTW6kEw/0E1BP+PwJjv+IRGBbT46/YxV3UQ==", "license": "MIT", "dependencies": { "lodash": "^4.17.19", - "victory-core": "37.1.1" + "victory-core": "37.1.2" }, "peerDependencies": { "react": ">=16.6.0" @@ -20411,74 +20233,6 @@ } } }, - "node_modules/webpack-bundle-analyzer": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", - "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", - "dev": true, - "dependencies": { - "@discoveryjs/json-ext": "0.5.7", - "acorn": "^8.0.4", - "acorn-walk": "^8.0.0", - "commander": "^7.2.0", - "debounce": "^1.2.1", - "escape-string-regexp": "^4.0.0", - "gzip-size": "^6.0.0", - "html-escaper": "^2.0.2", - "opener": "^1.5.2", - "picocolors": "^1.0.0", - "sirv": "^2.0.3", - "ws": "^7.3.1" - }, - "bin": { - "webpack-bundle-analyzer": "lib/bin/analyzer.js" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/webpack-bundle-analyzer/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/webpack-bundle-analyzer/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "dev": true, - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/webpack-cli": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", diff --git a/package.json b/package.json index cc8b31105..7f6d75470 100644 --- a/package.json +++ b/package.json @@ -59,12 +59,12 @@ "@patternfly/react-icons": "5.4.0", "@patternfly/react-table": "5.4.1", "@patternfly/react-tokens": "5.4.0", - "@redhat-cloud-services/frontend-components": "^4.2.15", + "@redhat-cloud-services/frontend-components": "^4.2.16", "@redhat-cloud-services/frontend-components-notifications": "^4.1.0", "@redhat-cloud-services/frontend-components-translations": "^3.2.8", "@redhat-cloud-services/frontend-components-utilities": "^4.0.17", "@redhat-cloud-services/rbac-client": "^2.2.5", - "@reduxjs/toolkit": "^2.2.7", + "@reduxjs/toolkit": "^2.3.0", "@unleash/proxy-client-react": "^4.3.1", "axios": "^1.7.7", "date-fns": "^4.1.0", @@ -73,23 +73,23 @@ "qs": "^6.13.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-intl": "6.7.0", + "react-intl": "6.8.0", "react-redux": "^9.1.2", - "react-router-dom": "^6.26.2", + "react-router-dom": "^6.27.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "typesafe-actions": "^5.1.0", - "victory": "^37.1.1" + "victory": "^37.1.2" }, "devDependencies": { "@eslint/compat": "^1.2.0", "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.12.0", - "@formatjs/cli": "^6.2.12", - "@formatjs/ecma402-abstract": "^2.0.0", - "@formatjs/icu-messageformat-parser": "^2.7.8", + "@formatjs/ecma402-abstract": "^2.2.0", + "@formatjs/fast-memoize": "^2.2.1", + "@formatjs/intl-localematcher": "^0.5.5", "@redhat-cloud-services/eslint-config-redhat-cloud-services": "^2.0.4", - "@redhat-cloud-services/frontend-components-config": "^6.3.0", + "@redhat-cloud-services/frontend-components-config": "^6.3.1", "@redhat-cloud-services/tsc-transform-imports": "^1.0.16", "@swc/core": "^1.7.26", "@swc/jest": "^0.2.36", @@ -99,17 +99,16 @@ "@types/jest": "^29.5.13", "@types/qs": "^6.9.16", "@types/react": "^18.3.11", - "@types/react-dom": "^18.3.0", + "@types/react-dom": "^18.3.1", "@types/react-redux": "^7.1.34", "@types/react-router-dom": "^5.3.3", - "@typescript-eslint/eslint-plugin": "^8.8.0", - "@typescript-eslint/parser": "^8.8.0", - "aphrodite": "^2.4.0", + "@typescript-eslint/eslint-plugin": "^8.9.0", + "@typescript-eslint/parser": "^8.9.0", "copy-webpack-plugin": "^12.0.2", "eslint": "^9.12.0", - "eslint-plugin-formatjs": "^5.0.0", + "eslint-plugin-formatjs": "^5.1.0", "eslint-plugin-jest-dom": "^5.4.0", - "eslint-plugin-jsdoc": "^50.3.1", + "eslint-plugin-jsdoc": "^50.4.1", "eslint-plugin-markdown": "^5.1.0", "eslint-plugin-patternfly-react": "^5.4.0", "eslint-plugin-prettier": "^5.2.1", @@ -118,24 +117,22 @@ "eslint-plugin-sort-keys-fix": "^1.1.2", "eslint-plugin-testing-library": "^6.3.0", "git-revision-webpack-plugin": "^5.0.0", - "globals": "^15.10.0", + "globals": "^15.11.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-mock-axios": "^4.7.3", "jest-transform-stub": "^2.0.0", - "jws": "^4.0.0", "npm-run-all": "^4.1.5", "prettier": "^3.3.3", "rimraf": "^6.0.1", "swc_mut_cjs_exports": "^0.99.0", "ts-jest": "^29.2.5", "ts-patch": "^3.2.1", - "typescript": "^5.6.2", - "webpack-bundle-analyzer": "^4.10.2" + "typescript": "^5.6.3" }, "overrides": { - "@typescript-eslint/eslint-plugin": "^8.8.0", + "@typescript-eslint/eslint-plugin": "^8.9.0", "eslint": "^9.12.0", "redux": "^5.0.1" }, diff --git a/src/api/resources/awsOcpResource.test.ts b/src/api/resources/awsOcpResource.test.ts index 512d1c169..bc714262b 100644 --- a/src/api/resources/awsOcpResource.test.ts +++ b/src/api/resources/awsOcpResource.test.ts @@ -5,5 +5,5 @@ import { ResourceType } from './resource'; test('runExport API request for OCP on AWS', () => { runResource(ResourceType.account, ''); - expect(axiosInstance.get).toBeCalledWith('resource-types/aws-accounts/'); + expect(axiosInstance.get).toBeCalledWith('resource-types/aws-accounts/?openshift=true'); }); diff --git a/src/api/resources/awsOcpResource.ts b/src/api/resources/awsOcpResource.ts index b2e0cad63..da004220d 100644 --- a/src/api/resources/awsOcpResource.ts +++ b/src/api/resources/awsOcpResource.ts @@ -15,6 +15,6 @@ export const ResourceTypePaths: Partial> = { export function runResource(resourceType: ResourceType, query: string) { const path = ResourceTypePaths[resourceType]; - const queryString = query ? `?${query}` : ''; + const queryString = query ? `?openshift=true&${query}` : '?openshift=true'; return axiosInstance.get(`${path}${queryString}`); } diff --git a/src/components/featureToggle/featureToggle.tsx b/src/components/featureToggle/featureToggle.tsx index a9b74de2f..ca7741019 100644 --- a/src/components/featureToggle/featureToggle.tsx +++ b/src/components/featureToggle/featureToggle.tsx @@ -5,9 +5,12 @@ import { useDispatch } from 'react-redux'; import { FeatureToggleActions } from 'store/featureToggle'; export const enum FeatureToggle { + accountInfoDetails = 'cost-management.ui.account-info-details', // https://issues.redhat.com/browse/COST-5386 accountInfoEmptyState = 'cost-management.ui.account-info-empty-state', // https://issues.redhat.com/browse/COST-5335 awsEc2Instances = 'cost-management.ui.aws-ec2-instances', // https://issues.redhat.com/browse/COST-4855 + chartSkeleton = 'cost-management.ui.chart-skeleton', // https://issues.redhat.com/browse/COST-5573 debug = 'cost-management.ui.debug', + detailsDateRange = 'cost-management.ui.details-date-range', // https://issues.redhat.com/browse/COST-5563 exports = 'cost-management.ui.exports', // Async exports https://issues.redhat.com/browse/COST-2223 finsights = 'cost-management.ui.finsights', // RHEL support for FINsights https://issues.redhat.com/browse/COST-3306 ibm = 'cost-management.ui.ibm', // IBM https://issues.redhat.com/browse/COST-935 @@ -19,6 +22,10 @@ const useIsToggleEnabled = (toggle: FeatureToggle) => { return client.isEnabled(toggle); }; +export const useIsAccountInfoDetailsToggleEnabled = () => { + return useIsToggleEnabled(FeatureToggle.accountInfoDetails); +}; + export const useIsAccountInfoEmptyStateToggleEnabled = () => { return useIsToggleEnabled(FeatureToggle.accountInfoEmptyState); }; @@ -27,10 +34,18 @@ export const useIsAwsEc2InstancesToggleEnabled = () => { return useIsToggleEnabled(FeatureToggle.awsEc2Instances); }; +export const useIsChartSkeletonToggleEnabled = () => { + return useIsToggleEnabled(FeatureToggle.chartSkeleton); +}; + export const useIsDebugToggleEnabled = () => { return useIsToggleEnabled(FeatureToggle.debug); }; +export const useIsDetailsDateRangeToggleEnabled = () => { + return useIsToggleEnabled(FeatureToggle.detailsDateRange); +}; + export const useIsExportsToggleEnabled = () => { return useIsToggleEnabled(FeatureToggle.exports); }; @@ -52,9 +67,12 @@ export const useFeatureToggle = () => { const dispatch = useDispatch(); const { auth } = useChrome(); + const isAccountInfoDetailsToggleEnabled = useIsAccountInfoDetailsToggleEnabled(); const isAccountInfoEmptyStateToggleEnabled = useIsAccountInfoEmptyStateToggleEnabled(); const isAwsEc2InstancesToggleEnabled = useIsAwsEc2InstancesToggleEnabled(); + const isChartSkeletonToggleEnabled = useIsChartSkeletonToggleEnabled(); const isDebugToggleEnabled = useIsDebugToggleEnabled(); + const isDetailsDateRangeToggleEnabled = useIsDetailsDateRangeToggleEnabled(); const isExportsToggleEnabled = useIsExportsToggleEnabled(); const isFinsightsToggleEnabled = useIsFinsightsToggleEnabled(); const isIbmToggleEnabled = useIsIbmToggleEnabled(); @@ -70,9 +88,12 @@ export const useFeatureToggle = () => { // Workaround for code that doesn't use hooks dispatch( FeatureToggleActions.setFeatureToggle({ + isAccountInfoDetailsToggleEnabled, isAccountInfoEmptyStateToggleEnabled, isAwsEc2InstancesToggleEnabled, + isChartSkeletonToggleEnabled, isDebugToggleEnabled, + isDetailsDateRangeToggleEnabled, isExportsToggleEnabled, isFinsightsToggleEnabled, isIbmToggleEnabled, @@ -84,9 +105,12 @@ export const useFeatureToggle = () => { fetchUser(identity => console.log('User identity:', identity)); } }, [ + isAccountInfoDetailsToggleEnabled, isAccountInfoEmptyStateToggleEnabled, isAwsEc2InstancesToggleEnabled, + isChartSkeletonToggleEnabled, isDebugToggleEnabled, + isDetailsDateRangeToggleEnabled, isExportsToggleEnabled, isFinsightsToggleEnabled, isIbmToggleEnabled, diff --git a/src/locales/messages.ts b/src/locales/messages.ts index 7322e8d9f..f42057b34 100644 --- a/src/locales/messages.ts +++ b/src/locales/messages.ts @@ -101,6 +101,11 @@ export default defineMessages({ description: 'Back', id: 'back', }, + backToIntegrations: { + defaultMessage: 'Back to integrations status', + description: 'Back to integrations status', + id: 'backToIntegrations', + }, breakdownBackToDetails: { defaultMessage: '{groupBy, select, ' + @@ -2028,8 +2033,10 @@ export default defineMessages({ 'description {Filter by description} ' + 'gcp_project {Filter by GCP project} ' + 'group {Filter by group} ' + + 'instance {Filter by instance} ' + 'name {Filter by name} ' + 'node {Filter by node} ' + + 'operating_system {Filter by operating system} ' + 'org_unit_id {Filter by organizational unit} ' + 'payer_tenant_id {Filter by account} ' + 'persistent_volume_claim {Filter by persistent volume claim} ' + @@ -2380,6 +2387,16 @@ export default defineMessages({ description: 'Integration', id: 'integration', }, + integrationsDetails: { + defaultMessage: 'Integrations details', + description: 'Integrations details', + id: 'integrationsDetails', + }, + integrationsStatus: { + defaultMessage: 'Integrations status', + description: 'Integrations status', + id: 'integrationsStatus', + }, lastProcessed: { defaultMessage: 'Last processed', description: 'Last processed', @@ -2586,6 +2603,11 @@ export default defineMessages({ description: 'No', id: 'no', }, + noCurrentData: { + defaultMessage: 'No data available for {dateRange}. You are viewing data for the previous month.', + description: 'No data available for Jan 1-31. You are viewing data for the previous month.', + id: 'noCurrentData', + }, noDataForDate: { defaultMessage: 'No data available for {dateRange}', description: 'No data available for Jan 1-31', @@ -3149,11 +3171,6 @@ export default defineMessages({ description: 'Rate must be a positive number', id: 'priceListPosNumberRate', }, - providerDetails: { - defaultMessage: 'Integrations details', - description: 'Integrations details', - id: 'providerDetails', - }, pvcTitle: { defaultMessage: 'Persistent Volume Claims', description: 'Persistent Volume Claims', @@ -3878,6 +3895,11 @@ export default defineMessages({ description: 'vCPU', id: 'vcpuTitle', }, + viewAll: { + defaultMessage: 'View all', + description: 'View all', + id: 'viewAll', + }, volumeTitle: { defaultMessage: 'Volume', description: 'Volume', diff --git a/src/routes/components/charts/costExplorerChart/costExplorerChart.tsx b/src/routes/components/charts/costExplorerChart/costExplorerChart.tsx index c1681b4ef..3d395a9dd 100644 --- a/src/routes/components/charts/costExplorerChart/costExplorerChart.tsx +++ b/src/routes/components/charts/costExplorerChart/costExplorerChart.tsx @@ -15,7 +15,6 @@ import messages from 'locales/messages'; import React from 'react'; import type { WrappedComponentProps } from 'react-intl'; import { injectIntl } from 'react-intl'; -import { default as ChartTheme } from 'routes/components/charts/chartTheme'; import { getMaxValue } from 'routes/components/charts/common/chartDatum'; import type { ChartSeries } from 'routes/components/charts/common/chartUtils'; import { @@ -38,6 +37,7 @@ interface CostExplorerChartOwnProps { baseHeight?: number; formatOptions?: FormatOptions; formatter?: Formatter; + isSkeleton?: boolean; legendItemsPerRow?: number; name?: string; padding?: any; @@ -97,7 +97,7 @@ class CostExplorerChartBase extends React.Component { - const { top1stData, top2ndData, top3rdData, top4thData, top5thData, top6thData } = this.props; + const { isSkeleton, top1stData, top2ndData, top3rdData, top4thData, top5thData, top6thData } = this.props; const series: ChartSeries[] = []; if (top1stData && top1stData.length) { @@ -108,13 +108,13 @@ class CostExplorerChartBase extends React.Component { const maxChars = 20; - return str.length > maxChars ? str.substring(0, maxChars - 1) + '...' : str; + return str?.length > maxChars ? str.substring(0, maxChars - 1) + '...' : str; }; private getTickValue = (t: number) => { @@ -430,7 +430,7 @@ class CostExplorerChartBase extends React.Component {series && series.length > 0 && ( diff --git a/src/routes/components/charts/historicalTrendChart/historicalTrendChart.tsx b/src/routes/components/charts/historicalTrendChart/historicalTrendChart.tsx index ed3710ebe..a1c7d80ae 100644 --- a/src/routes/components/charts/historicalTrendChart/historicalTrendChart.tsx +++ b/src/routes/components/charts/historicalTrendChart/historicalTrendChart.tsx @@ -291,7 +291,12 @@ class HistoricalTrendChartBase extends React.Component { return this.getChart(s, index); })} - + diff --git a/src/routes/components/dateRange/dateRange.scss b/src/routes/components/dateRange/dateRange.scss new file mode 100644 index 000000000..1ae639c0c --- /dev/null +++ b/src/routes/components/dateRange/dateRange.scss @@ -0,0 +1,7 @@ +@import url("~@patternfly/patternfly/base/patternfly-variables.css"); + +.dropdownOverride { + button.pf-v5-c-menu-toggle { + max-width: unset; + } +} diff --git a/src/routes/components/dateRange/dateRange.tsx b/src/routes/components/dateRange/dateRange.tsx new file mode 100644 index 000000000..0c47c5905 --- /dev/null +++ b/src/routes/components/dateRange/dateRange.tsx @@ -0,0 +1,95 @@ +import './dateRange.scss'; + +import type { MessageDescriptor } from '@formatjs/intl/src/types'; +import type { MenuToggleElement } from '@patternfly/react-core'; +import { Dropdown, DropdownItem, DropdownList, MenuToggle } from '@patternfly/react-core'; +import messages from 'locales/messages'; +import React from 'react'; +import { useIntl } from 'react-intl'; +import { getSinceDateRangeString } from 'utils/dates'; + +interface DateRangeOwnProps { + dateRangeType?: string; + isCurrentMonthData: boolean; + isDisabled?: boolean; + isExplorer?: boolean; + onSelect(value: string); +} + +type DateRangeProps = DateRangeOwnProps; + +const DateRange: React.FC = ({ dateRangeType, isCurrentMonthData, isExplorer, onSelect }) => { + const [isOpen, setIsOpen] = React.useState(false); + const intl = useIntl(); + + const onToggleClick = () => { + setIsOpen(!isOpen); + }; + + const getOptions = () => { + const options: { + isDisabled?: boolean; + label: MessageDescriptor; + value: string; + }[] = [ + { label: messages.explorerDateRange, value: 'current_month_to_date', isDisabled: isCurrentMonthData === false }, + { label: messages.explorerDateRange, value: 'previous_month' }, + ]; + if (isExplorer) { + options.push( + { label: messages.explorerDateRange, value: 'previous_month_to_date' }, + { label: messages.explorerDateRange, value: 'last_thirty_days' }, + { label: messages.explorerDateRange, value: 'last_sixty_days' }, + { label: messages.explorerDateRange, value: 'last_ninety_days' }, + { label: messages.explorerDateRange, value: 'custom' } + ); + } + return options; + }; + + const handleOnSelect = (_evt, value) => { + if (onSelect) { + onSelect(value); + } + setIsOpen(false); + }; + + return ( +
+ setIsOpen(val)} + toggle={(toggleRef: React.Ref) => ( + + {intl.formatMessage(messages.explorerDateRange, { value: dateRangeType })} + + )} + > + + {getOptions().map((option, index) => ( + + {intl.formatMessage(option.label, { value: option.value })} + + ))} + + +
+ ); +}; + +export { DateRange }; diff --git a/src/routes/components/dateRange/index.ts b/src/routes/components/dateRange/index.ts new file mode 100644 index 000000000..0f4b8e353 --- /dev/null +++ b/src/routes/components/dateRange/index.ts @@ -0,0 +1 @@ +export * from './dateRange'; diff --git a/src/routes/components/export/exportModal.tsx b/src/routes/components/export/exportModal.tsx index 3faa1c1ef..d6f65484b 100644 --- a/src/routes/components/export/exportModal.tsx +++ b/src/routes/components/export/exportModal.tsx @@ -45,9 +45,11 @@ export interface ExportModalOwnProps { showAggregateType?: boolean; // Monthly resolution filters are not valid with date range showFormatType?: boolean; // Format type; CVS / JSON showTimeScope?: boolean; // timeScope filters are not valid with date range + timeScopeValue?: number; } interface ExportModalStateProps { + isDetailsDateRangeToggleEnabled?: boolean; isExportsToggleEnabled?: boolean; } @@ -89,16 +91,17 @@ export class ExportModalBase extends React.Component(state => { return { + isDetailsDateRangeToggleEnabled: FeatureToggleSelectors.selectIsDetailsDateRangeToggleEnabled(state), isExportsToggleEnabled: FeatureToggleSelectors.selectIsExportsToggleEnabled(state), }; }); diff --git a/src/routes/components/groupBy/groupBy.tsx b/src/routes/components/groupBy/groupBy.tsx index 62ac07b80..d97bea0bc 100644 --- a/src/routes/components/groupBy/groupBy.tsx +++ b/src/routes/components/groupBy/groupBy.tsx @@ -15,7 +15,7 @@ import { connect } from 'react-redux'; import type { SelectWrapperOption } from 'routes/components/selectWrapper'; import { SelectWrapper } from 'routes/components/selectWrapper'; import type { PerspectiveType } from 'routes/explorer/explorerUtils'; -import { getDateRangeFromQuery } from 'routes/utils/dateRange'; +import { getTimeScopeValue } from 'routes/utils/timeScope'; import type { FetchStatus } from 'store/common'; import { createMapStateToProps } from 'store/common'; import { orgActions, orgSelectors } from 'store/orgs'; @@ -30,6 +30,7 @@ import { GroupByOrg } from './groupByOrg'; import { GroupBySelect } from './groupBySelect'; interface GroupByOwnProps extends RouterComponentProps, WrappedComponentProps { + endDate?: string; getIdKeyForGroupBy: (groupBy: Query['group_by']) => string; groupBy?: string; isDisabled?: boolean; @@ -44,6 +45,7 @@ interface GroupByOwnProps extends RouterComponentProps, WrappedComponentProps { showCostCategories?: boolean; showOrgs?: boolean; showTags?: boolean; + startDate?: string; tagPathsType: TagPathsType; } @@ -324,27 +326,25 @@ class GroupByBase extends React.Component { } const mapStateToProps = createMapStateToProps( - (state, { orgPathsType, router, resourcePathsType, tagPathsType }) => { + (state, { endDate, orgPathsType, router, resourcePathsType, startDate, tagPathsType }) => { const queryFromRoute = parseQuery(router.location.search); + const timeScopeValue = getTimeScopeValue(queryFromRoute); + // Use start and end dates with Cost Explorer // Default to current month filter for details pages - let tagFilter: any = { - filter: { - resolution: 'monthly', - time_scope_units: 'month', - time_scope_value: -1, - }, - }; - - // Replace with start and end dates for Cost Explorer - if (queryFromRoute.dateRangeType) { - const { end_date, start_date } = getDateRangeFromQuery(queryFromRoute); - - tagFilter = { - end_date, - start_date, - }; - } + const tagFilter = + startDate && endDate + ? { + end_date: endDate, + start_date: startDate, + } + : { + filter: { + resolution: 'monthly', + time_scope_units: 'month', + time_scope_value: timeScopeValue !== undefined ? timeScopeValue : -1, + }, + }; // Note: Omitting key_only would help to share a single, cached request -- the toolbar requires key values // However, for better server-side performance, we chose to use key_only here. diff --git a/src/routes/components/page/noData/noData.styles.ts b/src/routes/components/page/noData/noData.styles.ts new file mode 100644 index 000000000..1cb3c59b6 --- /dev/null +++ b/src/routes/components/page/noData/noData.styles.ts @@ -0,0 +1,9 @@ +import global_spacer_xl from '@patternfly/react-tokens/dist/js/global_spacer_xl'; +import type React from 'react'; + +export const styles = { + details: { + marginBottom: global_spacer_xl.value, + marginTop: global_spacer_xl.value, + }, +} as { [className: string]: React.CSSProperties }; diff --git a/src/routes/components/page/noData/noDataState.tsx b/src/routes/components/page/noData/noDataState.tsx index 7ce759d65..5efd0c998 100644 --- a/src/routes/components/page/noData/noDataState.tsx +++ b/src/routes/components/page/noData/noDataState.tsx @@ -1,4 +1,5 @@ import { + Bullseye, Button, EmptyState, EmptyStateBody, @@ -13,6 +14,8 @@ import React from 'react'; import type { WrappedComponentProps } from 'react-intl'; import { injectIntl } from 'react-intl'; +import { styles } from './noData.styles'; + interface NoDataStateOwnProps { detailsComponent?: React.ReactNode; showReload?: boolean; @@ -33,7 +36,7 @@ class NoDataStateBase extends React.Component { /> {intl.formatMessage(messages.noDataStateDesc, { - status: detailsComponent, + status: detailsComponent ? {detailsComponent} : '', })} diff --git a/src/routes/details/awsBreakdown/awsBreakdown.tsx b/src/routes/details/awsBreakdown/awsBreakdown.tsx index ebb70982e..983283e4f 100644 --- a/src/routes/details/awsBreakdown/awsBreakdown.tsx +++ b/src/routes/details/awsBreakdown/awsBreakdown.tsx @@ -15,6 +15,7 @@ import { BreakdownBase } from 'routes/details/components/breakdown'; import { getGroupById, getGroupByOrgValue, getGroupByValue } from 'routes/utils/groupBy'; import { filterProviders } from 'routes/utils/providers'; import { getQueryState } from 'routes/utils/queryState'; +import { getTimeScopeValue } from 'routes/utils/timeScope'; import { createMapStateToProps } from 'store/common'; import { FeatureToggleSelectors } from 'store/featureToggle'; import { providersQuery, providersSelectors } from 'store/providers'; @@ -56,6 +57,7 @@ const mapStateToProps = createMapStateToProps, + historicalDataComponent: , instancesComponent: groupBy === serviceKey && groupByValue === 'AmazonEC2' ? ( @@ -139,6 +141,7 @@ const mapStateToProps = createMapStateToProps { const dispatch: ThunkDispatch = useDispatch(); const queryFromRoute = useQueryFromRoute(); const queryState = useQueryState('details'); + const timeScopeValue = getTimeScopeValue(queryState); const reportQuery = { cost_type: costType, @@ -341,7 +343,7 @@ const useMapToProps = ({ costType, currency, query }): InstancesStateProps => { ...(query.filter || baseQuery.filter), resolution: 'monthly', time_scope_units: 'month', - time_scope_value: -1, + time_scope_value: timeScopeValue !== undefined ? timeScopeValue : -1, }, filter_by: { // Add filters here to apply logical OR/AND diff --git a/src/routes/details/awsBreakdown/instances/instancesToolbar.tsx b/src/routes/details/awsBreakdown/instances/instancesToolbar.tsx index fd3d60f96..2903c3e3b 100644 --- a/src/routes/details/awsBreakdown/instances/instancesToolbar.tsx +++ b/src/routes/details/awsBreakdown/instances/instancesToolbar.tsx @@ -13,9 +13,11 @@ import { DataToolbar } from 'routes/components/dataToolbar'; import type { ComputedReportItem } from 'routes/utils/computedReport/getComputedReportItems'; import { isEqual } from 'routes/utils/equal'; import type { Filter } from 'routes/utils/filter'; +import { getTimeScopeValue } from 'routes/utils/timeScope'; import type { FetchStatus } from 'store/common'; import { createMapStateToProps } from 'store/common'; import { tagActions, tagSelectors } from 'store/tags'; +import { useQueryState } from 'utils/hooks'; import { accountKey, regionKey, tagKey } from 'utils/props'; interface InstancesToolbarOwnProps { @@ -187,13 +189,16 @@ export class InstancesToolbarBase extends React.Component((state, props) => { + const queryState = useQueryState('details'); + const timeScopeValue = getTimeScopeValue(queryState); + // Note: Omitting key_only would help to share a single, cached request. Only the toolbar requires key values; // however, for better server-side performance, we chose to use key_only here. const baseQuery = { filter: { resolution: 'monthly', time_scope_units: 'month', - time_scope_value: -1, + time_scope_value: timeScopeValue !== undefined ? timeScopeValue : -1, }, key_only: true, limit: 1000, diff --git a/src/routes/details/awsDetails/awsDetails.tsx b/src/routes/details/awsDetails/awsDetails.tsx index 581496b2a..72dca13cb 100644 --- a/src/routes/details/awsDetails/awsDetails.tsx +++ b/src/routes/details/awsDetails/awsDetails.tsx @@ -1,6 +1,6 @@ import 'routes/components/dataTable/dataTable.scss'; -import { Pagination, PaginationVariant } from '@patternfly/react-core'; +import { Alert, Pagination, PaginationVariant } from '@patternfly/react-core'; import type { Providers } from 'api/providers'; import { ProviderType } from 'api/providers'; import type { AwsQuery } from 'api/queries/awsQuery'; @@ -20,12 +20,12 @@ import { Loading } from 'routes/components/page/loading'; import { NoData } from 'routes/components/page/noData'; import { NoProviders } from 'routes/components/page/noProviders'; import { NotAvailable } from 'routes/components/page/notAvailable'; -import { ProviderDetails } from 'routes/details/components/providerDetails'; +import { ProviderStatus } from 'routes/details/components/providerStatus'; import { getIdKeyForGroupBy } from 'routes/utils/computedReport/getComputedAwsReportItems'; import type { ComputedReportItem } from 'routes/utils/computedReport/getComputedReportItems'; import { getUnsortedComputedReportItems } from 'routes/utils/computedReport/getComputedReportItems'; import { getGroupByCostCategory, getGroupByOrgValue, getGroupByTagKey } from 'routes/utils/groupBy'; -import { filterProviders, hasCurrentMonthData } from 'routes/utils/providers'; +import { filterProviders, hasCurrentMonthData, hasPreviousMonthData } from 'routes/utils/providers'; import { getRouteForQuery } from 'routes/utils/query'; import { handleOnCostTypeSelect, @@ -36,10 +36,12 @@ import { handleOnSetPage, handleOnSort, } from 'routes/utils/queryNavigate'; +import { getTimeScopeValue } from 'routes/utils/timeScope'; import { createMapStateToProps, FetchStatus } from 'store/common'; import { FeatureToggleSelectors } from 'store/featureToggle'; import { providersQuery, providersSelectors } from 'store/providers'; import { reportActions, reportSelectors } from 'store/reports'; +import { getSinceDateRangeString } from 'utils/dates'; import { formatPath } from 'utils/paths'; import { awsCategoryPrefix, logicalOrPrefix, noPrefix, orgUnitIdKey, tagPrefix } from 'utils/props'; import type { RouterComponentProps } from 'utils/router'; @@ -55,6 +57,9 @@ interface AwsDetailsStateProps { costType: string; currency?: string; isAccountInfoEmptyStateToggleEnabled?: boolean; + isCurrentMonthData?: boolean; + isDetailsDateRangeToggleEnabled?: boolean; + isPreviousMonthData?: boolean; providers: Providers; providersError: AxiosError; providersFetchStatus: FetchStatus; @@ -63,6 +68,7 @@ interface AwsDetailsStateProps { reportError: AxiosError; reportFetchStatus: FetchStatus; reportQueryString: string; + timeScopeValue?: number; } interface AwsDetailsDispatchProps { @@ -109,14 +115,6 @@ class AwsDetails extends React.Component { }; public state: AwsDetailsState = { ...this.defaultState }; - constructor(stateProps, dispatchProps) { - super(stateProps, dispatchProps); - this.handleOnBulkSelect = this.handleOnBulkSelect.bind(this); - this.handleOnExportModalClose = this.handleOnExportModalClose.bind(this); - this.handleOnExportModalOpen = this.handleOnExportModalOpen.bind(this); - this.handleonSelect = this.handleonSelect.bind(this); - } - public componentDidMount() { this.updateReport(); } @@ -150,7 +148,7 @@ class AwsDetails extends React.Component { }; private getExportModal = (computedItems: ComputedReportItem[]) => { - const { query, report, reportQueryString } = this.props; + const { query, report, reportQueryString, timeScopeValue } = this.props; const { isAllSelected, isExportModalOpen, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -182,6 +180,7 @@ class AwsDetails extends React.Component { reportPathsType={reportPathsType} reportQueryString={reportQueryString} reportType={reportType} + timeScopeValue={timeScopeValue} /> ); }; @@ -216,7 +215,7 @@ class AwsDetails extends React.Component { }; private getTable = () => { - const { query, report, reportFetchStatus, reportQueryString, router } = this.props; + const { query, report, reportFetchStatus, reportQueryString, router, timeScopeValue } = this.props; const { isAllSelected, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -248,12 +247,13 @@ class AwsDetails extends React.Component { report={report} reportQueryString={reportQueryString} selectedItems={selectedItems} + timeScopeValue={timeScopeValue} /> ); }; private getToolbar = (computedItems: ComputedReportItem[]) => { - const { query, router, report } = this.props; + const { query, router, report, timeScopeValue } = this.props; const { isAllSelected, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -283,6 +283,7 @@ class AwsDetails extends React.Component { pagination={this.getPagination(isDisabled)} query={query} selectedItems={selectedItems} + timeScopeValue={timeScopeValue} /> ); }; @@ -308,11 +309,11 @@ class AwsDetails extends React.Component { } }; - public handleOnExportModalClose = (isOpen: boolean) => { + private handleOnExportModalClose = (isOpen: boolean) => { this.setState({ isExportModalOpen: isOpen }); }; - public handleOnExportModalOpen = () => { + private handleOnExportModalOpen = () => { this.setState({ isExportModalOpen: true }); }; @@ -369,6 +370,9 @@ class AwsDetails extends React.Component { currency, intl, isAccountInfoEmptyStateToggleEnabled, + isCurrentMonthData, + isDetailsDateRangeToggleEnabled, + isPreviousMonthData, providers, providersFetchStatus, query, @@ -376,6 +380,7 @@ class AwsDetails extends React.Component { reportError, reportFetchStatus, router, + timeScopeValue, } = this.props; const computedItems = this.getComputedItems(); @@ -395,11 +400,11 @@ class AwsDetails extends React.Component { if (noProviders) { return ; } - if (!hasCurrentMonthData(providers)) { + if (isDetailsDateRangeToggleEnabled ? !isCurrentMonthData && !isPreviousMonthData : !isCurrentMonthData) { return ( : undefined + isAccountInfoEmptyStateToggleEnabled ? : undefined } title={title} /> @@ -413,13 +418,27 @@ class AwsDetails extends React.Component { costType={costType} currency={currency} groupBy={groupById} + isCurrentMonthData={isCurrentMonthData} onCostTypeSelect={() => handleOnCostTypeSelect(query, router)} onCurrencySelect={() => handleOnCurrencySelect(query, router)} onGroupBySelect={this.handleOnGroupBySelect} + query={query} report={report} + timeScopeValue={timeScopeValue} />
-
{this.getToolbar(computedItems)}
+
+ {!isCurrentMonthData && isDetailsDateRangeToggleEnabled && ( + + )} + {this.getToolbar(computedItems)} +
{this.getExportModal(computedItems)} {reportFetchStatus === FetchStatus.inProgress ? ( @@ -439,13 +458,36 @@ class AwsDetails extends React.Component { const mapStateToProps = createMapStateToProps((state, { router }) => { const queryFromRoute = parseQuery(router.location.search); + const costType = getCostType(); const currency = getCurrency(); + // Check for current and previous data first + const providersQueryString = getProvidersQuery(providersQuery); + const providers = providersSelectors.selectProviders(state, ProviderType.all, providersQueryString); + const providersError = providersSelectors.selectProvidersError(state, ProviderType.all, providersQueryString); + const providersFetchStatus = providersSelectors.selectProvidersFetchStatus( + state, + ProviderType.all, + providersQueryString + ); + + // Fetch based on time scope value + const filteredProviders = filterProviders(providers, ProviderType.aws); + const isCurrentMonthData = hasCurrentMonthData(filteredProviders); + const isDetailsDateRangeToggleEnabled = FeatureToggleSelectors.selectIsDetailsDateRangeToggleEnabled(state); + + let timeScopeValue = getTimeScopeValue(queryFromRoute); + timeScopeValue = Number( + !isCurrentMonthData && isDetailsDateRangeToggleEnabled ? -2 : timeScopeValue !== undefined ? timeScopeValue : -1 + ); + const query: any = { ...baseQuery, ...queryFromRoute, }; + query.filter.time_scope_value = timeScopeValue; // Add time scope here for breakdown pages + const reportQuery = { cost_type: costType, currency, @@ -455,7 +497,6 @@ const mapStateToProps = createMapStateToProps { + protected defaultState: DetailsHeaderState = { + currentDateRangeType: + this.props.timeScopeValue === -2 ? DateRangeType.previousMonth : DateRangeType.currentMonthToDate, + }; + public state: DetailsHeaderState = { ...this.defaultState }; + private handleOnCostTypeSelect = (value: string) => { const { onCostTypeSelect } = this.props; @@ -70,21 +95,39 @@ class DetailsHeaderBase extends React.Component { } }; + private handleOnDateRangeSelected = (value: string) => { + const { query, router } = this.props; + + this.setState({ currentDateRangeType: value }, () => { + const newQuery = { + filter: {}, + ...JSON.parse(JSON.stringify(query)), + }; + newQuery.filter.time_scope_value = value === DateRangeType.previousMonth ? -2 : -1; + router.navigate(getRouteForQuery(newQuery, router.location, true), { replace: true }); + }); + }; + public render() { const { costType, currency, groupBy, + intl, + isAccountInfoDetailsToggleEnabled, + isCurrentMonthData, + isDetailsDateRangeToggleEnabled, isExportsToggleEnabled, onCurrencySelect, onGroupBySelect, providers, providersError, report, - intl, + timeScopeValue, } = this.props; - const showContent = report && !providersError && providers?.meta?.count > 0; + const { currentDateRangeType } = this.state; + const showContent = report && !providersError && providers?.meta?.count > 0; const hasCost = report?.meta?.total?.cost?.total; return ( @@ -102,7 +145,14 @@ class DetailsHeaderBase extends React.Component { - + {isAccountInfoDetailsToggleEnabled && ( + + + + + + )} + { + {isDetailsDateRangeToggleEnabled && ( + + + + )} @@ -132,7 +192,9 @@ class DetailsHeaderBase extends React.Component { hasCost ? report.meta.total.cost.total.units : 'USD' )} -
{getSinceDateRangeString()}
+
+ {getSinceDateRangeString(undefined, timeScopeValue === -2 ? 1 : 0, true)} +
)}
@@ -154,6 +216,8 @@ const mapStateToProps = createMapStateToProps !column.hidden); + const filteredRows = rows.map(({ ...row }) => { + row.cells = row.cells.filter(cell => !cell.hidden); + return row; + }); + this.setState({ - columns, - rows, + columns: filteredColumns, + rows: filteredRows, }); }; @@ -228,7 +241,7 @@ class DetailsTableBase extends React.Component { - const { intl } = this.props; + const { intl, timeScopeValue } = this.props; const value = formatCurrency(Math.abs(item.cost.total.value - item.delta_value), item.cost.total.units); const percentage = item.delta_percent !== null ? formatPercentage(Math.abs(item.delta_percent)) : 0; @@ -247,7 +260,11 @@ class DetailsTableBase extends React.Component @@ -268,7 +285,12 @@ class DetailsTableBase extends React.Component
- {getForDateRangeString(value)} + {getForDateRangeString( + value, + undefined, + timeScopeValue === -2 ? 2 : 1, + timeScopeValue === -2 ? true : false + )}
); diff --git a/src/routes/details/awsDetails/detailsToolbar.tsx b/src/routes/details/awsDetails/detailsToolbar.tsx index d532cad4a..d51d96132 100644 --- a/src/routes/details/awsDetails/detailsToolbar.tsx +++ b/src/routes/details/awsDetails/detailsToolbar.tsx @@ -39,6 +39,7 @@ interface DetailsToolbarOwnProps { pagination?: React.ReactNode; query?: AwsQuery; selectedItems?: ComputedReportItem[]; + timeScopeValue?: number; } interface DetailsToolbarStateProps { @@ -209,55 +210,61 @@ export class DetailsToolbarBase extends React.Component((state, props) => { - // Note: Omitting key_only would help to share a single, cached request. Only the toolbar requires key values; - // however, for better server-side performance, we chose to use key_only here. - const baseQuery = { - filter: { - resolution: 'monthly', - time_scope_units: 'month', - time_scope_value: -1, - }, - key_only: true, - limit: 1000, - }; +const mapStateToProps = createMapStateToProps( + (state, { timeScopeValue = -1 }) => { + // Note: Omitting key_only would help to share a single, cached request. Only the toolbar requires key values; + // however, for better server-side performance, we chose to use key_only here. + const baseQuery = { + filter: { + resolution: 'monthly', + time_scope_units: 'month', + time_scope_value: timeScopeValue, + }, + key_only: true, + limit: 1000, + }; - const resourceQueryString = getQuery({ - key_only: true, - }); - const resourceReport = resourceSelectors.selectResource(state, resourcePathsType, resourceType, resourceQueryString); - const resourceReportFetchStatus = resourceSelectors.selectResourceFetchStatus( - state, - resourcePathsType, - resourceType, - resourceQueryString - ); + const resourceQueryString = getQuery({ + key_only: true, + }); + const resourceReport = resourceSelectors.selectResource( + state, + resourcePathsType, + resourceType, + resourceQueryString + ); + const resourceReportFetchStatus = resourceSelectors.selectResourceFetchStatus( + state, + resourcePathsType, + resourceType, + resourceQueryString + ); - const tagQueryString = getQuery({ - ...baseQuery, - }); - const tagReport = tagSelectors.selectTag(state, tagPathsType, tagType, tagQueryString); - const tagReportFetchStatus = tagSelectors.selectTagFetchStatus(state, tagPathsType, tagType, tagQueryString); + const tagQueryString = getQuery({ + ...baseQuery, + }); + const tagReport = tagSelectors.selectTag(state, tagPathsType, tagType, tagQueryString); + const tagReportFetchStatus = tagSelectors.selectTagFetchStatus(state, tagPathsType, tagType, tagQueryString); - const orgQueryString = getQuery({ - ...baseQuery, - }); - const orgReport = orgSelectors.selectOrg(state, orgPathsType, orgType, orgQueryString); - const orgReportFetchStatus = orgSelectors.selectOrgFetchStatus(state, orgPathsType, orgType, orgQueryString); + const orgQueryString = getQuery({ + ...baseQuery, + }); + const orgReport = orgSelectors.selectOrg(state, orgPathsType, orgType, orgQueryString); + const orgReportFetchStatus = orgSelectors.selectOrgFetchStatus(state, orgPathsType, orgType, orgQueryString); - return { - orgReport, - orgReportFetchStatus, - orgQueryString, - resourceReport, - resourceReportFetchStatus, - resourceQueryString, - tagReport, - tagReportFetchStatus, - tagQueryString, - }; -}); + return { + orgReport, + orgReportFetchStatus, + orgQueryString, + resourceReport, + resourceReportFetchStatus, + resourceQueryString, + tagReport, + tagReportFetchStatus, + tagQueryString, + }; + } +); const mapDispatchToProps: DetailsToolbarDispatchProps = { fetchOrg: orgActions.fetchOrg, diff --git a/src/routes/details/azureBreakdown/azureBreakdown.tsx b/src/routes/details/azureBreakdown/azureBreakdown.tsx index 455fe3c20..3ad1ad55c 100644 --- a/src/routes/details/azureBreakdown/azureBreakdown.tsx +++ b/src/routes/details/azureBreakdown/azureBreakdown.tsx @@ -15,6 +15,7 @@ import { BreakdownBase } from 'routes/details/components/breakdown'; import { getGroupById, getGroupByValue } from 'routes/utils/groupBy'; import { filterProviders } from 'routes/utils/providers'; import { getQueryState } from 'routes/utils/queryState'; +import { getTimeScopeValue } from 'routes/utils/timeScope'; import { createMapStateToProps } from 'store/common'; import { providersQuery, providersSelectors } from 'store/providers'; import { reportActions, reportSelectors } from 'store/reports'; @@ -43,7 +44,9 @@ const mapStateToProps = createMapStateToProps, + historicalDataComponent: , providers: filterProviders(providers, ProviderType.azure), providersError, providersFetchStatus, @@ -110,6 +113,7 @@ const mapStateToProps = createMapStateToProps }; public state: AzureDetailsState = { ...this.defaultState }; - constructor(stateProps, dispatchProps) { - super(stateProps, dispatchProps); - this.handleOnBulkSelect = this.handleOnBulkSelect.bind(this); - this.handleOnExportModalClose = this.handleOnExportModalClose.bind(this); - this.handleOnExportModalOpen = this.handleOnExportModalOpen.bind(this); - this.handleOnSelect = this.handleOnSelect.bind(this); - } - public componentDidMount() { this.updateReport(); } @@ -144,7 +142,7 @@ class AzureDetails extends React.Component }; private getExportModal = (computedItems: ComputedReportItem[]) => { - const { query, report, reportQueryString } = this.props; + const { query, report, reportQueryString, timeScopeValue } = this.props; const { isAllSelected, isExportModalOpen, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -169,6 +167,7 @@ class AzureDetails extends React.Component reportPathsType={reportPathsType} reportQueryString={reportQueryString} reportType={reportType} + timeScopeValue={timeScopeValue} /> ); }; @@ -203,7 +202,7 @@ class AzureDetails extends React.Component }; private getTable = () => { - const { query, report, reportFetchStatus, reportQueryString, router } = this.props; + const { query, report, reportFetchStatus, reportQueryString, router, timeScopeValue } = this.props; const { isAllSelected, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -225,6 +224,7 @@ class AzureDetails extends React.Component report={report} reportQueryString={reportQueryString} selectedItems={selectedItems} + timeScopeValue={timeScopeValue} /> ); }; @@ -278,11 +278,11 @@ class AzureDetails extends React.Component } }; - public handleOnExportModalClose = (isOpen: boolean) => { + private handleOnExportModalClose = (isOpen: boolean) => { this.setState({ isExportModalOpen: isOpen }); }; - public handleOnExportModalOpen = () => { + private handleOnExportModalOpen = () => { this.setState({ isExportModalOpen: true }); }; @@ -328,14 +328,17 @@ class AzureDetails extends React.Component currency, intl, isAccountInfoEmptyStateToggleEnabled, + isCurrentMonthData, + isDetailsDateRangeToggleEnabled, + isPreviousMonthData, providers, providersFetchStatus, query, report, reportError, reportFetchStatus, - router, + timeScopeValue, } = this.props; const computedItems = this.getComputedItems(); @@ -355,11 +358,11 @@ class AzureDetails extends React.Component if (noProviders) { return ; } - if (!hasCurrentMonthData(providers)) { + if (isDetailsDateRangeToggleEnabled ? !isCurrentMonthData && !isPreviousMonthData : !isCurrentMonthData) { return ( : undefined + isAccountInfoEmptyStateToggleEnabled ? : undefined } title={title} /> @@ -372,12 +375,26 @@ class AzureDetails extends React.Component handleOnCurrencySelect(query, router)} onGroupBySelect={this.handleOnGroupBySelect} + query={query} report={report} + timeScopeValue={timeScopeValue} />
-
{this.getToolbar(computedItems)}
+
+ {!isCurrentMonthData && isDetailsDateRangeToggleEnabled && ( + + )} + {this.getToolbar(computedItems)} +
{this.getExportModal(computedItems)} {reportFetchStatus === FetchStatus.inProgress ? ( @@ -399,10 +416,32 @@ const mapStateToProps = createMapStateToProps(router.location.search); const currency = getCurrency(); + // Check for current and previous data first + const providersQueryString = getProvidersQuery(providersQuery); + const providers = providersSelectors.selectProviders(state, ProviderType.all, providersQueryString); + const providersError = providersSelectors.selectProvidersError(state, ProviderType.all, providersQueryString); + const providersFetchStatus = providersSelectors.selectProvidersFetchStatus( + state, + ProviderType.all, + providersQueryString + ); + + // Fetch based on time scope value + const filteredProviders = filterProviders(providers, ProviderType.azure); + const isCurrentMonthData = hasCurrentMonthData(filteredProviders); + const isDetailsDateRangeToggleEnabled = FeatureToggleSelectors.selectIsDetailsDateRangeToggleEnabled(state); + + let timeScopeValue = getTimeScopeValue(queryFromRoute); + timeScopeValue = Number( + !isCurrentMonthData && isDetailsDateRangeToggleEnabled ? -2 : timeScopeValue !== undefined ? timeScopeValue : -1 + ); + const query: any = { ...baseQuery, ...queryFromRoute, }; + query.filter.time_scope_value = timeScopeValue; // Add time scope here for breakdown pages + const reportQuery = { currency, delta: 'cost', @@ -411,7 +450,6 @@ const mapStateToProps = createMapStateToProps { + protected defaultState: DetailsHeaderState = { + currentDateRangeType: + this.props.timeScopeValue === -2 ? DateRangeType.previousMonth : DateRangeType.currentMonthToDate, + }; + public state: DetailsHeaderState = { ...this.defaultState }; + + private handleOnDateRangeSelected = (value: string) => { + const { query, router } = this.props; + + this.setState({ currentDateRangeType: value }, () => { + const newQuery = { + filter: {}, + ...JSON.parse(JSON.stringify(query)), + }; + newQuery.filter.time_scope_value = value === DateRangeType.previousMonth ? -2 : -1; + router.navigate(getRouteForQuery(newQuery, router.location, true), { replace: true }); + }); + }; + public render() { const { currency, groupBy, + intl, + isAccountInfoDetailsToggleEnabled, + isCurrentMonthData, + isDetailsDateRangeToggleEnabled, isExportsToggleEnabled, onCurrencySelect, onGroupBySelect, providers, providersError, report, - intl, + timeScopeValue, } = this.props; - const showContent = report && !providersError && providers?.meta?.count > 0; + const { currentDateRangeType } = this.state; + const showContent = report && !providersError && providers?.meta?.count > 0; const hasCost = report?.meta?.total?.cost?.total; return ( @@ -86,20 +129,35 @@ class DetailsHeaderBase extends React.Component { + {isAccountInfoDetailsToggleEnabled && ( + + + + + + )} - -
- + + + {isDetailsDateRangeToggleEnabled && ( + + -
-
+
+ )}
@@ -111,7 +169,9 @@ class DetailsHeaderBase extends React.Component { hasCost ? report.meta.total.cost.total.units : 'USD' )} -
{getSinceDateRangeString()}
+
+ {getSinceDateRangeString(undefined, timeScopeValue === -2 ? 1 : 0, true)} +
)}
@@ -133,6 +193,8 @@ const mapStateToProps = createMapStateToProps !column.hidden); + const filteredRows = rows.map(({ ...row }) => { + row.cells = row.cells.filter(cell => !cell.hidden); + return row; + }); + this.setState({ - columns, - rows, + columns: filteredColumns, + rows: filteredRows, }); }; @@ -200,7 +213,7 @@ class DetailsTableBase extends React.Component { - const { intl } = this.props; + const { intl, timeScopeValue } = this.props; const value = formatCurrency(Math.abs(item.cost.total.value - item.delta_value), item.cost.total.units); const percentage = item.delta_percent !== null ? formatPercentage(Math.abs(item.delta_percent)) : 0; @@ -219,7 +232,11 @@ class DetailsTableBase extends React.Component @@ -240,7 +257,12 @@ class DetailsTableBase extends React.Component
- {getForDateRangeString(value)} + {getForDateRangeString( + value, + undefined, + timeScopeValue === -2 ? 2 : 1, + timeScopeValue === -2 ? true : false + )}
); diff --git a/src/routes/details/azureDetails/detailsToolbar.tsx b/src/routes/details/azureDetails/detailsToolbar.tsx index 71575e7ec..0b9559de6 100644 --- a/src/routes/details/azureDetails/detailsToolbar.tsx +++ b/src/routes/details/azureDetails/detailsToolbar.tsx @@ -33,6 +33,7 @@ interface DetailsToolbarOwnProps { pagination?: React.ReactNode; query?: AzureQuery; selectedItems?: ComputedReportItem[]; + timeScopeValue?: number; } interface DetailsToolbarStateProps { @@ -162,27 +163,28 @@ export class DetailsToolbarBase extends React.Component((state, props) => { - // Note: Omitting key_only would help to share a single, cached request -- the toolbar requires key values - // However, for better server-side performance, we chose to use key_only here. - const tagQueryString = getQuery({ - filter: { - resolution: 'monthly', - time_scope_units: 'month', - time_scope_value: -1, - }, - key_only: true, - limit: 1000, - }); - const tagReport = tagSelectors.selectTag(state, tagPathsType, tagType, tagQueryString); - const tagReportFetchStatus = tagSelectors.selectTagFetchStatus(state, tagPathsType, tagType, tagQueryString); - return { - tagReportFetchStatus, - tagReport, - tagQueryString, - }; -}); +const mapStateToProps = createMapStateToProps( + (state, { timeScopeValue = -1 }) => { + // Note: Omitting key_only would help to share a single, cached request -- the toolbar requires key values + // However, for better server-side performance, we chose to use key_only here. + const tagQueryString = getQuery({ + filter: { + resolution: 'monthly', + time_scope_units: 'month', + time_scope_value: timeScopeValue, + }, + key_only: true, + limit: 1000, + }); + const tagReport = tagSelectors.selectTag(state, tagPathsType, tagType, tagQueryString); + const tagReportFetchStatus = tagSelectors.selectTagFetchStatus(state, tagPathsType, tagType, tagQueryString); + return { + tagReportFetchStatus, + tagReport, + tagQueryString, + }; + } +); const mapDispatchToProps: DetailsToolbarDispatchProps = { fetchTag: tagActions.fetchTag, diff --git a/src/routes/details/components/breakdown/breakdownBase.tsx b/src/routes/details/components/breakdown/breakdownBase.tsx index 69d0dc8e3..7b488e1db 100644 --- a/src/routes/details/components/breakdown/breakdownBase.tsx +++ b/src/routes/details/components/breakdown/breakdownBase.tsx @@ -15,7 +15,7 @@ import { Loading } from 'routes/components/page/loading'; import { NoData } from 'routes/components/page/noData'; import { NoProviders } from 'routes/components/page/noProviders'; import { NotAvailable } from 'routes/components/page/notAvailable'; -import { hasCurrentMonthData } from 'routes/utils/providers'; +import { hasCurrentMonthData, hasPreviousMonthData } from 'routes/utils/providers'; import { handleOnCostDistributionSelect, handleOnCostTypeSelect, @@ -70,6 +70,7 @@ export interface BreakdownStateProps { historicalDataComponent?: React.ReactNode; instancesComponent?: React.ReactNode; isAwsEc2InstancesToggleEnabled?: boolean; + isDetailsDateRangeToggleEnabled?: boolean; isOptimizationsTab?: boolean; optimizationsBadgeComponent?: React.ReactNode; optimizationsComponent?: React.ReactNode; @@ -88,6 +89,7 @@ export interface BreakdownStateProps { showCostDistribution?: boolean; showCostType?: boolean; tagPathsType?: TagPathsType; + timeScopeValue?: number; title?: string; } @@ -289,6 +291,7 @@ class BreakdownBase extends React.Component { detailsURL, emptyStateTitle, groupBy, + isDetailsDateRangeToggleEnabled, optimizationsComponent, providers, providersFetchStatus, @@ -301,6 +304,7 @@ class BreakdownBase extends React.Component { showCostDistribution, showCostType, tagPathsType, + timeScopeValue, title, } = this.props; const { activeTabKey } = this.state; @@ -320,7 +324,11 @@ class BreakdownBase extends React.Component { if (noProviders) { return ; } - if (!hasCurrentMonthData(providers)) { + if ( + isDetailsDateRangeToggleEnabled + ? !hasCurrentMonthData(providers) && !hasPreviousMonthData(providers) + : !hasCurrentMonthData(providers) + ) { return ; } } @@ -352,6 +360,7 @@ class BreakdownBase extends React.Component { showCurrency={!(optimizationsComponent && activeTabKey === 2)} tabs={this.getTabs(availableTabs)} tagPathsType={tagPathsType} + timeScopeValue={timeScopeValue} title={title} />
{this.getTabContent(availableTabs)}
diff --git a/src/routes/details/components/breakdown/breakdownHeader.tsx b/src/routes/details/components/breakdown/breakdownHeader.tsx index 91b755a32..541505091 100644 --- a/src/routes/details/components/breakdown/breakdownHeader.tsx +++ b/src/routes/details/components/breakdown/breakdownHeader.tsx @@ -59,6 +59,7 @@ interface BreakdownHeaderOwnProps extends RouterComponentProps { showCurrency?: boolean; tabs: React.ReactNode; tagPathsType: TagPathsType; + timeScopeValue?: number; title: string; } @@ -206,6 +207,7 @@ class BreakdownHeader extends React.Component { showCurrency, tabs, tagPathsType, + timeScopeValue, title, } = this.props; @@ -291,7 +293,10 @@ class BreakdownHeader extends React.Component {
{getTotalCostDateRangeString( - intl.formatMessage(messages.groupByValuesTitleCase, { value: groupByKey, count: 2 }) + intl.formatMessage(messages.groupByValuesTitleCase, { value: groupByKey, count: 2 }), + undefined, + timeScopeValue === -2 ? 1 : 0, + true )}
diff --git a/src/routes/details/components/historicalData/historicalDataBase.tsx b/src/routes/details/components/historicalData/historicalDataBase.tsx index b791d0b0e..b5512d72b 100644 --- a/src/routes/details/components/historicalData/historicalDataBase.tsx +++ b/src/routes/details/components/historicalData/historicalDataBase.tsx @@ -18,6 +18,7 @@ interface HistoricalDataOwnProps { costType?: string; currency?: string; groupBy?: string; + timeScopeValue?: number; } export interface HistoricalDataStateProps { @@ -39,7 +40,7 @@ class HistoricalDatasBase extends React.Component { // Returns cost chart private getCostChart = (widget: HistoricalDataWidget) => { - const { costDistribution, costType, currency, intl } = this.props; + const { costDistribution, costType, currency, intl, timeScopeValue } = this.props; return ( @@ -58,6 +59,7 @@ class HistoricalDatasBase extends React.Component { currency={currency} reportPathsType={widget.reportPathsType} reportType={widget.reportType} + timeScopeValue={timeScopeValue} /> @@ -66,7 +68,7 @@ class HistoricalDatasBase extends React.Component { // Returns network chart private getNetworkChart = (widget: HistoricalDataWidget) => { - const { groupBy, intl } = this.props; + const { groupBy, intl, timeScopeValue } = this.props; let showWidget = false; if (widget?.showWidgetOnGroupBy) { @@ -92,6 +94,7 @@ class HistoricalDatasBase extends React.Component { chartName={widget.chartName} reportPathsType={widget.reportPathsType} reportType={widget.reportType} + timeScopeValue={timeScopeValue} /> @@ -102,7 +105,7 @@ class HistoricalDatasBase extends React.Component { // Returns storage summary private getVolumeChart = (widget: HistoricalDataWidget) => { - const { groupBy, intl } = this.props; + const { groupBy, intl, timeScopeValue } = this.props; let showWidget = false; if (widget?.showWidgetOnGroupBy) { @@ -128,6 +131,7 @@ class HistoricalDatasBase extends React.Component { chartName={widget.chartName} reportPathsType={widget.reportPathsType} reportType={widget.reportType} + timeScopeValue={timeScopeValue} /> @@ -138,7 +142,7 @@ class HistoricalDatasBase extends React.Component { // Returns trend chart private getTrendChart = (widget: HistoricalDataWidget) => { - const { costType, currency, intl } = this.props; + const { costType, currency, intl, timeScopeValue } = this.props; return ( @@ -156,6 +160,7 @@ class HistoricalDatasBase extends React.Component { currency={currency} reportPathsType={widget.reportPathsType} reportType={widget.reportType} + timeScopeValue={timeScopeValue} /> @@ -164,7 +169,7 @@ class HistoricalDatasBase extends React.Component { // Returns usage chart private getUsageChart = (widget: HistoricalDataWidget) => { - const { intl } = this.props; + const { intl, timeScopeValue } = this.props; return ( @@ -180,6 +185,7 @@ class HistoricalDatasBase extends React.Component { chartName={widget.chartName} reportPathsType={widget.reportPathsType} reportType={widget.reportType} + timeScopeValue={timeScopeValue} /> diff --git a/src/routes/details/components/historicalData/historicalDataCostChart.tsx b/src/routes/details/components/historicalData/historicalDataCostChart.tsx index 11f9200ad..db66b5ee5 100644 --- a/src/routes/details/components/historicalData/historicalDataCostChart.tsx +++ b/src/routes/details/components/historicalData/historicalDataCostChart.tsx @@ -29,6 +29,7 @@ interface HistoricalDataCostChartOwnProps extends RouterComponentProps, WrappedC currency?: string; reportPathsType: ReportPathsType; reportType: ReportType; + timeScopeValue?: number; } interface HistoricalDataCostChartStateProps { @@ -147,7 +148,7 @@ class HistoricalDataCostChartBase extends React.Component( - (state, { costType, currency, reportPathsType, reportType, router }) => { + (state, { costType, currency, reportPathsType, reportType, router, timeScopeValue }) => { const queryFromRoute = parseQuery(router.location.search); const queryState = getQueryState(router.location, 'details'); @@ -182,7 +183,7 @@ const mapStateToProps = createMapStateToProps( - (state, { reportPathsType, reportType, router }) => { + (state, { reportPathsType, reportType, router, timeScopeValue }) => { const queryFromRoute = parseQuery(router.location.search); const queryState = getQueryState(router.location, 'details'); @@ -176,7 +177,7 @@ const mapStateToProps = createMapStateToProps( - (state, { costType, currency, reportPathsType, reportType, router }) => { + (state, { costType, currency, reportPathsType, reportType, router, timeScopeValue }) => { const queryFromRoute = parseQuery(router.location.search); const queryState = getQueryState(router.location, 'details'); @@ -189,7 +190,7 @@ const mapStateToProps = createMapStateToProps( - (state, { reportPathsType, reportType, router }) => { + (state, { reportPathsType, reportType, router, timeScopeValue }) => { const queryFromRoute = parseQuery(router.location.search); const queryState = getQueryState(router.location, 'details'); @@ -160,7 +161,7 @@ const mapStateToProps = createMapStateToProps( - (state, { reportPathsType, reportType, router }) => { + (state, { reportPathsType, reportType, router, timeScopeValue }) => { const queryFromRoute = parseQuery(router.location.search); const queryState = getQueryState(router.location, 'details'); @@ -157,7 +158,7 @@ const mapStateToProps = createMapStateToProps = ({ isStatusMsg, providerId, providerType, + uuId, }: OverallStatusProps) => { const { providers, providersError } = useMapToProps(); const intl = useIntl(); - if (!providers || providersError) { - return null; - } - - // Filter OCP providers to skip an extra API request - const filteredProviders = filterProviders(providers, providerType)?.data?.filter(data => data.status !== null); - const provider = filteredProviders?.find( - val => providerId === val.id || (clusterId && val.authentication?.credentials?.cluster_id === clusterId) - ); - const cloudProvider = providers?.data?.find(val => val.uuid === provider?.infrastructure?.uuid); + // Filter providers to skip an extra API request + const getFilteredProviders = () => { + return filterProviders(providers, providerType)?.data?.filter(data => data.status !== null); + }; - const getOverallStatus = (): { lastUpdated: string; msg: MessageDescriptor; status: StatusType } => { + const getOverallStatus = ( + provider, + cloudProvider + ): { lastUpdated: string; msg: MessageDescriptor; status: StatusType } => { let lastUpdated; let msg; let status; @@ -117,24 +116,111 @@ const OverallStatus: React.FC = ({ return { lastUpdated, msg, status }; }; - const overallStatus = getOverallStatus(); + const getAllStatus = () => { + let completeCount = 0; + let failedCount = 0; + let inProgressCount = 0; + let pausedCount = 0; + let pendingCount = 0; - if (isLastUpdated) { - return overallStatus.lastUpdated ? formatDate(overallStatus.lastUpdated) : null; - } - if (overallStatus.msg && overallStatus.status) { + const overallProviderStatus = []; + const filteredProviders = getFilteredProviders(); + + filteredProviders.map(provider => { + const cloudProvider = providers?.data?.find(val => val.uuid === provider?.infrastructure?.uuid); + overallProviderStatus.push(getOverallStatus(provider, cloudProvider)); + }); + + overallProviderStatus.map(overallStatus => { + if (overallStatus.status === StatusType.failed) { + failedCount++; + } + if (overallStatus.status === StatusType.paused) { + pausedCount++; + } + if (overallStatus.status === StatusType.inProgress) { + inProgressCount++; + } + if (overallStatus.status === StatusType.pending) { + pendingCount++; + } + if (overallStatus.status === StatusType.complete) { + completeCount++; + } + }); return ( <> - {getOverallStatusIcon(overallStatus.status)} - - {isStatusMsg - ? intl.formatMessage(messages.statusMsg, { value: overallStatus.status }) - : intl.formatMessage(overallStatus.msg)} - + {completeCount > 0 && ( + <> + {completeCount} + {getOverallStatusIcon(StatusType.complete)} + + )} + {failedCount > 0 && ( + <> + {failedCount} + {getOverallStatusIcon(StatusType.failed)} + + )} + {inProgressCount > 0 && ( + <> + {inProgressCount} + {getOverallStatusIcon(StatusType.inProgress)} + + )} + {pausedCount > 0 && ( + <> + {pausedCount} + {getOverallStatusIcon(StatusType.paused)} + + )} + {pendingCount > 0 && ( + <> + {pendingCount} + {getOverallStatusIcon(StatusType.pending)} + + )} ); + }; + + const getStatus = () => { + const filteredProviders = getFilteredProviders(); + const provider = filteredProviders?.find( + val => + providerId === val.id || + (clusterId && val.authentication?.credentials?.cluster_id === clusterId) || + uuId === val.uuid + ); + const cloudProvider = providers?.data?.find(val => val.uuid === provider?.infrastructure?.uuid); + const overallStatus = getOverallStatus(provider, cloudProvider); + + if (isLastUpdated) { + return overallStatus.lastUpdated ? formatDate(overallStatus.lastUpdated) : null; + } + if (overallStatus.msg && overallStatus.status) { + return ( + <> + {getOverallStatusIcon(overallStatus.status)} + + {isStatusMsg + ? intl.formatMessage(messages.statusMsg, { value: overallStatus.status }) + : intl.formatMessage(overallStatus.msg)} + + + ); + } + return null; + }; + + if (!providers || providersError) { + return null; + } + if (providerId || clusterId || uuId) { + return getStatus(); + } else { + return getAllStatus(); } - return null; }; const useMapToProps = (): OverallStatusStateProps => { diff --git a/src/routes/details/components/providerDetails/components/sourceLink.tsx b/src/routes/details/components/providerStatus/components/sourceLink.tsx similarity index 91% rename from src/routes/details/components/providerDetails/components/sourceLink.tsx rename to src/routes/details/components/providerStatus/components/sourceLink.tsx index e02c4a60a..2b97d1ece 100644 --- a/src/routes/details/components/providerDetails/components/sourceLink.tsx +++ b/src/routes/details/components/providerStatus/components/sourceLink.tsx @@ -2,7 +2,7 @@ import type { Provider } from 'api/providers'; import messages from 'locales/messages'; import React from 'react'; import { useIntl } from 'react-intl'; -import { normalize } from 'routes/details/components/providerDetails/utils/normailize'; +import { normalize } from 'routes/details/components/providerStatus/utils/normailize'; import { getReleasePath } from 'utils/paths'; import { styles } from './component.styles'; diff --git a/src/routes/details/components/providerStatus/index.ts b/src/routes/details/components/providerStatus/index.ts new file mode 100644 index 000000000..019d75fc8 --- /dev/null +++ b/src/routes/details/components/providerStatus/index.ts @@ -0,0 +1,3 @@ +export { ProviderBreakdownModal } from './providerBreakdownModal'; +export { ProviderDetailsModal } from './providerDetailsModal'; +export { ProviderStatus } from './providerStatus'; diff --git a/src/routes/details/components/providerDetails/providerDetailsContent.tsx b/src/routes/details/components/providerStatus/providerBreakdownContent.tsx similarity index 90% rename from src/routes/details/components/providerDetails/providerDetailsContent.tsx rename to src/routes/details/components/providerStatus/providerBreakdownContent.tsx index 1730c91e0..98db75cdd 100644 --- a/src/routes/details/components/providerDetails/providerDetailsContent.tsx +++ b/src/routes/details/components/providerStatus/providerBreakdownContent.tsx @@ -16,12 +16,13 @@ import { providersQuery, providersSelectors } from 'store/providers'; import { CloudData } from './components/cloudData'; import { ClusterData } from './components/clusterData'; import { Finalization } from './components/finalization'; -import { styles } from './providerDetails.styles'; +import { styles } from './providerStatus.styles'; interface ProviderDetailsContentOwnProps { clusterId?: string; providerId?: string; providerType: ProviderType; + uuId?: string; } interface ProviderDetailsContentStateProps { @@ -33,10 +34,11 @@ interface ProviderDetailsContentStateProps { type ProviderDetailsContentProps = ProviderDetailsContentOwnProps; -const ProviderDetailsContent: React.FC = ({ +const ProviderBreakdownContent: React.FC = ({ clusterId, providerId, providerType, + uuId, }: ProviderDetailsContentProps) => { const intl = useIntl(); @@ -59,7 +61,10 @@ const ProviderDetailsContent: React.FC = ({ // Filter OCP providers to skip an extra API request const filteredProviders = filterProviders(providers, providerType)?.data?.filter(data => data.status !== null); const provider = filteredProviders?.find( - val => providerId === val.id || (clusterId && val.authentication?.credentials?.cluster_id === clusterId) + val => + providerId === val.id || + (clusterId && val.authentication?.credentials?.cluster_id === clusterId) || + uuId === val.uuid ); if (providerType === ProviderType.ocp) { @@ -101,4 +106,4 @@ const useMapToProps = (): ProviderDetailsContentStateProps => { }; }; -export { ProviderDetailsContent }; +export { ProviderBreakdownContent }; diff --git a/src/routes/details/components/providerDetails/providerDetailsModal.tsx b/src/routes/details/components/providerStatus/providerBreakdownModal.tsx similarity index 69% rename from src/routes/details/components/providerDetails/providerDetailsModal.tsx rename to src/routes/details/components/providerStatus/providerBreakdownModal.tsx index aa3765084..46f997758 100644 --- a/src/routes/details/components/providerDetails/providerDetailsModal.tsx +++ b/src/routes/details/components/providerStatus/providerBreakdownModal.tsx @@ -6,23 +6,25 @@ import React, { useState } from 'react'; import { useIntl } from 'react-intl'; import { OverallStatus } from './components/overallStatus'; -import { styles } from './providerDetails.styles'; -import { ProviderDetailsContent } from './providerDetailsContent'; +import { ProviderBreakdownContent } from './providerBreakdownContent'; +import { styles } from './providerStatus.styles'; interface ProviderDetailsModalOwnProps { clusterId?: string; - showStatus?: boolean; + isOverallStatus?: boolean; providerId?: string; providerType: ProviderType; + uuId?: string; } type ProviderDetailsModalProps = ProviderDetailsModalOwnProps; -const ProviderDetailsModal: React.FC = ({ +const ProviderBreakdownModal: React.FC = ({ clusterId, + isOverallStatus, providerId, providerType, - showStatus = true, + uuId, }: ProviderDetailsModalProps) => { const intl = useIntl(); const [isOpen, setIsOpen] = useState(false); @@ -32,7 +34,7 @@ const ProviderDetailsModal: React.FC = ({ }; const handleOnClick = () => { - setIsOpen(!isOpen); + setIsOpen(true); }; // PatternFly modal appends to document.body, which is outside the scoped "costManagement" dom tree. @@ -40,18 +42,25 @@ const ProviderDetailsModal: React.FC = ({ return ( <> - {showStatus && } + {isOverallStatus && ( + + )} - + ); }; -export { ProviderDetailsModal }; +export { ProviderBreakdownModal }; diff --git a/src/routes/details/components/providerStatus/providerDetailsContent.tsx b/src/routes/details/components/providerStatus/providerDetailsContent.tsx new file mode 100644 index 000000000..582747df9 --- /dev/null +++ b/src/routes/details/components/providerStatus/providerDetailsContent.tsx @@ -0,0 +1,56 @@ +import { Button, ButtonVariant } from '@patternfly/react-core'; +import type { ProviderType } from 'api/providers'; +import messages from 'locales/messages'; +import React, { useState } from 'react'; +import { useIntl } from 'react-intl'; + +import { ProviderBreakdownContent } from './providerBreakdownContent'; +import { ProviderStatus } from './providerStatus'; +import { styles } from './providerStatus.styles'; + +interface ProviderDetailsContentOwnProps { + onBackClick?: () => void; + onDetailsClick?: () => void; + providerType: ProviderType; +} + +type ProviderDetailsContentProps = ProviderDetailsContentOwnProps; + +const ProviderDetailsContent: React.FC = ({ + onBackClick, + onDetailsClick, + providerType, +}: ProviderDetailsContentProps) => { + const intl = useIntl(); + const [isBreakdown, setIsBreakdown] = useState(false); + const [providerId, setProviderId] = useState(); + + const handleOnBackClick = () => { + setProviderId(undefined); + setIsBreakdown(false); + if (onBackClick) { + onBackClick(); + } + }; + + const handleOnDetailsClick = (id: string) => { + setProviderId(id); + setIsBreakdown(true); + if (onDetailsClick) { + onDetailsClick(); + } + }; + + return isBreakdown ? ( + <> + + + + ) : ( + + ); +}; + +export { ProviderDetailsContent }; diff --git a/src/routes/details/components/providerStatus/providerDetailsModal.tsx b/src/routes/details/components/providerStatus/providerDetailsModal.tsx new file mode 100644 index 000000000..024604d2f --- /dev/null +++ b/src/routes/details/components/providerStatus/providerDetailsModal.tsx @@ -0,0 +1,67 @@ +import { Button, ButtonVariant } from '@patternfly/react-core'; +import { Modal, ModalBody, ModalHeader, ModalVariant } from '@patternfly/react-core/next'; +import type { ProviderType } from 'api/providers'; +import messages from 'locales/messages'; +import React, { useState } from 'react'; +import { useIntl } from 'react-intl'; + +import { OverallStatus } from './components/overallStatus'; +import { ProviderDetailsContent } from './providerDetailsContent'; +import { styles } from './providerStatus.styles'; + +interface ProviderDetailsModalOwnProps { + providerType: ProviderType; +} + +type ProviderDetailsModalProps = ProviderDetailsModalOwnProps; + +const ProviderDetailsModal: React.FC = ({ providerType }: ProviderDetailsModalProps) => { + const intl = useIntl(); + const [isOpen, setIsOpen] = useState(false); + const [title, setTitle] = useState(messages.integrationsStatus); + const [variant, setVariant] = useState(ModalVariant.medium); + + const handleOnClose = () => { + setIsOpen(false); + }; + + const handleOnClick = () => { + setVariant(ModalVariant.medium); + setIsOpen(true); + }; + + const handleOnBackClick = () => { + setVariant(ModalVariant.medium); + setTitle(messages.integrationsStatus); + }; + + const handleOnDetailsClick = () => { + setVariant(ModalVariant.small); + setTitle(messages.integrationsDetails); + }; + + // PatternFly modal appends to document.body, which is outside the scoped "costManagement" dom tree. + // Use className="costManagement" to override PatternFly styles or append the modal to an element within the tree + + return ( + <> + {intl.formatMessage(messages.integrationsStatus)} + + + + + + + + + + ); +}; + +export { ProviderDetailsModal }; diff --git a/src/routes/details/components/providerDetails/providerDetails.styles.ts b/src/routes/details/components/providerStatus/providerStatus.styles.ts similarity index 51% rename from src/routes/details/components/providerDetails/providerDetails.styles.ts rename to src/routes/details/components/providerStatus/providerStatus.styles.ts index 944909be1..0c8be632b 100644 --- a/src/routes/details/components/providerDetails/providerDetails.styles.ts +++ b/src/routes/details/components/providerStatus/providerStatus.styles.ts @@ -1,17 +1,21 @@ import global_BackgroundColor_light_100 from '@patternfly/react-tokens/dist/js/global_BackgroundColor_light_100'; +import global_FontSize_sm from '@patternfly/react-tokens/dist/js/global_FontSize_sm'; import global_FontSize_xs from '@patternfly/react-tokens/dist/js/global_FontSize_xs'; -import global_spacer_xl from '@patternfly/react-tokens/dist/js/global_spacer_xl'; import type React from 'react'; export const styles = { + backButton: { + paddingBottom: global_FontSize_sm.var, + paddingLeft: 0, + paddingTop: 0, + }, dataDetailsButton: { - fontSize: global_FontSize_xs.value, + fontSize: global_FontSize_xs.var, }, loading: { - backgroundColor: global_BackgroundColor_light_100.value, + backgroundColor: global_BackgroundColor_light_100.var, }, - detailsTable: { - marginBottom: global_spacer_xl.value, - marginTop: global_spacer_xl.value, + statusLabel: { + marginRight: global_FontSize_xs.var, }, } as { [className: string]: React.CSSProperties }; diff --git a/src/routes/details/components/providerDetails/providerDetails.tsx b/src/routes/details/components/providerStatus/providerStatus.tsx similarity index 75% rename from src/routes/details/components/providerDetails/providerDetails.tsx rename to src/routes/details/components/providerStatus/providerStatus.tsx index f191acd4b..2942d2edf 100644 --- a/src/routes/details/components/providerDetails/providerDetails.tsx +++ b/src/routes/details/components/providerStatus/providerStatus.tsx @@ -1,4 +1,3 @@ -import { Bullseye } from '@patternfly/react-core'; import type { Providers } from 'api/providers'; import { ProviderType } from 'api/providers'; import { getProvidersQuery } from 'api/queries/providersQuery'; @@ -14,28 +13,29 @@ import type { RootState } from 'store'; import { FetchStatus } from 'store/common'; import { providersQuery, providersSelectors } from 'store/providers'; -import { styles } from './providerDetails.styles'; -import { ProviderDetailsTable } from './providerDetailsTable'; +import { styles } from './providerStatus.styles'; +import { ProviderTable } from './providerTable'; -interface ProviderDetailsOwnProps { +interface ProviderStatusOwnProps { + onClick?: (providerId: string) => void; providerType: ProviderType; } -interface ProviderDetailsStateProps { +interface ProviderStatusStateProps { providers: Providers; providersError: AxiosError; providersFetchStatus: FetchStatus; providersQueryString: string; } -type ProviderDetailsProps = ProviderDetailsOwnProps; +type ProviderStatusProps = ProviderStatusOwnProps; -const ProviderDetails: React.FC = ({ providerType }: ProviderDetailsProps) => { +const ProviderStatus: React.FC = ({ onClick, providerType }: ProviderStatusProps) => { const intl = useIntl(); const { providers, providersError, providersFetchStatus } = useMapToProps(); - const title = intl.formatMessage(messages.providerDetails); + const title = intl.formatMessage(messages.integrationsDetails); if (providersError) { return ; @@ -55,14 +55,10 @@ const ProviderDetails: React.FC = ({ providerType }: Provi return; } - return ( - - - - ); + return ; }; -const useMapToProps = (): ProviderDetailsStateProps => { +const useMapToProps = (): ProviderStatusStateProps => { // PermissionsWrapper has already made an API request const providersQueryString = getProvidersQuery(providersQuery); const providers = useSelector((state: RootState) => @@ -83,4 +79,4 @@ const useMapToProps = (): ProviderDetailsStateProps => { }; }; -export { ProviderDetails }; +export { ProviderStatus }; diff --git a/src/routes/details/components/providerDetails/providerDetailsTable.tsx b/src/routes/details/components/providerStatus/providerTable.tsx similarity index 57% rename from src/routes/details/components/providerDetails/providerDetailsTable.tsx rename to src/routes/details/components/providerStatus/providerTable.tsx index 2997dc3dd..ace42feda 100644 --- a/src/routes/details/components/providerDetails/providerDetailsTable.tsx +++ b/src/routes/details/components/providerStatus/providerTable.tsx @@ -1,3 +1,4 @@ +import { Button, ButtonVariant } from '@patternfly/react-core'; import type { Provider, ProviderType } from 'api/providers'; import messages from 'locales/messages'; import React, { useEffect, useState } from 'react'; @@ -6,16 +7,19 @@ import { DataTable } from 'routes/components/dataTable'; import { OverallStatus } from './components/overallStatus'; import { SourceLink } from './components/sourceLink'; -import { ProviderDetailsModal } from './providerDetailsModal'; +import { ProviderBreakdownModal } from './providerBreakdownModal'; +import { styles } from './providerStatus.styles'; -interface ProviderDetailsTableOwnProps { +interface ProviderTableOwnProps { + onClick?: (id: string) => void; providers?: Provider[]; providerType?: ProviderType; + showContent?: boolean; } -type ProviderDetailsTableProps = ProviderDetailsTableOwnProps; +type ProviderTableProps = ProviderTableOwnProps; -const ProviderDetailsTable: React.FC = ({ providers, providerType }) => { +const ProviderTable: React.FC = ({ onClick, providers, providerType }) => { const intl = useIntl(); const [columns, setColumns] = useState([]); const [rows, setRows] = useState([]); @@ -47,9 +51,17 @@ const ProviderDetailsTable: React.FC = ({ providers, newRows.push({ cells: [ { value: }, - { value: }, - { value: }, - { value: }, + { value: }, + { value: }, + { + value: onClick ? ( + + ) : ( + + ), + }, ], item, }); @@ -66,4 +78,4 @@ const ProviderDetailsTable: React.FC = ({ providers, return rows.length ? : null; }; -export { ProviderDetailsTable }; +export { ProviderTable }; diff --git a/src/routes/details/components/providerDetails/utils/format.ts b/src/routes/details/components/providerStatus/utils/format.ts similarity index 100% rename from src/routes/details/components/providerDetails/utils/format.ts rename to src/routes/details/components/providerStatus/utils/format.ts diff --git a/src/routes/details/components/providerDetails/utils/icon.tsx b/src/routes/details/components/providerStatus/utils/icon.tsx similarity index 100% rename from src/routes/details/components/providerDetails/utils/icon.tsx rename to src/routes/details/components/providerStatus/utils/icon.tsx diff --git a/src/routes/details/components/providerDetails/utils/normailize.ts b/src/routes/details/components/providerStatus/utils/normailize.ts similarity index 100% rename from src/routes/details/components/providerDetails/utils/normailize.ts rename to src/routes/details/components/providerStatus/utils/normailize.ts diff --git a/src/routes/details/components/providerDetails/utils/status.ts b/src/routes/details/components/providerStatus/utils/status.ts similarity index 97% rename from src/routes/details/components/providerDetails/utils/status.ts rename to src/routes/details/components/providerStatus/utils/status.ts index 7da4f4a90..68c6fdd79 100644 --- a/src/routes/details/components/providerDetails/utils/status.ts +++ b/src/routes/details/components/providerStatus/utils/status.ts @@ -2,7 +2,7 @@ import type { Provider } from 'api/providers'; import { ProviderType } from 'api/providers'; import messages from 'locales/messages'; import type { MessageDescriptor } from 'react-intl'; -import { normalize } from 'routes/details/components/providerDetails/utils/normailize'; +import { normalize } from 'routes/details/components/providerStatus/utils/normailize'; export const enum StatusType { complete = 'complete', diff --git a/src/routes/details/components/providerDetails/utils/variant.ts b/src/routes/details/components/providerStatus/utils/variant.ts similarity index 100% rename from src/routes/details/components/providerDetails/utils/variant.ts rename to src/routes/details/components/providerStatus/utils/variant.ts diff --git a/src/routes/details/components/pvcChart/modal/pvcContent.tsx b/src/routes/details/components/pvcChart/modal/pvcContent.tsx index ec1f85caa..823b669c4 100644 --- a/src/routes/details/components/pvcChart/modal/pvcContent.tsx +++ b/src/routes/details/components/pvcChart/modal/pvcContent.tsx @@ -18,6 +18,7 @@ import { LoadingState } from 'routes/components/state/loadingState'; import { getGroupById, getGroupByValue } from 'routes/utils/groupBy'; import * as queryUtils from 'routes/utils/query'; import { getQueryState } from 'routes/utils/queryState'; +import { getTimeScopeValue } from 'routes/utils/timeScope'; import type { RootState } from 'store'; import { FetchStatus } from 'store/common'; import { reportActions, reportSelectors } from 'store/reports'; @@ -46,12 +47,6 @@ export interface PvcContentMapProps { type PvcContentProps = PvcContentOwnProps; const baseQuery: OcpQuery = { - filter: { - time_scope_units: 'month', - time_scope_value: -1, - resolution: 'monthly', - }, - filter_by: {}, limit: 10, offset: 0, order_by: {}, @@ -184,12 +179,16 @@ const useMapToProps = ({ query }: PvcContentMapProps): PvcContentStateProps => { const groupBy = getGroupById(queryFromRoute); const groupByValue = getGroupByValue(queryFromRoute); + const timeScopeValue = getTimeScopeValue(queryState); const reportQuery: Query = { filter: { ...query.filter, limit: query.limit, offset: query.offset, + resolution: 'monthly', + time_scope_units: 'month', + time_scope_value: timeScopeValue !== undefined ? timeScopeValue : -1, }, filter_by: { // Add filters here to apply logical OR/AND diff --git a/src/routes/details/components/pvcChart/pvcChart.tsx b/src/routes/details/components/pvcChart/pvcChart.tsx index c7af55c84..2a26fe4eb 100644 --- a/src/routes/details/components/pvcChart/pvcChart.tsx +++ b/src/routes/details/components/pvcChart/pvcChart.tsx @@ -29,6 +29,7 @@ import { getGroupById, getGroupByValue } from 'routes/utils/groupBy'; import { noop } from 'routes/utils/noop'; import { getQueryState } from 'routes/utils/queryState'; import { skeletonWidth } from 'routes/utils/skeleton'; +import { getTimeScopeValue } from 'routes/utils/timeScope'; import { createMapStateToProps, FetchStatus } from 'store/common'; import { reportActions, reportSelectors } from 'store/reports'; import { formatUsage, unitsLookupKey } from 'utils/format'; @@ -73,9 +74,6 @@ const baseQuery: OcpQuery = { filter: { limit: 2, // Render 2 items max offset: 0, - time_scope_units: 'month', - time_scope_value: -1, - resolution: 'monthly', }, order_by: { request: 'desc', @@ -358,11 +356,15 @@ const mapStateToProps = createMapStateToProps, + historicalDataComponent: , + isDetailsDateRangeToggleEnabled: FeatureToggleSelectors.selectIsDetailsDateRangeToggleEnabled(state), providers: filterProviders(providers, ProviderType.gcp), providersError, providersFetchStatus, @@ -108,6 +113,7 @@ const mapStateToProps = createMapStateToProps { + protected defaultState: DetailsHeaderState = { + currentDateRangeType: + this.props.timeScopeValue === -2 ? DateRangeType.previousMonth : DateRangeType.currentMonthToDate, + }; + public state: DetailsHeaderState = { ...this.defaultState }; + + private handleOnDateRangeSelected = (value: string) => { + const { query, router } = this.props; + + this.setState({ currentDateRangeType: value }, () => { + const newQuery = { + filter: {}, + ...JSON.parse(JSON.stringify(query)), + }; + newQuery.filter.time_scope_value = value === DateRangeType.previousMonth ? -2 : -1; + router.navigate(getRouteForQuery(newQuery, router.location, true), { replace: true }); + }); + }; + public render() { const { currency, groupBy, + intl, + isAccountInfoDetailsToggleEnabled, + isCurrentMonthData, + isDetailsDateRangeToggleEnabled, isExportsToggleEnabled, onCurrencySelect, onGroupBySelect, providers, providersError, report, - intl, + timeScopeValue, } = this.props; - const showContent = report && !providersError && providers?.meta?.count > 0; + const { currentDateRangeType } = this.state; + const showContent = report && !providersError && providers?.meta?.count > 0; const hasCost = report?.meta?.total?.cost?.total; return ( @@ -87,20 +130,35 @@ class DetailsHeaderBase extends React.Component { + {isAccountInfoDetailsToggleEnabled && ( + + + + + + )} - -
- + + + {isDetailsDateRangeToggleEnabled && ( + + -
-
+
+ )}
@@ -112,7 +170,9 @@ class DetailsHeaderBase extends React.Component { hasCost ? report.meta.total.cost.total.units : 'USD' )} -
{getSinceDateRangeString()}
+
+ {getSinceDateRangeString(undefined, timeScopeValue === -2 ? 1 : 0, true)} +
)}
@@ -134,6 +194,8 @@ const mapStateToProps = createMapStateToProps !column.hidden); + const filteredRows = rows.map(({ ...row }) => { + row.cells = row.cells.filter(cell => !cell.hidden); + return row; + }); + this.setState({ - columns, - rows, + columns: filteredColumns, + rows: filteredRows, }); }; @@ -200,7 +211,7 @@ class DetailsTableBase extends React.Component { - const { intl } = this.props; + const { intl, timeScopeValue } = this.props; const value = formatCurrency(Math.abs(item.cost.total.value - item.delta_value), item.cost.total.units); const percentage = item.delta_percent !== null ? formatPercentage(Math.abs(item.delta_percent)) : 0; @@ -219,7 +230,11 @@ class DetailsTableBase extends React.Component @@ -240,7 +255,12 @@ class DetailsTableBase extends React.Component
- {getForDateRangeString(value)} + {getForDateRangeString( + value, + undefined, + timeScopeValue === -2 ? 2 : 1, + timeScopeValue === -2 ? true : false + )}
); diff --git a/src/routes/details/gcpDetails/detailsToolbar.tsx b/src/routes/details/gcpDetails/detailsToolbar.tsx index 883ae41af..196559e6d 100644 --- a/src/routes/details/gcpDetails/detailsToolbar.tsx +++ b/src/routes/details/gcpDetails/detailsToolbar.tsx @@ -34,6 +34,7 @@ interface DetailsToolbarOwnProps { pagination?: React.ReactNode; query?: GcpQuery; selectedItems?: ComputedReportItem[]; + timeScopeValue?: number; } interface DetailsToolbarStateProps { @@ -160,28 +161,29 @@ export class DetailsToolbarBase extends React.Component((state, props) => { - // Note: Omitting key_only would help to share a single, cached request -- the toolbar requires key values - // However, for better server-side performance, we chose to use key_only here. - const tagQueryString = getQuery({ - filter: { - resolution: 'monthly', - time_scope_units: 'month', - time_scope_value: -1, - }, - key_only: true, - limit: 1000, - }); - - const tagReport = tagSelectors.selectTag(state, tagPathsType, tagType, tagQueryString); - const tagReportFetchStatus = tagSelectors.selectTagFetchStatus(state, tagPathsType, tagType, tagQueryString); - return { - tagReport, - tagReportFetchStatus, - tagQueryString, - }; -}); +const mapStateToProps = createMapStateToProps( + (state, { timeScopeValue = -1 }) => { + // Note: Omitting key_only would help to share a single, cached request -- the toolbar requires key values + // However, for better server-side performance, we chose to use key_only here. + const tagQueryString = getQuery({ + filter: { + resolution: 'monthly', + time_scope_units: 'month', + time_scope_value: timeScopeValue, + }, + key_only: true, + limit: 1000, + }); + + const tagReport = tagSelectors.selectTag(state, tagPathsType, tagType, tagQueryString); + const tagReportFetchStatus = tagSelectors.selectTagFetchStatus(state, tagPathsType, tagType, tagQueryString); + return { + tagReport, + tagReportFetchStatus, + tagQueryString, + }; + } +); const mapDispatchToProps: DetailsToolbarDispatchProps = { fetchTag: tagActions.fetchTag, diff --git a/src/routes/details/gcpDetails/gcpDetails.tsx b/src/routes/details/gcpDetails/gcpDetails.tsx index f10ff25b8..47d558765 100644 --- a/src/routes/details/gcpDetails/gcpDetails.tsx +++ b/src/routes/details/gcpDetails/gcpDetails.tsx @@ -1,4 +1,4 @@ -import { Pagination, PaginationVariant } from '@patternfly/react-core'; +import { Alert, Pagination, PaginationVariant } from '@patternfly/react-core'; import type { Providers } from 'api/providers'; import { ProviderType } from 'api/providers'; import type { GcpQuery } from 'api/queries/gcpQuery'; @@ -18,12 +18,12 @@ import { Loading } from 'routes/components/page/loading'; import { NoData } from 'routes/components/page/noData'; import { NoProviders } from 'routes/components/page/noProviders'; import { NotAvailable } from 'routes/components/page/notAvailable'; -import { ProviderDetails } from 'routes/details/components/providerDetails'; +import { ProviderStatus } from 'routes/details/components/providerStatus'; import { getIdKeyForGroupBy } from 'routes/utils/computedReport/getComputedGcpReportItems'; import type { ComputedReportItem } from 'routes/utils/computedReport/getComputedReportItems'; import { getUnsortedComputedReportItems } from 'routes/utils/computedReport/getComputedReportItems'; import { getGroupByTagKey } from 'routes/utils/groupBy'; -import { filterProviders, hasCurrentMonthData } from 'routes/utils/providers'; +import { filterProviders, hasCurrentMonthData, hasPreviousMonthData } from 'routes/utils/providers'; import { getRouteForQuery } from 'routes/utils/query'; import { handleOnCurrencySelect, @@ -33,10 +33,12 @@ import { handleOnSetPage, handleOnSort, } from 'routes/utils/queryNavigate'; +import { getTimeScopeValue } from 'routes/utils/timeScope'; import { createMapStateToProps, FetchStatus } from 'store/common'; import { FeatureToggleSelectors } from 'store/featureToggle'; import { providersQuery, providersSelectors } from 'store/providers'; import { reportActions, reportSelectors } from 'store/reports'; +import { getSinceDateRangeString } from 'utils/dates'; import { formatPath } from 'utils/paths'; import { noPrefix, tagPrefix } from 'utils/props'; import type { RouterComponentProps } from 'utils/router'; @@ -51,6 +53,9 @@ import { styles } from './gcpDetails.styles'; interface GcpDetailsStateProps { currency?: string; isAccountInfoEmptyStateToggleEnabled?: boolean; + isCurrentMonthData?: boolean; + isDetailsDateRangeToggleEnabled?: boolean; + isPreviousMonthData?: boolean; providers: Providers; providersError: AxiosError; providersFetchStatus: FetchStatus; @@ -59,6 +64,7 @@ interface GcpDetailsStateProps { reportError: AxiosError; reportFetchStatus: FetchStatus; reportQueryString: string; + timeScopeValue?: number; } interface GcpDetailsDispatchProps { @@ -105,14 +111,6 @@ class GcpDetails extends React.Component { }; public state: GcpDetailsState = { ...this.defaultState }; - constructor(stateProps, dispatchProps) { - super(stateProps, dispatchProps); - this.handleOnBulkSelect = this.handleOnBulkSelect.bind(this); - this.handleOnExportModalClose = this.handleOnExportModalClose.bind(this); - this.handleOnExportModalOpen = this.handleOnExportModalOpen.bind(this); - this.handleonSelect = this.handleonSelect.bind(this); - } - public componentDidMount() { this.updateReport(); } @@ -144,7 +142,7 @@ class GcpDetails extends React.Component { }; private getExportModal = (computedItems: ComputedReportItem[]) => { - const { query, report, reportQueryString } = this.props; + const { query, report, reportQueryString, timeScopeValue } = this.props; const { isAllSelected, isExportModalOpen, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -169,6 +167,7 @@ class GcpDetails extends React.Component { reportPathsType={reportPathsType} reportQueryString={reportQueryString} reportType={reportType} + timeScopeValue={timeScopeValue} /> ); }; @@ -203,7 +202,7 @@ class GcpDetails extends React.Component { }; private getTable = () => { - const { query, report, reportFetchStatus, reportQueryString, router } = this.props; + const { query, report, reportFetchStatus, reportQueryString, router, timeScopeValue } = this.props; const { isAllSelected, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); const groupByTagKey = getGroupByTagKey(query); @@ -224,12 +223,13 @@ class GcpDetails extends React.Component { report={report} reportQueryString={reportQueryString} selectedItems={selectedItems} + timeScopeValue={timeScopeValue} /> ); }; private getToolbar = (computedItems: ComputedReportItem[]) => { - const { query, router, report } = this.props; + const { query, router, report, timeScopeValue } = this.props; const { isAllSelected, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -252,6 +252,7 @@ class GcpDetails extends React.Component { pagination={this.getPagination(isDisabled)} query={query} selectedItems={selectedItems} + timeScopeValue={timeScopeValue} /> ); }; @@ -277,11 +278,11 @@ class GcpDetails extends React.Component { } }; - public handleOnExportModalClose = (isOpen: boolean) => { + private handleOnExportModalClose = (isOpen: boolean) => { this.setState({ isExportModalOpen: isOpen }); }; - public handleOnExportModalOpen = () => { + private handleOnExportModalOpen = () => { this.setState({ isExportModalOpen: true }); }; @@ -327,6 +328,9 @@ class GcpDetails extends React.Component { currency, intl, isAccountInfoEmptyStateToggleEnabled, + isCurrentMonthData, + isDetailsDateRangeToggleEnabled, + isPreviousMonthData, providers, providersFetchStatus, query, @@ -334,6 +338,7 @@ class GcpDetails extends React.Component { reportError, reportFetchStatus, router, + timeScopeValue, } = this.props; const computedItems = this.getComputedItems(); @@ -353,11 +358,11 @@ class GcpDetails extends React.Component { if (noProviders) { return ; } - if (!hasCurrentMonthData(providers)) { + if (isDetailsDateRangeToggleEnabled ? !isCurrentMonthData && !isPreviousMonthData : !isCurrentMonthData) { return ( : undefined + isAccountInfoEmptyStateToggleEnabled ? : undefined } title={title} /> @@ -370,12 +375,26 @@ class GcpDetails extends React.Component { handleOnCurrencySelect(query, router)} onGroupBySelect={this.handleOnGroupBySelect} + query={query} report={report} + timeScopeValue={timeScopeValue} />
-
{this.getToolbar(computedItems)}
+
+ {!isCurrentMonthData && isDetailsDateRangeToggleEnabled && ( + + )} + {this.getToolbar(computedItems)} +
{this.getExportModal(computedItems)} {reportFetchStatus === FetchStatus.inProgress ? ( @@ -395,12 +414,35 @@ class GcpDetails extends React.Component { const mapStateToProps = createMapStateToProps((state, { router }) => { const queryFromRoute = parseQuery(router.location.search); + const currency = getCurrency(); + // Check for current and previous data first + const providersQueryString = getProvidersQuery(providersQuery); + const providers = providersSelectors.selectProviders(state, ProviderType.all, providersQueryString); + const providersError = providersSelectors.selectProvidersError(state, ProviderType.all, providersQueryString); + const providersFetchStatus = providersSelectors.selectProvidersFetchStatus( + state, + ProviderType.all, + providersQueryString + ); + + // Fetch based on time scope value + const filteredProviders = filterProviders(providers, ProviderType.gcp); + const isCurrentMonthData = hasCurrentMonthData(filteredProviders); + const isDetailsDateRangeToggleEnabled = FeatureToggleSelectors.selectIsDetailsDateRangeToggleEnabled(state); + + let timeScopeValue = getTimeScopeValue(queryFromRoute); + timeScopeValue = Number( + !isCurrentMonthData && isDetailsDateRangeToggleEnabled ? -2 : timeScopeValue !== undefined ? timeScopeValue : -1 + ); + const query: any = { ...baseQuery, ...queryFromRoute, }; + query.filter.time_scope_value = timeScopeValue; // Add time scope here for breakdown pages + const reportQuery = { currency, delta: 'cost', @@ -409,7 +451,6 @@ const mapStateToProps = createMapStateToProps, + historicalDataComponent: , providers: filterProviders(providers, ProviderType.ibm), providersError, providersFetchStatus, @@ -108,6 +111,7 @@ const mapStateToProps = createMapStateToProps { + protected defaultState: DetailsHeaderState = { + currentDateRangeType: + this.props.timeScopeValue === -2 ? DateRangeType.previousMonth : DateRangeType.currentMonthToDate, + }; + public state: DetailsHeaderState = { ...this.defaultState }; + + private handleOnDateRangeSelected = (value: string) => { + const { query, router } = this.props; + + this.setState({ currentDateRangeType: value }, () => { + const newQuery = { + filter: {}, + ...JSON.parse(JSON.stringify(query)), + }; + newQuery.filter.time_scope_value = value === DateRangeType.previousMonth ? -2 : -1; + router.navigate(getRouteForQuery(newQuery, router.location, true), { replace: true }); + }); + }; + public render() { const { currency, groupBy, + intl, + isAccountInfoDetailsToggleEnabled, + isCurrentMonthData, + isDetailsDateRangeToggleEnabled, isExportsToggleEnabled, onCurrencySelect, onGroupBySelect, providers, providersError, report, - intl, + timeScopeValue, } = this.props; - const showContent = report && !providersError && providers?.meta?.count > 0; + const { currentDateRangeType } = this.state; + const showContent = report && !providersError && providers?.meta?.count > 0; const hasCost = report?.meta?.total?.cost?.total; return ( @@ -87,20 +130,35 @@ class DetailsHeaderBase extends React.Component { + {isAccountInfoDetailsToggleEnabled && ( + + + + + + )} - -
- + + + {isDetailsDateRangeToggleEnabled && ( + + -
-
+
+ )}
@@ -112,7 +170,9 @@ class DetailsHeaderBase extends React.Component { hasCost ? report.meta.total.cost.total.units : 'USD' )} -
{getSinceDateRangeString()}
+
+ {getSinceDateRangeString(undefined, timeScopeValue === -2 ? 1 : 0, true)} +
)}
@@ -134,6 +194,8 @@ const mapStateToProps = createMapStateToProps !column.hidden); + const filteredRows = rows.map(({ ...row }) => { + row.cells = row.cells.filter(cell => !cell.hidden); + return row; + }); + this.setState({ - columns, - rows, + columns: filteredColumns, + rows: filteredRows, }); }; @@ -200,7 +211,7 @@ class DetailsTableBase extends React.Component { - const { intl } = this.props; + const { intl, timeScopeValue } = this.props; const value = formatCurrency(Math.abs(item.cost.total.value - item.delta_value), item.cost.total.units); const percentage = item.delta_percent !== null ? formatPercentage(Math.abs(item.delta_percent)) : 0; @@ -219,7 +230,11 @@ class DetailsTableBase extends React.Component @@ -240,7 +255,12 @@ class DetailsTableBase extends React.Component
- {getForDateRangeString(value)} + {getForDateRangeString( + value, + undefined, + timeScopeValue === -2 ? 2 : 1, + timeScopeValue === -2 ? true : false + )}
); diff --git a/src/routes/details/ibmDetails/detailsToolbar.tsx b/src/routes/details/ibmDetails/detailsToolbar.tsx index af6f37f3f..3a4ca85da 100644 --- a/src/routes/details/ibmDetails/detailsToolbar.tsx +++ b/src/routes/details/ibmDetails/detailsToolbar.tsx @@ -34,6 +34,7 @@ interface DetailsToolbarOwnProps { pagination?: React.ReactNode; query?: IbmQuery; selectedItems?: ComputedReportItem[]; + timeScopeValue?: number; } interface DetailsToolbarStateProps { @@ -160,28 +161,29 @@ export class DetailsToolbarBase extends React.Component((state, props) => { - // Note: Omitting key_only would help to share a single, cached request -- the toolbar requires key values - // However, for better server-side performance, we chose to use key_only here. - const tagQueryString = getQuery({ - filter: { - resolution: 'monthly', - time_scope_units: 'month', - time_scope_value: -1, - }, - key_only: true, - limit: 1000, - }); - - const tagReport = tagSelectors.selectTag(state, tagPathsType, tagType, tagQueryString); - const tagReportFetchStatus = tagSelectors.selectTagFetchStatus(state, tagPathsType, tagType, tagQueryString); - return { - tagReport, - tagReportFetchStatus, - tagQueryString, - }; -}); +const mapStateToProps = createMapStateToProps( + (state, { timeScopeValue = -1 }) => { + // Note: Omitting key_only would help to share a single, cached request -- the toolbar requires key values + // However, for better server-side performance, we chose to use key_only here. + const tagQueryString = getQuery({ + filter: { + resolution: 'monthly', + time_scope_units: 'month', + time_scope_value: timeScopeValue, + }, + key_only: true, + limit: 1000, + }); + + const tagReport = tagSelectors.selectTag(state, tagPathsType, tagType, tagQueryString); + const tagReportFetchStatus = tagSelectors.selectTagFetchStatus(state, tagPathsType, tagType, tagQueryString); + return { + tagReport, + tagReportFetchStatus, + tagQueryString, + }; + } +); const mapDispatchToProps: DetailsToolbarDispatchProps = { fetchTag: tagActions.fetchTag, diff --git a/src/routes/details/ibmDetails/ibmDetails.tsx b/src/routes/details/ibmDetails/ibmDetails.tsx index 1af6f03db..2c3e319ae 100644 --- a/src/routes/details/ibmDetails/ibmDetails.tsx +++ b/src/routes/details/ibmDetails/ibmDetails.tsx @@ -1,4 +1,4 @@ -import { Pagination, PaginationVariant } from '@patternfly/react-core'; +import { Alert, Pagination, PaginationVariant } from '@patternfly/react-core'; import type { Providers } from 'api/providers'; import { ProviderType } from 'api/providers'; import type { IbmQuery } from 'api/queries/ibmQuery'; @@ -18,12 +18,12 @@ import { Loading } from 'routes/components/page/loading'; import { NoData } from 'routes/components/page/noData'; import { NoProviders } from 'routes/components/page/noProviders'; import { NotAvailable } from 'routes/components/page/notAvailable'; -import { ProviderDetails } from 'routes/details/components/providerDetails'; +import { ProviderStatus } from 'routes/details/components/providerStatus'; import { getIdKeyForGroupBy } from 'routes/utils/computedReport/getComputedIbmReportItems'; import type { ComputedReportItem } from 'routes/utils/computedReport/getComputedReportItems'; import { getUnsortedComputedReportItems } from 'routes/utils/computedReport/getComputedReportItems'; import { getGroupByTagKey } from 'routes/utils/groupBy'; -import { hasCurrentMonthData } from 'routes/utils/providers'; +import { hasCurrentMonthData, hasPreviousMonthData } from 'routes/utils/providers'; import { filterProviders } from 'routes/utils/providers'; import { getRouteForQuery } from 'routes/utils/query'; import { @@ -34,10 +34,12 @@ import { handleOnSetPage, handleOnSort, } from 'routes/utils/queryNavigate'; +import { getTimeScopeValue } from 'routes/utils/timeScope'; import { createMapStateToProps, FetchStatus } from 'store/common'; import { FeatureToggleSelectors } from 'store/featureToggle'; import { providersQuery, providersSelectors } from 'store/providers'; import { reportActions, reportSelectors } from 'store/reports'; +import { getSinceDateRangeString } from 'utils/dates'; import { formatPath } from 'utils/paths'; import { noPrefix, tagPrefix } from 'utils/props'; import type { RouterComponentProps } from 'utils/router'; @@ -52,6 +54,9 @@ import { styles } from './ibmDetails.styles'; interface IbmDetailsStateProps { currency?: string; isAccountInfoEmptyStateToggleEnabled?: boolean; + isCurrentMonthData?: boolean; + isDetailsDateRangeToggleEnabled?: boolean; + isPreviousMonthData?: boolean; providers: Providers; providersError: AxiosError; providersFetchStatus: FetchStatus; @@ -60,6 +65,7 @@ interface IbmDetailsStateProps { reportError: AxiosError; reportFetchStatus: FetchStatus; reportQueryString: string; + timeScopeValue?: number; } interface IbmDetailsDispatchProps { @@ -106,14 +112,6 @@ class IbmDetails extends React.Component { }; public state: IbmDetailsState = { ...this.defaultState }; - constructor(stateProps, dispatchProps) { - super(stateProps, dispatchProps); - this.handleOnBulkSelect = this.handleOnBulkSelect.bind(this); - this.handleOnExportModalClose = this.handleOnExportModalClose.bind(this); - this.handleOnExportModalOpen = this.handleOnExportModalOpen.bind(this); - this.handleonSelect = this.handleonSelect.bind(this); - } - public componentDidMount() { this.updateReport(); } @@ -145,7 +143,7 @@ class IbmDetails extends React.Component { }; private getExportModal = (computedItems: ComputedReportItem[]) => { - const { query, report, reportQueryString } = this.props; + const { query, report, reportQueryString, timeScopeValue } = this.props; const { isAllSelected, isExportModalOpen, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -170,6 +168,7 @@ class IbmDetails extends React.Component { reportPathsType={reportPathsType} reportQueryString={reportQueryString} reportType={reportType} + timeScopeValue={timeScopeValue} /> ); }; @@ -204,7 +203,7 @@ class IbmDetails extends React.Component { }; private getTable = () => { - const { query, report, reportFetchStatus, reportQueryString, router } = this.props; + const { query, report, reportFetchStatus, reportQueryString, router, timeScopeValue } = this.props; const { isAllSelected, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -226,12 +225,13 @@ class IbmDetails extends React.Component { report={report} reportQueryString={reportQueryString} selectedItems={selectedItems} + timeScopeValue={timeScopeValue} /> ); }; private getToolbar = (computedItems: ComputedReportItem[]) => { - const { query, router, report } = this.props; + const { query, router, report, timeScopeValue } = this.props; const { isAllSelected, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -254,6 +254,7 @@ class IbmDetails extends React.Component { pagination={this.getPagination(isDisabled)} query={query} selectedItems={selectedItems} + timeScopeValue={timeScopeValue} /> ); }; @@ -279,11 +280,11 @@ class IbmDetails extends React.Component { } }; - public handleOnExportModalClose = (isOpen: boolean) => { + private handleOnExportModalClose = (isOpen: boolean) => { this.setState({ isExportModalOpen: isOpen }); }; - public handleOnExportModalOpen = () => { + private handleOnExportModalOpen = () => { this.setState({ isExportModalOpen: true }); }; @@ -329,6 +330,9 @@ class IbmDetails extends React.Component { currency, intl, isAccountInfoEmptyStateToggleEnabled, + isCurrentMonthData, + isDetailsDateRangeToggleEnabled, + isPreviousMonthData, providers, providersFetchStatus, query, @@ -336,6 +340,7 @@ class IbmDetails extends React.Component { reportError, reportFetchStatus, router, + timeScopeValue, } = this.props; const computedItems = this.getComputedItems(); @@ -355,11 +360,11 @@ class IbmDetails extends React.Component { if (noProviders) { return ; } - if (!hasCurrentMonthData(providers)) { + if (isDetailsDateRangeToggleEnabled ? !isCurrentMonthData && !isPreviousMonthData : !isCurrentMonthData) { return ( : undefined + isAccountInfoEmptyStateToggleEnabled ? : undefined } title={title} /> @@ -372,12 +377,26 @@ class IbmDetails extends React.Component { handleOnCurrencySelect(query, router)} onGroupBySelect={this.handleOnGroupBySelect} + query={query} report={report} + timeScopeValue={timeScopeValue} />
-
{this.getToolbar(computedItems)}
+
+ {!isCurrentMonthData && isDetailsDateRangeToggleEnabled && ( + + )} + {this.getToolbar(computedItems)} +
{this.getExportModal(computedItems)} {reportFetchStatus === FetchStatus.inProgress ? ( @@ -397,12 +416,35 @@ class IbmDetails extends React.Component { const mapStateToProps = createMapStateToProps((state, { router }) => { const queryFromRoute = parseQuery(router.location.search); + const currency = getCurrency(); + // Check for current and previous data first + const providersQueryString = getProvidersQuery(providersQuery); + const providers = providersSelectors.selectProviders(state, ProviderType.all, providersQueryString); + const providersError = providersSelectors.selectProvidersError(state, ProviderType.all, providersQueryString); + const providersFetchStatus = providersSelectors.selectProvidersFetchStatus( + state, + ProviderType.all, + providersQueryString + ); + + // Fetch based on time scope value + const filteredProviders = filterProviders(providers, ProviderType.ibm); + const isCurrentMonthData = hasCurrentMonthData(filteredProviders); + const isDetailsDateRangeToggleEnabled = FeatureToggleSelectors.selectIsDetailsDateRangeToggleEnabled(state); + + let timeScopeValue = getTimeScopeValue(queryFromRoute); + timeScopeValue = Number( + !isCurrentMonthData && isDetailsDateRangeToggleEnabled ? -2 : timeScopeValue !== undefined ? timeScopeValue : -1 + ); + const query: any = { ...baseQuery, ...queryFromRoute, }; + query.filter.time_scope_value = timeScopeValue; // Add time scope here for breakdown pages + const reportQuery = { currency, delta: 'cost', @@ -411,7 +453,6 @@ const mapStateToProps = createMapStateToProps( const groupBy = getGroupById(queryFromRoute); const groupByValue = getGroupByValue(queryFromRoute); + const currency = getCurrency(); + const timeScopeValue = getTimeScopeValue(queryState); const query = { ...queryFromRoute }; const reportQuery = { @@ -51,7 +54,7 @@ const mapStateToProps = createMapStateToProps( filter: { resolution: 'monthly', time_scope_units: 'month', - time_scope_value: -1, + time_scope_value: timeScopeValue !== undefined ? timeScopeValue : -1, }, filter_by: { // Add filters here to apply logical OR/AND @@ -95,7 +98,7 @@ const mapStateToProps = createMapStateToProps( emptyStateTitle: intl.formatMessage(messages.ociDetailsTitle), groupBy, groupByValue, - historicalDataComponent: , + historicalDataComponent: , providers: filterProviders(providers, ProviderType.oci), providersError, providersFetchStatus, @@ -108,6 +111,7 @@ const mapStateToProps = createMapStateToProps( reportPathsType, reportQueryString, tagPathsType: TagPathsType.oci, + timeScopeValue, title: groupByValue, }; }); diff --git a/src/routes/details/ociDetails/detailsHeader.styles.ts b/src/routes/details/ociDetails/detailsHeader.styles.ts index d55ff51cc..b4c026fd7 100644 --- a/src/routes/details/ociDetails/detailsHeader.styles.ts +++ b/src/routes/details/ociDetails/detailsHeader.styles.ts @@ -26,6 +26,9 @@ export const styles = { perspectiveContainer: { alignItems: 'unset', }, + status: { + marginBottom: global_spacer_sm.var, + }, title: { paddingBottom: global_spacer_sm.var, }, diff --git a/src/routes/details/ociDetails/detailsHeader.tsx b/src/routes/details/ociDetails/detailsHeader.tsx index 1397f156b..6b130c8d5 100644 --- a/src/routes/details/ociDetails/detailsHeader.tsx +++ b/src/routes/details/ociDetails/detailsHeader.tsx @@ -2,6 +2,7 @@ import { Flex, FlexItem, Title, TitleSizes } from '@patternfly/react-core'; import type { Providers } from 'api/providers'; import { ProviderType } from 'api/providers'; import { getProvidersQuery } from 'api/queries/providersQuery'; +import type { Query } from 'api/queries/query'; import type { OciReport } from 'api/reports/ociReports'; import { TagPathsType } from 'api/tags/tag'; import type { AxiosError } from 'axios'; @@ -12,28 +13,39 @@ import type { WrappedComponentProps } from 'react-intl'; import { injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { Currency } from 'routes/components/currency'; +import { DateRange } from 'routes/components/dateRange'; import { GroupBy } from 'routes/components/groupBy'; +import { ProviderDetailsModal } from 'routes/details/components/providerStatus'; import type { ComputedOciReportItemsParams } from 'routes/utils/computedReport/getComputedOciReportItems'; import { getIdKeyForGroupBy } from 'routes/utils/computedReport/getComputedOciReportItems'; +import { DateRangeType } from 'routes/utils/dateRange'; import { filterProviders } from 'routes/utils/providers'; +import { getRouteForQuery } from 'routes/utils/query'; import type { FetchStatus } from 'store/common'; import { createMapStateToProps } from 'store/common'; import { FeatureToggleSelectors } from 'store/featureToggle'; import { providersQuery, providersSelectors } from 'store/providers'; import { getSinceDateRangeString } from 'utils/dates'; import { formatCurrency } from 'utils/format'; +import type { RouterComponentProps } from 'utils/router'; +import { withRouter } from 'utils/router'; import { styles } from './detailsHeader.styles'; interface DetailsHeaderOwnProps { currency?: string; groupBy?: string; + isCurrentMonthData?: boolean; onCurrencySelect(value: string); onGroupBySelect(value: string); + query?: Query; report: OciReport; + timeScopeValue?: number; } interface DetailsHeaderStateProps { + isAccountInfoDetailsToggleEnabled?: boolean; + isDetailsDateRangeToggleEnabled?: boolean; isExportsToggleEnabled?: boolean; providers: Providers; providersError: AxiosError; @@ -41,7 +53,14 @@ interface DetailsHeaderStateProps { providersQueryString?: string; } -type DetailsHeaderProps = DetailsHeaderOwnProps & DetailsHeaderStateProps & WrappedComponentProps; +interface DetailsHeaderState { + currentDateRangeType?: string; +} + +type DetailsHeaderProps = DetailsHeaderOwnProps & + DetailsHeaderStateProps & + RouterComponentProps & + WrappedComponentProps; const groupByOptions: { label: string; @@ -55,18 +74,42 @@ const groupByOptions: { const tagPathsType = TagPathsType.oci; class DetailsHeaderBase extends React.Component { + protected defaultState: DetailsHeaderState = { + currentDateRangeType: + this.props.timeScopeValue === -2 ? DateRangeType.previousMonth : DateRangeType.currentMonthToDate, + }; + public state: DetailsHeaderState = { ...this.defaultState }; + + private handleOnDateRangeSelected = (value: string) => { + const { query, router } = this.props; + + this.setState({ currentDateRangeType: value }, () => { + const newQuery = { + filter: {}, + ...JSON.parse(JSON.stringify(query)), + }; + newQuery.filter.time_scope_value = value === DateRangeType.previousMonth ? -2 : -1; + router.navigate(getRouteForQuery(newQuery, router.location, true), { replace: true }); + }); + }; + public render() { const { currency, groupBy, + intl, + isAccountInfoDetailsToggleEnabled, + isCurrentMonthData, + isDetailsDateRangeToggleEnabled, isExportsToggleEnabled, onCurrencySelect, onGroupBySelect, providers, providersError, report, - intl, + timeScopeValue, } = this.props; + const { currentDateRangeType } = this.state; const showContent = report && !providersError && providers?.meta?.count > 0; const hasCost = report?.meta?.total?.cost?.total; @@ -86,20 +129,35 @@ class DetailsHeaderBase extends React.Component { + {isAccountInfoDetailsToggleEnabled && ( + + + + + + )} - -
- + + + {isDetailsDateRangeToggleEnabled && ( + + -
-
+
+ )}
@@ -111,7 +169,9 @@ class DetailsHeaderBase extends React.Component { hasCost ? report.meta.total.cost.total.units : 'USD' )} -
{getSinceDateRangeString()}
+
+ {getSinceDateRangeString(undefined, timeScopeValue === -2 ? 1 : 0, true)} +
)}
@@ -133,6 +193,8 @@ const mapStateToProps = createMapStateToProps !column.hidden); + const filteredRows = rows.map(({ ...row }) => { + row.cells = row.cells.filter(cell => !cell.hidden); + return row; + }); + this.setState({ - columns, - rows, + columns: filteredColumns, + rows: filteredRows, }); }; @@ -200,7 +211,7 @@ class DetailsTableBase extends React.Component { - const { intl } = this.props; + const { intl, timeScopeValue } = this.props; const value = formatCurrency(Math.abs(item.cost.total.value - item.delta_value), item.cost.total.units); const percentage = item.delta_percent !== null ? formatPercentage(Math.abs(item.delta_percent)) : 0; @@ -219,7 +230,11 @@ class DetailsTableBase extends React.Component @@ -240,7 +255,12 @@ class DetailsTableBase extends React.Component
- {getForDateRangeString(value)} + {getForDateRangeString( + value, + undefined, + timeScopeValue === -2 ? 2 : 1, + timeScopeValue === -2 ? true : false + )}
); diff --git a/src/routes/details/ociDetails/detailsToolbar.tsx b/src/routes/details/ociDetails/detailsToolbar.tsx index 5bcb25b60..a7e5259cd 100644 --- a/src/routes/details/ociDetails/detailsToolbar.tsx +++ b/src/routes/details/ociDetails/detailsToolbar.tsx @@ -33,6 +33,7 @@ interface DetailsToolbarOwnProps { pagination?: React.ReactNode; query?: OciQuery; selectedItems?: ComputedReportItem[]; + timeScopeValue?: number; } interface DetailsToolbarStateProps { @@ -165,27 +166,28 @@ export class DetailsToolbarBase extends React.Component((state, props) => { - // Note: Omitting key_only would help to share a single, cached request -- the toolbar requires key values - // However, for better server-side performance, we chose to use key_only here. - const tagQueryString = getQuery({ - filter: { - resolution: 'monthly', - time_scope_units: 'month', - time_scope_value: -1, - }, - key_only: true, - limit: 1000, - }); - const tagReport = tagSelectors.selectTag(state, tagPathsType, tagType, tagQueryString); - const tagReportFetchStatus = tagSelectors.selectTagFetchStatus(state, tagPathsType, tagType, tagQueryString); - return { - tagReportFetchStatus, - tagReport, - tagQueryString, - }; -}); +const mapStateToProps = createMapStateToProps( + (state, { timeScopeValue = -1 }) => { + // Note: Omitting key_only would help to share a single, cached request -- the toolbar requires key values + // However, for better server-side performance, we chose to use key_only here. + const tagQueryString = getQuery({ + filter: { + resolution: 'monthly', + time_scope_units: 'month', + time_scope_value: timeScopeValue, + }, + key_only: true, + limit: 1000, + }); + const tagReport = tagSelectors.selectTag(state, tagPathsType, tagType, tagQueryString); + const tagReportFetchStatus = tagSelectors.selectTagFetchStatus(state, tagPathsType, tagType, tagQueryString); + return { + tagReportFetchStatus, + tagReport, + tagQueryString, + }; + } +); const mapDispatchToProps: DetailsToolbarDispatchProps = { fetchTag: tagActions.fetchTag, diff --git a/src/routes/details/ociDetails/ociDetails.tsx b/src/routes/details/ociDetails/ociDetails.tsx index 3301980ab..7fe9de029 100644 --- a/src/routes/details/ociDetails/ociDetails.tsx +++ b/src/routes/details/ociDetails/ociDetails.tsx @@ -1,4 +1,4 @@ -import { Pagination, PaginationVariant } from '@patternfly/react-core'; +import { Alert, Pagination, PaginationVariant } from '@patternfly/react-core'; import type { Providers } from 'api/providers'; import { ProviderType } from 'api/providers'; import type { OciQuery } from 'api/queries/ociQuery'; @@ -18,12 +18,12 @@ import { Loading } from 'routes/components/page/loading'; import { NoData } from 'routes/components/page/noData'; import { NoProviders } from 'routes/components/page/noProviders'; import { NotAvailable } from 'routes/components/page/notAvailable'; -import { ProviderDetails } from 'routes/details/components/providerDetails'; +import { ProviderStatus } from 'routes/details/components/providerStatus'; import { getIdKeyForGroupBy } from 'routes/utils/computedReport/getComputedOciReportItems'; import type { ComputedReportItem } from 'routes/utils/computedReport/getComputedReportItems'; import { getUnsortedComputedReportItems } from 'routes/utils/computedReport/getComputedReportItems'; import { getGroupByTagKey } from 'routes/utils/groupBy'; -import { filterProviders, hasCurrentMonthData } from 'routes/utils/providers'; +import { filterProviders, hasCurrentMonthData, hasPreviousMonthData } from 'routes/utils/providers'; import { getRouteForQuery } from 'routes/utils/query'; import { handleOnCurrencySelect, @@ -33,10 +33,12 @@ import { handleOnSetPage, handleOnSort, } from 'routes/utils/queryNavigate'; +import { getTimeScopeValue } from 'routes/utils/timeScope'; import { createMapStateToProps, FetchStatus } from 'store/common'; import { FeatureToggleSelectors } from 'store/featureToggle'; import { providersQuery, providersSelectors } from 'store/providers'; import { reportActions, reportSelectors } from 'store/reports'; +import { getSinceDateRangeString } from 'utils/dates'; import { formatPath } from 'utils/paths'; import { noPrefix, tagPrefix } from 'utils/props'; import type { RouterComponentProps } from 'utils/router'; @@ -51,6 +53,9 @@ import { styles } from './ociDetails.styles'; interface OciDetailsStateProps { currency?: string; isAccountInfoEmptyStateToggleEnabled?: boolean; + isCurrentMonthData?: boolean; + isDetailsDateRangeToggleEnabled?: boolean; + isPreviousMonthData?: boolean; providers: Providers; providersError: AxiosError; providersFetchStatus: FetchStatus; @@ -59,6 +64,7 @@ interface OciDetailsStateProps { reportError: AxiosError; reportFetchStatus: FetchStatus; reportQueryString: string; + timeScopeValue?: number; } interface OciDetailsDispatchProps { @@ -105,14 +111,6 @@ class OciDetails extends React.Component { }; public state: OciDetailsState = { ...this.defaultState }; - constructor(stateProps, dispatchProps) { - super(stateProps, dispatchProps); - this.handleOnBulkSelect = this.handleOnBulkSelect.bind(this); - this.handleOnExportModalClose = this.handleOnExportModalClose.bind(this); - this.handleOnExportModalOpen = this.handleOnExportModalOpen.bind(this); - this.handleonSelect = this.handleonSelect.bind(this); - } - public componentDidMount() { this.updateReport(); } @@ -144,7 +142,7 @@ class OciDetails extends React.Component { }; private getExportModal = (computedItems: ComputedReportItem[]) => { - const { query, report, reportQueryString } = this.props; + const { query, report, reportQueryString, timeScopeValue } = this.props; const { isAllSelected, isExportModalOpen, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -169,6 +167,7 @@ class OciDetails extends React.Component { reportPathsType={reportPathsType} reportQueryString={reportQueryString} reportType={reportType} + timeScopeValue={timeScopeValue} /> ); }; @@ -203,7 +202,7 @@ class OciDetails extends React.Component { }; private getTable = () => { - const { query, report, reportFetchStatus, reportQueryString, router } = this.props; + const { query, report, reportFetchStatus, reportQueryString, router, timeScopeValue } = this.props; const { isAllSelected, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -225,12 +224,13 @@ class OciDetails extends React.Component { report={report} reportQueryString={reportQueryString} selectedItems={selectedItems} + timeScopeValue={timeScopeValue} /> ); }; private getToolbar = (computedItems: ComputedReportItem[]) => { - const { query, router, report } = this.props; + const { query, router, report, timeScopeValue } = this.props; const { isAllSelected, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -253,6 +253,7 @@ class OciDetails extends React.Component { pagination={this.getPagination(isDisabled)} query={query} selectedItems={selectedItems} + timeScopeValue={timeScopeValue} /> ); }; @@ -278,11 +279,11 @@ class OciDetails extends React.Component { } }; - public handleOnExportModalClose = (isOpen: boolean) => { + private handleOnExportModalClose = (isOpen: boolean) => { this.setState({ isExportModalOpen: isOpen }); }; - public handleOnExportModalOpen = () => { + private handleOnExportModalOpen = () => { this.setState({ isExportModalOpen: true }); }; @@ -328,14 +329,17 @@ class OciDetails extends React.Component { currency, intl, isAccountInfoEmptyStateToggleEnabled, + isCurrentMonthData, + isDetailsDateRangeToggleEnabled, + isPreviousMonthData, providers, providersFetchStatus, query, report, reportError, reportFetchStatus, - router, + timeScopeValue, } = this.props; const computedItems = this.getComputedItems(); @@ -355,11 +359,11 @@ class OciDetails extends React.Component { if (noProviders) { return ; } - if (!hasCurrentMonthData(providers)) { + if (isDetailsDateRangeToggleEnabled ? !isCurrentMonthData && !isPreviousMonthData : !isCurrentMonthData) { return ( : undefined + isAccountInfoEmptyStateToggleEnabled ? : undefined } title={title} /> @@ -372,12 +376,26 @@ class OciDetails extends React.Component { handleOnCurrencySelect(query, router)} onGroupBySelect={this.handleOnGroupBySelect} + query={query} report={report} + timeScopeValue={timeScopeValue} />
-
{this.getToolbar(computedItems)}
+
+ {!isCurrentMonthData && isDetailsDateRangeToggleEnabled && ( + + )} + {this.getToolbar(computedItems)} +
{this.getExportModal(computedItems)} {reportFetchStatus === FetchStatus.inProgress ? ( @@ -397,12 +415,35 @@ class OciDetails extends React.Component { const mapStateToProps = createMapStateToProps((state, { router }) => { const queryFromRoute = parseQuery(router.location.search); + const currency = getCurrency(); + // Check for current and previous data first + const providersQueryString = getProvidersQuery(providersQuery); + const providers = providersSelectors.selectProviders(state, ProviderType.all, providersQueryString); + const providersError = providersSelectors.selectProvidersError(state, ProviderType.all, providersQueryString); + const providersFetchStatus = providersSelectors.selectProvidersFetchStatus( + state, + ProviderType.all, + providersQueryString + ); + + // Fetch based on time scope value + const filteredProviders = filterProviders(providers, ProviderType.oci); + const isCurrentMonthData = hasCurrentMonthData(filteredProviders); + const isDetailsDateRangeToggleEnabled = FeatureToggleSelectors.selectIsDetailsDateRangeToggleEnabled(state); + + let timeScopeValue = getTimeScopeValue(queryFromRoute); + timeScopeValue = Number( + !isCurrentMonthData && isDetailsDateRangeToggleEnabled ? -2 : timeScopeValue !== undefined ? timeScopeValue : -1 + ); + const query: any = { ...baseQuery, ...queryFromRoute, }; + query.filter.time_scope_value = timeScopeValue; // Add time scope here for breakdown pages + const reportQuery = { currency, delta: 'cost', @@ -411,7 +452,6 @@ const mapStateToProps = createMapStateToProps : undefined, dataDetailsComponent: groupBy === 'cluster' ? ( - + ) : undefined, costDistribution, costOverviewComponent: ( @@ -121,7 +123,12 @@ const mapStateToProps = createMapStateToProps + ), isOptimizationsTab: queryFromRoute.optimizationsTab !== undefined, optimizationsComponent: groupBy === 'project' && groupByValue !== '*' ? : undefined, @@ -138,6 +145,7 @@ const mapStateToProps = createMapStateToProps { - protected defaultState: DetailsHeaderState = {}; + protected defaultState: DetailsHeaderState = { + currentDateRangeType: + this.props.timeScopeValue === -2 ? DateRangeType.previousMonth : DateRangeType.currentMonthToDate, + }; public state: DetailsHeaderState = { ...this.defaultState }; + private handleOnDateRangeSelected = (value: string) => { + const { query, router } = this.props; + + this.setState({ currentDateRangeType: value }, () => { + const newQuery = { + filter: {}, + ...JSON.parse(JSON.stringify(query)), + }; + newQuery.filter.time_scope_value = value === DateRangeType.previousMonth ? -2 : -1; + router.navigate(getRouteForQuery(newQuery, router.location, true), { replace: true }); + }); + }; + public render() { const { costDistribution, currency, groupBy, + intl, + isAccountInfoDetailsToggleEnabled, + isCurrentMonthData, + isDetailsDateRangeToggleEnabled, isExportsToggleEnabled, onCostDistributionSelect, onCurrencySelect, @@ -79,8 +115,10 @@ class DetailsHeaderBase extends React.Component 0; const showCostDistribution = groupBy === 'project' && report?.meta?.distributed_overhead === true; @@ -122,25 +160,40 @@ class DetailsHeaderBase extends React.Component - - -
- -
+ {isAccountInfoDetailsToggleEnabled && ( + + + + + + )} + + + {showCostDistribution && ( )} + {isDetailsDateRangeToggleEnabled && ( + + + + )}
@@ -157,7 +210,9 @@ class DetailsHeaderBase extends React.Component -
{getSinceDateRangeString()}
+
+ {getSinceDateRangeString(undefined, timeScopeValue === -2 ? 1 : 0, true)} +
)}
@@ -179,6 +234,8 @@ const mapStateToProps = createMapStateToProps { - const { costDistribution, intl } = this.props; + const { costDistribution, intl, timeScopeValue } = this.props; const reportItemValue = costDistribution ? costDistribution : ComputedReportItemValueType.total; const value = formatCurrency( @@ -440,7 +442,11 @@ class DetailsTableBase extends React.Component @@ -458,7 +464,12 @@ class DetailsTableBase extends React.Component
- {getForDateRangeString(value)} + {getForDateRangeString( + value, + undefined, + timeScopeValue === -2 ? 2 : 1, + timeScopeValue === -2 ? true : false + )}
); diff --git a/src/routes/details/ocpDetails/detailsToolbar.tsx b/src/routes/details/ocpDetails/detailsToolbar.tsx index 383dae42b..0af210f88 100644 --- a/src/routes/details/ocpDetails/detailsToolbar.tsx +++ b/src/routes/details/ocpDetails/detailsToolbar.tsx @@ -34,6 +34,7 @@ interface DetailsToolbarOwnProps { pagination?: React.ReactNode; query?: OcpQuery; selectedItems?: ComputedReportItem[]; + timeScopeValue?: number; } interface DetailsToolbarStateProps { @@ -169,27 +170,28 @@ export class DetailsToolbarBase extends React.Component((state, props) => { - // Note: Omitting key_only would help to share a single, cached request -- the toolbar requires key values - // However, for better server-side performance, we chose to use key_only here. - const tagQueryString = getQuery({ - filter: { - resolution: 'monthly', - time_scope_units: 'month', - time_scope_value: -1, - }, - key_only: true, - limit: 1000, - }); - const tagReport = tagSelectors.selectTag(state, tagPathsType, tagType, tagQueryString); - const tagReportFetchStatus = tagSelectors.selectTagFetchStatus(state, tagPathsType, tagType, tagQueryString); - return { - tagReport, - tagReportFetchStatus, - tagQueryString, - }; -}); +const mapStateToProps = createMapStateToProps( + (state, { timeScopeValue }) => { + // Note: Omitting key_only would help to share a single, cached request -- the toolbar requires key values + // However, for better server-side performance, we chose to use key_only here. + const tagQueryString = getQuery({ + filter: { + resolution: 'monthly', + time_scope_units: 'month', + time_scope_value: timeScopeValue, + }, + key_only: true, + limit: 1000, + }); + const tagReport = tagSelectors.selectTag(state, tagPathsType, tagType, tagQueryString); + const tagReportFetchStatus = tagSelectors.selectTagFetchStatus(state, tagPathsType, tagType, tagQueryString); + return { + tagReport, + tagReportFetchStatus, + tagQueryString, + }; + } +); const mapDispatchToProps: DetailsToolbarDispatchProps = { fetchTag: tagActions.fetchTag, diff --git a/src/routes/details/ocpDetails/ocpDetails.tsx b/src/routes/details/ocpDetails/ocpDetails.tsx index 36879065f..276b6e5f0 100644 --- a/src/routes/details/ocpDetails/ocpDetails.tsx +++ b/src/routes/details/ocpDetails/ocpDetails.tsx @@ -1,4 +1,4 @@ -import { Pagination, PaginationVariant } from '@patternfly/react-core'; +import { Alert, Pagination, PaginationVariant } from '@patternfly/react-core'; import type { Providers } from 'api/providers'; import { ProviderType } from 'api/providers'; import type { OcpQuery } from 'api/queries/ocpQuery'; @@ -22,12 +22,12 @@ import { NoProviders } from 'routes/components/page/noProviders'; import { NotAvailable } from 'routes/components/page/notAvailable'; import type { ColumnManagementModalOption } from 'routes/details/components/columnManagement'; import { ColumnManagementModal, initHiddenColumns } from 'routes/details/components/columnManagement'; -import { ProviderDetails } from 'routes/details/components/providerDetails'; +import { ProviderStatus } from 'routes/details/components/providerStatus'; import { getIdKeyForGroupBy } from 'routes/utils/computedReport/getComputedOcpReportItems'; import type { ComputedReportItem } from 'routes/utils/computedReport/getComputedReportItems'; import { getUnsortedComputedReportItems } from 'routes/utils/computedReport/getComputedReportItems'; import { getGroupById, getGroupByTagKey } from 'routes/utils/groupBy'; -import { filterProviders, hasCurrentMonthData } from 'routes/utils/providers'; +import { filterProviders, hasCurrentMonthData, hasPreviousMonthData } from 'routes/utils/providers'; import { getRouteForQuery } from 'routes/utils/query'; import { handleOnCostDistributionSelect, @@ -38,10 +38,12 @@ import { handleOnSetPage, handleOnSort, } from 'routes/utils/queryNavigate'; +import { getTimeScopeValue } from 'routes/utils/timeScope'; import { createMapStateToProps, FetchStatus } from 'store/common'; import { FeatureToggleSelectors } from 'store/featureToggle'; import { providersQuery, providersSelectors } from 'store/providers'; import { reportActions, reportSelectors } from 'store/reports'; +import { getSinceDateRangeString } from 'utils/dates'; import { formatPath } from 'utils/paths'; import { noPrefix, platformCategoryKey, tagPrefix } from 'utils/props'; import type { RouterComponentProps } from 'utils/router'; @@ -56,7 +58,11 @@ import { styles } from './ocpDetails.styles'; export interface OcpDetailsStateProps { costDistribution?: string; currency?: string; + currentDateRangeType?: string; isAccountInfoEmptyStateToggleEnabled?: boolean; + isCurrentMonthData?: boolean; + isDetailsDateRangeToggleEnabled?: boolean; + isPreviousMonthData?: boolean; providers: Providers; providersFetchStatus: FetchStatus; query: OcpQuery; @@ -64,6 +70,7 @@ export interface OcpDetailsStateProps { reportError: AxiosError; reportFetchStatus: FetchStatus; reportQueryString: string; + timeScopeValue?: number; } interface OcpDetailsDispatchProps { @@ -130,18 +137,6 @@ class OcpDetails extends React.Component { }; public state: OcpDetailsState = { ...this.defaultState }; - constructor(stateProps, dispatchProps) { - super(stateProps, dispatchProps); - this.handleOnBulkSelect = this.handleOnBulkSelect.bind(this); - this.handleOnColumnManagementModalClose = this.handleOnColumnManagementModalClose.bind(this); - this.handleOnColumnManagementModalOpen = this.handleOnColumnManagementModalOpen.bind(this); - this.handleOnColumnManagementModalSave = this.handleOnColumnManagementModalSave.bind(this); - this.handleOnExportModalClose = this.handleOnExportModalClose.bind(this); - this.handleOnExportModalOpen = this.handleOnExportModalOpen.bind(this); - this.handleOnPlatformCostsChanged = this.handleOnPlatformCostsChanged.bind(this); - this.handleOnSelect = this.handleOnSelect.bind(this); - } - public componentDidMount() { this.updateReport(); } @@ -191,7 +186,7 @@ class OcpDetails extends React.Component { }; private getExportModal = (computedItems: ComputedReportItem[]) => { - const { query, report, reportQueryString } = this.props; + const { query, report, reportQueryString, timeScopeValue } = this.props; const { isAllSelected, isExportModalOpen, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -216,6 +211,7 @@ class OcpDetails extends React.Component { reportPathsType={reportPathsType} reportQueryString={reportQueryString} reportType={reportType} + timeScopeValue={timeScopeValue} /> ); }; @@ -250,7 +246,8 @@ class OcpDetails extends React.Component { }; private getTable = () => { - const { costDistribution, query, report, reportFetchStatus, reportQueryString, router } = this.props; + const { costDistribution, query, report, reportFetchStatus, reportQueryString, router, timeScopeValue } = + this.props; const { hiddenColumns, isAllSelected, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -274,12 +271,13 @@ class OcpDetails extends React.Component { report={report} reportQueryString={reportQueryString} selectedItems={selectedItems} + timeScopeValue={timeScopeValue} /> ); }; private getToolbar = (computedItems: ComputedReportItem[]) => { - const { query, report, router } = this.props; + const { query, report, router, timeScopeValue } = this.props; const { isAllSelected, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -304,6 +302,7 @@ class OcpDetails extends React.Component { pagination={this.getPagination(isDisabled)} query={query} selectedItems={selectedItems} + timeScopeValue={timeScopeValue} /> ); }; @@ -329,23 +328,23 @@ class OcpDetails extends React.Component { } }; - public handleOnColumnManagementModalClose = (isOpen: boolean) => { + private handleOnColumnManagementModalClose = (isOpen: boolean) => { this.setState({ isColumnManagementModalOpen: isOpen }); }; - public handleOnColumnManagementModalOpen = () => { + private handleOnColumnManagementModalOpen = () => { this.setState({ isColumnManagementModalOpen: true }); }; - public handleOnColumnManagementModalSave = (hiddenColumns: Set) => { + private handleOnColumnManagementModalSave = (hiddenColumns: Set) => { this.setState({ hiddenColumns }); }; - public handleOnExportModalClose = (isOpen: boolean) => { + private handleOnExportModalClose = (isOpen: boolean) => { this.setState({ isExportModalOpen: isOpen }); }; - public handleOnExportModalOpen = () => { + private handleOnExportModalOpen = () => { this.setState({ isExportModalOpen: true }); }; @@ -405,6 +404,9 @@ class OcpDetails extends React.Component { currency, intl, isAccountInfoEmptyStateToggleEnabled, + isCurrentMonthData, + isDetailsDateRangeToggleEnabled, + isPreviousMonthData, providers, providersFetchStatus, query, @@ -412,6 +414,7 @@ class OcpDetails extends React.Component { reportError, reportFetchStatus, router, + timeScopeValue, } = this.props; const computedItems = this.getComputedItems(); @@ -431,11 +434,11 @@ class OcpDetails extends React.Component { if (noProviders) { return ; } - if (!hasCurrentMonthData(providers)) { + if (isDetailsDateRangeToggleEnabled ? !isCurrentMonthData && !isPreviousMonthData : !isCurrentMonthData) { return ( : undefined + isAccountInfoEmptyStateToggleEnabled ? : undefined } title={title} /> @@ -449,13 +452,27 @@ class OcpDetails extends React.Component { costDistribution={costDistribution} currency={currency} groupBy={groupById} + isCurrentMonthData={isCurrentMonthData} onCostDistributionSelect={() => handleOnCostDistributionSelect(query, router)} onCurrencySelect={() => handleOnCurrencySelect(query, router)} onGroupBySelect={this.handleOnGroupBySelect} + query={query} report={report} + timeScopeValue={timeScopeValue} />
-
{this.getToolbar(computedItems)}
+
+ {!isCurrentMonthData && isDetailsDateRangeToggleEnabled && ( + + )} + {this.getToolbar(computedItems)} +
{this.getExportModal(computedItems)} {this.getColumnManagementModal()} {reportFetchStatus === FetchStatus.inProgress ? ( @@ -481,6 +498,25 @@ const mapStateToProps = createMapStateToProps, + historicalDataComponent: , providers: filterProviders(providers, ProviderType.rhel), providersFetchStatus, providerType: ProviderType.rhel, @@ -109,6 +112,7 @@ const mapStateToProps = createMapStateToProps { - protected defaultState: DetailsHeaderState = {}; + protected defaultState: DetailsHeaderState = { + currentDateRangeType: + this.props.timeScopeValue === -2 ? DateRangeType.previousMonth : DateRangeType.currentMonthToDate, + }; public state: DetailsHeaderState = { ...this.defaultState }; + private handleOnDateRangeSelected = (value: string) => { + const { query, router } = this.props; + + this.setState({ currentDateRangeType: value }, () => { + const newQuery = { + filter: {}, + ...JSON.parse(JSON.stringify(query)), + }; + newQuery.filter.time_scope_value = value === DateRangeType.previousMonth ? -2 : -1; + router.navigate(getRouteForQuery(newQuery, router.location, true), { replace: true }); + }); + }; + public render() { const { currency, groupBy, + intl, + isAccountInfoDetailsToggleEnabled, + isCurrentMonthData, + isDetailsDateRangeToggleEnabled, isExportsToggleEnabled, onCurrencySelect, onGroupBySelect, providers, providersError, report, - intl, + timeScopeValue, } = this.props; + const { currentDateRangeType } = this.state; + const showContent = report && !providersError && providers?.meta?.count > 0; let cost: string | React.ReactNode = ; @@ -112,20 +153,35 @@ class DetailsHeaderBase extends React.Component { + {isAccountInfoDetailsToggleEnabled && ( + + + + + + )} - -
- + + + {isDetailsDateRangeToggleEnabled && ( + + -
-
+
+ )}
@@ -142,7 +198,9 @@ class DetailsHeaderBase extends React.Component { {cost} -
{getSinceDateRangeString()}
+
+ {getSinceDateRangeString(undefined, timeScopeValue === -2 ? 1 : 0, true)} +
)}
@@ -164,6 +222,8 @@ const mapStateToProps = createMapStateToProps
); diff --git a/src/routes/details/rhelDetails/detailsToolbar.tsx b/src/routes/details/rhelDetails/detailsToolbar.tsx index d85af1b33..6f87a4a85 100644 --- a/src/routes/details/rhelDetails/detailsToolbar.tsx +++ b/src/routes/details/rhelDetails/detailsToolbar.tsx @@ -33,6 +33,7 @@ interface DetailsToolbarOwnProps { pagination?: React.ReactNode; query?: OcpQuery; selectedItems?: ComputedReportItem[]; + timeScopeValue?: number; } interface DetailsToolbarStateProps { @@ -161,27 +162,28 @@ export class DetailsToolbarBase extends React.Component { } } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const mapStateToProps = createMapStateToProps((state, props) => { - // Note: Omitting key_only would help to share a single, cached request -- the toolbar requires key values - // However, for better server-side performance, we chose to use key_only here. - const tagQueryString = getQuery({ - filter: { - resolution: 'monthly', - time_scope_units: 'month', - time_scope_value: -1, - }, - key_only: true, - limit: 1000, - }); - const tagReport = tagSelectors.selectTag(state, tagPathsType, tagType, tagQueryString); - const tagReportFetchStatus = tagSelectors.selectTagFetchStatus(state, tagPathsType, tagType, tagQueryString); - return { - tagReport, - tagReportFetchStatus, - tagQueryString, - }; -}); +const mapStateToProps = createMapStateToProps( + (state, { timeScopeValue = -1 }) => { + // Note: Omitting key_only would help to share a single, cached request -- the toolbar requires key values + // However, for better server-side performance, we chose to use key_only here. + const tagQueryString = getQuery({ + filter: { + resolution: 'monthly', + time_scope_units: 'month', + time_scope_value: timeScopeValue, + }, + key_only: true, + limit: 1000, + }); + const tagReport = tagSelectors.selectTag(state, tagPathsType, tagType, tagQueryString); + const tagReportFetchStatus = tagSelectors.selectTagFetchStatus(state, tagPathsType, tagType, tagQueryString); + return { + tagReport, + tagReportFetchStatus, + tagQueryString, + }; + } +); const mapDispatchToProps: DetailsToolbarDispatchProps = { fetchTag: tagActions.fetchTag, diff --git a/src/routes/details/rhelDetails/rhelDetails.tsx b/src/routes/details/rhelDetails/rhelDetails.tsx index 18e5c232f..952840e91 100644 --- a/src/routes/details/rhelDetails/rhelDetails.tsx +++ b/src/routes/details/rhelDetails/rhelDetails.tsx @@ -1,4 +1,4 @@ -import { Pagination, PaginationVariant } from '@patternfly/react-core'; +import { Alert, Pagination, PaginationVariant } from '@patternfly/react-core'; import type { Providers } from 'api/providers'; import { ProviderType } from 'api/providers'; import { getProvidersQuery } from 'api/queries/providersQuery'; @@ -21,12 +21,12 @@ import { NoProviders } from 'routes/components/page/noProviders'; import { NotAvailable } from 'routes/components/page/notAvailable'; import type { ColumnManagementModalOption } from 'routes/details/components/columnManagement'; import { ColumnManagementModal, initHiddenColumns } from 'routes/details/components/columnManagement'; -import { ProviderDetails } from 'routes/details/components/providerDetails'; +import { ProviderStatus } from 'routes/details/components/providerStatus'; import type { ComputedReportItem } from 'routes/utils/computedReport/getComputedReportItems'; import { getUnsortedComputedReportItems } from 'routes/utils/computedReport/getComputedReportItems'; import { getIdKeyForGroupBy } from 'routes/utils/computedReport/getComputedRhelReportItems'; import { getGroupByTagKey } from 'routes/utils/groupBy'; -import { filterProviders, hasCurrentMonthData } from 'routes/utils/providers'; +import { filterProviders, hasCurrentMonthData, hasPreviousMonthData } from 'routes/utils/providers'; import { getRouteForQuery } from 'routes/utils/query'; import { handleOnCurrencySelect, @@ -36,10 +36,12 @@ import { handleOnSetPage, handleOnSort, } from 'routes/utils/queryNavigate'; +import { getTimeScopeValue } from 'routes/utils/timeScope'; import { createMapStateToProps, FetchStatus } from 'store/common'; import { FeatureToggleSelectors } from 'store/featureToggle'; import { providersQuery, providersSelectors } from 'store/providers'; import { reportActions, reportSelectors } from 'store/reports'; +import { getSinceDateRangeString } from 'utils/dates'; import { formatPath } from 'utils/paths'; import { noPrefix, tagPrefix } from 'utils/props'; import type { RouterComponentProps } from 'utils/router'; @@ -54,6 +56,9 @@ import { styles } from './rhelDetails.styles'; interface RhelDetailsStateProps { currency?: string; isAccountInfoEmptyStateToggleEnabled?: boolean; + isCurrentMonthData?: boolean; + isDetailsDateRangeToggleEnabled?: boolean; + isPreviousMonthData?: boolean; providers: Providers; providersFetchStatus: FetchStatus; query: RhelQuery; @@ -61,6 +66,7 @@ interface RhelDetailsStateProps { reportError: AxiosError; reportFetchStatus: FetchStatus; reportQueryString: string; + timeScopeValue?: number; } interface RhelDetailsDispatchProps { @@ -127,17 +133,6 @@ class RhelDetails extends React.Component { }; public state: RhelDetailsState = { ...this.defaultState }; - constructor(stateProps, dispatchProps) { - super(stateProps, dispatchProps); - this.handleOnBulkSelect = this.handleOnBulkSelect.bind(this); - this.handleOnColumnManagementModalClose = this.handleOnColumnManagementModalClose.bind(this); - this.handleOnColumnManagementModalOpen = this.handleOnColumnManagementModalOpen.bind(this); - this.handleOnColumnManagementModalSave = this.handleOnColumnManagementModalSave.bind(this); - this.handleOnExportModalClose = this.handleOnExportModalClose.bind(this); - this.handleOnExportModalOpen = this.handleOnExportModalOpen.bind(this); - this.handleonSelect = this.handleonSelect.bind(this); - } - public componentDidMount() { this.updateReport(); } @@ -187,7 +182,7 @@ class RhelDetails extends React.Component { }; private getExportModal = (computedItems: ComputedReportItem[]) => { - const { query, report, reportQueryString } = this.props; + const { query, report, reportQueryString, timeScopeValue } = this.props; const { isAllSelected, isExportModalOpen, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -212,6 +207,7 @@ class RhelDetails extends React.Component { reportPathsType={reportPathsType} reportQueryString={reportQueryString} reportType={reportType} + timeScopeValue={timeScopeValue} /> ); }; @@ -246,7 +242,7 @@ class RhelDetails extends React.Component { }; private getTable = () => { - const { query, report, reportFetchStatus, reportQueryString, router } = this.props; + const { query, report, reportFetchStatus, reportQueryString, router, timeScopeValue } = this.props; const { hiddenColumns, isAllSelected, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -268,12 +264,13 @@ class RhelDetails extends React.Component { report={report} reportQueryString={reportQueryString} selectedItems={selectedItems} + timeScopeValue={timeScopeValue} /> ); }; private getToolbar = (computedItems: ComputedReportItem[]) => { - const { query, report, router } = this.props; + const { query, report, router, timeScopeValue } = this.props; const { isAllSelected, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -297,6 +294,7 @@ class RhelDetails extends React.Component { pagination={this.getPagination()} query={query} selectedItems={selectedItems} + timeScopeValue={timeScopeValue} /> ); }; @@ -322,23 +320,23 @@ class RhelDetails extends React.Component { } }; - public handleOnColumnManagementModalClose = (isOpen: boolean) => { + private handleOnColumnManagementModalClose = (isOpen: boolean) => { this.setState({ isColumnManagementModalOpen: isOpen }); }; - public handleOnColumnManagementModalOpen = () => { + private handleOnColumnManagementModalOpen = () => { this.setState({ isColumnManagementModalOpen: true }); }; - public handleOnColumnManagementModalSave = (hiddenColumns: Set) => { + private handleOnColumnManagementModalSave = (hiddenColumns: Set) => { this.setState({ hiddenColumns }); }; - public handleOnExportModalClose = (isOpen: boolean) => { + private handleOnExportModalClose = (isOpen: boolean) => { this.setState({ isExportModalOpen: isOpen }); }; - public handleOnExportModalOpen = () => { + private handleOnExportModalOpen = () => { this.setState({ isExportModalOpen: true }); }; @@ -385,6 +383,9 @@ class RhelDetails extends React.Component { currency, intl, isAccountInfoEmptyStateToggleEnabled, + isCurrentMonthData, + isDetailsDateRangeToggleEnabled, + isPreviousMonthData, providers, providersFetchStatus, query, @@ -392,6 +393,7 @@ class RhelDetails extends React.Component { reportError, reportFetchStatus, router, + timeScopeValue, } = this.props; const computedItems = this.getComputedItems(); @@ -411,11 +413,11 @@ class RhelDetails extends React.Component { if (noProviders) { return ; } - if (!hasCurrentMonthData(providers)) { + if (isDetailsDateRangeToggleEnabled ? !isCurrentMonthData && !isPreviousMonthData : !isCurrentMonthData) { return ( : undefined + isAccountInfoEmptyStateToggleEnabled ? : undefined } title={title} /> @@ -428,12 +430,26 @@ class RhelDetails extends React.Component { handleOnCurrencySelect(query, router)} onGroupBySelect={this.handleOnGroupBySelect} + query={query} report={report} + timeScopeValue={timeScopeValue} />
-
{this.getToolbar(computedItems)}
+
+ {!isCurrentMonthData && isDetailsDateRangeToggleEnabled && ( + + )} + {this.getToolbar(computedItems)} +
{this.getExportModal(computedItems)} {this.getColumnManagementModal()} {reportFetchStatus === FetchStatus.inProgress ? ( @@ -454,12 +470,34 @@ class RhelDetails extends React.Component { const mapStateToProps = createMapStateToProps((state, { router }) => { const queryFromRoute = parseQuery(router.location.search); + const currency = getCurrency(); + // Check for current and previous data first + const providersQueryString = getProvidersQuery(providersQuery); + const providers = providersSelectors.selectProviders(state, ProviderType.all, providersQueryString); + const providersFetchStatus = providersSelectors.selectProvidersFetchStatus( + state, + ProviderType.all, + providersQueryString + ); + + // Fetch based on time scope value + const filteredProviders = filterProviders(providers, ProviderType.ocp); + const isCurrentMonthData = hasCurrentMonthData(filteredProviders); + const isDetailsDateRangeToggleEnabled = FeatureToggleSelectors.selectIsDetailsDateRangeToggleEnabled(state); + + let timeScopeValue = getTimeScopeValue(queryFromRoute); + timeScopeValue = Number( + !isCurrentMonthData && isDetailsDateRangeToggleEnabled ? -2 : timeScopeValue !== undefined ? timeScopeValue : -1 + ); + const query: any = { ...baseQuery, ...queryFromRoute, }; + query.filter.time_scope_value = timeScopeValue; // Add time scope here for breakdown pages + const reportQuery = { currency, delta: 'cost', @@ -468,7 +506,6 @@ const mapStateToProps = createMapStateToProps { }; public state: ExplorerState = { ...this.defaultState }; - constructor(stateProps, dispatchProps) { - super(stateProps, dispatchProps); - this.handleOnBulkSelect = this.handleOnBulkSelect.bind(this); - this.handleOnExportModalClose = this.handleOnExportModalClose.bind(this); - this.handleOnExportModalOpen = this.handleOnExportModalOpen.bind(this); - this.handleOnPerspectiveClick = this.handleOnPerspectiveClick.bind(this); - this.handleOnSelect = this.handleOnSelect.bind(this); - } - public componentDidMount() { this.updateReport(); } @@ -179,6 +177,50 @@ class Explorer extends React.Component { return computedItems; }; + private getEmptyProviderState = () => { + const { isAccountInfoEmptyStateToggleEnabled } = this.props; + + const { perspective } = this.props; + + let providerType; + switch (perspective) { + case PerspectiveType.aws: + case PerspectiveType.awsOcp: + providerType = ProviderType.aws; + break; + case PerspectiveType.azure: + case PerspectiveType.azureOcp: + providerType = ProviderType.azure; + break; + case PerspectiveType.gcp: + case PerspectiveType.gcpOcp: + providerType = ProviderType.gcp; + break; + case PerspectiveType.ibm: + case PerspectiveType.ibmOcp: + providerType = ProviderType.ibm; + break; + case PerspectiveType.oci: + providerType = ProviderType.oci; + break; + case PerspectiveType.ocp: + case PerspectiveType.ocpCloud: + providerType = ProviderType.ocp; + break; + case PerspectiveType.rhel: + providerType = ProviderType.rhel; + break; + } + + return ( + : undefined + } + /> + ); + }; + private getExportModal = (computedItems: ComputedReportItem[]) => { const { perspective, query, report, reportQueryString } = this.props; const { isAllSelected, isExportModalOpen, selectedItems } = this.state; @@ -252,7 +294,7 @@ class Explorer extends React.Component { }; private getTable = () => { - const { costDistribution, perspective, query, report, reportFetchStatus, router } = this.props; + const { costDistribution, endDate, perspective, query, report, reportFetchStatus, router, startDate } = this.props; const { isAllSelected, selectedItems } = this.state; const groupById = getIdKeyForGroupBy(query.group_by); @@ -263,6 +305,7 @@ class Explorer extends React.Component { return ( { query={query} report={report} selectedItems={selectedItems} + startDate={startDate} /> ); }; @@ -331,12 +375,16 @@ class Explorer extends React.Component { } }; - private handleOnDatePickerSelect = (startDate: Date, endDate: Date) => { + private handleOnDateRangeSelect = (dateRangeType: string) => { const { query, router } = this.props; - this.setState({ startDate, endDate }, () => { - router.navigate(getRouteForQuery(query, router.location, true), { replace: true }); - }); + const newQuery = { + ...JSON.parse(JSON.stringify(query)), + dateRangeType, + start_date: undefined, + end_date: undefined, + }; + router.navigate(getRouteForQuery(newQuery, router.location, true), { replace: true }); }; private handleOnExportModalClose = (isOpen: boolean) => { @@ -443,9 +491,15 @@ class Explorer extends React.Component { costDistribution, costType, currency, + dateRangeType, + endDate, gcpProviders, ibmProviders, intl, + isChartSkeletonToggleEnabled, + isCurrentMonthData, + isDataAvailable, + isDetailsDateRangeToggleEnabled, ocpProviders, providersFetchStatus, perspective, @@ -455,6 +509,7 @@ class Explorer extends React.Component { reportError, reportFetchStatus, router, + startDate, } = this.props; // Note: No need to test OCP on cloud here, since that requires at least one provider @@ -505,12 +560,16 @@ class Explorer extends React.Component { return ; } + const isDateRangeSelected = query.dateRangeType !== undefined; + return (
{ ? `${tagPrefix}${groupByTagKey}` : groupById } + isCurrentMonthData={isCurrentMonthData} onCostDistributionSelect={() => handleOnCostDistributionSelect(query, router)} onCostTypeSelect={() => handleOnCostTypeSelect(query, router)} onCurrencySelect={() => handleOnCurrencySelect(query, router)} - onDatePickerSelect={this.handleOnDatePickerSelect} + onDateRangeSelect={this.handleOnDateRangeSelect} onFilterAdded={filter => handleOnFilterAdded(query, router, filter)} onFilterRemoved={filter => handleOnFilterRemoved(query, router, filter)} onGroupBySelect={this.handleOnGroupBySelect} onPerspectiveClicked={this.handleOnPerspectiveClick} perspective={perspective} report={report} + startDate={startDate} /> - {itemsTotal > 0 && ( -
-
- + {!isDataAvailable && isDetailsDateRangeToggleEnabled ? ( + this.getEmptyProviderState() + ) : ( + <> + {isDetailsDateRangeToggleEnabled ? ( +
+ {!isCurrentMonthData && !isDateRangeSelected && dateRangeType === DateRangeType.previousMonth && ( + + )} +
+ +
+
+ ) : ( + (itemsTotal > 0 || isChartSkeletonToggleEnabled) && ( +
+
+ +
+
+ ) + )} +
+
{this.getToolbar(computedItems)}
+ {this.getExportModal(computedItems)} + {reportFetchStatus === FetchStatus.inProgress ? ( + + ) : ( + <> +
{this.getTable()}
+
+
{this.getPagination(isDisabled, true)}
+
+ + )}
-
+ )} -
-
{this.getToolbar(computedItems)}
- {this.getExportModal(computedItems)} - {reportFetchStatus === FetchStatus.inProgress ? ( - - ) : ( - <> -
{this.getTable()}
-
-
{this.getPagination(isDisabled, true)}
-
- - )} -
); } @@ -598,15 +701,12 @@ const mapStateToProps = createMapStateToProps { - const { end_date, start_date } = this.props; + const { endDate, startDate } = this.props; const result = []; items.map(datums => { @@ -213,8 +213,8 @@ class ExplorerChartBase extends React.Component 0 ? datums[0] : []} top2ndData={datums.length > 1 ? datums[1] : []} top3rdData={datums.length > 2 ? datums[2] : []} @@ -269,11 +276,9 @@ class ExplorerChartBase extends React.Component( - (state, { costType, currency, perspective, router }) => { + (state, { costType, currency, endDate, perspective, router, startDate }) => { const queryFromRoute = parseQuery(router.location.search); - const { end_date, start_date } = getDateRangeFromQuery(queryFromRoute); - const groupBy = queryFromRoute.group_by ? getGroupById(queryFromRoute) : getGroupByDefault(perspective); const group_by = queryFromRoute.group_by ? queryFromRoute.group_by : { [groupBy]: '*' }; // Ensure group_by key is not undefined @@ -287,12 +292,12 @@ const mapStateToProps = createMapStateToProps(); public componentDidMount() { - const { router } = this.props; - const queryFromRoute = parseQuery(router.location.search); - const dateRangeType = getDateRangeTypeDefault(queryFromRoute); - const { end_date, start_date } = getDateRangeFromQuery(queryFromRoute); + const { dateRangeType, endDate, startDate } = this.props; if (this.startDateRef?.current) { this.startDateRef.current.setCalendarOpen(dateRangeType !== DateRangeType.custom); } - if (dateRangeType === DateRangeType.custom) { + if (dateRangeType === DateRangeType.custom && endDate && startDate) { this.setState({ - startDate: new Date(start_date + 'T00:00:00'), - endDate: new Date(end_date + 'T00:00:00'), + startDate: new Date(startDate + 'T00:00:00'), + endDate: new Date(endDate + 'T00:00:00'), }); } } diff --git a/src/routes/explorer/explorerDateRange.tsx b/src/routes/explorer/explorerDateRange.tsx deleted file mode 100644 index d431476bf..000000000 --- a/src/routes/explorer/explorerDateRange.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import type { MessageDescriptor } from '@formatjs/intl/src/types'; -import React from 'react'; -import type { WrappedComponentProps } from 'react-intl'; -import { injectIntl } from 'react-intl'; -import type { SelectWrapperOption } from 'routes/components/selectWrapper'; -import { SelectWrapper } from 'routes/components/selectWrapper'; - -interface ExplorerDateRangeOwnProps { - dateRangeType?: string; - isDisabled?: boolean; - onSelect(value: string); - options?: { - label: MessageDescriptor; - value: string; - }[]; -} - -interface ExplorerDateRangeState { - // TBD... -} - -type ExplorerDateRangeProps = ExplorerDateRangeOwnProps & WrappedComponentProps; - -class ExplorerDateRangeBase extends React.Component { - protected defaultState: ExplorerDateRangeState = { - // TBD... - }; - public state: ExplorerDateRangeState = { ...this.defaultState }; - - private getSelect = () => { - const { dateRangeType, isDisabled } = this.props; - - const selectOptions = this.getSelectOptions(); - const selection = selectOptions.find(option => option.value === dateRangeType); - - return ( - - ); - }; - - private getSelectOptions = (): SelectWrapperOption[] => { - const { intl, options } = this.props; - - const selectOptions: SelectWrapperOption[] = []; - - options.map(option => { - selectOptions.push({ - toString: () => intl.formatMessage(option.label, { value: option.value }), - value: option.value, - }); - }); - return selectOptions; - }; - - private handleOnSelect = (_evt, selection: SelectWrapperOption) => { - const { onSelect } = this.props; - - if (onSelect) { - onSelect(selection.value); - } - }; - - public render() { - return this.getSelect(); - } -} - -const ExplorerDateRange = injectIntl(ExplorerDateRangeBase); - -export { ExplorerDateRange }; diff --git a/src/routes/explorer/explorerFilter.tsx b/src/routes/explorer/explorerFilter.tsx index d110d2b38..7800d9846 100644 --- a/src/routes/explorer/explorerFilter.tsx +++ b/src/routes/explorer/explorerFilter.tsx @@ -4,7 +4,7 @@ import type { ToolbarChipGroup } from '@patternfly/react-core'; import type { Org, OrgPathsType } from 'api/orgs/org'; import { OrgType } from 'api/orgs/org'; import type { Query } from 'api/queries/query'; -import { getQuery, parseQuery } from 'api/queries/query'; +import { getQuery } from 'api/queries/query'; import type { Resource, ResourcePathsType } from 'api/resources/resource'; import { ResourceType } from 'api/resources/resource'; import type { Tag, TagPathsType } from 'api/tags/tag'; @@ -15,7 +15,8 @@ import type { WrappedComponentProps } from 'react-intl'; import { injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { DataToolbar } from 'routes/components/dataToolbar'; -import { DateRangeType, getDateRangeFromQuery, getDateRangeTypeDefault } from 'routes/utils/dateRange'; +import { DateRange } from 'routes/components/dateRange'; +import { DateRangeType, getDateRangeById } from 'routes/utils/dateRange'; import { isEqual } from 'routes/utils/equal'; import type { Filter } from 'routes/utils/filter'; import { getRouteForQuery } from 'routes/utils/query'; @@ -31,10 +32,8 @@ import type { RouterComponentProps } from 'utils/router'; import { withRouter } from 'utils/router'; import { ExplorerDatePicker } from './explorerDatePicker'; -import { ExplorerDateRange } from './explorerDateRange'; import { styles } from './explorerFilter.styles'; import { - dateRangeOptions, getGroupByOptions, getOrgReportPathsType, getResourcePathsType, @@ -43,17 +42,21 @@ import { } from './explorerUtils'; interface ExplorerFilterOwnProps extends RouterComponentProps, WrappedComponentProps { + dateRangeType: DateRangeType; + endDate?: string; groupBy: string; + isCurrentMonthData?: boolean; isDisabled?: boolean; + onDateRangeSelect(value: string); onFilterAdded(filter: Filter); onFilterRemoved(filter: Filter); perspective: PerspectiveType; pagination?: React.ReactNode; query?: Query; + startDate?: string; } interface ExplorerFilterStateProps { - dateRangeType: DateRangeType; isOcpCloudGroupBysToggleEnabled?: boolean; orgPathsType?: OrgPathsType; orgQueryString?: string; @@ -77,7 +80,6 @@ interface ExplorerFilterDispatchProps { interface ExplorerFilterState { categoryOptions?: ToolbarChipGroup[]; - currentDateRangeType?: string; showDatePicker?: boolean; } @@ -94,31 +96,29 @@ export class ExplorerFilterBase extends React.Component { @@ -130,6 +130,7 @@ export class ExplorerFilterBase extends React.Component 0) { @@ -159,26 +160,34 @@ export class ExplorerFilterBase extends React.Component { - const { isDisabled } = this.props; - const { currentDateRangeType } = this.state; + const { dateRangeType, isCurrentMonthData, isDisabled } = this.props; return ( - ); }; private getDatePickerComponent = () => { + const { dateRangeType, endDate, startDate } = this.props; const { showDatePicker } = this.state; - return showDatePicker ? : undefined; + return showDatePicker ? ( + + ) : null; }; - private handleOnDatePickerSelected = (startDate: Date, endDate: Date) => { + private handleOnDatePickerSelect = (startDate: Date, endDate: Date) => { const { query, router } = this.props; const { start_date, end_date } = formatStartEndDate(startDate, endDate); @@ -192,19 +201,14 @@ export class ExplorerFilterBase extends React.Component { - const { query, router } = this.props; + private handleOnDateRangeSelect = (value: string) => { + const { onDateRangeSelect } = this.props; + const currentDateRange = getDateRangeById(value); const showDatePicker = value === DateRangeType.custom; - this.setState({ currentDateRangeType: value, showDatePicker }, () => { - if (!showDatePicker) { - const newQuery = { - ...JSON.parse(JSON.stringify(query)), - dateRangeType: value, - start_date: undefined, - end_date: undefined, - }; - router.navigate(getRouteForQuery(newQuery, router.location, true), { replace: true }); + this.setState({ showDatePicker }, () => { + if (onDateRangeSelect && !showDatePicker) { + onDateRangeSelect(currentDateRange); } }); }; @@ -253,8 +257,8 @@ export class ExplorerFilterBase extends React.Component( - (state, { perspective, router }) => { - const queryFromRoute = parseQuery(router.location.search); - const dateRangeType = getDateRangeTypeDefault(queryFromRoute); - const { end_date, start_date } = getDateRangeFromQuery(queryFromRoute); - + (state, { endDate, perspective, startDate }) => { // Omitting key_only to share a single request -- the toolbar needs key values const orgQueryString = getQuery({ - end_date, - start_date, + end_date: endDate, + start_date: startDate, limit: 1000, }); @@ -313,8 +313,8 @@ const mapStateToProps = createMapStateToProps { protected defaultState: ExplorerHeaderState = { - // TBD... + currentPerspective: this.props.perspective, }; public state: ExplorerHeaderState = { ...this.defaultState }; @@ -170,6 +175,7 @@ class ExplorerHeaderBase extends React.Component{this.getPerspective(noProviders)} @@ -335,12 +348,17 @@ class ExplorerHeaderBase extends React.Component ); diff --git a/src/routes/explorer/explorerSkeletonData.tsx b/src/routes/explorer/explorerSkeletonData.tsx new file mode 100644 index 000000000..1bac47285 --- /dev/null +++ b/src/routes/explorer/explorerSkeletonData.tsx @@ -0,0 +1,61 @@ +const top1stData = [ + 1108.53, 1108.12, 1109.35, 1109.71, 1109.73, 1109.81, 1109.69, 1108.75, 1109.03, 1108.87, 1108.62, 1108.57, 1108.95, + 1109.85, 1109.83, 1109.78, 1109.57, 1108.47, 1108.53, 1108.9, 1109.8, 1109.91, 1111.42, 1109.74, 1109.61, 1109.6, + 1109.73, 1112.55, 1109.65, 1109.78, 231.21, +]; +const top2ndData = [ + 1343.03, 903.56, 953.58, 609.13, 1185.89, 1501.3, 887.25, 598.46, 913.52, 1957.66, 1645.23, 544.67, 1203.28, 1251.59, + 552.12, 673.74, 753.94, 1392.65, 435.82, 1269.72, 1288.04, 936.65, 823.59, 824.14, 823.54, 824.06, 480.64, 417.32, + 1003.14, 1001.25, 583.4, +]; +const top3rdData = [ + 374.82, 383.76, 319.87, 104.59, 118.03, 337.04, 823.11, 579.26, 295.51, 531.44, 105.67, 104.6, 971.38, 286.35, 568.39, + 238.03, 192.37, 60.09, 78.2, 237.03, 209.01, 224.73, 326.58, 200.96, 68.54, 99.04, 300.24, 232.8, 107.08, 267.67, + 55.62, +]; +const top4thData = [ + 120.66, 60.11, 8.44, 8.45, 20.26, 75.93, 32.08, 11.92, 22.96, 67.38, 25.19, 17.91, 67.42, 47.74, 67.47, 67.31, 71.37, + 52.86, 11.64, 26.41, 31.85, 67.31, 19.95, 59.17, 7.32, 7.32, 7.33, 328.35, 8.08, 8.69, 1.19, +]; +const top5thData = [ + 123.35, 123.04, 123.13, 123.06, 124.51, 123.46, 123.21, 123.22, 123.28, 123.14, 123.19, 129.31, 123.18, 123.42, + 123.56, 123.46, 123.16, 123.19, 126.51, 123.28, 123.44, 123.38, 123.43, 123.38, 123.25, 129.4, 123.7, 123.12, 123.15, + 119.73, 21.57, +]; +const top6thData = [ + 1535.93, 1403.04, 1356.65, 1282.12, 1264.35, 1286.7, 1511.64, 1388.88, 1370.51, 1319.67, 1155.13, 975.7, 1030.38, + 1129.76, 1218.14, 1117.55, 1196.87, 997.1, 830.46, 1031.18, 1215.34, 1373, 1405.28, 1431.94, 1164.6, 1031.98, 1360.85, + 1548.71, 1206.01, 1179.92, +]; + +const getData = (name: string, values: number[], count: number) => { + const datum = []; + + let index = 0; + while (count > index) { + datum.push({ + date: `${index}`, + key: name, + name, + units: 'USD', + x: `${index}`, + y: values[index % values.length], + }); + index++; + } + return datum; +}; + +export const getExplorerSkeletonData = (count = 0) => { + // There will always be at least one day + const days = count ? count : 1; + + return [ + getData('top1stData', top1stData, days), + getData('top2ndData', top2ndData, days), + getData('top3rdData', top3rdData, days), + getData('top4thData', top4thData, days), + getData('top5thData', top5thData, days), + getData('top6thData', top6thData, days), // Others + ]; +}; diff --git a/src/routes/explorer/explorerTable.tsx b/src/routes/explorer/explorerTable.tsx index 7089bd11e..9497ac551 100644 --- a/src/routes/explorer/explorerTable.tsx +++ b/src/routes/explorer/explorerTable.tsx @@ -24,7 +24,6 @@ import { Tr, } from '@patternfly/react-table'; import type { Query } from 'api/queries/query'; -import { parseQuery } from 'api/queries/query'; import type { Report } from 'api/reports/report'; import { format } from 'date-fns'; import messages from 'locales/messages'; @@ -36,7 +35,6 @@ import { ComputedReportItemType, ComputedReportItemValueType } from 'routes/comp import { EmptyFilterState } from 'routes/components/state/emptyFilterState'; import type { ComputedReportItem } from 'routes/utils/computedReport/getComputedReportItems'; import { getUnsortedComputedReportItems } from 'routes/utils/computedReport/getComputedReportItems'; -import { getDateRangeFromQuery } from 'routes/utils/dateRange'; import { createMapStateToProps } from 'store/common'; import { formatCurrency } from 'utils/format'; import { classificationDefault, classificationUnallocated, noPrefix } from 'utils/props'; @@ -48,6 +46,7 @@ import { PerspectiveType } from './explorerUtils'; interface ExplorerTableOwnProps extends RouterComponentProps, WrappedComponentProps { costDistribution?: string; + endDate?: string; groupBy: string; groupByCostCategory?: string; groupByOrg?: string; @@ -60,11 +59,11 @@ interface ExplorerTableOwnProps extends RouterComponentProps, WrappedComponentPr query: Query; report: Report; selectedItems?: ComputedReportItem[]; + startDate?: string; } interface ExplorerTableStateProps { - end_date?: string; - start_date?: string; + // TBD... } interface ExplorerTableDispatchProps { @@ -118,7 +117,7 @@ class ExplorerTableBase extends React.Component { const { costDistribution, - end_date, + endDate, groupBy, groupByCostCategory, groupByOrg, @@ -127,7 +126,7 @@ class ExplorerTableBase extends React.Component((state, { router }) => { - const queryFromRoute = parseQuery(router.location.search); - const { end_date, start_date } = getDateRangeFromQuery(queryFromRoute); - +const mapStateToProps = createMapStateToProps(() => { return { - end_date, - start_date, + // TBD }; }); diff --git a/src/routes/explorer/explorerToolbar.tsx b/src/routes/explorer/explorerToolbar.tsx index a9805fed9..8d13df404 100644 --- a/src/routes/explorer/explorerToolbar.tsx +++ b/src/routes/explorer/explorerToolbar.tsx @@ -1,4 +1,3 @@ -import type { ToolbarChipGroup } from '@patternfly/react-core'; import React from 'react'; import type { WrappedComponentProps } from 'react-intl'; import { injectIntl } from 'react-intl'; @@ -33,7 +32,7 @@ interface ExplorerToolbarDispatchProps { } interface ExplorerToolbarState { - categoryOptions?: ToolbarChipGroup[]; + // TBD... } type ExplorerToolbarProps = ExplorerToolbarOwnProps & diff --git a/src/routes/explorer/explorerUtils.ts b/src/routes/explorer/explorerUtils.ts index 2b811af38..7c00dafcd 100644 --- a/src/routes/explorer/explorerUtils.ts +++ b/src/routes/explorer/explorerUtils.ts @@ -1,4 +1,3 @@ -import type { MessageDescriptor } from '@formatjs/intl/src/types'; import { OrgPathsType } from 'api/orgs/org'; import type { Providers } from 'api/providers'; import type { Query } from 'api/queries/query'; @@ -6,14 +5,13 @@ import { ReportPathsType, ReportType } from 'api/reports/report'; import { ResourcePathsType } from 'api/resources/resource'; import { TagPathsType } from 'api/tags/tag'; import type { UserAccess } from 'api/userAccess'; -import messages from 'locales/messages'; import type { ComputedAwsReportItemsParams } from 'routes/utils/computedReport/getComputedAwsReportItems'; import type { ComputedAzureReportItemsParams } from 'routes/utils/computedReport/getComputedAzureReportItems'; import type { ComputedGcpReportItemsParams } from 'routes/utils/computedReport/getComputedGcpReportItems'; import type { ComputedIbmReportItemsParams } from 'routes/utils/computedReport/getComputedIbmReportItems'; import type { ComputedOciReportItemsParams } from 'routes/utils/computedReport/getComputedOciReportItems'; import type { ComputedOcpReportItemsParams } from 'routes/utils/computedReport/getComputedOcpReportItems'; -import { hasCloudProvider } from 'routes/utils/providers'; +import { hasCloudProvider, hasCurrentMonthData, hasData, hasPreviousMonthData } from 'routes/utils/providers'; import { hasAwsAccess, hasAzureAccess, @@ -55,24 +53,12 @@ export const baseQuery: Query = { }, }; -export const dateRangeOptions: { - label: MessageDescriptor; - value: string; -}[] = [ - { label: messages.explorerDateRange, value: 'current_month_to_date' }, - { label: messages.explorerDateRange, value: 'previous_month' }, - { label: messages.explorerDateRange, value: 'previous_month_to_date' }, - { label: messages.explorerDateRange, value: 'last_thirty_days' }, - { label: messages.explorerDateRange, value: 'last_sixty_days' }, - { label: messages.explorerDateRange, value: 'last_ninety_days' }, - { label: messages.explorerDateRange, value: 'custom' }, -]; - export const groupByAwsOptions: { label: string; value: ComputedAwsReportItemsParams['idKey']; + resourceKey?: string; }[] = [ - { label: 'account', value: 'account' }, + { label: 'account', value: 'account', resourceKey: 'account_alias' }, { label: 'service', value: 'service' }, { label: 'region', value: 'region' }, ]; @@ -167,6 +153,7 @@ export const getPerspectiveDefault = ({ const perspective = queryFromRoute.perspective; // Upon page refresh, perspective param takes precedence + // Todo: Add ocp here? switch (perspective) { case PerspectiveType.aws: case PerspectiveType.awsOcp: @@ -289,6 +276,78 @@ export const getGroupByOptions = (perspective: string, isOcpCloudGroupBysToggleE return result; }; +export const getIsDataAvailable = ({ + awsProviders, + azureProviders, + ociProviders, + gcpProviders, + ibmProviders, + ocpProviders, + perspective, + rhelProviders, +}: { + awsProviders: Providers; + azureProviders: Providers; + ociProviders: Providers; + gcpProviders: Providers; + ibmProviders: Providers; + ocpProviders: Providers; + perspective: string; + rhelProviders: Providers; +}) => { + let isCurrentMonthData; + let isDataAvailable; + let isPreviousMonthData; + + switch (perspective) { + case PerspectiveType.aws: + case PerspectiveType.awsOcp: + isDataAvailable = hasData(awsProviders); + isCurrentMonthData = hasCurrentMonthData(awsProviders); + isPreviousMonthData = hasPreviousMonthData(awsProviders); + break; + case PerspectiveType.azure: + case PerspectiveType.azureOcp: + isDataAvailable = hasData(azureProviders); + isCurrentMonthData = hasCurrentMonthData(azureProviders); + isPreviousMonthData = hasPreviousMonthData(azureProviders); + break; + case PerspectiveType.gcp: + case PerspectiveType.gcpOcp: + isDataAvailable = hasData(gcpProviders); + isCurrentMonthData = hasCurrentMonthData(gcpProviders); + isPreviousMonthData = hasPreviousMonthData(gcpProviders); + break; + case PerspectiveType.ibm: + case PerspectiveType.ibmOcp: + isDataAvailable = hasData(ibmProviders); + isCurrentMonthData = hasCurrentMonthData(ibmProviders); + isPreviousMonthData = hasPreviousMonthData(ibmProviders); + break; + case PerspectiveType.oci: + isDataAvailable = hasData(ociProviders); + isCurrentMonthData = hasCurrentMonthData(ociProviders); + isPreviousMonthData = hasPreviousMonthData(ociProviders); + break; + case PerspectiveType.ocp: + case PerspectiveType.ocpCloud: + isDataAvailable = hasData(ocpProviders); + isCurrentMonthData = hasCurrentMonthData(ocpProviders); + isPreviousMonthData = hasPreviousMonthData(ocpProviders); + break; + case PerspectiveType.rhel: + isDataAvailable = hasData(rhelProviders); + isCurrentMonthData = hasCurrentMonthData(rhelProviders); + isPreviousMonthData = hasPreviousMonthData(rhelProviders); + break; + } + return { + isCurrentMonthData, + isDataAvailable, + isPreviousMonthData, + }; +}; + export const getOrgReportPathsType = (perspective: string) => { let result; switch (perspective) { diff --git a/src/routes/utils/dateRange.ts b/src/routes/utils/dateRange.ts index 85b59d694..34a331320 100644 --- a/src/routes/utils/dateRange.ts +++ b/src/routes/utils/dateRange.ts @@ -19,6 +19,25 @@ export const enum DateRangeType { lastThirtyDays = 'last_thirty_days', // Last 30 days (Dec 18 - Jan 17) } +export const getDateRangeById = (value: string) => { + switch (value) { + case 'current_month_to_date': + return DateRangeType.currentMonthToDate; + case 'custom': + return DateRangeType.custom; + case 'previous_month': + return DateRangeType.previousMonth; + case 'previous_month_to_date': + return DateRangeType.previousMonthToDate; + case 'last_ninety_days': + return DateRangeType.lastNinetyDays; + case 'last_sixty_days': + return DateRangeType.lastSixtyDays; + case 'last_thirty_days': + return DateRangeType.lastThirtyDays; + } +}; + export const getDateRange = (dateRangeType: DateRangeType, isFormatted = true) => { const endDate = new Date(); const startDate = new Date(); @@ -56,24 +75,22 @@ export const getDateRange = (dateRangeType: DateRangeType, isFormatted = true) = return dateRange; }; -export const getDateRangeTypeDefault = (queryFromRoute: Query) => { - return queryFromRoute.dateRangeType || DateRangeType.currentMonthToDate; +export const getDateRangeTypeDefault = (queryFromRoute: Query, defaultToPreviousMonth: boolean): DateRangeType => { + if (queryFromRoute.dateRangeType) { + return queryFromRoute.dateRangeType; + } + return defaultToPreviousMonth ? DateRangeType.previousMonth : DateRangeType.currentMonthToDate; }; -export const getDateRangeFromQuery = (queryFromRoute: Query) => { - let end_date; - let start_date; - - if (queryFromRoute.dateRangeType === DateRangeType.custom) { - end_date = queryFromRoute.end_date; - start_date = queryFromRoute.start_date; - } - if (!(end_date && start_date)) { - const dateRangeType = getDateRangeTypeDefault(queryFromRoute); - return getDateRange(dateRangeType); - } +export const getDateRangeFromQuery = (queryFromRoute: Query, defaultToPreviousMonth: boolean = false) => { + const dateRangeType = getDateRangeTypeDefault(queryFromRoute, defaultToPreviousMonth); + const dateRange = + dateRangeType === DateRangeType.custom + ? { start_date: queryFromRoute.start_date, end_date: queryFromRoute.end_date } + : getDateRange(dateRangeType); return { - end_date, - start_date, + dateRangeType, + end_date: dateRange.end_date, + start_date: dateRange.start_date, }; }; diff --git a/src/routes/utils/timeScope.ts b/src/routes/utils/timeScope.ts new file mode 100644 index 000000000..4b8c63640 --- /dev/null +++ b/src/routes/utils/timeScope.ts @@ -0,0 +1,11 @@ +import type { Query } from 'api/queries/query'; + +export const getTimeScope = (query: Query) => { + const filters = query?.filter ? Object.keys(query.filter) : []; + return filters.find(key => key === 'time_scope_value'); +}; + +export const getTimeScopeValue = (query: Query) => { + const timeScope = getTimeScope(query); + return timeScope ? query.filter[timeScope] : undefined; +}; diff --git a/src/store/featureToggle/__snapshots__/featureToggle.test.ts.snap b/src/store/featureToggle/__snapshots__/featureToggle.test.ts.snap index 45946a25e..3964d3add 100644 --- a/src/store/featureToggle/__snapshots__/featureToggle.test.ts.snap +++ b/src/store/featureToggle/__snapshots__/featureToggle.test.ts.snap @@ -3,9 +3,12 @@ exports[`default state 1`] = ` { "hasFeatureToggle": false, + "isAccountInfoDetailsToggleEnabled": false, "isAccountInfoEmptyStateToggleEnabled": false, "isAwsEc2InstancesToggleEnabled": false, + "isChartSkeletonToggleEnabled": false, "isDebugToggleEnabled": false, + "isDetailsDateRangeToggleEnabled": false, "isExportsToggleEnabled": false, "isFinsightsToggleEnabled": false, "isIbmToggleEnabled": false, diff --git a/src/store/featureToggle/featureToggleActions.ts b/src/store/featureToggle/featureToggleActions.ts index ba0f03901..7b26bacaa 100644 --- a/src/store/featureToggle/featureToggleActions.ts +++ b/src/store/featureToggle/featureToggleActions.ts @@ -1,9 +1,12 @@ import { createAction } from 'typesafe-actions'; export interface FeatureToggleActionMeta { + isAccountInfoDetailsToggleEnabled?: boolean; isAccountInfoEmptyStateToggleEnabled?: boolean; isAwsEc2InstancesToggleEnabled?: boolean; + isChartSkeletonToggleEnabled?: boolean; isDebugToggleEnabled?: boolean; + isDetailsDateRangeToggleEnabled?: boolean; isExportsToggleEnabled?: boolean; isFinsightsToggleEnabled?: boolean; isIbmToggleEnabled?: boolean; diff --git a/src/store/featureToggle/featureToggleReducer.ts b/src/store/featureToggle/featureToggleReducer.ts index f31821667..9b9cdf954 100644 --- a/src/store/featureToggle/featureToggleReducer.ts +++ b/src/store/featureToggle/featureToggleReducer.ts @@ -8,9 +8,12 @@ export type FeatureToggleAction = ActionType state[stateKey]; export const selectHasFeatureToggle = (state: RootState) => selectFeatureToggleState(state).hasFeatureToggle; +export const selectIsAccountInfoDetailsToggleEnabled = (state: RootState) => + selectFeatureToggleState(state).isAccountInfoDetailsToggleEnabled; export const selectIsAccountInfoEmptyStateToggleEnabled = (state: RootState) => selectFeatureToggleState(state).isAccountInfoEmptyStateToggleEnabled; export const selectIsAwsEc2InstancesToggleEnabled = (state: RootState) => selectFeatureToggleState(state).isAwsEc2InstancesToggleEnabled; +export const selectIsChartSkeletonToggleEnabled = (state: RootState) => + selectFeatureToggleState(state).isChartSkeletonToggleEnabled; export const selectIsDebugToggleEnabled = (state: RootState) => selectFeatureToggleState(state).isDebugToggleEnabled; +export const selectIsDetailsDateRangeToggleEnabled = (state: RootState) => + selectFeatureToggleState(state).isDetailsDateRangeToggleEnabled; export const selectIsExportsToggleEnabled = (state: RootState) => selectFeatureToggleState(state).isExportsToggleEnabled; export const selectIsFinsightsToggleEnabled = (state: RootState) => diff --git a/src/utils/dates.ts b/src/utils/dates.ts index fb7c7e274..eebb4262d 100644 --- a/src/utils/dates.ts +++ b/src/utils/dates.ts @@ -27,7 +27,8 @@ export const getToday = (hrs: number = 0, min: number = 0, sec: number = 0, ms: export const getNoDataForDateRangeString = ( message: MessageDescriptor = messages.noDataForDate, - offset: number = 1 + offset: number = 0, + isEndOfMonth = false ) => { const endDate = getToday(); const startDate = getToday(); @@ -36,7 +37,11 @@ export const getNoDataForDateRangeString = ( if (offset) { startDate.setMonth(startDate.getMonth() - offset); - endDate.setMonth(endDate.getMonth() - offset); + if (isEndOfMonth) { + endDate.setMonth(endDate.getMonth() - offset + 1, 0); + } else { + endDate.setMonth(endDate.getMonth() - offset); + } } const dateRange = intl.formatDateTimeRange(startDate, endDate, { day: 'numeric', @@ -48,7 +53,8 @@ export const getNoDataForDateRangeString = ( export const getForDateRangeString = ( value: string | number, message: MessageDescriptor = messages.forDate, - offset = 1 + offset = 0, + isEndOfMonth = false ) => { const endDate = getToday(); const startDate = getToday(); @@ -57,7 +63,11 @@ export const getForDateRangeString = ( if (offset) { startDate.setMonth(startDate.getMonth() - offset); - endDate.setMonth(endDate.getMonth() - offset); + if (isEndOfMonth) { + endDate.setMonth(endDate.getMonth() - offset + 1, 0); + } else { + endDate.setMonth(endDate.getMonth() - offset); + } } const dateRange = intl.formatDateTimeRange(startDate, endDate, { day: 'numeric', @@ -66,11 +76,24 @@ export const getForDateRangeString = ( return intl.formatMessage(message, { dateRange, value }); }; -export const getSinceDateRangeString = (message: MessageDescriptor = messages.sinceDate) => { +export const getSinceDateRangeString = ( + message: MessageDescriptor = messages.sinceDate, + offset = 0, + isEndOfMonth = false +) => { const endDate = getToday(); const startDate = getToday(); startDate.setDate(1); + + if (offset) { + startDate.setMonth(startDate.getMonth() - offset); + if (isEndOfMonth) { + endDate.setMonth(endDate.getMonth() - offset + 1, 0); + } else { + endDate.setMonth(endDate.getMonth() - offset); + } + } const dateRange = intl.formatDateTimeRange(startDate, endDate, { day: 'numeric', month: 'long', @@ -80,12 +103,23 @@ export const getSinceDateRangeString = (message: MessageDescriptor = messages.si export const getTotalCostDateRangeString = ( value: string | number, - message: MessageDescriptor = messages.breakdownTotalCostDate + message: MessageDescriptor = messages.breakdownTotalCostDate, + offset = 0, + isEndOfMonth = false ) => { const endDate = getToday(); const startDate = getToday(); startDate.setDate(1); + + if (offset) { + startDate.setMonth(startDate.getMonth() - offset); + if (isEndOfMonth) { + endDate.setMonth(endDate.getMonth() - offset + 1, 0); + } else { + endDate.setMonth(endDate.getMonth() - offset); + } + } const dateRange = intl.formatDateTimeRange(startDate, endDate, { day: 'numeric', month: 'long', diff --git a/test/testEnv.ts b/test/testEnv.ts index 4ddc8f58d..5c1d8e485 100644 --- a/test/testEnv.ts +++ b/test/testEnv.ts @@ -1,7 +1,3 @@ -import { StyleSheetTestUtils } from 'aphrodite'; - -StyleSheetTestUtils.suppressStyleInjection(); - export interface Global { insights: any; }