@backstage-community/plugin-cost-insights 0.12.25 → 0.12.26
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.
- package/CHANGELOG.md +8 -0
- package/dist/alerts/ProjectGrowthAlert.esm.js +24 -0
- package/dist/alerts/ProjectGrowthAlert.esm.js.map +1 -0
- package/dist/alerts/UnlabeledDataflowAlert.esm.js +25 -0
- package/dist/alerts/UnlabeledDataflowAlert.esm.js.map +1 -0
- package/dist/api/CostInsightsApi.esm.js +8 -0
- package/dist/api/CostInsightsApi.esm.js.map +1 -0
- package/dist/components/ActionItems/ActionItemCard.esm.js +46 -0
- package/dist/components/ActionItems/ActionItemCard.esm.js.map +1 -0
- package/dist/components/ActionItems/ActionItems.esm.js +90 -0
- package/dist/components/ActionItems/ActionItems.esm.js.map +1 -0
- package/dist/components/AlertInsights/AlertDialog.esm.js +123 -0
- package/dist/components/AlertInsights/AlertDialog.esm.js.map +1 -0
- package/dist/components/AlertInsights/AlertInsights.esm.js +157 -0
- package/dist/components/AlertInsights/AlertInsights.esm.js.map +1 -0
- package/dist/components/AlertInsights/AlertInsightsHeader.esm.js +17 -0
- package/dist/components/AlertInsights/AlertInsightsHeader.esm.js.map +1 -0
- package/dist/components/AlertInsights/AlertInsightsSection.esm.js +57 -0
- package/dist/components/AlertInsights/AlertInsightsSection.esm.js.map +1 -0
- package/dist/components/AlertInsights/AlertInsightsSectionHeader.esm.js +30 -0
- package/dist/components/AlertInsights/AlertInsightsSectionHeader.esm.js.map +1 -0
- package/dist/components/AlertInsights/AlertStatusSummary.esm.js +93 -0
- package/dist/components/AlertInsights/AlertStatusSummary.esm.js.map +1 -0
- package/dist/components/AlertInsights/AlertStatusSummaryButton.esm.js +35 -0
- package/dist/components/AlertInsights/AlertStatusSummaryButton.esm.js.map +1 -0
- package/dist/components/AlertInstructionsLayout/AlertInstructionsLayout.esm.js +47 -0
- package/dist/components/AlertInstructionsLayout/AlertInstructionsLayout.esm.js.map +1 -0
- package/dist/components/BarChart/BarChart.esm.js +167 -0
- package/dist/components/BarChart/BarChart.esm.js.map +1 -0
- package/dist/components/BarChart/BarChartLabel.esm.js +31 -0
- package/dist/components/BarChart/BarChartLabel.esm.js.map +1 -0
- package/dist/components/BarChart/BarChartLegend.esm.js +47 -0
- package/dist/components/BarChart/BarChartLegend.esm.js.map +1 -0
- package/dist/components/BarChart/BarChartStepper.esm.js +91 -0
- package/dist/components/BarChart/BarChartStepper.esm.js.map +1 -0
- package/dist/components/BarChart/BarChartStepperButton.esm.js +27 -0
- package/dist/components/BarChart/BarChartStepperButton.esm.js.map +1 -0
- package/dist/components/BarChart/BarChartSteps.esm.js +25 -0
- package/dist/components/BarChart/BarChartSteps.esm.js.map +1 -0
- package/dist/components/BarChart/BarChartTick.esm.js +29 -0
- package/dist/components/BarChart/BarChartTick.esm.js.map +1 -0
- package/dist/components/BarChart/BarChartTooltip.esm.js +30 -0
- package/dist/components/BarChart/BarChartTooltip.esm.js.map +1 -0
- package/dist/components/BarChart/BarChartTooltipItem.esm.js +24 -0
- package/dist/components/BarChart/BarChartTooltipItem.esm.js.map +1 -0
- package/dist/components/CopyUrlToClipboard/CopyUrlToClipboard.esm.js +44 -0
- package/dist/components/CopyUrlToClipboard/CopyUrlToClipboard.esm.js.map +1 -0
- package/dist/components/CostGrowth/CostGrowth.esm.js +55 -0
- package/dist/components/CostGrowth/CostGrowth.esm.js.map +1 -0
- package/dist/components/CostGrowth/CostGrowthIndicator.esm.js +31 -0
- package/dist/components/CostGrowth/CostGrowthIndicator.esm.js.map +1 -0
- package/dist/components/CostInsightsHeader/CostInsightsHeader.esm.js +57 -0
- package/dist/components/CostInsightsHeader/CostInsightsHeader.esm.js.map +1 -0
- package/dist/components/CostInsightsLayout/CostInsightsLayout.esm.js +35 -0
- package/dist/components/CostInsightsLayout/CostInsightsLayout.esm.js.map +1 -0
- package/dist/components/CostInsightsNavigation/CostInsightsNavigation.esm.js +87 -0
- package/dist/components/CostInsightsNavigation/CostInsightsNavigation.esm.js.map +1 -0
- package/dist/components/CostInsightsPage/CostInsightsPage.esm.js +229 -0
- package/dist/components/CostInsightsPage/CostInsightsPage.esm.js.map +1 -0
- package/dist/components/CostInsightsPage/CostInsightsPageRoot.esm.js +15 -0
- package/dist/components/CostInsightsPage/CostInsightsPageRoot.esm.js.map +1 -0
- package/dist/components/CostInsightsPage/CostInsightsThemeProvider.esm.js +24 -0
- package/dist/components/CostInsightsPage/CostInsightsThemeProvider.esm.js.map +1 -0
- package/dist/components/CostInsightsPage/index.esm.js +2 -0
- package/dist/components/CostInsightsPage/index.esm.js.map +1 -0
- package/dist/components/CostInsightsPage/selector.esm.js +15 -0
- package/dist/components/CostInsightsPage/selector.esm.js.map +1 -0
- package/dist/components/CostInsightsTabs/CostInsightsTabs.esm.js +89 -0
- package/dist/components/CostInsightsTabs/CostInsightsTabs.esm.js.map +1 -0
- package/dist/components/CostInsightsTabs/selector.esm.js +17 -0
- package/dist/components/CostInsightsTabs/selector.esm.js.map +1 -0
- package/dist/components/CostOverviewCard/CostOverviewBreakdownChart.esm.js +196 -0
- package/dist/components/CostOverviewCard/CostOverviewBreakdownChart.esm.js.map +1 -0
- package/dist/components/CostOverviewCard/CostOverviewCard.esm.js +99 -0
- package/dist/components/CostOverviewCard/CostOverviewCard.esm.js.map +1 -0
- package/dist/components/CostOverviewCard/CostOverviewChart.esm.js +157 -0
- package/dist/components/CostOverviewCard/CostOverviewChart.esm.js.map +1 -0
- package/dist/components/CostOverviewCard/CostOverviewHeader.esm.js +25 -0
- package/dist/components/CostOverviewCard/CostOverviewHeader.esm.js.map +1 -0
- package/dist/components/CostOverviewCard/CostOverviewLegend.esm.js +54 -0
- package/dist/components/CostOverviewCard/CostOverviewLegend.esm.js.map +1 -0
- package/dist/components/CostOverviewCard/selector.esm.js +22 -0
- package/dist/components/CostOverviewCard/selector.esm.js.map +1 -0
- package/dist/components/CurrencySelect/CurrencySelect.esm.js +52 -0
- package/dist/components/CurrencySelect/CurrencySelect.esm.js.map +1 -0
- package/dist/{esm/index-BPzWHt3A.esm.js → components/EntityCosts/EntityCosts.esm.js} +14 -80
- package/dist/{esm/index-BPzWHt3A.esm.js.map → components/EntityCosts/EntityCosts.esm.js.map} +1 -1
- package/dist/components/EntityCosts/index.esm.js +2 -0
- package/dist/components/EntityCosts/index.esm.js.map +1 -0
- package/dist/{esm/index-BA6m_qFS.esm.js → components/LabelDataflowInstructionsPage/LabelDataflowInstructionsPage.esm.js} +2 -79
- package/dist/components/LabelDataflowInstructionsPage/LabelDataflowInstructionsPage.esm.js.map +1 -0
- package/dist/components/LabelDataflowInstructionsPage/index.esm.js +2 -0
- package/dist/components/LabelDataflowInstructionsPage/index.esm.js.map +1 -0
- package/dist/components/LegendItem/LegendItem.esm.js +44 -0
- package/dist/components/LegendItem/LegendItem.esm.js.map +1 -0
- package/dist/components/MetricSelect/MetricSelect.esm.js +37 -0
- package/dist/components/MetricSelect/MetricSelect.esm.js.map +1 -0
- package/dist/components/PeriodSelect/PeriodSelect.esm.js +72 -0
- package/dist/components/PeriodSelect/PeriodSelect.esm.js.map +1 -0
- package/dist/components/ProductInsights/ProductInsights.esm.js +98 -0
- package/dist/components/ProductInsights/ProductInsights.esm.js.map +1 -0
- package/dist/components/ProductInsightsCard/ProductEntityDialog.esm.js +42 -0
- package/dist/components/ProductInsightsCard/ProductEntityDialog.esm.js.map +1 -0
- package/dist/components/ProductInsightsCard/ProductEntityTable.esm.js +139 -0
- package/dist/components/ProductInsightsCard/ProductEntityTable.esm.js.map +1 -0
- package/dist/components/ProductInsightsCard/ProductInsightsCard.esm.js +83 -0
- package/dist/components/ProductInsightsCard/ProductInsightsCard.esm.js.map +1 -0
- package/dist/components/ProductInsightsCard/ProductInsightsCardList.esm.js +34 -0
- package/dist/components/ProductInsightsCard/ProductInsightsCardList.esm.js.map +1 -0
- package/dist/components/ProductInsightsCard/ProductInsightsChart.esm.js +148 -0
- package/dist/components/ProductInsightsCard/ProductInsightsChart.esm.js.map +1 -0
- package/dist/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.esm.js +20 -0
- package/dist/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.esm.js.map +1 -0
- package/dist/components/ProjectGrowthAlertCard/ProjectGrowthAlertChart.esm.js +46 -0
- package/dist/components/ProjectGrowthAlertCard/ProjectGrowthAlertChart.esm.js.map +1 -0
- package/dist/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.esm.js +111 -0
- package/dist/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.esm.js.map +1 -0
- package/dist/components/ProjectGrowthInstructionsPage/index.esm.js +2 -0
- package/dist/components/ProjectGrowthInstructionsPage/index.esm.js.map +1 -0
- package/dist/components/ProjectSelect/ProjectSelect.esm.js +47 -0
- package/dist/components/ProjectSelect/ProjectSelect.esm.js.map +1 -0
- package/dist/components/UnlabeledDataflowAlertCard/UnlabeledDataflowAlertCard.esm.js +41 -0
- package/dist/components/UnlabeledDataflowAlertCard/UnlabeledDataflowAlertCard.esm.js.map +1 -0
- package/dist/components/WhyCostsMatter/WhyCostsMatter.esm.js +21 -0
- package/dist/components/WhyCostsMatter/WhyCostsMatter.esm.js.map +1 -0
- package/dist/example/alerts/KubernetesMigrationAlert.esm.js +86 -0
- package/dist/example/alerts/KubernetesMigrationAlert.esm.js.map +1 -0
- package/dist/example/client.esm.js +194 -0
- package/dist/example/client.esm.js.map +1 -0
- package/dist/example/components/KubernetesMigrationAlertCard/KubernetesMigrationAlertCard.esm.js +33 -0
- package/dist/example/components/KubernetesMigrationAlertCard/KubernetesMigrationAlertCard.esm.js.map +1 -0
- package/dist/{esm/index-BTIHr4n_.esm.js → example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChart.esm.js} +69 -48
- package/dist/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChart.esm.js.map +1 -0
- package/dist/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChartLegend.esm.js +107 -0
- package/dist/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChartLegend.esm.js.map +1 -0
- package/dist/example/forms/KubernetesMigrationDismissForm.esm.js +57 -0
- package/dist/example/forms/KubernetesMigrationDismissForm.esm.js.map +1 -0
- package/dist/forms/AlertAcceptForm.esm.js +27 -0
- package/dist/forms/AlertAcceptForm.esm.js.map +1 -0
- package/dist/forms/AlertDismissForm.esm.js +100 -0
- package/dist/forms/AlertDismissForm.esm.js.map +1 -0
- package/dist/forms/AlertSnoozeForm.esm.js +52 -0
- package/dist/forms/AlertSnoozeForm.esm.js.map +1 -0
- package/dist/hooks/useConfig.esm.js +153 -0
- package/dist/hooks/useConfig.esm.js.map +1 -0
- package/dist/hooks/useCurrency.esm.js +26 -0
- package/dist/hooks/useCurrency.esm.js.map +1 -0
- package/dist/hooks/useFilters.esm.js +67 -0
- package/dist/hooks/useFilters.esm.js.map +1 -0
- package/dist/hooks/useGroups.esm.js +55 -0
- package/dist/hooks/useGroups.esm.js.map +1 -0
- package/dist/hooks/useLastCompleteBillingDate.esm.js +65 -0
- package/dist/hooks/useLastCompleteBillingDate.esm.js.map +1 -0
- package/dist/hooks/useLoading.esm.js +54 -0
- package/dist/hooks/useLoading.esm.js.map +1 -0
- package/dist/hooks/useScroll.esm.js +24 -0
- package/dist/hooks/useScroll.esm.js.map +1 -0
- package/dist/index.esm.js +37 -22
- package/dist/index.esm.js.map +1 -1
- package/dist/plugin.esm.js +55 -0
- package/dist/plugin.esm.js.map +1 -0
- package/dist/testUtils/filters.esm.js +8 -0
- package/dist/testUtils/filters.esm.js.map +1 -0
- package/dist/testUtils/loading.esm.js +14 -0
- package/dist/testUtils/loading.esm.js.map +1 -0
- package/dist/testUtils/mockData.esm.js +485 -0
- package/dist/testUtils/mockData.esm.js.map +1 -0
- package/dist/testUtils/products.esm.js +49 -0
- package/dist/testUtils/products.esm.js.map +1 -0
- package/dist/testUtils/providers.esm.js +41 -0
- package/dist/testUtils/providers.esm.js.map +1 -0
- package/dist/testUtils/testUtils.esm.js +142 -0
- package/dist/testUtils/testUtils.esm.js.map +1 -0
- package/dist/types/Alert.esm.js +66 -0
- package/dist/types/Alert.esm.js.map +1 -0
- package/dist/types/ChangeStatistic.esm.js +15 -0
- package/dist/types/ChangeStatistic.esm.js.map +1 -0
- package/dist/types/CurrencyType.esm.js +10 -0
- package/dist/types/CurrencyType.esm.js.map +1 -0
- package/dist/types/DateFormat.esm.js +4 -0
- package/dist/types/DateFormat.esm.js.map +1 -0
- package/dist/types/Duration.esm.js +10 -0
- package/dist/types/Duration.esm.js.map +1 -0
- package/dist/types/Icon.esm.js +12 -0
- package/dist/types/Icon.esm.js.map +1 -0
- package/dist/utils/alerts.esm.js +72 -0
- package/dist/utils/alerts.esm.js.map +1 -0
- package/dist/utils/assert.esm.js +29 -0
- package/dist/utils/assert.esm.js.map +1 -0
- package/dist/utils/change.esm.js +58 -0
- package/dist/utils/change.esm.js.map +1 -0
- package/dist/utils/charts.esm.js +9 -0
- package/dist/utils/charts.esm.js.map +1 -0
- package/dist/utils/config.esm.js +19 -0
- package/dist/utils/config.esm.js.map +1 -0
- package/dist/utils/currency.esm.js +56 -0
- package/dist/utils/currency.esm.js.map +1 -0
- package/dist/utils/duration.esm.js +55 -0
- package/dist/utils/duration.esm.js.map +1 -0
- package/dist/utils/filters.esm.js +14 -0
- package/dist/utils/filters.esm.js.map +1 -0
- package/dist/utils/formatters.esm.js +103 -0
- package/dist/utils/formatters.esm.js.map +1 -0
- package/dist/utils/grammar.esm.js +14 -0
- package/dist/utils/grammar.esm.js.map +1 -0
- package/dist/utils/graphs.esm.js +48 -0
- package/dist/utils/graphs.esm.js.map +1 -0
- package/dist/utils/history.esm.js +32 -0
- package/dist/utils/history.esm.js.map +1 -0
- package/dist/utils/loading.esm.js +51 -0
- package/dist/utils/loading.esm.js.map +1 -0
- package/dist/utils/navigation.esm.js +56 -0
- package/dist/utils/navigation.esm.js.map +1 -0
- package/dist/utils/scroll.esm.js +38 -0
- package/dist/utils/scroll.esm.js.map +1 -0
- package/dist/utils/sort.esm.js +10 -0
- package/dist/utils/sort.esm.js.map +1 -0
- package/dist/utils/styles.esm.js +562 -0
- package/dist/utils/styles.esm.js.map +1 -0
- package/dist/utils/sum.esm.js +4 -0
- package/dist/utils/sum.esm.js.map +1 -0
- package/package.json +17 -12
- package/dist/esm/index-BA6m_qFS.esm.js.map +0 -1
- package/dist/esm/index-BFM2mP6Z.esm.js +0 -4084
- package/dist/esm/index-BFM2mP6Z.esm.js.map +0 -1
- package/dist/esm/index-BTIHr4n_.esm.js.map +0 -1
- package/dist/esm/index-DaitkcPN.esm.js +0 -1136
- package/dist/esm/index-DaitkcPN.esm.js.map +0 -1
- package/dist/esm/selector-D7iXJwd9.esm.js +0 -594
- package/dist/esm/selector-D7iXJwd9.esm.js.map +0 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React, { useRef, useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import Box from '@material-ui/core/Box';
|
|
3
|
+
import Typography from '@material-ui/core/Typography';
|
|
4
|
+
import Alert from '@material-ui/lab/Alert';
|
|
5
|
+
import { costInsightsApiRef } from '../../api/CostInsightsApi.esm.js';
|
|
6
|
+
import { ProductInsightsCardList } from '../ProductInsightsCard/ProductInsightsCardList.esm.js';
|
|
7
|
+
import { intervalsOf, DEFAULT_DURATION } from '../../utils/duration.esm.js';
|
|
8
|
+
import { DefaultLoadingAction, settledResponseOf, initialStatesOf } from '../../utils/loading.esm.js';
|
|
9
|
+
import { totalAggregationSort } from '../../utils/sort.esm.js';
|
|
10
|
+
import '../../hooks/useConfig.esm.js';
|
|
11
|
+
import '../../hooks/useCurrency.esm.js';
|
|
12
|
+
import '../../hooks/useFilters.esm.js';
|
|
13
|
+
import '../../hooks/useGroups.esm.js';
|
|
14
|
+
import { useLoading } from '../../hooks/useLoading.esm.js';
|
|
15
|
+
import '../../hooks/useScroll.esm.js';
|
|
16
|
+
import { useLastCompleteBillingDate } from '../../hooks/useLastCompleteBillingDate.esm.js';
|
|
17
|
+
import { useApi } from '@backstage/core-plugin-api';
|
|
18
|
+
|
|
19
|
+
const mapLoadingToProps = ({ dispatch }) => (isLoading) => dispatch({ [DefaultLoadingAction.CostInsightsProducts]: isLoading });
|
|
20
|
+
const ProductInsights = ({
|
|
21
|
+
group,
|
|
22
|
+
project,
|
|
23
|
+
products,
|
|
24
|
+
onLoaded
|
|
25
|
+
}) => {
|
|
26
|
+
const client = useApi(costInsightsApiRef);
|
|
27
|
+
const onceRef = useRef(false);
|
|
28
|
+
const [initialStates, setStates] = useState([]);
|
|
29
|
+
const [error, setError] = useState(null);
|
|
30
|
+
const lastCompleteBillingDate = useLastCompleteBillingDate();
|
|
31
|
+
const dispatchLoading = useLoading(mapLoadingToProps);
|
|
32
|
+
const dispatchLoadingProducts = useCallback(dispatchLoading, []);
|
|
33
|
+
const onSelectAsyncMemo = useCallback(
|
|
34
|
+
async function onSelectAsync(product, duration) {
|
|
35
|
+
return client.getProductInsights({
|
|
36
|
+
group,
|
|
37
|
+
project,
|
|
38
|
+
product: product.kind,
|
|
39
|
+
intervals: intervalsOf(duration, lastCompleteBillingDate)
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
[client, group, project, lastCompleteBillingDate]
|
|
43
|
+
);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
async function getAllProductInsights() {
|
|
46
|
+
try {
|
|
47
|
+
dispatchLoadingProducts(true);
|
|
48
|
+
const responses = await Promise.allSettled(
|
|
49
|
+
products.map(
|
|
50
|
+
(product) => client.getProductInsights({
|
|
51
|
+
group,
|
|
52
|
+
project,
|
|
53
|
+
product: product.kind,
|
|
54
|
+
intervals: intervalsOf(DEFAULT_DURATION, lastCompleteBillingDate)
|
|
55
|
+
})
|
|
56
|
+
)
|
|
57
|
+
).then(settledResponseOf);
|
|
58
|
+
const updatedInitialStates = initialStatesOf(products, responses).sort(
|
|
59
|
+
totalAggregationSort
|
|
60
|
+
);
|
|
61
|
+
setStates(updatedInitialStates);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
setError(e);
|
|
64
|
+
} finally {
|
|
65
|
+
dispatchLoadingProducts(false);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
getAllProductInsights();
|
|
69
|
+
}, [
|
|
70
|
+
client,
|
|
71
|
+
group,
|
|
72
|
+
project,
|
|
73
|
+
products,
|
|
74
|
+
lastCompleteBillingDate,
|
|
75
|
+
dispatchLoadingProducts
|
|
76
|
+
]);
|
|
77
|
+
useEffect(
|
|
78
|
+
function handleOnLoaded() {
|
|
79
|
+
if (onceRef.current) {
|
|
80
|
+
const initialProducts = initialStates.map((state) => state.product);
|
|
81
|
+
onLoaded(initialProducts);
|
|
82
|
+
} else {
|
|
83
|
+
onceRef.current = true;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
[initialStates, onLoaded]
|
|
87
|
+
);
|
|
88
|
+
return /* @__PURE__ */ React.createElement(Box, { px: 3, py: 6 }, /* @__PURE__ */ React.createElement(Box, { mt: 0, mb: 5, textAlign: "center" }, /* @__PURE__ */ React.createElement(Typography, { variant: "h4", gutterBottom: true }, "Your team's product usage")), error ? /* @__PURE__ */ React.createElement(Alert, { severity: "error" }, error.message) : /* @__PURE__ */ React.createElement(
|
|
89
|
+
ProductInsightsCardList,
|
|
90
|
+
{
|
|
91
|
+
initialStates,
|
|
92
|
+
onSelectAsync: onSelectAsyncMemo
|
|
93
|
+
}
|
|
94
|
+
));
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export { ProductInsights };
|
|
98
|
+
//# sourceMappingURL=ProductInsights.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductInsights.esm.js","sources":["../../../src/components/ProductInsights/ProductInsights.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 React, { useCallback, useEffect, useRef, useState } from 'react';\nimport Box from '@material-ui/core/Box';\nimport Typography from '@material-ui/core/Typography';\nimport { default as Alert } from '@material-ui/lab/Alert';\nimport { costInsightsApiRef } from '../../api';\nimport { ProductInsightsCardList } from '../ProductInsightsCard/ProductInsightsCardList';\nimport { Duration } from '../../types';\nimport {\n Entity,\n Maybe,\n Product,\n} from '@backstage-community/plugin-cost-insights-common';\nimport { intervalsOf, DEFAULT_DURATION } from '../../utils/duration';\nimport {\n DefaultLoadingAction,\n initialStatesOf,\n settledResponseOf,\n ProductState,\n} from '../../utils/loading';\nimport { totalAggregationSort } from '../../utils/sort';\nimport {\n useLoading,\n useLastCompleteBillingDate,\n MapLoadingToProps,\n} from '../../hooks';\nimport { useApi } from '@backstage/core-plugin-api';\n\ntype LoadingProps = (isLoading: boolean) => void;\n\nconst mapLoadingToProps: MapLoadingToProps<LoadingProps> =\n ({ dispatch }) =>\n (isLoading: boolean) =>\n dispatch({ [DefaultLoadingAction.CostInsightsProducts]: isLoading });\n\ntype ProductInsightsProps = {\n group: string;\n project: Maybe<string>;\n products: Product[];\n onLoaded: (entities: Product[]) => void;\n};\n\nexport const ProductInsights = ({\n group,\n project,\n products,\n onLoaded,\n}: ProductInsightsProps) => {\n const client = useApi(costInsightsApiRef);\n const onceRef = useRef(false);\n const [initialStates, setStates] = useState<ProductState[]>([]);\n const [error, setError] = useState<Maybe<Error>>(null);\n const lastCompleteBillingDate = useLastCompleteBillingDate();\n const dispatchLoading = useLoading(mapLoadingToProps);\n\n /* eslint-disable react-hooks/exhaustive-deps */\n // See @CostInsightsPage\n const dispatchLoadingProducts = useCallback(dispatchLoading, []);\n /* eslint-enable react-hooks/exhaustive-deps */\n\n const onSelectAsyncMemo = useCallback(\n async function onSelectAsync(\n product: Product,\n duration: Duration,\n ): Promise<Entity> {\n return client.getProductInsights({\n group: group,\n project: project,\n product: product.kind,\n intervals: intervalsOf(duration, lastCompleteBillingDate),\n });\n },\n [client, group, project, lastCompleteBillingDate],\n );\n\n useEffect(() => {\n async function getAllProductInsights() {\n try {\n dispatchLoadingProducts(true);\n const responses = await Promise.allSettled(\n products.map(product =>\n client.getProductInsights({\n group: group,\n project: project,\n product: product.kind,\n intervals: intervalsOf(DEFAULT_DURATION, lastCompleteBillingDate),\n }),\n ),\n ).then(settledResponseOf);\n\n const updatedInitialStates = initialStatesOf(products, responses).sort(\n totalAggregationSort,\n );\n setStates(updatedInitialStates);\n } catch (e) {\n setError(e);\n } finally {\n dispatchLoadingProducts(false);\n }\n }\n\n getAllProductInsights();\n }, [\n client,\n group,\n project,\n products,\n lastCompleteBillingDate,\n dispatchLoadingProducts,\n ]);\n\n useEffect(\n function handleOnLoaded() {\n if (onceRef.current) {\n const initialProducts = initialStates.map(state => state.product);\n onLoaded(initialProducts);\n } else {\n onceRef.current = true;\n }\n },\n [initialStates, onLoaded],\n );\n\n return (\n <Box px={3} py={6}>\n <Box mt={0} mb={5} textAlign=\"center\">\n <Typography variant=\"h4\" gutterBottom>\n Your team's product usage\n </Typography>\n </Box>\n {error ? (\n <Alert severity=\"error\">{error.message}</Alert>\n ) : (\n <ProductInsightsCardList\n initialStates={initialStates}\n onSelectAsync={onSelectAsyncMemo}\n />\n )}\n </Box>\n );\n};\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import Dialog from '@material-ui/core/Dialog';
|
|
3
|
+
import IconButton from '@material-ui/core/IconButton';
|
|
4
|
+
import CloseIcon from '@material-ui/icons/Close';
|
|
5
|
+
import { useEntityDialogStyles } from '../../utils/styles.esm.js';
|
|
6
|
+
import { ProductEntityTable } from './ProductEntityTable.esm.js';
|
|
7
|
+
import { findAlways } from '../../utils/assert.esm.js';
|
|
8
|
+
import { HeaderTabs } from '@backstage/core-components';
|
|
9
|
+
|
|
10
|
+
const ProductEntityDialog = ({
|
|
11
|
+
open,
|
|
12
|
+
entity,
|
|
13
|
+
options = {},
|
|
14
|
+
onClose
|
|
15
|
+
}) => {
|
|
16
|
+
const classes = useEntityDialogStyles();
|
|
17
|
+
const labels = Object.keys(entity.entities);
|
|
18
|
+
const [selectedLabel, setSelectedLabel] = useState(
|
|
19
|
+
findAlways(labels, (_) => true)
|
|
20
|
+
);
|
|
21
|
+
const tabs = labels.map((label, index) => ({
|
|
22
|
+
id: index.toString(),
|
|
23
|
+
label: `Breakdown by ${label}`
|
|
24
|
+
}));
|
|
25
|
+
return /* @__PURE__ */ React.createElement(Dialog, { open, onClose, scroll: "body", fullWidth: true, maxWidth: "lg" }, /* @__PURE__ */ React.createElement(IconButton, { className: classes.closeButton, onClick: onClose }, /* @__PURE__ */ React.createElement(CloseIcon, null)), /* @__PURE__ */ React.createElement(
|
|
26
|
+
HeaderTabs,
|
|
27
|
+
{
|
|
28
|
+
tabs,
|
|
29
|
+
onChange: (index) => setSelectedLabel(labels[index])
|
|
30
|
+
}
|
|
31
|
+
), /* @__PURE__ */ React.createElement(
|
|
32
|
+
ProductEntityTable,
|
|
33
|
+
{
|
|
34
|
+
entityLabel: selectedLabel,
|
|
35
|
+
entity,
|
|
36
|
+
options
|
|
37
|
+
}
|
|
38
|
+
));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export { ProductEntityDialog };
|
|
42
|
+
//# sourceMappingURL=ProductEntityDialog.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductEntityDialog.esm.js","sources":["../../../src/components/ProductInsightsCard/ProductEntityDialog.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 React, { useState } from 'react';\nimport Dialog from '@material-ui/core/Dialog';\nimport IconButton from '@material-ui/core/IconButton';\nimport { default as CloseButton } from '@material-ui/icons/Close';\nimport { useEntityDialogStyles as useStyles } from '../../utils/styles';\nimport { Entity } from '@backstage-community/plugin-cost-insights-common';\nimport {\n ProductEntityTable,\n ProductEntityTableOptions,\n} from './ProductEntityTable';\nimport { findAlways } from '../../utils/assert';\nimport { HeaderTabs } from '@backstage/core-components';\n\ntype ProductEntityDialogProps = {\n open: boolean;\n entity: Entity;\n options?: ProductEntityTableOptions;\n onClose: () => void;\n};\n\nexport const ProductEntityDialog = ({\n open,\n entity,\n options = {},\n onClose,\n}: ProductEntityDialogProps) => {\n const classes = useStyles();\n const labels = Object.keys(entity.entities);\n const [selectedLabel, setSelectedLabel] = useState(\n findAlways(labels, _ => true),\n );\n\n const tabs = labels.map((label, index) => ({\n id: index.toString(),\n label: `Breakdown by ${label}`,\n }));\n\n return (\n <Dialog open={open} onClose={onClose} scroll=\"body\" fullWidth maxWidth=\"lg\">\n <IconButton className={classes.closeButton} onClick={onClose}>\n <CloseButton />\n </IconButton>\n <HeaderTabs\n tabs={tabs}\n onChange={index => setSelectedLabel(labels[index])}\n />\n <ProductEntityTable\n entityLabel={selectedLabel}\n entity={entity}\n options={options}\n />\n </Dialog>\n );\n};\n"],"names":["useStyles","CloseButton"],"mappings":";;;;;;;;;AAoCO,MAAM,sBAAsB,CAAC;AAAA,EAClC,IAAA;AAAA,EACA,MAAA;AAAA,EACA,UAAU,EAAC;AAAA,EACX,OAAA;AACF,CAAgC,KAAA;AAC9B,EAAA,MAAM,UAAUA,qBAAU,EAAA,CAAA;AAC1B,EAAA,MAAM,MAAS,GAAA,MAAA,CAAO,IAAK,CAAA,MAAA,CAAO,QAAQ,CAAA,CAAA;AAC1C,EAAM,MAAA,CAAC,aAAe,EAAA,gBAAgB,CAAI,GAAA,QAAA;AAAA,IACxC,UAAA,CAAW,MAAQ,EAAA,CAAA,CAAA,KAAK,IAAI,CAAA;AAAA,GAC9B,CAAA;AAEA,EAAA,MAAM,IAAO,GAAA,MAAA,CAAO,GAAI,CAAA,CAAC,OAAO,KAAW,MAAA;AAAA,IACzC,EAAA,EAAI,MAAM,QAAS,EAAA;AAAA,IACnB,KAAA,EAAO,gBAAgB,KAAK,CAAA,CAAA;AAAA,GAC5B,CAAA,CAAA,CAAA;AAEF,EACE,uBAAA,KAAA,CAAA,aAAA,CAAC,UAAO,IAAY,EAAA,OAAA,EAAkB,QAAO,MAAO,EAAA,SAAA,EAAS,MAAC,QAAS,EAAA,IAAA,EAAA,sCACpE,UAAW,EAAA,EAAA,SAAA,EAAW,QAAQ,WAAa,EAAA,OAAA,EAAS,2BAClD,KAAA,CAAA,aAAA,CAAAC,SAAA,EAAA,IAAY,CACf,CACA,kBAAA,KAAA,CAAA,aAAA;AAAA,IAAC,UAAA;AAAA,IAAA;AAAA,MACC,IAAA;AAAA,MACA,QAAU,EAAA,CAAA,KAAA,KAAS,gBAAiB,CAAA,MAAA,CAAO,KAAK,CAAC,CAAA;AAAA,KAAA;AAAA,GAEnD,kBAAA,KAAA,CAAA,aAAA;AAAA,IAAC,kBAAA;AAAA,IAAA;AAAA,MACC,WAAa,EAAA,aAAA;AAAA,MACb,MAAA;AAAA,MACA,OAAA;AAAA,KAAA;AAAA,GAEJ,CAAA,CAAA;AAEJ;;;;"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import classnames from 'classnames';
|
|
3
|
+
import Typography from '@material-ui/core/Typography';
|
|
4
|
+
import { formatChange } from '../../utils/formatters.esm.js';
|
|
5
|
+
import { useEntityDialogStyles } from '../../utils/styles.esm.js';
|
|
6
|
+
import '../../types/Alert.esm.js';
|
|
7
|
+
import '../../utils/currency.esm.js';
|
|
8
|
+
import 'luxon';
|
|
9
|
+
import '../../utils/duration.esm.js';
|
|
10
|
+
import { useConfig } from '../../hooks/useConfig.esm.js';
|
|
11
|
+
import '../../hooks/useCurrency.esm.js';
|
|
12
|
+
import '../../hooks/useFilters.esm.js';
|
|
13
|
+
import '../../hooks/useGroups.esm.js';
|
|
14
|
+
import '../../hooks/useLoading.esm.js';
|
|
15
|
+
import '../../hooks/useScroll.esm.js';
|
|
16
|
+
import '../../hooks/useLastCompleteBillingDate.esm.js';
|
|
17
|
+
import { CostGrowthIndicator } from '../CostGrowth/CostGrowthIndicator.esm.js';
|
|
18
|
+
import { Table } from '@backstage/core-components';
|
|
19
|
+
|
|
20
|
+
const createRenderer = (baseCurrency) => (col, classes) => {
|
|
21
|
+
return function render(rowData) {
|
|
22
|
+
const row = rowData;
|
|
23
|
+
const rowStyles = classnames(classes.row, {
|
|
24
|
+
[classes.rowTotal]: row.id === "total",
|
|
25
|
+
[classes.colFirst]: col === "label",
|
|
26
|
+
[classes.colLast]: col === "change"
|
|
27
|
+
});
|
|
28
|
+
switch (col) {
|
|
29
|
+
case "previous":
|
|
30
|
+
case "current":
|
|
31
|
+
return /* @__PURE__ */ React.createElement(Typography, { className: rowStyles }, baseCurrency.format(row[col]));
|
|
32
|
+
case "change":
|
|
33
|
+
return /* @__PURE__ */ React.createElement(
|
|
34
|
+
CostGrowthIndicator,
|
|
35
|
+
{
|
|
36
|
+
className: rowStyles,
|
|
37
|
+
change: row.change,
|
|
38
|
+
formatter: formatChange
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
default:
|
|
42
|
+
return /* @__PURE__ */ React.createElement(Typography, { className: rowStyles }, row.label);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
function createSorter(field) {
|
|
47
|
+
return function rowSort(data1, data2) {
|
|
48
|
+
const a = data1;
|
|
49
|
+
const b = data2;
|
|
50
|
+
if (a.id === "total") return 1;
|
|
51
|
+
if (b.id === "total") return 1;
|
|
52
|
+
if (field === "label") return a.label.localeCompare(b.label);
|
|
53
|
+
if (field === "change") {
|
|
54
|
+
if (formatChange(a[field]) === "\u221E" || formatChange(b[field]) === "-\u221E")
|
|
55
|
+
return 1;
|
|
56
|
+
if (formatChange(a[field]) === "-\u221E" || formatChange(b[field]) === "\u221E")
|
|
57
|
+
return -1;
|
|
58
|
+
return a[field].ratio - b[field].ratio;
|
|
59
|
+
}
|
|
60
|
+
return b.previous + b.current - (a.previous + a.current);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const ProductEntityTable = ({
|
|
64
|
+
entityLabel,
|
|
65
|
+
entity,
|
|
66
|
+
options
|
|
67
|
+
}) => {
|
|
68
|
+
const classes = useEntityDialogStyles();
|
|
69
|
+
const { baseCurrency } = useConfig();
|
|
70
|
+
const entities = entity.entities[entityLabel];
|
|
71
|
+
const data = Object.assign(
|
|
72
|
+
{
|
|
73
|
+
previousName: "Previous",
|
|
74
|
+
currentName: "Current"
|
|
75
|
+
},
|
|
76
|
+
options
|
|
77
|
+
);
|
|
78
|
+
const firstColClasses = classnames(classes.column, classes.colFirst);
|
|
79
|
+
const lastColClasses = classnames(classes.column, classes.colLast);
|
|
80
|
+
const columns = [
|
|
81
|
+
{
|
|
82
|
+
field: "label",
|
|
83
|
+
title: /* @__PURE__ */ React.createElement(Typography, { className: firstColClasses }, entityLabel),
|
|
84
|
+
render: createRenderer(baseCurrency)("label", classes),
|
|
85
|
+
customSort: createSorter("label"),
|
|
86
|
+
width: "33.33%"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
field: "previous",
|
|
90
|
+
title: /* @__PURE__ */ React.createElement(Typography, { className: classes.column }, data.previousName),
|
|
91
|
+
align: "right",
|
|
92
|
+
render: createRenderer(baseCurrency)("previous", classes),
|
|
93
|
+
customSort: createSorter("previous")
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
field: "current",
|
|
97
|
+
title: /* @__PURE__ */ React.createElement(Typography, { className: classes.column }, data.currentName),
|
|
98
|
+
align: "right",
|
|
99
|
+
render: createRenderer(baseCurrency)("current", classes),
|
|
100
|
+
customSort: createSorter("current")
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
field: "change",
|
|
104
|
+
title: /* @__PURE__ */ React.createElement(Typography, { className: lastColClasses }, "Change"),
|
|
105
|
+
align: "right",
|
|
106
|
+
render: createRenderer(baseCurrency)("change", classes),
|
|
107
|
+
customSort: createSorter("change")
|
|
108
|
+
}
|
|
109
|
+
];
|
|
110
|
+
const rowData = entities.map((e) => ({
|
|
111
|
+
id: e.id || "Unknown",
|
|
112
|
+
label: e.id || "Unknown",
|
|
113
|
+
previous: e.aggregation[0],
|
|
114
|
+
current: e.aggregation[1],
|
|
115
|
+
change: e.change
|
|
116
|
+
})).concat({
|
|
117
|
+
id: "total",
|
|
118
|
+
label: "Total",
|
|
119
|
+
previous: entity.aggregation[0],
|
|
120
|
+
current: entity.aggregation[1],
|
|
121
|
+
change: entity.change
|
|
122
|
+
}).sort(createSorter());
|
|
123
|
+
return /* @__PURE__ */ React.createElement(
|
|
124
|
+
Table,
|
|
125
|
+
{
|
|
126
|
+
columns,
|
|
127
|
+
data: rowData,
|
|
128
|
+
title: entity.id || "Unlabeled",
|
|
129
|
+
options: {
|
|
130
|
+
paging: false,
|
|
131
|
+
search: false,
|
|
132
|
+
hideFilterIcons: true
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export { ProductEntityTable };
|
|
139
|
+
//# sourceMappingURL=ProductEntityTable.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductEntityTable.esm.js","sources":["../../../src/components/ProductInsightsCard/ProductEntityTable.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 React from 'react';\nimport classnames from 'classnames';\nimport Typography from '@material-ui/core/Typography';\nimport { formatChange } from '../../utils/formatters';\nimport { useEntityDialogStyles as useStyles } from '../../utils/styles';\nimport { CostGrowthIndicator } from '../CostGrowth';\nimport { BarChartOptions } from '../../types';\nimport {\n ChangeStatistic,\n Entity,\n} from '@backstage-community/plugin-cost-insights-common';\nimport { Table, TableColumn } from '@backstage/core-components';\nimport { useConfig } from '../../hooks';\n\nexport type ProductEntityTableOptions = Partial<\n Pick<BarChartOptions, 'previousName' | 'currentName'>\n>;\n\ntype RowData = {\n id: string;\n label: string;\n previous: number;\n current: number;\n change: ChangeStatistic;\n};\n\nconst createRenderer =\n (baseCurrency: Intl.NumberFormat) =>\n (col: keyof RowData, classes: Record<string, string>) => {\n return function render(rowData: {}): JSX.Element {\n const row = rowData as RowData;\n const rowStyles = classnames(classes.row, {\n [classes.rowTotal]: row.id === 'total',\n [classes.colFirst]: col === 'label',\n [classes.colLast]: col === 'change',\n });\n\n switch (col) {\n case 'previous':\n case 'current':\n return (\n <Typography className={rowStyles}>\n {baseCurrency.format(row[col])}\n </Typography>\n );\n case 'change':\n return (\n <CostGrowthIndicator\n className={rowStyles}\n change={row.change}\n formatter={formatChange}\n />\n );\n default:\n return <Typography className={rowStyles}>{row.label}</Typography>;\n }\n };\n };\n\n// material-table does not support fixed rows. Override the sorting algorithm\n// to force Total row to bottom by default or when a user sort toggles a column.\nfunction createSorter(field?: keyof Omit<RowData, 'id'>) {\n return function rowSort(data1: {}, data2: {}): number {\n const a = data1 as RowData;\n const b = data2 as RowData;\n if (a.id === 'total') return 1;\n if (b.id === 'total') return 1;\n if (field === 'label') return a.label.localeCompare(b.label);\n if (field === 'change') {\n if (formatChange(a[field]) === '∞' || formatChange(b[field]) === '-∞')\n return 1;\n if (formatChange(a[field]) === '-∞' || formatChange(b[field]) === '∞')\n return -1;\n return a[field].ratio! - b[field].ratio!;\n }\n\n return b.previous + b.current - (a.previous + a.current);\n };\n}\n\ntype ProductEntityTableProps = {\n entityLabel: string;\n entity: Entity;\n options: ProductEntityTableOptions;\n};\n\nexport const ProductEntityTable = ({\n entityLabel,\n entity,\n options,\n}: ProductEntityTableProps) => {\n const classes = useStyles();\n const { baseCurrency } = useConfig();\n const entities = entity.entities[entityLabel];\n\n const data = Object.assign(\n {\n previousName: 'Previous',\n currentName: 'Current',\n },\n options,\n );\n\n const firstColClasses = classnames(classes.column, classes.colFirst);\n const lastColClasses = classnames(classes.column, classes.colLast);\n\n const columns: TableColumn[] = [\n {\n field: 'label',\n title: <Typography className={firstColClasses}>{entityLabel}</Typography>,\n render: createRenderer(baseCurrency)('label', classes),\n customSort: createSorter('label'),\n width: '33.33%',\n },\n {\n field: 'previous',\n title: (\n <Typography className={classes.column}>{data.previousName}</Typography>\n ),\n align: 'right',\n render: createRenderer(baseCurrency)('previous', classes),\n customSort: createSorter('previous'),\n },\n {\n field: 'current',\n title: (\n <Typography className={classes.column}>{data.currentName}</Typography>\n ),\n align: 'right',\n render: createRenderer(baseCurrency)('current', classes),\n customSort: createSorter('current'),\n },\n {\n field: 'change',\n title: <Typography className={lastColClasses}>Change</Typography>,\n align: 'right',\n render: createRenderer(baseCurrency)('change', classes),\n customSort: createSorter('change'),\n },\n ];\n\n const rowData: RowData[] = entities\n .map(e => ({\n id: e.id || 'Unknown',\n label: e.id || 'Unknown',\n previous: e.aggregation[0],\n current: e.aggregation[1],\n change: e.change,\n }))\n .concat({\n id: 'total',\n label: 'Total',\n previous: entity.aggregation[0],\n current: entity.aggregation[1],\n change: entity.change,\n })\n .sort(createSorter());\n\n return (\n <Table\n columns={columns}\n data={rowData}\n title={entity.id || 'Unlabeled'}\n options={{\n paging: false,\n search: false,\n hideFilterIcons: true,\n }}\n />\n );\n};\n"],"names":["useStyles"],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React, { useRef, useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import pluralize from 'pluralize';
|
|
3
|
+
import Typography from '@material-ui/core/Typography';
|
|
4
|
+
import Alert from '@material-ui/lab/Alert';
|
|
5
|
+
import { PeriodSelect } from '../PeriodSelect/PeriodSelect.esm.js';
|
|
6
|
+
import { ProductInsightsChart } from './ProductInsightsChart.esm.js';
|
|
7
|
+
import { useProductInsightsCardStyles } from '../../utils/styles.esm.js';
|
|
8
|
+
import { DefaultLoadingAction } from '../../utils/loading.esm.js';
|
|
9
|
+
import '../../hooks/useConfig.esm.js';
|
|
10
|
+
import '../../hooks/useCurrency.esm.js';
|
|
11
|
+
import '../../hooks/useFilters.esm.js';
|
|
12
|
+
import '../../hooks/useGroups.esm.js';
|
|
13
|
+
import { useLoading } from '../../hooks/useLoading.esm.js';
|
|
14
|
+
import '../../hooks/useScroll.esm.js';
|
|
15
|
+
import { useLastCompleteBillingDate } from '../../hooks/useLastCompleteBillingDate.esm.js';
|
|
16
|
+
import { findAnyKey } from '../../utils/assert.esm.js';
|
|
17
|
+
import { ScrollAnchor } from '../../utils/scroll.esm.js';
|
|
18
|
+
import { InfoCard } from '@backstage/core-components';
|
|
19
|
+
|
|
20
|
+
const mapLoadingToProps = ({ dispatch }) => (isLoading) => dispatch({ [DefaultLoadingAction.CostInsightsProducts]: isLoading });
|
|
21
|
+
const ProductInsightsCard = ({
|
|
22
|
+
initialState,
|
|
23
|
+
product,
|
|
24
|
+
onSelectAsync
|
|
25
|
+
}) => {
|
|
26
|
+
const classes = useProductInsightsCardStyles();
|
|
27
|
+
const mountedRef = useRef(false);
|
|
28
|
+
const [error, setError] = useState(null);
|
|
29
|
+
const dispatchLoading = useLoading(mapLoadingToProps);
|
|
30
|
+
const lastCompleteBillingDate = useLastCompleteBillingDate();
|
|
31
|
+
const [entity, setEntity] = useState(initialState.entity);
|
|
32
|
+
const [duration, setDuration] = useState(initialState.duration);
|
|
33
|
+
const dispatchLoadingProduct = useCallback(dispatchLoading, []);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
async function handleOnSelectAsync() {
|
|
36
|
+
dispatchLoadingProduct(true);
|
|
37
|
+
try {
|
|
38
|
+
const e = await onSelectAsync(product, duration);
|
|
39
|
+
setEntity(e);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
setEntity(null);
|
|
42
|
+
setError(e);
|
|
43
|
+
} finally {
|
|
44
|
+
dispatchLoadingProduct(false);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (mountedRef.current) {
|
|
48
|
+
handleOnSelectAsync();
|
|
49
|
+
} else {
|
|
50
|
+
mountedRef.current = true;
|
|
51
|
+
}
|
|
52
|
+
}, [product, duration, onSelectAsync, dispatchLoadingProduct]);
|
|
53
|
+
const entityKey = findAnyKey(entity?.entities);
|
|
54
|
+
const entities = entityKey ? entity.entities[entityKey] : [];
|
|
55
|
+
const subheader = entityKey && entities.length ? `${pluralize(entityKey, entities.length, true)}, sorted by cost` : null;
|
|
56
|
+
const headerProps = {
|
|
57
|
+
classes,
|
|
58
|
+
action: /* @__PURE__ */ React.createElement(PeriodSelect, { duration, onSelect: setDuration })
|
|
59
|
+
};
|
|
60
|
+
if (error || !entity) {
|
|
61
|
+
return /* @__PURE__ */ React.createElement(InfoCard, { title: product.name, headerProps }, /* @__PURE__ */ React.createElement(ScrollAnchor, { id: product.kind }), /* @__PURE__ */ React.createElement(Alert, { severity: "error" }, error ? error.message : `Error: Could not fetch product insights for ${product.name}`));
|
|
62
|
+
}
|
|
63
|
+
return /* @__PURE__ */ React.createElement(
|
|
64
|
+
InfoCard,
|
|
65
|
+
{
|
|
66
|
+
title: product.name,
|
|
67
|
+
subheader,
|
|
68
|
+
headerProps
|
|
69
|
+
},
|
|
70
|
+
/* @__PURE__ */ React.createElement(ScrollAnchor, { id: product.kind }),
|
|
71
|
+
entities.length ? /* @__PURE__ */ React.createElement(
|
|
72
|
+
ProductInsightsChart,
|
|
73
|
+
{
|
|
74
|
+
entity,
|
|
75
|
+
duration,
|
|
76
|
+
billingDate: lastCompleteBillingDate
|
|
77
|
+
}
|
|
78
|
+
) : /* @__PURE__ */ React.createElement(Typography, null, "There are no ", product.name, " costs within this time frame for your team's projects.")
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export { ProductInsightsCard };
|
|
83
|
+
//# sourceMappingURL=ProductInsightsCard.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductInsightsCard.esm.js","sources":["../../../src/components/ProductInsightsCard/ProductInsightsCard.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 React, {\n PropsWithChildren,\n useCallback,\n useEffect,\n useRef,\n useState,\n} from 'react';\nimport pluralize from 'pluralize';\nimport Typography from '@material-ui/core/Typography';\nimport { default as Alert } from '@material-ui/lab/Alert';\nimport { PeriodSelect } from '../PeriodSelect';\nimport { ProductInsightsChart } from './ProductInsightsChart';\nimport { useProductInsightsCardStyles as useStyles } from '../../utils/styles';\nimport { DefaultLoadingAction } from '../../utils/loading';\nimport { Duration } from '../../types';\nimport {\n Entity,\n Maybe,\n Product,\n} from '@backstage-community/plugin-cost-insights-common';\nimport {\n MapLoadingToProps,\n useLastCompleteBillingDate,\n useLoading,\n} from '../../hooks';\nimport { findAnyKey } from '../../utils/assert';\nimport { ScrollAnchor } from '../../utils/scroll';\nimport { InfoCard } from '@backstage/core-components';\n\ntype LoadingProps = (isLoading: boolean) => void;\n\nexport type ProductInsightsCardProps = {\n product: Product;\n initialState: {\n entity: Maybe<Entity>;\n duration: Duration;\n };\n onSelectAsync: (product: Product, duration: Duration) => Promise<Entity>;\n};\n\nconst mapLoadingToProps: MapLoadingToProps<LoadingProps> =\n ({ dispatch }) =>\n (isLoading: boolean) =>\n dispatch({ [DefaultLoadingAction.CostInsightsProducts]: isLoading });\n\nexport const ProductInsightsCard = ({\n initialState,\n product,\n onSelectAsync,\n}: PropsWithChildren<ProductInsightsCardProps>) => {\n const classes = useStyles();\n const mountedRef = useRef(false);\n const [error, setError] = useState<Maybe<Error>>(null);\n const dispatchLoading = useLoading(mapLoadingToProps);\n const lastCompleteBillingDate = useLastCompleteBillingDate();\n const [entity, setEntity] = useState<Maybe<Entity>>(initialState.entity);\n const [duration, setDuration] = useState<Duration>(initialState.duration);\n\n /* eslint-disable react-hooks/exhaustive-deps */\n const dispatchLoadingProduct = useCallback(dispatchLoading, []);\n /* eslint-enable react-hooks/exhaustive-deps */\n\n useEffect(() => {\n async function handleOnSelectAsync() {\n dispatchLoadingProduct(true);\n try {\n const e = await onSelectAsync(product, duration);\n setEntity(e);\n } catch (e) {\n setEntity(null);\n setError(e);\n } finally {\n dispatchLoadingProduct(false);\n }\n }\n\n if (mountedRef.current) {\n handleOnSelectAsync();\n } else {\n mountedRef.current = true;\n }\n }, [product, duration, onSelectAsync, dispatchLoadingProduct]);\n\n // Only a single entities Record for the root product entity is supported\n const entityKey = findAnyKey(entity?.entities);\n const entities = entityKey ? entity!.entities[entityKey] : [];\n\n const subheader =\n entityKey && entities.length\n ? `${pluralize(entityKey, entities.length, true)}, sorted by cost`\n : null;\n const headerProps = {\n classes: classes,\n action: <PeriodSelect duration={duration} onSelect={setDuration} />,\n };\n\n if (error || !entity) {\n return (\n <InfoCard title={product.name} headerProps={headerProps}>\n <ScrollAnchor id={product.kind} />\n <Alert severity=\"error\">\n {error\n ? error.message\n : `Error: Could not fetch product insights for ${product.name}`}\n </Alert>\n </InfoCard>\n );\n }\n\n return (\n <InfoCard\n title={product.name}\n subheader={subheader}\n headerProps={headerProps}\n >\n <ScrollAnchor id={product.kind} />\n {entities.length ? (\n <ProductInsightsChart\n entity={entity}\n duration={duration}\n billingDate={lastCompleteBillingDate}\n />\n ) : (\n <Typography>\n There are no {product.name} costs within this time frame for your\n team's projects.\n </Typography>\n )}\n </InfoCard>\n );\n};\n"],"names":["useStyles"],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Box from '@material-ui/core/Box';
|
|
3
|
+
import CircularProgress from '@material-ui/core/CircularProgress';
|
|
4
|
+
import Collapse from '@material-ui/core/Collapse';
|
|
5
|
+
import { ProductInsightsCard } from './ProductInsightsCard.esm.js';
|
|
6
|
+
|
|
7
|
+
const ProductInsightsCardList = ({
|
|
8
|
+
initialStates,
|
|
9
|
+
onSelectAsync
|
|
10
|
+
}) => {
|
|
11
|
+
if (!initialStates.length) {
|
|
12
|
+
return /* @__PURE__ */ React.createElement(Box, { display: "flex", justifyContent: "space-around", alignItems: "center" }, /* @__PURE__ */ React.createElement(CircularProgress, null));
|
|
13
|
+
}
|
|
14
|
+
return /* @__PURE__ */ React.createElement(Collapse, { in: true, timeout: 1e3 }, initialStates.map(({ product, entity, duration }) => /* @__PURE__ */ React.createElement(
|
|
15
|
+
Box,
|
|
16
|
+
{
|
|
17
|
+
key: product.kind,
|
|
18
|
+
mb: 6,
|
|
19
|
+
position: "relative",
|
|
20
|
+
"data-testid": `product-list-item-${product.kind}`
|
|
21
|
+
},
|
|
22
|
+
/* @__PURE__ */ React.createElement(
|
|
23
|
+
ProductInsightsCard,
|
|
24
|
+
{
|
|
25
|
+
product,
|
|
26
|
+
onSelectAsync,
|
|
27
|
+
initialState: { entity, duration }
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
)));
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export { ProductInsightsCardList };
|
|
34
|
+
//# sourceMappingURL=ProductInsightsCardList.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductInsightsCardList.esm.js","sources":["../../../src/components/ProductInsightsCard/ProductInsightsCardList.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 React from 'react';\nimport Box from '@material-ui/core/Box';\nimport CircularProgress from '@material-ui/core/CircularProgress';\nimport Collapse from '@material-ui/core/Collapse';\nimport { ProductInsightsCard } from './ProductInsightsCard';\nimport { Duration } from '../../types';\nimport {\n Entity,\n Product,\n} from '@backstage-community/plugin-cost-insights-common';\nimport { ProductState } from '../../utils/loading';\n\ntype ProductInsightsCardListProps = {\n initialStates: ProductState[];\n onSelectAsync: (product: Product, duration: Duration) => Promise<Entity>;\n};\n\nexport const ProductInsightsCardList = ({\n initialStates,\n onSelectAsync,\n}: ProductInsightsCardListProps) => {\n if (!initialStates.length) {\n return (\n <Box display=\"flex\" justifyContent=\"space-around\" alignItems=\"center\">\n <CircularProgress />\n </Box>\n );\n }\n\n return (\n <Collapse in timeout={1000}>\n {initialStates.map(({ product, entity, duration }) => (\n <Box\n key={product.kind}\n mb={6}\n position=\"relative\"\n data-testid={`product-list-item-${product.kind}`}\n >\n <ProductInsightsCard\n product={product}\n onSelectAsync={onSelectAsync}\n initialState={{ entity: entity, duration: duration }}\n />\n </Box>\n ))}\n </Collapse>\n );\n};\n"],"names":[],"mappings":";;;;;;AAiCO,MAAM,0BAA0B,CAAC;AAAA,EACtC,aAAA;AAAA,EACA,aAAA;AACF,CAAoC,KAAA;AAClC,EAAI,IAAA,CAAC,cAAc,MAAQ,EAAA;AACzB,IACE,uBAAA,KAAA,CAAA,aAAA,CAAC,GAAI,EAAA,EAAA,OAAA,EAAQ,MAAO,EAAA,cAAA,EAAe,gBAAe,UAAW,EAAA,QAAA,EAAA,kBAC1D,KAAA,CAAA,aAAA,CAAA,gBAAA,EAAA,IAAiB,CACpB,CAAA,CAAA;AAAA,GAEJ;AAEA,EAAA,uBACG,KAAA,CAAA,aAAA,CAAA,QAAA,EAAA,EAAS,EAAE,EAAA,IAAA,EAAC,OAAS,EAAA,GAAA,EAAA,EACnB,aAAc,CAAA,GAAA,CAAI,CAAC,EAAE,OAAS,EAAA,MAAA,EAAQ,UACrC,qBAAA,KAAA,CAAA,aAAA;AAAA,IAAC,GAAA;AAAA,IAAA;AAAA,MACC,KAAK,OAAQ,CAAA,IAAA;AAAA,MACb,EAAI,EAAA,CAAA;AAAA,MACJ,QAAS,EAAA,UAAA;AAAA,MACT,aAAA,EAAa,CAAqB,kBAAA,EAAA,OAAA,CAAQ,IAAI,CAAA,CAAA;AAAA,KAAA;AAAA,oBAE9C,KAAA,CAAA,aAAA;AAAA,MAAC,mBAAA;AAAA,MAAA;AAAA,QACC,OAAA;AAAA,QACA,aAAA;AAAA,QACA,YAAA,EAAc,EAAE,MAAA,EAAgB,QAAmB,EAAA;AAAA,OAAA;AAAA,KACrD;AAAA,GAEH,CACH,CAAA,CAAA;AAEJ;;;;"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import pluralize from 'pluralize';
|
|
3
|
+
import Box from '@material-ui/core/Box';
|
|
4
|
+
import Typography from '@material-ui/core/Typography';
|
|
5
|
+
import FullScreenIcon from '@material-ui/icons/Fullscreen';
|
|
6
|
+
import { LegendItem } from '../LegendItem/LegendItem.esm.js';
|
|
7
|
+
import { ProductEntityDialog } from './ProductEntityDialog.esm.js';
|
|
8
|
+
import { CostGrowth } from '../CostGrowth/CostGrowth.esm.js';
|
|
9
|
+
import { CostGrowthIndicator } from '../CostGrowth/CostGrowthIndicator.esm.js';
|
|
10
|
+
import { BarChart } from '../BarChart/BarChart.esm.js';
|
|
11
|
+
import { BarChartLegend } from '../BarChart/BarChartLegend.esm.js';
|
|
12
|
+
import { BarChartTooltip } from '../BarChart/BarChartTooltip.esm.js';
|
|
13
|
+
import { BarChartTooltipItem } from '../BarChart/BarChartTooltipItem.esm.js';
|
|
14
|
+
import { assertAlways, findAnyKey, isUndefined, findAlways, notEmpty } from '../../utils/assert.esm.js';
|
|
15
|
+
import { formatPeriod, formatChange } from '../../utils/formatters.esm.js';
|
|
16
|
+
import { resourceOf, isLabeled, isUnlabeled, isInvalid, tooltipItemOf, titleOf } from '../../utils/graphs.esm.js';
|
|
17
|
+
import { useProductInsightsChartStyles, useBarChartLayoutStyles } from '../../utils/styles.esm.js';
|
|
18
|
+
import { choose } from '../../utils/change.esm.js';
|
|
19
|
+
import { useConfig } from '../../hooks/useConfig.esm.js';
|
|
20
|
+
import '../../hooks/useCurrency.esm.js';
|
|
21
|
+
import '../../hooks/useFilters.esm.js';
|
|
22
|
+
import '../../hooks/useGroups.esm.js';
|
|
23
|
+
import '../../hooks/useLoading.esm.js';
|
|
24
|
+
import '../../hooks/useScroll.esm.js';
|
|
25
|
+
import '../../hooks/useLastCompleteBillingDate.esm.js';
|
|
26
|
+
|
|
27
|
+
const ProductInsightsChart = ({
|
|
28
|
+
billingDate,
|
|
29
|
+
entity,
|
|
30
|
+
duration
|
|
31
|
+
}) => {
|
|
32
|
+
const classes = useProductInsightsChartStyles();
|
|
33
|
+
const layoutClasses = useBarChartLayoutStyles();
|
|
34
|
+
const { baseCurrency } = useConfig();
|
|
35
|
+
const entities = useMemo(() => {
|
|
36
|
+
const entityLabel = assertAlways(findAnyKey(entity.entities));
|
|
37
|
+
return entity.entities[entityLabel] ?? [];
|
|
38
|
+
}, [entity]);
|
|
39
|
+
const [activeLabel, setActive] = useState();
|
|
40
|
+
const [selectLabel, setSelected] = useState();
|
|
41
|
+
const isSelected = useMemo(() => !isUndefined(selectLabel), [selectLabel]);
|
|
42
|
+
const isClickable = useMemo(() => {
|
|
43
|
+
const breakdowns = Object.keys(
|
|
44
|
+
entities.find((e) => e.id === activeLabel)?.entities ?? {}
|
|
45
|
+
);
|
|
46
|
+
return breakdowns.length > 0;
|
|
47
|
+
}, [entities, activeLabel]);
|
|
48
|
+
const costStart = entity.aggregation[0];
|
|
49
|
+
const costEnd = entity.aggregation[1];
|
|
50
|
+
const resources = entities.map(resourceOf);
|
|
51
|
+
const options = {
|
|
52
|
+
previousName: formatPeriod(duration, billingDate, false),
|
|
53
|
+
currentName: formatPeriod(duration, billingDate, true)
|
|
54
|
+
};
|
|
55
|
+
const onMouseMove = (data) => {
|
|
56
|
+
if (isLabeled(data)) {
|
|
57
|
+
setActive(data.activeLabel);
|
|
58
|
+
} else if (isUnlabeled(data)) {
|
|
59
|
+
setActive(null);
|
|
60
|
+
} else {
|
|
61
|
+
setActive(void 0);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const onClick = (data) => {
|
|
65
|
+
if (isLabeled(data)) {
|
|
66
|
+
setSelected(data.activeLabel);
|
|
67
|
+
} else if (isUnlabeled(data)) {
|
|
68
|
+
setSelected(null);
|
|
69
|
+
} else {
|
|
70
|
+
setSelected(void 0);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const renderProductInsightsTooltip = ({
|
|
74
|
+
label,
|
|
75
|
+
payload = []
|
|
76
|
+
}) => {
|
|
77
|
+
if (isInvalid({ label, payload })) return null;
|
|
78
|
+
const id = label === "" ? null : label;
|
|
79
|
+
const title = titleOf(label);
|
|
80
|
+
const items = payload.map(tooltipItemOf(baseCurrency)).filter(notEmpty);
|
|
81
|
+
const activeEntity = findAlways(entities, (e) => e.id === id);
|
|
82
|
+
const breakdowns = Object.keys(activeEntity.entities);
|
|
83
|
+
if (breakdowns.length) {
|
|
84
|
+
const subtitle = breakdowns.map((b) => pluralize(b, activeEntity.entities[b].length, true)).join(", ");
|
|
85
|
+
return /* @__PURE__ */ React.createElement(
|
|
86
|
+
BarChartTooltip,
|
|
87
|
+
{
|
|
88
|
+
title,
|
|
89
|
+
subtitle,
|
|
90
|
+
topRight: !!activeEntity.change.ratio && /* @__PURE__ */ React.createElement(
|
|
91
|
+
CostGrowthIndicator,
|
|
92
|
+
{
|
|
93
|
+
formatter: formatChange,
|
|
94
|
+
change: activeEntity.change,
|
|
95
|
+
className: classes.indicator
|
|
96
|
+
}
|
|
97
|
+
),
|
|
98
|
+
actions: /* @__PURE__ */ React.createElement(Box, { className: classes.actions }, /* @__PURE__ */ React.createElement(FullScreenIcon, null), /* @__PURE__ */ React.createElement(Typography, null, "Click for breakdown"))
|
|
99
|
+
},
|
|
100
|
+
items.map((item, index) => /* @__PURE__ */ React.createElement(BarChartTooltipItem, { key: `${item.label}-${index}`, item }))
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return /* @__PURE__ */ React.createElement(
|
|
104
|
+
BarChartTooltip,
|
|
105
|
+
{
|
|
106
|
+
title,
|
|
107
|
+
topRight: !!activeEntity.change.ratio && /* @__PURE__ */ React.createElement(
|
|
108
|
+
CostGrowthIndicator,
|
|
109
|
+
{
|
|
110
|
+
formatter: formatChange,
|
|
111
|
+
change: activeEntity.change,
|
|
112
|
+
className: classes.indicator
|
|
113
|
+
}
|
|
114
|
+
),
|
|
115
|
+
content: id ? null : "This product has costs that are not labeled and therefore can't be attributed to a specific entity."
|
|
116
|
+
},
|
|
117
|
+
items.map((item, index) => /* @__PURE__ */ React.createElement(BarChartTooltipItem, { key: `${item.label}-${index}`, item }))
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
const barChartProps = isClickable ? { onClick } : {};
|
|
121
|
+
return /* @__PURE__ */ React.createElement(Box, { className: layoutClasses.wrapper }, /* @__PURE__ */ React.createElement(BarChartLegend, { costStart, costEnd, options }, /* @__PURE__ */ React.createElement(
|
|
122
|
+
LegendItem,
|
|
123
|
+
{
|
|
124
|
+
title: choose(["Cost Savings", "Cost Excess"], entity.change)
|
|
125
|
+
},
|
|
126
|
+
/* @__PURE__ */ React.createElement(CostGrowth, { change: entity.change, duration })
|
|
127
|
+
)), /* @__PURE__ */ React.createElement(
|
|
128
|
+
BarChart,
|
|
129
|
+
{
|
|
130
|
+
resources,
|
|
131
|
+
tooltip: renderProductInsightsTooltip,
|
|
132
|
+
onMouseMove,
|
|
133
|
+
options,
|
|
134
|
+
...barChartProps
|
|
135
|
+
}
|
|
136
|
+
), isSelected && entities.length && /* @__PURE__ */ React.createElement(
|
|
137
|
+
ProductEntityDialog,
|
|
138
|
+
{
|
|
139
|
+
open: isSelected,
|
|
140
|
+
onClose: () => setSelected(void 0),
|
|
141
|
+
entity: findAlways(entities, (e) => e.id === selectLabel),
|
|
142
|
+
options
|
|
143
|
+
}
|
|
144
|
+
));
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export { ProductInsightsChart };
|
|
148
|
+
//# sourceMappingURL=ProductInsightsChart.esm.js.map
|