@backstage-community/plugin-cost-insights 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/alpha/plugin.esm.js +1 -0
  3. package/dist/alpha/plugin.esm.js.map +1 -1
  4. package/dist/alpha/router.esm.js +1 -0
  5. package/dist/alpha/router.esm.js.map +1 -1
  6. package/dist/components/CostGrowth/CostGrowth.esm.js +11 -2
  7. package/dist/components/CostGrowth/CostGrowth.esm.js.map +1 -1
  8. package/dist/components/CostInsightsPage/CostInsightsPage.esm.js +5 -1
  9. package/dist/components/CostInsightsPage/CostInsightsPage.esm.js.map +1 -1
  10. package/dist/components/CostOverviewCard/CostOverviewBreakdownChart.esm.js +20 -6
  11. package/dist/components/CostOverviewCard/CostOverviewBreakdownChart.esm.js.map +1 -1
  12. package/dist/components/CostOverviewCard/CostOverviewCard.esm.js +17 -4
  13. package/dist/components/CostOverviewCard/CostOverviewCard.esm.js.map +1 -1
  14. package/dist/components/CostOverviewCard/CostOverviewLegend.esm.js +11 -3
  15. package/dist/components/CostOverviewCard/CostOverviewLegend.esm.js.map +1 -1
  16. package/dist/components/CostOverviewCard/selector.esm.js +3 -2
  17. package/dist/components/CostOverviewCard/selector.esm.js.map +1 -1
  18. package/dist/components/EntityCosts/EntityCosts.esm.js +5 -1
  19. package/dist/components/EntityCosts/EntityCosts.esm.js.map +1 -1
  20. package/dist/components/PeriodSelect/DateRangePicker.esm.js +218 -0
  21. package/dist/components/PeriodSelect/DateRangePicker.esm.js.map +1 -0
  22. package/dist/components/ProductInsights/ProductInsights.esm.js +9 -2
  23. package/dist/components/ProductInsights/ProductInsights.esm.js.map +1 -1
  24. package/dist/components/ProductInsightsCard/ProductInsightsCard.esm.js +32 -8
  25. package/dist/components/ProductInsightsCard/ProductInsightsCard.esm.js.map +1 -1
  26. package/dist/components/ProductInsightsCard/ProductInsightsChart.esm.js +24 -4
  27. package/dist/components/ProductInsightsCard/ProductInsightsChart.esm.js.map +1 -1
  28. package/dist/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.esm.js +1 -0
  29. package/dist/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.esm.js.map +1 -1
  30. package/dist/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChart.esm.js +1 -0
  31. package/dist/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChart.esm.js.map +1 -1
  32. package/dist/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChartLegend.esm.js +1 -0
  33. package/dist/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChartLegend.esm.js.map +1 -1
  34. package/dist/index.d.ts +16 -1
  35. package/dist/index.esm.js +1 -0
  36. package/dist/index.esm.js.map +1 -1
  37. package/dist/testUtils/testUtils.esm.js +30 -11
  38. package/dist/testUtils/testUtils.esm.js.map +1 -1
  39. package/dist/types/Duration.esm.js +1 -0
  40. package/dist/types/Duration.esm.js.map +1 -1
  41. package/dist/utils/change.esm.js +20 -6
  42. package/dist/utils/change.esm.js.map +1 -1
  43. package/dist/utils/currency.esm.js +6 -1
  44. package/dist/utils/currency.esm.js.map +1 -1
  45. package/dist/utils/duration.esm.js +41 -5
  46. package/dist/utils/duration.esm.js.map +1 -1
  47. package/dist/utils/formatters.esm.js +25 -4
  48. package/dist/utils/formatters.esm.js.map +1 -1
  49. package/package.json +12 -10
  50. package/dist/components/PeriodSelect/PeriodSelect.esm.js +0 -72
  51. package/dist/components/PeriodSelect/PeriodSelect.esm.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectGrowthInstructionsPage.esm.js","sources":["../../../src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DateTime } from 'luxon';\nimport Box from '@material-ui/core/Box';\nimport Typography from '@material-ui/core/Typography';\nimport { AlertInstructionsLayout } from '../AlertInstructionsLayout';\nimport { ProductInsightsChart } from '../ProductInsightsCard';\nimport {\n Alert,\n DEFAULT_DATE_FORMAT,\n Duration,\n ProjectGrowthData,\n} from '../../types';\nimport {\n Entity,\n Product,\n} from '@backstage-community/plugin-cost-insights-common';\nimport { ProjectGrowthAlert } from '../../alerts';\nimport { InfoCard } from '@backstage/core-components';\n\nconst today = DateTime.now().toFormat(DEFAULT_DATE_FORMAT);\n\nexport const ProjectGrowthInstructionsPage = () => {\n const alertData: ProjectGrowthData = {\n project: 'example-project',\n periodStart: 'Q1 2020',\n periodEnd: 'Q2 2020',\n aggregation: [60000, 120000],\n change: {\n ratio: 1,\n amount: 60000,\n },\n products: [\n {\n id: 'Compute Engine',\n aggregation: [58000, 118000],\n },\n {\n id: 'Cloud Dataflow',\n aggregation: [1200, 1500],\n },\n {\n id: 'Cloud Storage',\n aggregation: [800, 500],\n },\n ],\n };\n\n const projectGrowthAlert: Alert = new ProjectGrowthAlert(alertData);\n\n const product: Product = {\n kind: 'ComputeEngine',\n name: 'Compute Engine',\n };\n\n const entity: Entity = {\n id: 'example-id',\n aggregation: [20_000, 60_000],\n change: {\n ratio: 3,\n amount: 40_000,\n },\n entities: {\n service: [\n {\n id: 'service-one',\n aggregation: [18_200, 58_500],\n entities: {},\n change: { ratio: 2.21, amount: 40_300 },\n },\n {\n id: 'service-two',\n aggregation: [1200, 1300],\n entities: {},\n change: { ratio: 0.083, amount: 100 },\n },\n {\n id: 'service-three',\n aggregation: [600, 200],\n entities: {},\n change: { ratio: -0.666, amount: -400 },\n },\n ],\n },\n };\n\n return (\n <AlertInstructionsLayout title=\"Investigating Growth\">\n <Typography variant=\"h1\">Investigating cloud cost growth</Typography>\n <Typography paragraph>\n Cost Insights shows an alert when costs for a particular billing entity,\n such as a GCP project, have grown at a rate faster than our alerting\n threshold. The responsible team should follow this guide to decide\n whether this warrants further investigation.\n </Typography>\n\n <Box mt={4}>\n <Typography variant=\"h3\">Is the growth expected?</Typography>\n <Typography paragraph>\n The first question to ask is whether growth is expected. Perhaps a new\n product has been deployed, or additional regions added for\n reliability.\n </Typography>\n <Typography paragraph>\n Many services increase cost linearly with load. Has the demand\n increased? This may happen as you open new markets, or run marketing\n offers. Costs should be compared against a business metric, such as\n daily users, to normalize natural increases from business growth.\n </Typography>\n <Typography paragraph>\n Seasonal variance may also cause cost growth; yearly campaigns, an\n increase in demand during certain times of year.\n </Typography>\n <Typography paragraph>\n Cloud costs will often go up before they go down, in the case of\n migrations. Teams moving to new infrastructure may run in both the old\n and new environment during the migration.\n </Typography>\n </Box>\n\n <Box mt={4}>\n <Typography variant=\"h3\">Is the growth significant?</Typography>\n <Typography paragraph>\n Next, evaluate whether the growth is significant. This helps avoid\n premature optimization, where cost in engineering time is more than\n would be saved from the optimization over a reasonable time frame.\n </Typography>\n <Typography paragraph>\n We recommend reframing the cost growth itself in terms of engineering\n time. How much engineering time, for an <i>average</i> fully-loaded\n engineer cost at the company, is being overspent each month? Compare\n this to expected engineering time for optimization to decide whether\n the optimization is worthwhile.\n </Typography>\n </Box>\n\n <Box mt={4}>\n <Typography variant=\"h3\">\n Identifying which cloud product contributed most\n </Typography>\n <Typography paragraph>\n For projects meeting the alert threshold, Cost Insights shows a cost\n comparison of cloud products over the examined time period:\n </Typography>\n <Box mt={2} mb={2}>\n {projectGrowthAlert.element}\n </Box>\n <Typography paragraph>\n This allows you to quickly see which cloud products contributed to the\n growth in cloud costs.\n </Typography>\n </Box>\n\n <Box mt={4}>\n <Typography variant=\"h3\">\n Identifying the responsible workload\n </Typography>\n <Typography paragraph>\n After identifying the cloud product, use the corresponding product\n panel in Cost Insights to find a particular workload (or <i>entity</i>\n ) that has grown in cost:\n </Typography>\n <Box mt={2} mb={2}>\n <InfoCard title={product.name} subheader=\"3 entities, sorted by cost\">\n <ProductInsightsChart\n billingDate={today}\n duration={Duration.P3M}\n entity={entity}\n />\n </InfoCard>\n </Box>\n <Typography paragraph>\n From here, you can dig into commit history or deployment logs to find\n probable causes of an unexpected spike in cost.\n </Typography>\n </Box>\n\n <Box mt={4}>\n <Typography variant=\"h3\">Optimizing the workload</Typography>\n <Typography paragraph>\n Workload optimization varies between cloud products, but there are a\n few general optimization areas to consider:\n </Typography>\n <Typography variant=\"h5\">Retention</Typography>\n <Typography paragraph>\n Is the workload or storage necessary? Truly idle or unused resources\n can be cleaned up for immediate cost savings. For storage, how long do\n we need the data? Many cloud products support retention policies to\n automatically delete data after a certain time period.\n </Typography>\n <Typography variant=\"h5\">Efficiency</Typography>\n <Typography paragraph>\n Is the workload using cloud resources efficiently? For compute\n resources, do the utilization metrics look reasonable? Autoscaling\n infrastructure, such as Kubernetes, can run workloads more efficiently\n without compromising reliability.\n </Typography>\n <Typography variant=\"h5\">Lifecycle</Typography>\n <Typography paragraph>\n Is the workload using an optimal pricing model? Some cloud products\n offer better pricing for data that is accessed less frequently.\n </Typography>\n </Box>\n </AlertInstructionsLayout>\n );\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAM,KAAQ,GAAA,QAAA,CAAS,GAAI,EAAA,CAAE,SAAS,mBAAmB,CAAA;AAElD,MAAM,gCAAgC,MAAM;AACjD,EAAA,MAAM,SAA+B,GAAA;AAAA,IACnC,OAAS,EAAA,iBAAA;AAAA,IACT,WAAa,EAAA,SAAA;AAAA,IACb,SAAW,EAAA,SAAA;AAAA,IACX,WAAA,EAAa,CAAC,GAAA,EAAO,IAAM,CAAA;AAAA,IAC3B,MAAQ,EAAA;AAAA,MACN,KAAO,EAAA,CAAA;AAAA,MACP,MAAQ,EAAA;AAAA,KACV;AAAA,IACA,QAAU,EAAA;AAAA,MACR;AAAA,QACE,EAAI,EAAA,gBAAA;AAAA,QACJ,WAAA,EAAa,CAAC,IAAA,EAAO,KAAM;AAAA,OAC7B;AAAA,MACA;AAAA,QACE,EAAI,EAAA,gBAAA;AAAA,QACJ,WAAA,EAAa,CAAC,IAAA,EAAM,IAAI;AAAA,OAC1B;AAAA,MACA;AAAA,QACE,EAAI,EAAA,eAAA;AAAA,QACJ,WAAA,EAAa,CAAC,GAAA,EAAK,GAAG;AAAA;AACxB;AACF,GACF;AAEA,EAAM,MAAA,kBAAA,GAA4B,IAAI,kBAAA,CAAmB,SAAS,CAAA;AAElE,EAAA,MAAM,OAAmB,GAAA;AAAA,IACvB,IAAM,EAAA,eAAA;AAAA,IACN,IAAM,EAAA;AAAA,GACR;AAEA,EAAA,MAAM,MAAiB,GAAA;AAAA,IACrB,EAAI,EAAA,YAAA;AAAA,IACJ,WAAA,EAAa,CAAC,GAAA,EAAQ,GAAM,CAAA;AAAA,IAC5B,MAAQ,EAAA;AAAA,MACN,KAAO,EAAA,CAAA;AAAA,MACP,MAAQ,EAAA;AAAA,KACV;AAAA,IACA,QAAU,EAAA;AAAA,MACR,OAAS,EAAA;AAAA,QACP;AAAA,UACE,EAAI,EAAA,aAAA;AAAA,UACJ,WAAA,EAAa,CAAC,KAAA,EAAQ,KAAM,CAAA;AAAA,UAC5B,UAAU,EAAC;AAAA,UACX,MAAQ,EAAA,EAAE,KAAO,EAAA,IAAA,EAAM,QAAQ,KAAO;AAAA,SACxC;AAAA,QACA;AAAA,UACE,EAAI,EAAA,aAAA;AAAA,UACJ,WAAA,EAAa,CAAC,IAAA,EAAM,IAAI,CAAA;AAAA,UACxB,UAAU,EAAC;AAAA,UACX,MAAQ,EAAA,EAAE,KAAO,EAAA,KAAA,EAAO,QAAQ,GAAI;AAAA,SACtC;AAAA,QACA;AAAA,UACE,EAAI,EAAA,eAAA;AAAA,UACJ,WAAA,EAAa,CAAC,GAAA,EAAK,GAAG,CAAA;AAAA,UACtB,UAAU,EAAC;AAAA,UACX,MAAQ,EAAA,EAAE,KAAO,EAAA,CAAA,KAAA,EAAQ,QAAQ,CAAK,GAAA;AAAA;AACxC;AACF;AACF,GACF;AAEA,EACE,uBAAA,IAAA,CAAC,uBAAwB,EAAA,EAAA,KAAA,EAAM,sBAC7B,EAAA,QAAA,EAAA;AAAA,oBAAC,GAAA,CAAA,UAAA,EAAA,EAAW,OAAQ,EAAA,IAAA,EAAK,QAA+B,EAAA,iCAAA,EAAA,CAAA;AAAA,oBACvD,GAAA,CAAA,UAAA,EAAA,EAAW,SAAS,EAAA,IAAA,EAAC,QAKtB,EAAA,+PAAA,EAAA,CAAA;AAAA,oBAEA,IAAA,CAAC,GAAI,EAAA,EAAA,EAAA,EAAI,CACP,EAAA,QAAA,EAAA;AAAA,sBAAC,GAAA,CAAA,UAAA,EAAA,EAAW,OAAQ,EAAA,IAAA,EAAK,QAAuB,EAAA,yBAAA,EAAA,CAAA;AAAA,sBAC/C,GAAA,CAAA,UAAA,EAAA,EAAW,SAAS,EAAA,IAAA,EAAC,QAItB,EAAA,gJAAA,EAAA,CAAA;AAAA,sBACC,GAAA,CAAA,UAAA,EAAA,EAAW,SAAS,EAAA,IAAA,EAAC,QAKtB,EAAA,2QAAA,EAAA,CAAA;AAAA,sBACC,GAAA,CAAA,UAAA,EAAA,EAAW,SAAS,EAAA,IAAA,EAAC,QAGtB,EAAA,qHAAA,EAAA,CAAA;AAAA,sBACC,GAAA,CAAA,UAAA,EAAA,EAAW,SAAS,EAAA,IAAA,EAAC,QAItB,EAAA,mLAAA,EAAA;AAAA,KACF,EAAA,CAAA;AAAA,oBAEA,IAAA,CAAC,GAAI,EAAA,EAAA,EAAA,EAAI,CACP,EAAA,QAAA,EAAA;AAAA,sBAAC,GAAA,CAAA,UAAA,EAAA,EAAW,OAAQ,EAAA,IAAA,EAAK,QAA0B,EAAA,4BAAA,EAAA,CAAA;AAAA,sBAClD,GAAA,CAAA,UAAA,EAAA,EAAW,SAAS,EAAA,IAAA,EAAC,QAItB,EAAA,2MAAA,EAAA,CAAA;AAAA,sBACA,IAAA,CAAC,UAAW,EAAA,EAAA,SAAA,EAAS,IAAC,EAAA,QAAA,EAAA;AAAA,QAAA,gHAAA;AAAA,wBAEoB,GAAA,CAAC,OAAE,QAAO,EAAA,SAAA,EAAA,CAAA;AAAA,QAAI;AAAA,OAIxD,EAAA;AAAA,KACF,EAAA,CAAA;AAAA,oBAEA,IAAA,CAAC,GAAI,EAAA,EAAA,EAAA,EAAI,CACP,EAAA,QAAA,EAAA;AAAA,sBAAC,GAAA,CAAA,UAAA,EAAA,EAAW,OAAQ,EAAA,IAAA,EAAK,QAEzB,EAAA,kDAAA,EAAA,CAAA;AAAA,sBACC,GAAA,CAAA,UAAA,EAAA,EAAW,SAAS,EAAA,IAAA,EAAC,QAGtB,EAAA,kIAAA,EAAA,CAAA;AAAA,0BACC,GAAI,EAAA,EAAA,EAAA,EAAI,GAAG,EAAI,EAAA,CAAA,EACb,6BAAmB,OACtB,EAAA,CAAA;AAAA,sBACC,GAAA,CAAA,UAAA,EAAA,EAAW,SAAS,EAAA,IAAA,EAAC,QAGtB,EAAA,+FAAA,EAAA;AAAA,KACF,EAAA,CAAA;AAAA,oBAEA,IAAA,CAAC,GAAI,EAAA,EAAA,EAAA,EAAI,CACP,EAAA,QAAA,EAAA;AAAA,sBAAC,GAAA,CAAA,UAAA,EAAA,EAAW,OAAQ,EAAA,IAAA,EAAK,QAEzB,EAAA,sCAAA,EAAA,CAAA;AAAA,sBACA,IAAA,CAAC,UAAW,EAAA,EAAA,SAAA,EAAS,IAAC,EAAA,QAAA,EAAA;AAAA,QAAA,8HAAA;AAAA,wBAEqC,GAAA,CAAC,OAAE,QAAM,EAAA,QAAA,EAAA,CAAA;AAAA,QAAI;AAAA,OAExE,EAAA,CAAA;AAAA,sBACC,GAAA,CAAA,GAAA,EAAA,EAAI,EAAI,EAAA,CAAA,EAAG,EAAI,EAAA,CAAA,EACd,QAAC,kBAAA,GAAA,CAAA,QAAA,EAAA,EAAS,KAAO,EAAA,OAAA,CAAQ,IAAM,EAAA,SAAA,EAAU,4BACvC,EAAA,QAAA,kBAAA,GAAA;AAAA,QAAC,oBAAA;AAAA,QAAA;AAAA,UACC,WAAa,EAAA,KAAA;AAAA,UACb,UAAU,QAAS,CAAA,GAAA;AAAA,UACnB;AAAA;AAAA,SAEJ,CACF,EAAA,CAAA;AAAA,sBACC,GAAA,CAAA,UAAA,EAAA,EAAW,SAAS,EAAA,IAAA,EAAC,QAGtB,EAAA,uHAAA,EAAA;AAAA,KACF,EAAA,CAAA;AAAA,oBAEA,IAAA,CAAC,GAAI,EAAA,EAAA,EAAA,EAAI,CACP,EAAA,QAAA,EAAA;AAAA,sBAAC,GAAA,CAAA,UAAA,EAAA,EAAW,OAAQ,EAAA,IAAA,EAAK,QAAuB,EAAA,yBAAA,EAAA,CAAA;AAAA,sBAC/C,GAAA,CAAA,UAAA,EAAA,EAAW,SAAS,EAAA,IAAA,EAAC,QAGtB,EAAA,kHAAA,EAAA,CAAA;AAAA,sBACC,GAAA,CAAA,UAAA,EAAA,EAAW,OAAQ,EAAA,IAAA,EAAK,QAAS,EAAA,WAAA,EAAA,CAAA;AAAA,sBACjC,GAAA,CAAA,UAAA,EAAA,EAAW,SAAS,EAAA,IAAA,EAAC,QAKtB,EAAA,wQAAA,EAAA,CAAA;AAAA,sBACC,GAAA,CAAA,UAAA,EAAA,EAAW,OAAQ,EAAA,IAAA,EAAK,QAAU,EAAA,YAAA,EAAA,CAAA;AAAA,sBAClC,GAAA,CAAA,UAAA,EAAA,EAAW,SAAS,EAAA,IAAA,EAAC,QAKtB,EAAA,4OAAA,EAAA,CAAA;AAAA,sBACC,GAAA,CAAA,UAAA,EAAA,EAAW,OAAQ,EAAA,IAAA,EAAK,QAAS,EAAA,WAAA,EAAA,CAAA;AAAA,sBACjC,GAAA,CAAA,UAAA,EAAA,EAAW,SAAS,EAAA,IAAA,EAAC,QAGtB,EAAA,qIAAA,EAAA;AAAA,KACF,EAAA;AAAA,GACF,EAAA,CAAA;AAEJ;;;;"}
1
+ {"version":3,"file":"ProjectGrowthInstructionsPage.esm.js","sources":["../../../src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DateTime } from 'luxon';\nimport Box from '@material-ui/core/Box';\nimport Typography from '@material-ui/core/Typography';\nimport { AlertInstructionsLayout } from '../AlertInstructionsLayout';\nimport { ProductInsightsChart } from '../ProductInsightsCard';\nimport {\n Alert,\n DEFAULT_DATE_FORMAT,\n Duration,\n ProjectGrowthData,\n} from '../../types';\nimport {\n Entity,\n Product,\n} from '@backstage-community/plugin-cost-insights-common';\nimport { ProjectGrowthAlert } from '../../alerts';\nimport { InfoCard } from '@backstage/core-components';\n\nconst today = DateTime.now().toFormat(DEFAULT_DATE_FORMAT);\n\nexport const ProjectGrowthInstructionsPage = () => {\n const alertData: ProjectGrowthData = {\n project: 'example-project',\n periodStart: 'Q1 2020',\n periodEnd: 'Q2 2020',\n aggregation: [60000, 120000],\n change: {\n ratio: 1,\n amount: 60000,\n },\n products: [\n {\n id: 'Compute Engine',\n aggregation: [58000, 118000],\n },\n {\n id: 'Cloud Dataflow',\n aggregation: [1200, 1500],\n },\n {\n id: 'Cloud Storage',\n aggregation: [800, 500],\n },\n ],\n };\n\n const projectGrowthAlert: Alert = new ProjectGrowthAlert(alertData);\n\n const product: Product = {\n kind: 'ComputeEngine',\n name: 'Compute Engine',\n };\n\n const entity: Entity = {\n id: 'example-id',\n aggregation: [20_000, 60_000],\n change: {\n ratio: 3,\n amount: 40_000,\n },\n entities: {\n service: [\n {\n id: 'service-one',\n aggregation: [18_200, 58_500],\n entities: {},\n change: { ratio: 2.21, amount: 40_300 },\n },\n {\n id: 'service-two',\n aggregation: [1200, 1300],\n entities: {},\n change: { ratio: 0.083, amount: 100 },\n },\n {\n id: 'service-three',\n aggregation: [600, 200],\n entities: {},\n change: { ratio: -0.666, amount: -400 },\n },\n ],\n },\n };\n\n return (\n <AlertInstructionsLayout title=\"Investigating Growth\">\n <Typography variant=\"h1\">Investigating cloud cost growth</Typography>\n <Typography paragraph>\n Cost Insights shows an alert when costs for a particular billing entity,\n such as a GCP project, have grown at a rate faster than our alerting\n threshold. The responsible team should follow this guide to decide\n whether this warrants further investigation.\n </Typography>\n\n <Box mt={4}>\n <Typography variant=\"h3\">Is the growth expected?</Typography>\n <Typography paragraph>\n The first question to ask is whether growth is expected. Perhaps a new\n product has been deployed, or additional regions added for\n reliability.\n </Typography>\n <Typography paragraph>\n Many services increase cost linearly with load. Has the demand\n increased? This may happen as you open new markets, or run marketing\n offers. Costs should be compared against a business metric, such as\n daily users, to normalize natural increases from business growth.\n </Typography>\n <Typography paragraph>\n Seasonal variance may also cause cost growth; yearly campaigns, an\n increase in demand during certain times of year.\n </Typography>\n <Typography paragraph>\n Cloud costs will often go up before they go down, in the case of\n migrations. Teams moving to new infrastructure may run in both the old\n and new environment during the migration.\n </Typography>\n </Box>\n\n <Box mt={4}>\n <Typography variant=\"h3\">Is the growth significant?</Typography>\n <Typography paragraph>\n Next, evaluate whether the growth is significant. This helps avoid\n premature optimization, where cost in engineering time is more than\n would be saved from the optimization over a reasonable time frame.\n </Typography>\n <Typography paragraph>\n We recommend reframing the cost growth itself in terms of engineering\n time. How much engineering time, for an <i>average</i> fully-loaded\n engineer cost at the company, is being overspent each month? Compare\n this to expected engineering time for optimization to decide whether\n the optimization is worthwhile.\n </Typography>\n </Box>\n\n <Box mt={4}>\n <Typography variant=\"h3\">\n Identifying which cloud product contributed most\n </Typography>\n <Typography paragraph>\n For projects meeting the alert threshold, Cost Insights shows a cost\n comparison of cloud products over the examined time period:\n </Typography>\n <Box mt={2} mb={2}>\n {projectGrowthAlert.element}\n </Box>\n <Typography paragraph>\n This allows you to quickly see which cloud products contributed to the\n growth in cloud costs.\n </Typography>\n </Box>\n\n <Box mt={4}>\n <Typography variant=\"h3\">\n Identifying the responsible workload\n </Typography>\n <Typography paragraph>\n After identifying the cloud product, use the corresponding product\n panel in Cost Insights to find a particular workload (or <i>entity</i>\n ) that has grown in cost:\n </Typography>\n <Box mt={2} mb={2}>\n <InfoCard title={product.name} subheader=\"3 entities, sorted by cost\">\n <ProductInsightsChart\n billingDate={today}\n duration={Duration.P3M}\n entity={entity}\n />\n </InfoCard>\n </Box>\n <Typography paragraph>\n From here, you can dig into commit history or deployment logs to find\n probable causes of an unexpected spike in cost.\n </Typography>\n </Box>\n\n <Box mt={4}>\n <Typography variant=\"h3\">Optimizing the workload</Typography>\n <Typography paragraph>\n Workload optimization varies between cloud products, but there are a\n few general optimization areas to consider:\n </Typography>\n <Typography variant=\"h5\">Retention</Typography>\n <Typography paragraph>\n Is the workload or storage necessary? Truly idle or unused resources\n can be cleaned up for immediate cost savings. For storage, how long do\n we need the data? Many cloud products support retention policies to\n automatically delete data after a certain time period.\n </Typography>\n <Typography variant=\"h5\">Efficiency</Typography>\n <Typography paragraph>\n Is the workload using cloud resources efficiently? For compute\n resources, do the utilization metrics look reasonable? Autoscaling\n infrastructure, such as Kubernetes, can run workloads more efficiently\n without compromising reliability.\n </Typography>\n <Typography variant=\"h5\">Lifecycle</Typography>\n <Typography paragraph>\n Is the workload using an optimal pricing model? Some cloud products\n offer better pricing for data that is accessed less frequently.\n </Typography>\n </Box>\n </AlertInstructionsLayout>\n );\n};\n"],"names":[],"mappings":"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"}
@@ -76,6 +76,7 @@ import '@material-ui/core/Tabs';
76
76
  import 'recharts';
77
77
  import '@material-ui/core/styles/colorManipulator';
78
78
  import '@material-ui/icons/Fullscreen';
79
+ import '../../../components/PeriodSelect/DateRangePicker.esm.js';
79
80
  import '@material-ui/core/CircularProgress';
80
81
  import '@backstage/core-components';
81
82
  import '@material-ui/core/Menu';
@@ -1 +1 @@
1
- {"version":3,"file":"KubernetesMigrationBarChart.esm.js","sources":["../../../../src/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChart.tsx"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { BarChart } from '../../../components';\nimport {\n BarChartOptions,\n CostInsightsTheme,\n ResourceData,\n} from '../../../types';\nimport { Entity } from '@backstage-community/plugin-cost-insights-common';\nimport { useTheme } from '@material-ui/core/styles';\n\ntype MigrationBarChartProps = {\n currentProduct: string;\n comparedProduct: string;\n services: Array<Entity>;\n};\n\nexport const KubernetesMigrationBarChart = ({\n currentProduct,\n comparedProduct,\n services,\n}: MigrationBarChartProps) => {\n const theme = useTheme<CostInsightsTheme>();\n\n const options: BarChartOptions = {\n previousFill: theme.palette.magenta,\n currentFill: theme.palette.yellow,\n previousName: comparedProduct,\n currentName: currentProduct,\n };\n\n const resources: ResourceData[] = services.map(service => ({\n name: service.id,\n previous: service.aggregation[0],\n current: service.aggregation[1],\n }));\n\n return <BarChart resources={resources} options={options} />;\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BO,MAAM,8BAA8B,CAAC;AAAA,EAC1C,cAAA;AAAA,EACA,eAAA;AAAA,EACA;AACF,CAA8B,KAAA;AAC5B,EAAA,MAAM,QAAQ,QAA4B,EAAA;AAE1C,EAAA,MAAM,OAA2B,GAAA;AAAA,IAC/B,YAAA,EAAc,MAAM,OAAQ,CAAA,OAAA;AAAA,IAC5B,WAAA,EAAa,MAAM,OAAQ,CAAA,MAAA;AAAA,IAC3B,YAAc,EAAA,eAAA;AAAA,IACd,WAAa,EAAA;AAAA,GACf;AAEA,EAAM,MAAA,SAAA,GAA4B,QAAS,CAAA,GAAA,CAAI,CAAY,OAAA,MAAA;AAAA,IACzD,MAAM,OAAQ,CAAA,EAAA;AAAA,IACd,QAAA,EAAU,OAAQ,CAAA,WAAA,CAAY,CAAC,CAAA;AAAA,IAC/B,OAAA,EAAS,OAAQ,CAAA,WAAA,CAAY,CAAC;AAAA,GAC9B,CAAA,CAAA;AAEF,EAAO,uBAAA,GAAA,CAAC,QAAS,EAAA,EAAA,SAAA,EAAsB,OAAkB,EAAA,CAAA;AAC3D;;;;"}
1
+ {"version":3,"file":"KubernetesMigrationBarChart.esm.js","sources":["../../../../src/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChart.tsx"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { BarChart } from '../../../components';\nimport {\n BarChartOptions,\n CostInsightsTheme,\n ResourceData,\n} from '../../../types';\nimport { Entity } from '@backstage-community/plugin-cost-insights-common';\nimport { useTheme } from '@material-ui/core/styles';\n\ntype MigrationBarChartProps = {\n currentProduct: string;\n comparedProduct: string;\n services: Array<Entity>;\n};\n\nexport const KubernetesMigrationBarChart = ({\n currentProduct,\n comparedProduct,\n services,\n}: MigrationBarChartProps) => {\n const theme = useTheme<CostInsightsTheme>();\n\n const options: BarChartOptions = {\n previousFill: theme.palette.magenta,\n currentFill: theme.palette.yellow,\n previousName: comparedProduct,\n currentName: currentProduct,\n };\n\n const resources: ResourceData[] = services.map(service => ({\n name: service.id,\n previous: service.aggregation[0],\n current: service.aggregation[1],\n }));\n\n return <BarChart resources={resources} options={options} />;\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BO,MAAM,8BAA8B,CAAC;AAAA,EAC1C,cAAA;AAAA,EACA,eAAA;AAAA,EACA;AACF,CAA8B,KAAA;AAC5B,EAAA,MAAM,QAAQ,QAA4B,EAAA;AAE1C,EAAA,MAAM,OAA2B,GAAA;AAAA,IAC/B,YAAA,EAAc,MAAM,OAAQ,CAAA,OAAA;AAAA,IAC5B,WAAA,EAAa,MAAM,OAAQ,CAAA,MAAA;AAAA,IAC3B,YAAc,EAAA,eAAA;AAAA,IACd,WAAa,EAAA;AAAA,GACf;AAEA,EAAM,MAAA,SAAA,GAA4B,QAAS,CAAA,GAAA,CAAI,CAAY,OAAA,MAAA;AAAA,IACzD,MAAM,OAAQ,CAAA,EAAA;AAAA,IACd,QAAA,EAAU,OAAQ,CAAA,WAAA,CAAY,CAAC,CAAA;AAAA,IAC/B,OAAA,EAAS,OAAQ,CAAA,WAAA,CAAY,CAAC;AAAA,GAC9B,CAAA,CAAA;AAEF,EAAO,uBAAA,GAAA,CAAC,QAAS,EAAA,EAAA,SAAA,EAAsB,OAAkB,EAAA,CAAA;AAC3D;;;;"}
@@ -82,6 +82,7 @@ import '@material-ui/core/Tab';
82
82
  import '@material-ui/core/Tabs';
83
83
  import '@material-ui/core/styles/colorManipulator';
84
84
  import '@material-ui/icons/Fullscreen';
85
+ import '../../../components/PeriodSelect/DateRangePicker.esm.js';
85
86
  import '@material-ui/core/CircularProgress';
86
87
  import '@backstage/core-components';
87
88
  import '@material-ui/core/Menu';
@@ -1 +1 @@
1
- {"version":3,"file":"KubernetesMigrationBarChartLegend.esm.js","sources":["../../../../src/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChartLegend.tsx"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport Box from '@material-ui/core/Box';\nimport { useTheme } from '@material-ui/core/styles';\nimport { CostGrowth, LegendItem } from '../../../components';\nimport { CostInsightsTheme, Duration } from '../../../types';\nimport { ChangeStatistic } from '@backstage-community/plugin-cost-insights-common';\nimport { monthOf } from '../../../utils/formatters';\n\nexport type MigrationBarChartLegendProps = {\n change: ChangeStatistic;\n startDate: string;\n currentProduct: string;\n comparedProduct: string;\n};\n\nexport const KubernetesMigrationBarChartLegend = ({\n currentProduct,\n comparedProduct,\n change,\n startDate,\n}: MigrationBarChartLegendProps) => {\n const theme = useTheme<CostInsightsTheme>();\n return (\n <Box display=\"flex\" flexDirection=\"row\">\n <Box marginRight={2}>\n <LegendItem\n title={monthOf(startDate)}\n markerColor={theme.palette.magenta}\n >\n {currentProduct}\n </LegendItem>\n </Box>\n <Box marginRight={2}>\n <LegendItem title=\"Estimated Cost\" markerColor={theme.palette.yellow}>\n {comparedProduct}\n </LegendItem>\n </Box>\n <LegendItem title=\"Total Savings\">\n <CostGrowth change={change} duration={Duration.P30D} />\n </LegendItem>\n </Box>\n );\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BO,MAAM,oCAAoC,CAAC;AAAA,EAChD,cAAA;AAAA,EACA,eAAA;AAAA,EACA,MAAA;AAAA,EACA;AACF,CAAoC,KAAA;AAClC,EAAA,MAAM,QAAQ,QAA4B,EAAA;AAC1C,EAAA,uBACG,IAAA,CAAA,GAAA,EAAA,EAAI,OAAQ,EAAA,MAAA,EAAO,eAAc,KAChC,EAAA,QAAA,EAAA;AAAA,oBAAC,GAAA,CAAA,GAAA,EAAA,EAAI,aAAa,CAChB,EAAA,QAAA,kBAAA,GAAA;AAAA,MAAC,UAAA;AAAA,MAAA;AAAA,QACC,KAAA,EAAO,QAAQ,SAAS,CAAA;AAAA,QACxB,WAAA,EAAa,MAAM,OAAQ,CAAA,OAAA;AAAA,QAE1B,QAAA,EAAA;AAAA;AAAA,KAEL,EAAA,CAAA;AAAA,oBACC,GAAA,CAAA,GAAA,EAAA,EAAI,WAAa,EAAA,CAAA,EAChB,QAAC,kBAAA,GAAA,CAAA,UAAA,EAAA,EAAW,KAAM,EAAA,gBAAA,EAAiB,WAAa,EAAA,KAAA,CAAM,OAAQ,CAAA,MAAA,EAC3D,2BACH,CACF,EAAA,CAAA;AAAA,oBACA,GAAA,CAAC,UAAW,EAAA,EAAA,KAAA,EAAM,eAChB,EAAA,QAAA,kBAAA,GAAA,CAAC,cAAW,MAAgB,EAAA,QAAA,EAAU,QAAS,CAAA,IAAA,EAAM,CACvD,EAAA;AAAA,GACF,EAAA,CAAA;AAEJ;;;;"}
1
+ {"version":3,"file":"KubernetesMigrationBarChartLegend.esm.js","sources":["../../../../src/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChartLegend.tsx"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport Box from '@material-ui/core/Box';\nimport { useTheme } from '@material-ui/core/styles';\nimport { CostGrowth, LegendItem } from '../../../components';\nimport { CostInsightsTheme, Duration } from '../../../types';\nimport { ChangeStatistic } from '@backstage-community/plugin-cost-insights-common';\nimport { monthOf } from '../../../utils/formatters';\n\nexport type MigrationBarChartLegendProps = {\n change: ChangeStatistic;\n startDate: string;\n currentProduct: string;\n comparedProduct: string;\n};\n\nexport const KubernetesMigrationBarChartLegend = ({\n currentProduct,\n comparedProduct,\n change,\n startDate,\n}: MigrationBarChartLegendProps) => {\n const theme = useTheme<CostInsightsTheme>();\n return (\n <Box display=\"flex\" flexDirection=\"row\">\n <Box marginRight={2}>\n <LegendItem\n title={monthOf(startDate)}\n markerColor={theme.palette.magenta}\n >\n {currentProduct}\n </LegendItem>\n </Box>\n <Box marginRight={2}>\n <LegendItem title=\"Estimated Cost\" markerColor={theme.palette.yellow}>\n {comparedProduct}\n </LegendItem>\n </Box>\n <LegendItem title=\"Total Savings\">\n <CostGrowth change={change} duration={Duration.P30D} />\n </LegendItem>\n </Box>\n );\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BO,MAAM,oCAAoC,CAAC;AAAA,EAChD,cAAA;AAAA,EACA,eAAA;AAAA,EACA,MAAA;AAAA,EACA;AACF,CAAoC,KAAA;AAClC,EAAA,MAAM,QAAQ,QAA4B,EAAA;AAC1C,EAAA,uBACG,IAAA,CAAA,GAAA,EAAA,EAAI,OAAQ,EAAA,MAAA,EAAO,eAAc,KAChC,EAAA,QAAA,EAAA;AAAA,oBAAC,GAAA,CAAA,GAAA,EAAA,EAAI,aAAa,CAChB,EAAA,QAAA,kBAAA,GAAA;AAAA,MAAC,UAAA;AAAA,MAAA;AAAA,QACC,KAAA,EAAO,QAAQ,SAAS,CAAA;AAAA,QACxB,WAAA,EAAa,MAAM,OAAQ,CAAA,OAAA;AAAA,QAE1B,QAAA,EAAA;AAAA;AAAA,KAEL,EAAA,CAAA;AAAA,oBACC,GAAA,CAAA,GAAA,EAAA,EAAI,WAAa,EAAA,CAAA,EAChB,QAAC,kBAAA,GAAA,CAAA,UAAA,EAAA,EAAW,KAAM,EAAA,gBAAA,EAAiB,WAAa,EAAA,KAAA,CAAM,OAAQ,CAAA,MAAA,EAC3D,2BACH,CACF,EAAA,CAAA;AAAA,oBACA,GAAA,CAAC,UAAW,EAAA,EAAA,KAAA,EAAM,eAChB,EAAA,QAAA,kBAAA,GAAA,CAAC,cAAW,MAAgB,EAAA,QAAA,EAAU,QAAS,CAAA,IAAA,EAAM,CACvD,EAAA;AAAA,GACF,EAAA,CAAA;AAEJ;;;;"}
package/dist/index.d.ts CHANGED
@@ -35,13 +35,16 @@ declare const CostInsightsLabelDataflowInstructionsPage: () => react_jsx_runtime
35
35
  * it's September 15, P3M represents costs for Q2 and P30D represents August 16 -
36
36
  * September 15.
37
37
  *
38
+ * CUSTOM is used when a user selects a custom date range via the date picker.
39
+ *
38
40
  * @public
39
41
  */
40
42
  declare enum Duration {
41
43
  P7D = "P7D",
42
44
  P30D = "P30D",
43
45
  P90D = "P90D",
44
- P3M = "P3M"
46
+ P3M = "P3M",
47
+ CUSTOM = "CUSTOM"
45
48
  }
46
49
 
47
50
  /**
@@ -245,6 +248,10 @@ interface PageFilters {
245
248
  project: Maybe$1<string>;
246
249
  duration: Duration;
247
250
  metric: string | null;
251
+ customDateRange?: {
252
+ start: string;
253
+ end: string;
254
+ };
248
255
  }
249
256
  /**
250
257
  * @public
@@ -256,6 +263,10 @@ type ProductFilters = Array<ProductPeriod>;
256
263
  interface ProductPeriod {
257
264
  duration: Duration;
258
265
  productType: string;
266
+ customDateRange?: {
267
+ start: string;
268
+ end: string;
269
+ };
259
270
  }
260
271
 
261
272
  /** @public */
@@ -570,6 +581,10 @@ declare const BarChartTooltipItem: (props: BarChartTooltipItemProps) => react_js
570
581
  type CostGrowthProps = {
571
582
  change: ChangeStatistic$1;
572
583
  duration: Duration;
584
+ customDateRange?: {
585
+ start: string;
586
+ end: string;
587
+ };
573
588
  };
574
589
  /** @public */
575
590
  declare const CostGrowth: (props: CostGrowthProps) => react_jsx_runtime.JSX.Element;
package/dist/index.esm.js CHANGED
@@ -83,6 +83,7 @@ import 'recharts';
83
83
  export { LegendItem } from './components/LegendItem/LegendItem.esm.js';
84
84
  import '@material-ui/core/styles/colorManipulator';
85
85
  import '@material-ui/icons/Fullscreen';
86
+ import './components/PeriodSelect/DateRangePicker.esm.js';
86
87
  import '@material-ui/core/CircularProgress';
87
88
  import '@backstage/core-components';
88
89
  import '@material-ui/core/Menu';
@@ -1 +1 @@
1
- {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -2,27 +2,48 @@ import { DateTime } from 'luxon';
2
2
  import regression from 'regression';
3
3
  import '../types/Alert.esm.js';
4
4
  import { DEFAULT_DATE_FORMAT } from '../types/DateFormat.esm.js';
5
+ import { Duration } from '../types/Duration.esm.js';
5
6
  import { inclusiveEndDateOf, inclusiveStartDateOf } from '../utils/duration.esm.js';
6
7
  import { MockEventsInsights, MockBigQueryInsights, MockCloudStorageInsights, MockCloudDataflowInsights, MockComputeEngineInsights } from './mockData.esm.js';
7
8
 
8
9
  function parseIntervals(intervals) {
9
10
  const match = intervals.match(
10
- /\/(?<duration>P\d+[DM])\/(?<date>\d{4}-\d{2}-\d{2})/
11
+ /R(?<repeats>\d+)\/P(?<days>\d+)[DM]\/(?<date>\d{4}-\d{2}-\d{2})/
11
12
  );
12
- if (Object.keys(match?.groups || {}).length !== 2) {
13
+ if (Object.keys(match?.groups || {}).length !== 3) {
13
14
  throw new Error(`Invalid intervals: ${intervals}`);
14
15
  }
15
- const { duration, date } = match.groups;
16
+ const { days, date } = match.groups;
17
+ const dayCount = parseInt(days, 10);
18
+ const endDate = date;
19
+ if (dayCount === 7) {
20
+ return { duration: Duration.P7D, endDate };
21
+ } else if (dayCount === 30) {
22
+ return { duration: Duration.P30D, endDate };
23
+ } else if (dayCount === 90) {
24
+ return { duration: Duration.P90D, endDate };
25
+ }
26
+ const endDateObj = DateTime.fromISO(endDate);
27
+ const startDate = endDateObj.minus({ days: dayCount }).toFormat(DEFAULT_DATE_FORMAT);
16
28
  return {
17
- duration,
18
- endDate: date
29
+ duration: Duration.CUSTOM,
30
+ endDate,
31
+ customDateRange: {
32
+ start: startDate,
33
+ end: endDateObj.minus({ days: 1 }).toFormat(DEFAULT_DATE_FORMAT)
34
+ }
19
35
  };
20
36
  }
21
37
  function aggregationFor(intervals, baseline) {
22
- const { duration, endDate } = parseIntervals(intervals);
23
- const inclusiveEndDate = inclusiveEndDateOf(duration, endDate);
38
+ const { duration, endDate, customDateRange } = parseIntervals(intervals);
39
+ const inclusiveEndDate = inclusiveEndDateOf(
40
+ duration,
41
+ endDate,
42
+ customDateRange
43
+ );
44
+ const startDate = duration === Duration.CUSTOM && customDateRange ? customDateRange.start : inclusiveStartDateOf(duration, inclusiveEndDate, customDateRange);
24
45
  const days = DateTime.fromISO(endDate).diff(
25
- DateTime.fromISO(inclusiveStartDateOf(duration, inclusiveEndDate)),
46
+ DateTime.fromISO(startDate),
26
47
  "days"
27
48
  ).days;
28
49
  function nextDelta() {
@@ -34,9 +55,7 @@ function aggregationFor(intervals, baseline) {
34
55
  return [...Array(days).keys()].reduce(
35
56
  (values, i) => {
36
57
  const last = values.length ? values[values.length - 1].amount : baseline;
37
- const date = DateTime.fromISO(
38
- inclusiveStartDateOf(duration, inclusiveEndDate)
39
- ).plus({ days: i }).toFormat(DEFAULT_DATE_FORMAT);
58
+ const date = DateTime.fromISO(startDate).plus({ days: i }).toFormat(DEFAULT_DATE_FORMAT);
40
59
  const amount = Math.max(0, last + nextDelta());
41
60
  values.push({
42
61
  date,
@@ -1 +1 @@
1
- {"version":3,"file":"testUtils.esm.js","sources":["../../src/testUtils/testUtils.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DateTime } from 'luxon';\nimport regression, { DataPoint } from 'regression';\nimport { Duration, DEFAULT_DATE_FORMAT } from '../types';\nimport {\n ChangeStatistic,\n Entity,\n Trendline,\n DateAggregation,\n} from '@backstage-community/plugin-cost-insights-common';\nimport { inclusiveEndDateOf, inclusiveStartDateOf } from '../utils/duration';\nimport {\n MockComputeEngineInsights,\n MockCloudDataflowInsights,\n MockCloudStorageInsights,\n MockBigQueryInsights,\n MockEventsInsights,\n} from './mockData';\n\ntype IntervalFields = {\n duration: Duration;\n endDate: string;\n};\n\nfunction parseIntervals(intervals: string): IntervalFields {\n const match = intervals.match(\n /\\/(?<duration>P\\d+[DM])\\/(?<date>\\d{4}-\\d{2}-\\d{2})/,\n );\n if (Object.keys(match?.groups || {}).length !== 2) {\n throw new Error(`Invalid intervals: ${intervals}`);\n }\n const { duration, date } = match!.groups!;\n return {\n duration: duration as Duration,\n endDate: date,\n };\n}\n\nexport function aggregationFor(\n intervals: string,\n baseline: number,\n): DateAggregation[] {\n const { duration, endDate } = parseIntervals(intervals);\n const inclusiveEndDate = inclusiveEndDateOf(duration, endDate);\n const days = DateTime.fromISO(endDate).diff(\n DateTime.fromISO(inclusiveStartDateOf(duration, inclusiveEndDate)),\n 'days',\n ).days;\n\n function nextDelta(): number {\n const varianceFromBaseline = 0.15;\n // Let's give positive vibes in trendlines - higher change for positive delta with >0.5 value\n const positiveTrendChance = 0.55;\n const normalization = positiveTrendChance - 1;\n return baseline * (Math.random() + normalization) * varianceFromBaseline;\n }\n\n return [...Array(days).keys()].reduce(\n (values: DateAggregation[], i: number): DateAggregation[] => {\n const last = values.length ? values[values.length - 1].amount : baseline;\n const date = DateTime.fromISO(\n inclusiveStartDateOf(duration, inclusiveEndDate),\n )\n .plus({ days: i })\n .toFormat(DEFAULT_DATE_FORMAT);\n const amount = Math.max(0, last + nextDelta());\n values.push({\n date: date,\n amount: amount,\n });\n return values;\n },\n [],\n );\n}\n\nexport function changeOf(aggregation: DateAggregation[]): ChangeStatistic {\n const firstAmount = aggregation.length ? aggregation[0].amount : 0;\n const lastAmount = aggregation.length\n ? aggregation[aggregation.length - 1].amount\n : 0;\n\n // if either the first or last amounts are zero, the rate of increase/decrease is infinite\n if (!firstAmount || !lastAmount) {\n return {\n amount: lastAmount - firstAmount,\n };\n }\n\n return {\n ratio: (lastAmount - firstAmount) / firstAmount,\n amount: lastAmount - firstAmount,\n };\n}\n\nexport function trendlineOf(aggregation: DateAggregation[]): Trendline {\n const data: ReadonlyArray<DataPoint> = aggregation.map(a => [\n Date.parse(a.date) / 1000,\n a.amount,\n ]);\n const result = regression.linear(data, { precision: 5 });\n return {\n slope: result.equation[0],\n intercept: result.equation[1],\n };\n}\n\nexport function entityOf(product: string): Entity {\n switch (product) {\n case 'computeEngine':\n return MockComputeEngineInsights;\n case 'cloudDataflow':\n return MockCloudDataflowInsights;\n case 'cloudStorage':\n return MockCloudStorageInsights;\n case 'bigQuery':\n return MockBigQueryInsights;\n case 'events':\n return MockEventsInsights;\n default:\n throw new Error(\n `Cannot get insights for ${product}. Make sure product matches product property in app-info.yaml`,\n );\n }\n}\n\nexport const getGroupedProducts = (intervals: string) => [\n {\n id: 'Cloud Dataflow',\n aggregation: aggregationFor(intervals, 1_700),\n },\n {\n id: 'Compute Engine',\n aggregation: aggregationFor(intervals, 350),\n },\n {\n id: 'Cloud Storage',\n aggregation: aggregationFor(intervals, 1_300),\n },\n {\n id: 'BigQuery',\n aggregation: aggregationFor(intervals, 2_000),\n },\n {\n id: 'Cloud SQL',\n aggregation: aggregationFor(intervals, 750),\n },\n {\n id: 'Cloud Spanner',\n aggregation: aggregationFor(intervals, 50),\n },\n {\n id: 'Cloud Pub/Sub',\n aggregation: aggregationFor(intervals, 1_000),\n },\n {\n id: 'Cloud Bigtable',\n aggregation: aggregationFor(intervals, 250),\n },\n];\n\nexport const getGroupedProjects = (intervals: string) => [\n {\n id: 'project-a',\n aggregation: aggregationFor(intervals, 1_700),\n },\n {\n id: 'project-b',\n aggregation: aggregationFor(intervals, 350),\n },\n {\n id: 'project-c',\n aggregation: aggregationFor(intervals, 1_300),\n },\n];\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"testUtils.esm.js","sources":["../../src/testUtils/testUtils.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DateTime } from 'luxon';\nimport regression, { DataPoint } from 'regression';\nimport { Duration, DEFAULT_DATE_FORMAT } from '../types';\nimport {\n ChangeStatistic,\n Entity,\n Trendline,\n DateAggregation,\n} from '@backstage-community/plugin-cost-insights-common';\nimport { inclusiveEndDateOf, inclusiveStartDateOf } from '../utils/duration';\nimport {\n MockComputeEngineInsights,\n MockCloudDataflowInsights,\n MockCloudStorageInsights,\n MockBigQueryInsights,\n MockEventsInsights,\n} from './mockData';\n\ntype IntervalFields = {\n duration: Duration;\n endDate: string;\n customDateRange?: { start: string; end: string };\n};\n\nfunction parseIntervals(intervals: string): IntervalFields {\n const match = intervals.match(\n /R(?<repeats>\\d+)\\/P(?<days>\\d+)[DM]\\/(?<date>\\d{4}-\\d{2}-\\d{2})/,\n );\n if (Object.keys(match?.groups || {}).length !== 3) {\n throw new Error(`Invalid intervals: ${intervals}`);\n }\n const { days, date } = match!.groups!;\n const dayCount = parseInt(days, 10);\n\n // Try to match standard durations\n const endDate = date;\n if (dayCount === 7) {\n return { duration: Duration.P7D, endDate };\n } else if (dayCount === 30) {\n return { duration: Duration.P30D, endDate };\n } else if (dayCount === 90) {\n return { duration: Duration.P90D, endDate };\n }\n\n // For custom durations (non-standard day counts), return CUSTOM with customDateRange\n const endDateObj = DateTime.fromISO(endDate);\n // endDate is exclusive, so subtract the full dayCount to get the inclusive start date\n const startDate = endDateObj\n .minus({ days: dayCount })\n .toFormat(DEFAULT_DATE_FORMAT);\n\n return {\n duration: Duration.CUSTOM,\n endDate,\n customDateRange: {\n start: startDate,\n end: endDateObj.minus({ days: 1 }).toFormat(DEFAULT_DATE_FORMAT),\n },\n };\n}\n\nexport function aggregationFor(\n intervals: string,\n baseline: number,\n): DateAggregation[] {\n const { duration, endDate, customDateRange } = parseIntervals(intervals);\n const inclusiveEndDate = inclusiveEndDateOf(\n duration,\n endDate,\n customDateRange,\n );\n\n // For custom ranges, use the actual selected range, not the comparison period\n const startDate =\n duration === Duration.CUSTOM && customDateRange\n ? customDateRange.start\n : inclusiveStartDateOf(duration, inclusiveEndDate, customDateRange);\n\n const days = DateTime.fromISO(endDate).diff(\n DateTime.fromISO(startDate),\n 'days',\n ).days;\n\n function nextDelta(): number {\n const varianceFromBaseline = 0.15;\n // Let's give positive vibes in trendlines - higher change for positive delta with >0.5 value\n const positiveTrendChance = 0.55;\n const normalization = positiveTrendChance - 1;\n return baseline * (Math.random() + normalization) * varianceFromBaseline;\n }\n\n return [...Array(days).keys()].reduce(\n (values: DateAggregation[], i: number): DateAggregation[] => {\n const last = values.length ? values[values.length - 1].amount : baseline;\n const date = DateTime.fromISO(startDate)\n .plus({ days: i })\n .toFormat(DEFAULT_DATE_FORMAT);\n const amount = Math.max(0, last + nextDelta());\n values.push({\n date: date,\n amount: amount,\n });\n return values;\n },\n [],\n );\n}\n\nexport function changeOf(aggregation: DateAggregation[]): ChangeStatistic {\n const firstAmount = aggregation.length ? aggregation[0].amount : 0;\n const lastAmount = aggregation.length\n ? aggregation[aggregation.length - 1].amount\n : 0;\n\n // if either the first or last amounts are zero, the rate of increase/decrease is infinite\n if (!firstAmount || !lastAmount) {\n return {\n amount: lastAmount - firstAmount,\n };\n }\n\n return {\n ratio: (lastAmount - firstAmount) / firstAmount,\n amount: lastAmount - firstAmount,\n };\n}\n\nexport function trendlineOf(aggregation: DateAggregation[]): Trendline {\n const data: ReadonlyArray<DataPoint> = aggregation.map(a => [\n Date.parse(a.date) / 1000,\n a.amount,\n ]);\n const result = regression.linear(data, { precision: 5 });\n return {\n slope: result.equation[0],\n intercept: result.equation[1],\n };\n}\n\nexport function entityOf(product: string): Entity {\n switch (product) {\n case 'computeEngine':\n return MockComputeEngineInsights;\n case 'cloudDataflow':\n return MockCloudDataflowInsights;\n case 'cloudStorage':\n return MockCloudStorageInsights;\n case 'bigQuery':\n return MockBigQueryInsights;\n case 'events':\n return MockEventsInsights;\n default:\n throw new Error(\n `Cannot get insights for ${product}. Make sure product matches product property in app-info.yaml`,\n );\n }\n}\n\nexport const getGroupedProducts = (intervals: string) => [\n {\n id: 'Cloud Dataflow',\n aggregation: aggregationFor(intervals, 1_700),\n },\n {\n id: 'Compute Engine',\n aggregation: aggregationFor(intervals, 350),\n },\n {\n id: 'Cloud Storage',\n aggregation: aggregationFor(intervals, 1_300),\n },\n {\n id: 'BigQuery',\n aggregation: aggregationFor(intervals, 2_000),\n },\n {\n id: 'Cloud SQL',\n aggregation: aggregationFor(intervals, 750),\n },\n {\n id: 'Cloud Spanner',\n aggregation: aggregationFor(intervals, 50),\n },\n {\n id: 'Cloud Pub/Sub',\n aggregation: aggregationFor(intervals, 1_000),\n },\n {\n id: 'Cloud Bigtable',\n aggregation: aggregationFor(intervals, 250),\n },\n];\n\nexport const getGroupedProjects = (intervals: string) => [\n {\n id: 'project-a',\n aggregation: aggregationFor(intervals, 1_700),\n },\n {\n id: 'project-b',\n aggregation: aggregationFor(intervals, 350),\n },\n {\n id: 'project-c',\n aggregation: aggregationFor(intervals, 1_300),\n },\n];\n"],"names":[],"mappings":"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"}
@@ -3,6 +3,7 @@ var Duration = /* @__PURE__ */ ((Duration2) => {
3
3
  Duration2["P30D"] = "P30D";
4
4
  Duration2["P90D"] = "P90D";
5
5
  Duration2["P3M"] = "P3M";
6
+ Duration2["CUSTOM"] = "CUSTOM";
6
7
  return Duration2;
7
8
  })(Duration || {});
8
9
 
@@ -1 +1 @@
1
- {"version":3,"file":"Duration.esm.js","sources":["../../src/types/Duration.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Time periods for cost comparison; slight abuse of ISO 8601 periods. We take P3M to mean\n * 'last completed quarter', and P30D/P90D to be '[month|quarter] relative to today'. So if\n * it's September 15, P3M represents costs for Q2 and P30D represents August 16 -\n * September 15.\n *\n * @public\n */\nexport enum Duration {\n P7D = 'P7D',\n P30D = 'P30D',\n P90D = 'P90D',\n P3M = 'P3M',\n}\n"],"names":["Duration"],"mappings":"AAwBY,IAAA,QAAA,qBAAAA,SAAL,KAAA;AACL,EAAAA,UAAA,KAAM,CAAA,GAAA,KAAA;AACN,EAAAA,UAAA,MAAO,CAAA,GAAA,MAAA;AACP,EAAAA,UAAA,MAAO,CAAA,GAAA,MAAA;AACP,EAAAA,UAAA,KAAM,CAAA,GAAA,KAAA;AAJI,EAAAA,OAAAA,SAAAA;AAAA,CAAA,EAAA,QAAA,IAAA,EAAA;;;;"}
1
+ {"version":3,"file":"Duration.esm.js","sources":["../../src/types/Duration.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Time periods for cost comparison; slight abuse of ISO 8601 periods. We take P3M to mean\n * 'last completed quarter', and P30D/P90D to be '[month|quarter] relative to today'. So if\n * it's September 15, P3M represents costs for Q2 and P30D represents August 16 -\n * September 15.\n *\n * CUSTOM is used when a user selects a custom date range via the date picker.\n *\n * @public\n */\nexport enum Duration {\n P7D = 'P7D',\n P30D = 'P30D',\n P90D = 'P90D',\n P3M = 'P3M',\n CUSTOM = 'CUSTOM',\n}\n"],"names":["Duration"],"mappings":"AA0BY,IAAA,QAAA,qBAAAA,SAAL,KAAA;AACL,EAAAA,UAAA,KAAM,CAAA,GAAA,KAAA;AACN,EAAAA,UAAA,MAAO,CAAA,GAAA,MAAA;AACP,EAAAA,UAAA,MAAO,CAAA,GAAA,MAAA;AACP,EAAAA,UAAA,KAAM,CAAA,GAAA,KAAA;AACN,EAAAA,UAAA,QAAS,CAAA,GAAA,QAAA;AALC,EAAAA,OAAAA,SAAAA;AAAA,CAAA,EAAA,QAAA,IAAA,EAAA;;;;"}
@@ -1,6 +1,7 @@
1
1
  import '../types/Alert.esm.js';
2
2
  import { ChangeThreshold, GrowthType } from '../types/ChangeStatistic.esm.js';
3
- import { Duration, DateTime } from 'luxon';
3
+ import { Duration } from '../types/Duration.esm.js';
4
+ import { Duration as Duration$1, DateTime } from 'luxon';
4
5
  import { inclusiveStartDateOf } from './duration.esm.js';
5
6
  import { notEmpty } from './assert.esm.js';
6
7
 
@@ -23,13 +24,14 @@ function growthOf(change, engineerThreshold) {
23
24
  }
24
25
  return GrowthType.Negligible;
25
26
  }
26
- function getComparedChange(dailyCost, metricData, duration, lastCompleteBillingDate) {
27
+ function getComparedChange(dailyCost, metricData, duration, lastCompleteBillingDate, customDateRange) {
27
28
  const dailyCostRatio = dailyCost.change?.ratio;
28
29
  const metricDataRatio = metricData.change?.ratio;
29
30
  const previousPeriodTotal = getPreviousPeriodTotalCost(
30
31
  dailyCost.aggregation,
31
32
  duration,
32
- lastCompleteBillingDate
33
+ lastCompleteBillingDate,
34
+ customDateRange
33
35
  );
34
36
  if (!notEmpty(dailyCostRatio) || !notEmpty(metricDataRatio)) {
35
37
  return {
@@ -41,9 +43,21 @@ function getComparedChange(dailyCost, metricData, duration, lastCompleteBillingD
41
43
  amount: previousPeriodTotal * (dailyCostRatio - metricDataRatio)
42
44
  };
43
45
  }
44
- function getPreviousPeriodTotalCost(aggregation, duration, inclusiveEndDate) {
45
- const luxonDuration = Duration.fromISO(duration);
46
- const startDate = inclusiveStartDateOf(duration, inclusiveEndDate);
46
+ function getPreviousPeriodTotalCost(aggregation, duration, inclusiveEndDate, customDateRange, comparisonMode = false) {
47
+ if (duration === Duration.CUSTOM && !comparisonMode) {
48
+ return 0;
49
+ }
50
+ const luxonDuration = duration === Duration.CUSTOM && customDateRange ? Duration$1.fromMillis(
51
+ DateTime.fromISO(customDateRange.end).diff(
52
+ DateTime.fromISO(customDateRange.start)
53
+ ).milliseconds
54
+ ) : Duration$1.fromISO(duration);
55
+ const startDate = inclusiveStartDateOf(
56
+ duration,
57
+ inclusiveEndDate,
58
+ customDateRange,
59
+ comparisonMode
60
+ );
47
61
  const nextPeriodStart = DateTime.fromISO(startDate).plus(luxonDuration);
48
62
  return aggregation.reduce((acc, costByDate) => {
49
63
  return DateTime.fromISO(costByDate.date) < nextPeriodStart ? acc + costByDate.amount : acc;
@@ -1 +1 @@
1
- {"version":3,"file":"change.esm.js","sources":["../../src/utils/change.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ChangeThreshold, GrowthType, Duration } from '../types';\nimport {\n Cost,\n ChangeStatistic,\n MetricData,\n DateAggregation,\n} from '@backstage-community/plugin-cost-insights-common';\nimport { DateTime, Duration as LuxonDuration } from 'luxon';\nimport { inclusiveStartDateOf } from './duration';\nimport { notEmpty } from './assert';\n\n// Used for displaying status colors\nexport function growthOf(\n change: ChangeStatistic,\n engineerThreshold: number,\n): GrowthType {\n const exceedsEngineerThreshold = Math.abs(change.amount) >= engineerThreshold;\n\n if (notEmpty(change.ratio)) {\n if (exceedsEngineerThreshold && change.ratio >= ChangeThreshold.upper) {\n return GrowthType.Excess;\n }\n\n if (exceedsEngineerThreshold && change.ratio <= ChangeThreshold.lower) {\n return GrowthType.Savings;\n }\n } else {\n if (exceedsEngineerThreshold && change.amount > 0) {\n return GrowthType.Excess;\n }\n if (exceedsEngineerThreshold && change.amount < 0) {\n return GrowthType.Savings;\n }\n }\n\n return GrowthType.Negligible;\n}\n\n// Used by <CostOverviewCard /> for displaying engineer totals\nexport function getComparedChange(\n dailyCost: Cost,\n metricData: MetricData,\n duration: Duration,\n lastCompleteBillingDate: string, // YYYY-MM-DD,\n): ChangeStatistic {\n const dailyCostRatio = dailyCost.change?.ratio;\n const metricDataRatio = metricData.change?.ratio;\n const previousPeriodTotal = getPreviousPeriodTotalCost(\n dailyCost.aggregation,\n duration,\n lastCompleteBillingDate,\n );\n\n // if either ratio cannot be calculated, no compared ratio can be calculated\n if (!notEmpty(dailyCostRatio) || !notEmpty(metricDataRatio)) {\n return {\n amount: previousPeriodTotal,\n };\n }\n\n return {\n ratio: dailyCostRatio - metricDataRatio,\n amount: previousPeriodTotal * (dailyCostRatio - metricDataRatio),\n };\n}\n\nexport function getPreviousPeriodTotalCost(\n aggregation: DateAggregation[],\n duration: Duration,\n inclusiveEndDate: string,\n): number {\n const luxonDuration = LuxonDuration.fromISO(duration);\n const startDate = inclusiveStartDateOf(duration, inclusiveEndDate);\n const nextPeriodStart = DateTime.fromISO(startDate).plus(luxonDuration);\n // Add up costs that incurred before the start of the next period.\n return aggregation.reduce((acc, costByDate) => {\n return DateTime.fromISO(costByDate.date) < nextPeriodStart\n ? acc + costByDate.amount\n : acc;\n }, 0);\n}\n\nexport function choose<T>(\n [savings, excess]: [T, T],\n change: ChangeStatistic,\n): T {\n const isSavings = (change.ratio ?? change.amount) <= 0;\n return isSavings ? savings : excess;\n}\n"],"names":["LuxonDuration"],"mappings":";;;;;;AA4BgB,SAAA,QAAA,CACd,QACA,iBACY,EAAA;AACZ,EAAA,MAAM,wBAA2B,GAAA,IAAA,CAAK,GAAI,CAAA,MAAA,CAAO,MAAM,CAAK,IAAA,iBAAA;AAE5D,EAAI,IAAA,QAAA,CAAS,MAAO,CAAA,KAAK,CAAG,EAAA;AAC1B,IAAA,IAAI,wBAA4B,IAAA,MAAA,CAAO,KAAS,IAAA,eAAA,CAAgB,KAAO,EAAA;AACrE,MAAA,OAAO,UAAW,CAAA,MAAA;AAAA;AAGpB,IAAA,IAAI,wBAA4B,IAAA,MAAA,CAAO,KAAS,IAAA,eAAA,CAAgB,KAAO,EAAA;AACrE,MAAA,OAAO,UAAW,CAAA,OAAA;AAAA;AACpB,GACK,MAAA;AACL,IAAI,IAAA,wBAAA,IAA4B,MAAO,CAAA,MAAA,GAAS,CAAG,EAAA;AACjD,MAAA,OAAO,UAAW,CAAA,MAAA;AAAA;AAEpB,IAAI,IAAA,wBAAA,IAA4B,MAAO,CAAA,MAAA,GAAS,CAAG,EAAA;AACjD,MAAA,OAAO,UAAW,CAAA,OAAA;AAAA;AACpB;AAGF,EAAA,OAAO,UAAW,CAAA,UAAA;AACpB;AAGO,SAAS,iBACd,CAAA,SAAA,EACA,UACA,EAAA,QAAA,EACA,uBACiB,EAAA;AACjB,EAAM,MAAA,cAAA,GAAiB,UAAU,MAAQ,EAAA,KAAA;AACzC,EAAM,MAAA,eAAA,GAAkB,WAAW,MAAQ,EAAA,KAAA;AAC3C,EAAA,MAAM,mBAAsB,GAAA,0BAAA;AAAA,IAC1B,SAAU,CAAA,WAAA;AAAA,IACV,QAAA;AAAA,IACA;AAAA,GACF;AAGA,EAAA,IAAI,CAAC,QAAS,CAAA,cAAc,KAAK,CAAC,QAAA,CAAS,eAAe,CAAG,EAAA;AAC3D,IAAO,OAAA;AAAA,MACL,MAAQ,EAAA;AAAA,KACV;AAAA;AAGF,EAAO,OAAA;AAAA,IACL,OAAO,cAAiB,GAAA,eAAA;AAAA,IACxB,MAAA,EAAQ,uBAAuB,cAAiB,GAAA,eAAA;AAAA,GAClD;AACF;AAEgB,SAAA,0BAAA,CACd,WACA,EAAA,QAAA,EACA,gBACQ,EAAA;AACR,EAAM,MAAA,aAAA,GAAgBA,QAAc,CAAA,OAAA,CAAQ,QAAQ,CAAA;AACpD,EAAM,MAAA,SAAA,GAAY,oBAAqB,CAAA,QAAA,EAAU,gBAAgB,CAAA;AACjE,EAAA,MAAM,kBAAkB,QAAS,CAAA,OAAA,CAAQ,SAAS,CAAA,CAAE,KAAK,aAAa,CAAA;AAEtE,EAAA,OAAO,WAAY,CAAA,MAAA,CAAO,CAAC,GAAA,EAAK,UAAe,KAAA;AAC7C,IAAO,OAAA,QAAA,CAAS,QAAQ,UAAW,CAAA,IAAI,IAAI,eACvC,GAAA,GAAA,GAAM,WAAW,MACjB,GAAA,GAAA;AAAA,KACH,CAAC,CAAA;AACN;AAEO,SAAS,MACd,CAAA,CAAC,OAAS,EAAA,MAAM,GAChB,MACG,EAAA;AACH,EAAA,MAAM,SAAa,GAAA,CAAA,MAAA,CAAO,KAAS,IAAA,MAAA,CAAO,MAAW,KAAA,CAAA;AACrD,EAAA,OAAO,YAAY,OAAU,GAAA,MAAA;AAC/B;;;;"}
1
+ {"version":3,"file":"change.esm.js","sources":["../../src/utils/change.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ChangeThreshold, GrowthType, Duration } from '../types';\nimport {\n Cost,\n ChangeStatistic,\n MetricData,\n DateAggregation,\n} from '@backstage-community/plugin-cost-insights-common';\nimport { DateTime, Duration as LuxonDuration } from 'luxon';\nimport { inclusiveStartDateOf } from './duration';\nimport { notEmpty } from './assert';\n\n// Used for displaying status colors\nexport function growthOf(\n change: ChangeStatistic,\n engineerThreshold: number,\n): GrowthType {\n const exceedsEngineerThreshold = Math.abs(change.amount) >= engineerThreshold;\n\n if (notEmpty(change.ratio)) {\n if (exceedsEngineerThreshold && change.ratio >= ChangeThreshold.upper) {\n return GrowthType.Excess;\n }\n\n if (exceedsEngineerThreshold && change.ratio <= ChangeThreshold.lower) {\n return GrowthType.Savings;\n }\n } else {\n if (exceedsEngineerThreshold && change.amount > 0) {\n return GrowthType.Excess;\n }\n if (exceedsEngineerThreshold && change.amount < 0) {\n return GrowthType.Savings;\n }\n }\n\n return GrowthType.Negligible;\n}\n\n// Used by <CostOverviewCard /> for displaying engineer totals\nexport function getComparedChange(\n dailyCost: Cost,\n metricData: MetricData,\n duration: Duration,\n lastCompleteBillingDate: string, // YYYY-MM-DD,\n customDateRange?: { start: string; end: string },\n): ChangeStatistic {\n const dailyCostRatio = dailyCost.change?.ratio;\n const metricDataRatio = metricData.change?.ratio;\n const previousPeriodTotal = getPreviousPeriodTotalCost(\n dailyCost.aggregation,\n duration,\n lastCompleteBillingDate,\n customDateRange,\n );\n\n // if either ratio cannot be calculated, no compared ratio can be calculated\n if (!notEmpty(dailyCostRatio) || !notEmpty(metricDataRatio)) {\n return {\n amount: previousPeriodTotal,\n };\n }\n\n return {\n ratio: dailyCostRatio - metricDataRatio,\n amount: previousPeriodTotal * (dailyCostRatio - metricDataRatio),\n };\n}\n\nexport function getPreviousPeriodTotalCost(\n aggregation: DateAggregation[],\n duration: Duration,\n inclusiveEndDate: string,\n customDateRange?: { start: string; end: string },\n comparisonMode: boolean = false,\n): number {\n // For custom date ranges in non-comparison mode, we don't show a previous period\n if (duration === Duration.CUSTOM && !comparisonMode) {\n return 0;\n }\n\n const luxonDuration =\n duration === Duration.CUSTOM && customDateRange\n ? LuxonDuration.fromMillis(\n DateTime.fromISO(customDateRange.end).diff(\n DateTime.fromISO(customDateRange.start),\n ).milliseconds,\n )\n : LuxonDuration.fromISO(duration);\n const startDate = inclusiveStartDateOf(\n duration,\n inclusiveEndDate,\n customDateRange,\n comparisonMode,\n );\n const nextPeriodStart = DateTime.fromISO(startDate).plus(luxonDuration);\n // Add up costs that incurred before the start of the next period.\n return aggregation.reduce((acc, costByDate) => {\n return DateTime.fromISO(costByDate.date) < nextPeriodStart\n ? acc + costByDate.amount\n : acc;\n }, 0);\n}\n\nexport function choose<T>(\n [savings, excess]: [T, T],\n change: ChangeStatistic,\n): T {\n const isSavings = (change.ratio ?? change.amount) <= 0;\n return isSavings ? savings : excess;\n}\n"],"names":["LuxonDuration"],"mappings":"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"}
@@ -3,7 +3,7 @@ import { CurrencyType } from '../types/CurrencyType.esm.js';
3
3
  import { Duration } from '../types/Duration.esm.js';
4
4
  import { assertNever } from './assert.esm.js';
5
5
 
6
- const rateOf = (cost, duration) => {
6
+ const rateOf = (cost, duration, customDays) => {
7
7
  switch (duration) {
8
8
  case Duration.P30D:
9
9
  return cost / 12;
@@ -11,6 +11,11 @@ const rateOf = (cost, duration) => {
11
11
  case Duration.P90D:
12
12
  case Duration.P3M:
13
13
  return cost / 4;
14
+ case Duration.CUSTOM:
15
+ if (customDays) {
16
+ return cost / 365 * customDays;
17
+ }
18
+ return cost;
14
19
  default:
15
20
  return assertNever(duration);
16
21
  }
@@ -1 +1 @@
1
- {"version":3,"file":"currency.esm.js","sources":["../../src/utils/currency.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { Currency, CurrencyType, Duration } from '../types';\nimport { assertNever } from './assert';\n\nexport const rateOf = (cost: number, duration: Duration) => {\n switch (duration) {\n case Duration.P30D:\n return cost / 12;\n case Duration.P7D:\n case Duration.P90D:\n case Duration.P3M:\n return cost / 4;\n default:\n return assertNever(duration);\n }\n};\n\nexport const defaultCurrencies: Currency[] = [\n {\n kind: null,\n label: 'Engineers 🛠',\n unit: 'engineer',\n },\n {\n kind: CurrencyType.USD,\n label: 'US Dollars 💵',\n unit: 'dollar',\n prefix: '$',\n rate: 1,\n },\n {\n kind: CurrencyType.CarbonOffsetTons,\n label: 'Carbon Offset Tons ♻️⚖️s',\n unit: 'carbon offset ton',\n rate: 3.5,\n },\n {\n kind: CurrencyType.Beers,\n label: 'Beers 🍺',\n unit: 'beer',\n rate: 4.5,\n },\n {\n kind: CurrencyType.IceCream,\n label: 'Pints of Ice Cream 🍦',\n unit: 'ice cream pint',\n rate: 5.5,\n },\n];\n\nexport const createCurrencyFormat = (\n currency: string = 'USD',\n locale: string = 'en-US',\n) =>\n new Intl.NumberFormat(locale, {\n currency,\n style: 'currency',\n });\n"],"names":[],"mappings":";;;;;AAkBa,MAAA,MAAA,GAAS,CAAC,IAAA,EAAc,QAAuB,KAAA;AAC1D,EAAA,QAAQ,QAAU;AAAA,IAChB,KAAK,QAAS,CAAA,IAAA;AACZ,MAAA,OAAO,IAAO,GAAA,EAAA;AAAA,IAChB,KAAK,QAAS,CAAA,GAAA;AAAA,IACd,KAAK,QAAS,CAAA,IAAA;AAAA,IACd,KAAK,QAAS,CAAA,GAAA;AACZ,MAAA,OAAO,IAAO,GAAA,CAAA;AAAA,IAChB;AACE,MAAA,OAAO,YAAY,QAAQ,CAAA;AAAA;AAEjC;AAEO,MAAM,iBAAgC,GAAA;AAAA,EAC3C;AAAA,IACE,IAAM,EAAA,IAAA;AAAA,IACN,KAAO,EAAA,qBAAA;AAAA,IACP,IAAM,EAAA;AAAA,GACR;AAAA,EACA;AAAA,IACE,MAAM,YAAa,CAAA,GAAA;AAAA,IACnB,KAAO,EAAA,sBAAA;AAAA,IACP,IAAM,EAAA,QAAA;AAAA,IACN,MAAQ,EAAA,GAAA;AAAA,IACR,IAAM,EAAA;AAAA,GACR;AAAA,EACA;AAAA,IACE,MAAM,YAAa,CAAA,gBAAA;AAAA,IACnB,KAAO,EAAA,8CAAA;AAAA,IACP,IAAM,EAAA,mBAAA;AAAA,IACN,IAAM,EAAA;AAAA,GACR;AAAA,EACA;AAAA,IACE,MAAM,YAAa,CAAA,KAAA;AAAA,IACnB,KAAO,EAAA,iBAAA;AAAA,IACP,IAAM,EAAA,MAAA;AAAA,IACN,IAAM,EAAA;AAAA,GACR;AAAA,EACA;AAAA,IACE,MAAM,YAAa,CAAA,QAAA;AAAA,IACnB,KAAO,EAAA,8BAAA;AAAA,IACP,IAAM,EAAA,gBAAA;AAAA,IACN,IAAM,EAAA;AAAA;AAEV;AAEa,MAAA,oBAAA,GAAuB,CAClC,QAAmB,GAAA,KAAA,EACnB,SAAiB,OAEjB,KAAA,IAAI,IAAK,CAAA,YAAA,CAAa,MAAQ,EAAA;AAAA,EAC5B,QAAA;AAAA,EACA,KAAO,EAAA;AACT,CAAC;;;;"}
1
+ {"version":3,"file":"currency.esm.js","sources":["../../src/utils/currency.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { Currency, CurrencyType, Duration } from '../types';\nimport { assertNever } from './assert';\n\nexport const rateOf = (\n cost: number,\n duration: Duration,\n customDays?: number,\n) => {\n switch (duration) {\n case Duration.P30D:\n return cost / 12;\n case Duration.P7D:\n case Duration.P90D:\n case Duration.P3M:\n return cost / 4;\n case Duration.CUSTOM:\n // For custom durations, calculate rate based on number of days\n // Assuming cost is annual, calculate proportional rate\n if (customDays) {\n // Annual cost divided by days in year, multiplied by custom period days\n return (cost / 365) * customDays;\n }\n // Fallback: return the cost as-is (rate of 1)\n return cost;\n default:\n return assertNever(duration);\n }\n};\n\nexport const defaultCurrencies: Currency[] = [\n {\n kind: null,\n label: 'Engineers 🛠',\n unit: 'engineer',\n },\n {\n kind: CurrencyType.USD,\n label: 'US Dollars 💵',\n unit: 'dollar',\n prefix: '$',\n rate: 1,\n },\n {\n kind: CurrencyType.CarbonOffsetTons,\n label: 'Carbon Offset Tons ♻️⚖️s',\n unit: 'carbon offset ton',\n rate: 3.5,\n },\n {\n kind: CurrencyType.Beers,\n label: 'Beers 🍺',\n unit: 'beer',\n rate: 4.5,\n },\n {\n kind: CurrencyType.IceCream,\n label: 'Pints of Ice Cream 🍦',\n unit: 'ice cream pint',\n rate: 5.5,\n },\n];\n\nexport const createCurrencyFormat = (\n currency: string = 'USD',\n locale: string = 'en-US',\n) =>\n new Intl.NumberFormat(locale, {\n currency,\n style: 'currency',\n });\n"],"names":[],"mappings":";;;;;AAkBO,MAAM,MAAS,GAAA,CACpB,IACA,EAAA,QAAA,EACA,UACG,KAAA;AACH,EAAA,QAAQ,QAAU;AAAA,IAChB,KAAK,QAAS,CAAA,IAAA;AACZ,MAAA,OAAO,IAAO,GAAA,EAAA;AAAA,IAChB,KAAK,QAAS,CAAA,GAAA;AAAA,IACd,KAAK,QAAS,CAAA,IAAA;AAAA,IACd,KAAK,QAAS,CAAA,GAAA;AACZ,MAAA,OAAO,IAAO,GAAA,CAAA;AAAA,IAChB,KAAK,QAAS,CAAA,MAAA;AAGZ,MAAA,IAAI,UAAY,EAAA;AAEd,QAAA,OAAQ,OAAO,GAAO,GAAA,UAAA;AAAA;AAGxB,MAAO,OAAA,IAAA;AAAA,IACT;AACE,MAAA,OAAO,YAAY,QAAQ,CAAA;AAAA;AAEjC;AAEO,MAAM,iBAAgC,GAAA;AAAA,EAC3C;AAAA,IACE,IAAM,EAAA,IAAA;AAAA,IACN,KAAO,EAAA,qBAAA;AAAA,IACP,IAAM,EAAA;AAAA,GACR;AAAA,EACA;AAAA,IACE,MAAM,YAAa,CAAA,GAAA;AAAA,IACnB,KAAO,EAAA,sBAAA;AAAA,IACP,IAAM,EAAA,QAAA;AAAA,IACN,MAAQ,EAAA,GAAA;AAAA,IACR,IAAM,EAAA;AAAA,GACR;AAAA,EACA;AAAA,IACE,MAAM,YAAa,CAAA,gBAAA;AAAA,IACnB,KAAO,EAAA,8CAAA;AAAA,IACP,IAAM,EAAA,mBAAA;AAAA,IACN,IAAM,EAAA;AAAA,GACR;AAAA,EACA;AAAA,IACE,MAAM,YAAa,CAAA,KAAA;AAAA,IACnB,KAAO,EAAA,iBAAA;AAAA,IACP,IAAM,EAAA,MAAA;AAAA,IACN,IAAM,EAAA;AAAA,GACR;AAAA,EACA;AAAA,IACE,MAAM,YAAa,CAAA,QAAA;AAAA,IACnB,KAAO,EAAA,8BAAA;AAAA,IACP,IAAM,EAAA,gBAAA;AAAA,IACN,IAAM,EAAA;AAAA;AAEV;AAEa,MAAA,oBAAA,GAAuB,CAClC,QAAmB,GAAA,KAAA,EACnB,SAAiB,OAEjB,KAAA,IAAI,IAAK,CAAA,YAAA,CAAa,MAAQ,EAAA;AAAA,EAC5B,QAAA;AAAA,EACA,KAAO,EAAA;AACT,CAAC;;;;"}
@@ -5,7 +5,16 @@ import { Duration } from '../types/Duration.esm.js';
5
5
  import { assertNever } from './assert.esm.js';
6
6
 
7
7
  const DEFAULT_DURATION = Duration.P30D;
8
- function inclusiveStartDateOf(duration, inclusiveEndDate) {
8
+ function inclusiveStartDateOf(duration, inclusiveEndDate, customDateRange, comparisonMode = false) {
9
+ if (duration === Duration.CUSTOM && customDateRange) {
10
+ if (comparisonMode) {
11
+ const startDate = DateTime.fromISO(customDateRange.start);
12
+ const endDate = DateTime.fromISO(customDateRange.end);
13
+ const daysDiff = Math.round(endDate.diff(startDate, "days").days) + 1;
14
+ return startDate.minus({ days: daysDiff }).toFormat(DEFAULT_DATE_FORMAT);
15
+ }
16
+ return customDateRange.start;
17
+ }
9
18
  switch (duration) {
10
19
  case Duration.P7D:
11
20
  case Duration.P30D:
@@ -17,11 +26,16 @@ function inclusiveStartDateOf(duration, inclusiveEndDate) {
17
26
  return DateTime.fromISO(inclusiveEndDate).startOf("quarter").minus(
18
27
  Duration$1.fromISO(duration).plus(Duration$1.fromISO(duration))
19
28
  ).toFormat(DEFAULT_DATE_FORMAT);
29
+ case Duration.CUSTOM:
30
+ throw new Error("CUSTOM duration requires customDateRange parameter");
20
31
  default:
21
32
  return assertNever(duration);
22
33
  }
23
34
  }
24
- function exclusiveEndDateOf(duration, inclusiveEndDate) {
35
+ function exclusiveEndDateOf(duration, inclusiveEndDate, customDateRange) {
36
+ if (duration === Duration.CUSTOM && customDateRange) {
37
+ return DateTime.fromISO(customDateRange.end).plus({ days: 1 }).toFormat(DEFAULT_DATE_FORMAT);
38
+ }
25
39
  switch (duration) {
26
40
  case Duration.P7D:
27
41
  case Duration.P30D:
@@ -29,14 +43,36 @@ function exclusiveEndDateOf(duration, inclusiveEndDate) {
29
43
  return DateTime.fromISO(inclusiveEndDate).plus({ days: 1 }).toFormat(DEFAULT_DATE_FORMAT);
30
44
  case Duration.P3M:
31
45
  return DateTime.fromISO(quarterEndDate(inclusiveEndDate)).plus({ days: 1 }).toFormat(DEFAULT_DATE_FORMAT);
46
+ case Duration.CUSTOM:
47
+ throw new Error("CUSTOM duration requires customDateRange parameter");
32
48
  default:
33
49
  return assertNever(duration);
34
50
  }
35
51
  }
36
- function inclusiveEndDateOf(duration, inclusiveEndDate) {
37
- return DateTime.fromISO(exclusiveEndDateOf(duration, inclusiveEndDate)).minus({ days: 1 }).toFormat(DEFAULT_DATE_FORMAT);
52
+ function inclusiveEndDateOf(duration, inclusiveEndDate, customDateRange) {
53
+ return DateTime.fromISO(
54
+ exclusiveEndDateOf(duration, inclusiveEndDate, customDateRange)
55
+ ).minus({ days: 1 }).toFormat(DEFAULT_DATE_FORMAT);
38
56
  }
39
- function intervalsOf(duration, inclusiveEndDate, repeating = 2) {
57
+ function intervalsOf(duration, inclusiveEndDate, repeating = 2, customDateRange, comparisonMode = false) {
58
+ if (duration === Duration.CUSTOM && customDateRange) {
59
+ const startDate = DateTime.fromISO(customDateRange.start);
60
+ const endDate = DateTime.fromISO(customDateRange.end);
61
+ const totalDays = Math.round(endDate.diff(startDate, "days").days) + 1;
62
+ if (comparisonMode) {
63
+ const intervalDays = Math.floor(totalDays / 2);
64
+ return `R${repeating}/P${intervalDays}D/${exclusiveEndDateOf(
65
+ duration,
66
+ inclusiveEndDate,
67
+ customDateRange
68
+ )}`;
69
+ }
70
+ return `R${repeating}/P${totalDays}D/${exclusiveEndDateOf(
71
+ duration,
72
+ inclusiveEndDate,
73
+ customDateRange
74
+ )}`;
75
+ }
40
76
  return `R${repeating}/${duration}/${exclusiveEndDateOf(
41
77
  duration,
42
78
  inclusiveEndDate
@@ -1 +1 @@
1
- {"version":3,"file":"duration.esm.js","sources":["../../src/utils/duration.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DateTime, Duration as LuxonDuration } from 'luxon';\nimport { Duration, DEFAULT_DATE_FORMAT } from '../types';\nimport { assertNever } from './assert';\n\nexport const DEFAULT_DURATION = Duration.P30D;\n\n/**\n * Derive the start date of a given period, assuming two repeating intervals.\n *\n * @param duration - see comment on Duration enum\n * @param inclusiveEndDate - from CostInsightsApi.getLastCompleteBillingDate\n */\nexport function inclusiveStartDateOf(\n duration: Duration,\n inclusiveEndDate: string,\n): string {\n switch (duration) {\n case Duration.P7D:\n case Duration.P30D:\n case Duration.P90D:\n return DateTime.fromISO(inclusiveEndDate)\n .minus(\n LuxonDuration.fromISO(duration).plus(LuxonDuration.fromISO(duration)),\n )\n .toFormat(DEFAULT_DATE_FORMAT);\n case Duration.P3M:\n return DateTime.fromISO(inclusiveEndDate)\n .startOf('quarter')\n .minus(\n LuxonDuration.fromISO(duration).plus(LuxonDuration.fromISO(duration)),\n )\n .toFormat(DEFAULT_DATE_FORMAT);\n default:\n return assertNever(duration);\n }\n}\n\nexport function exclusiveEndDateOf(\n duration: Duration,\n inclusiveEndDate: string,\n): string {\n switch (duration) {\n case Duration.P7D:\n case Duration.P30D:\n case Duration.P90D:\n return DateTime.fromISO(inclusiveEndDate)\n .plus({ days: 1 })\n .toFormat(DEFAULT_DATE_FORMAT);\n case Duration.P3M:\n return DateTime.fromISO(quarterEndDate(inclusiveEndDate))\n .plus({ days: 1 })\n .toFormat(DEFAULT_DATE_FORMAT);\n default:\n return assertNever(duration);\n }\n}\n\nexport function inclusiveEndDateOf(\n duration: Duration,\n inclusiveEndDate: string,\n): string {\n return DateTime.fromISO(exclusiveEndDateOf(duration, inclusiveEndDate))\n .minus({ days: 1 })\n .toFormat(DEFAULT_DATE_FORMAT);\n}\n\n// https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals\nexport function intervalsOf(\n duration: Duration,\n inclusiveEndDate: string,\n repeating: number = 2,\n) {\n return `R${repeating}/${duration}/${exclusiveEndDateOf(\n duration,\n inclusiveEndDate,\n )}`;\n}\n\nexport function quarterEndDate(inclusiveEndDate: string): string {\n const endDate = DateTime.fromISO(inclusiveEndDate);\n const endOfQuarter = endDate.endOf('quarter').toFormat(DEFAULT_DATE_FORMAT);\n if (endOfQuarter === inclusiveEndDate) {\n return endDate.toFormat(DEFAULT_DATE_FORMAT);\n }\n return endDate\n .startOf('quarter')\n .minus({ days: 1 })\n .toFormat(DEFAULT_DATE_FORMAT);\n}\n"],"names":["LuxonDuration"],"mappings":";;;;;;AAoBO,MAAM,mBAAmB,QAAS,CAAA;AAQzB,SAAA,oBAAA,CACd,UACA,gBACQ,EAAA;AACR,EAAA,QAAQ,QAAU;AAAA,IAChB,KAAK,QAAS,CAAA,GAAA;AAAA,IACd,KAAK,QAAS,CAAA,IAAA;AAAA,IACd,KAAK,QAAS,CAAA,IAAA;AACZ,MAAO,OAAA,QAAA,CAAS,OAAQ,CAAA,gBAAgB,CACrC,CAAA,KAAA;AAAA,QACCA,UAAA,CAAc,QAAQ,QAAQ,CAAA,CAAE,KAAKA,UAAc,CAAA,OAAA,CAAQ,QAAQ,CAAC;AAAA,OACtE,CACC,SAAS,mBAAmB,CAAA;AAAA,IACjC,KAAK,QAAS,CAAA,GAAA;AACZ,MAAA,OAAO,SAAS,OAAQ,CAAA,gBAAgB,CACrC,CAAA,OAAA,CAAQ,SAAS,CACjB,CAAA,KAAA;AAAA,QACCA,UAAA,CAAc,QAAQ,QAAQ,CAAA,CAAE,KAAKA,UAAc,CAAA,OAAA,CAAQ,QAAQ,CAAC;AAAA,OACtE,CACC,SAAS,mBAAmB,CAAA;AAAA,IACjC;AACE,MAAA,OAAO,YAAY,QAAQ,CAAA;AAAA;AAEjC;AAEgB,SAAA,kBAAA,CACd,UACA,gBACQ,EAAA;AACR,EAAA,QAAQ,QAAU;AAAA,IAChB,KAAK,QAAS,CAAA,GAAA;AAAA,IACd,KAAK,QAAS,CAAA,IAAA;AAAA,IACd,KAAK,QAAS,CAAA,IAAA;AACZ,MAAO,OAAA,QAAA,CAAS,OAAQ,CAAA,gBAAgB,CACrC,CAAA,IAAA,CAAK,EAAE,IAAA,EAAM,CAAE,EAAC,CAChB,CAAA,QAAA,CAAS,mBAAmB,CAAA;AAAA,IACjC,KAAK,QAAS,CAAA,GAAA;AACZ,MAAA,OAAO,QAAS,CAAA,OAAA,CAAQ,cAAe,CAAA,gBAAgB,CAAC,CAAA,CACrD,IAAK,CAAA,EAAE,IAAM,EAAA,CAAA,EAAG,CAAA,CAChB,SAAS,mBAAmB,CAAA;AAAA,IACjC;AACE,MAAA,OAAO,YAAY,QAAQ,CAAA;AAAA;AAEjC;AAEgB,SAAA,kBAAA,CACd,UACA,gBACQ,EAAA;AACR,EAAA,OAAO,QAAS,CAAA,OAAA,CAAQ,kBAAmB,CAAA,QAAA,EAAU,gBAAgB,CAAC,CAAA,CACnE,KAAM,CAAA,EAAE,IAAM,EAAA,CAAA,EAAG,CAAA,CACjB,SAAS,mBAAmB,CAAA;AACjC;AAGO,SAAS,WACd,CAAA,QAAA,EACA,gBACA,EAAA,SAAA,GAAoB,CACpB,EAAA;AACA,EAAA,OAAO,CAAI,CAAA,EAAA,SAAS,CAAI,CAAA,EAAA,QAAQ,CAAI,CAAA,EAAA,kBAAA;AAAA,IAClC,QAAA;AAAA,IACA;AAAA,GACD,CAAA,CAAA;AACH;AAEO,SAAS,eAAe,gBAAkC,EAAA;AAC/D,EAAM,MAAA,OAAA,GAAU,QAAS,CAAA,OAAA,CAAQ,gBAAgB,CAAA;AACjD,EAAA,MAAM,eAAe,OAAQ,CAAA,KAAA,CAAM,SAAS,CAAA,CAAE,SAAS,mBAAmB,CAAA;AAC1E,EAAA,IAAI,iBAAiB,gBAAkB,EAAA;AACrC,IAAO,OAAA,OAAA,CAAQ,SAAS,mBAAmB,CAAA;AAAA;AAE7C,EAAO,OAAA,OAAA,CACJ,OAAQ,CAAA,SAAS,CACjB,CAAA,KAAA,CAAM,EAAE,IAAA,EAAM,CAAE,EAAC,CACjB,CAAA,QAAA,CAAS,mBAAmB,CAAA;AACjC;;;;"}
1
+ {"version":3,"file":"duration.esm.js","sources":["../../src/utils/duration.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DateTime, Duration as LuxonDuration } from 'luxon';\nimport { Duration, DEFAULT_DATE_FORMAT } from '../types';\nimport { assertNever } from './assert';\n\nexport const DEFAULT_DURATION = Duration.P30D;\n\n/**\n * Derive the start date of a given period, assuming two repeating intervals.\n *\n * @param duration - see comment on Duration enum\n * @param inclusiveEndDate - from CostInsightsApi.getLastCompleteBillingDate\n * @param customDateRange - optional custom date range for Duration.CUSTOM\n * @param comparisonMode - if true, creates comparison period for custom ranges (default: false)\n */\nexport function inclusiveStartDateOf(\n duration: Duration,\n inclusiveEndDate: string,\n customDateRange?: { start: string; end: string },\n comparisonMode: boolean = false,\n): string {\n if (duration === Duration.CUSTOM && customDateRange) {\n if (comparisonMode) {\n // For comparison mode, calculate a comparison period of equal length before the selected range\n const startDate = DateTime.fromISO(customDateRange.start);\n const endDate = DateTime.fromISO(customDateRange.end);\n // Include both start and end dates (add 1 day for inclusive calculation)\n const daysDiff = Math.round(endDate.diff(startDate, 'days').days) + 1;\n // Return start of comparison period (twice the range length before the end)\n return startDate.minus({ days: daysDiff }).toFormat(DEFAULT_DATE_FORMAT);\n }\n // For non-comparison mode, return the start date as-is\n return customDateRange.start;\n }\n\n switch (duration) {\n case Duration.P7D:\n case Duration.P30D:\n case Duration.P90D:\n return DateTime.fromISO(inclusiveEndDate)\n .minus(\n LuxonDuration.fromISO(duration).plus(LuxonDuration.fromISO(duration)),\n )\n .toFormat(DEFAULT_DATE_FORMAT);\n case Duration.P3M:\n return DateTime.fromISO(inclusiveEndDate)\n .startOf('quarter')\n .minus(\n LuxonDuration.fromISO(duration).plus(LuxonDuration.fromISO(duration)),\n )\n .toFormat(DEFAULT_DATE_FORMAT);\n case Duration.CUSTOM:\n throw new Error('CUSTOM duration requires customDateRange parameter');\n default:\n return assertNever(duration);\n }\n}\n\nexport function exclusiveEndDateOf(\n duration: Duration,\n inclusiveEndDate: string,\n customDateRange?: { start: string; end: string },\n): string {\n if (duration === Duration.CUSTOM && customDateRange) {\n // For custom ranges, add 1 day to the end date for exclusive end\n return DateTime.fromISO(customDateRange.end)\n .plus({ days: 1 })\n .toFormat(DEFAULT_DATE_FORMAT);\n }\n\n switch (duration) {\n case Duration.P7D:\n case Duration.P30D:\n case Duration.P90D:\n return DateTime.fromISO(inclusiveEndDate)\n .plus({ days: 1 })\n .toFormat(DEFAULT_DATE_FORMAT);\n case Duration.P3M:\n return DateTime.fromISO(quarterEndDate(inclusiveEndDate))\n .plus({ days: 1 })\n .toFormat(DEFAULT_DATE_FORMAT);\n case Duration.CUSTOM:\n throw new Error('CUSTOM duration requires customDateRange parameter');\n default:\n return assertNever(duration);\n }\n}\n\nexport function inclusiveEndDateOf(\n duration: Duration,\n inclusiveEndDate: string,\n customDateRange?: { start: string; end: string },\n): string {\n return DateTime.fromISO(\n exclusiveEndDateOf(duration, inclusiveEndDate, customDateRange),\n )\n .minus({ days: 1 })\n .toFormat(DEFAULT_DATE_FORMAT);\n}\n\n// https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals\nexport function intervalsOf(\n duration: Duration,\n inclusiveEndDate: string,\n repeating: number = 2,\n customDateRange?: { start: string; end: string },\n comparisonMode: boolean = false,\n) {\n if (duration === Duration.CUSTOM && customDateRange) {\n const startDate = DateTime.fromISO(customDateRange.start);\n const endDate = DateTime.fromISO(customDateRange.end);\n // Add 1 to include both start and end dates\n const totalDays = Math.round(endDate.diff(startDate, 'days').days) + 1;\n\n if (comparisonMode) {\n // For comparison mode, split the range in half to create two comparison periods\n const intervalDays = Math.floor(totalDays / 2);\n return `R${repeating}/P${intervalDays}D/${exclusiveEndDateOf(\n duration,\n inclusiveEndDate,\n customDateRange,\n )}`;\n }\n\n // For non-comparison mode, use the full date range as a single interval\n return `R${repeating}/P${totalDays}D/${exclusiveEndDateOf(\n duration,\n inclusiveEndDate,\n customDateRange,\n )}`;\n }\n\n return `R${repeating}/${duration}/${exclusiveEndDateOf(\n duration,\n inclusiveEndDate,\n )}`;\n}\n\nexport function quarterEndDate(inclusiveEndDate: string): string {\n const endDate = DateTime.fromISO(inclusiveEndDate);\n const endOfQuarter = endDate.endOf('quarter').toFormat(DEFAULT_DATE_FORMAT);\n if (endOfQuarter === inclusiveEndDate) {\n return endDate.toFormat(DEFAULT_DATE_FORMAT);\n }\n return endDate\n .startOf('quarter')\n .minus({ days: 1 })\n .toFormat(DEFAULT_DATE_FORMAT);\n}\n"],"names":["LuxonDuration"],"mappings":"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"}
@@ -79,21 +79,42 @@ function formatLastTwoLookaheadQuarters(inclusiveEndDate) {
79
79
  ).toFormat("'Q'q yyyy");
80
80
  return `${start} vs ${end}`;
81
81
  }
82
- const formatRelativePeriod = (duration, date, isEndDate) => {
82
+ const formatRelativePeriod = (duration, date, isEndDate, customDays) => {
83
+ if (duration === Duration.CUSTOM && customDays !== void 0) {
84
+ return isEndDate ? `Last ${customDays} Days` : `First ${customDays} Days`;
85
+ }
83
86
  const periodStart = isEndDate ? inclusiveStartDateOf(duration, date) : date;
84
87
  const periodEnd = isEndDate ? date : inclusiveEndDateOf(duration, date);
85
- const days = Duration$1.fromISO(duration).days;
88
+ const days = customDays ?? Duration$1.fromISO(duration).days;
86
89
  if (![periodStart, periodEnd].includes(date)) {
87
90
  throw new Error(`Invalid relative date ${date} for duration ${duration}`);
88
91
  }
89
92
  return date === periodStart ? `First ${days} Days` : `Last ${days} Days`;
90
93
  };
91
- function formatPeriod(duration, date, isEndDate) {
94
+ function formatPeriod(duration, date, isEndDate, customDateRange, comparisonMode = false) {
92
95
  switch (duration) {
93
96
  case Duration.P3M:
94
97
  return quarterOf(
95
- isEndDate ? inclusiveEndDateOf(duration, date) : inclusiveStartDateOf(duration, date)
98
+ isEndDate ? inclusiveEndDateOf(duration, date, customDateRange) : inclusiveStartDateOf(duration, date, customDateRange)
96
99
  );
100
+ case Duration.CUSTOM:
101
+ if (customDateRange && !comparisonMode) {
102
+ const startDate = DateTime.fromISO(customDateRange.start);
103
+ const endDate = DateTime.fromISO(customDateRange.end);
104
+ return `${startDate.toFormat("MMM dd")} - ${endDate.toFormat(
105
+ "MMM dd, yyyy"
106
+ )}`;
107
+ }
108
+ if (customDateRange && comparisonMode) {
109
+ const startDate = DateTime.fromISO(customDateRange.start);
110
+ const endDate = DateTime.fromISO(customDateRange.end);
111
+ const totalDays = Math.round(endDate.diff(startDate, "days").days) + 1;
112
+ const firstPeriodDays = Math.floor(totalDays / 2);
113
+ const lastPeriodDays = totalDays - firstPeriodDays;
114
+ const periodDays = isEndDate ? lastPeriodDays : firstPeriodDays;
115
+ return formatRelativePeriod(duration, date, isEndDate, periodDays);
116
+ }
117
+ return DateTime.fromISO(date).toFormat("MMM dd, yyyy");
97
118
  default:
98
119
  return formatRelativePeriod(duration, date, isEndDate);
99
120
  }