@letthem/backstage-plugin-cost-insights 0.1.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.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # Cost Insights Frontend Plugin
2
+
3
+ Backstage frontend plugin for EC2 cost visualization.
4
+
5
+ ## What It Shows
6
+
7
+ - Resource-level EC2 cost breakdown
8
+ - Daily trend chart for selected date range
9
+ - Monthly comparison chart
10
+ - Top cost resources
11
+ - Forecast card (month-end estimate)
12
+
13
+ ## Requirements
14
+
15
+ - `@internal/plugin-cost-insights-backend` must be installed and enabled
16
+ - Backend must expose the plugin ID `cost-insights`
17
+
18
+ ## Route Integration
19
+
20
+ ```tsx
21
+ import { CostInsightsPage } from '@internal/plugin-cost-insights';
22
+
23
+ <Route path="/cost-insights/ec2" element={<CostInsightsPage />} />
24
+ ```
25
+
26
+ ## Runtime Behavior
27
+
28
+ - On load, frontend calls backend `/config` to get:
29
+ - environments
30
+ - defaultEnvironment
31
+ - Environment toggle is rendered dynamically from backend config.
32
+ - Data request uses `/product/ec2/insights?intervals=<start>/<end>&environment=<env>`.
33
+
34
+ ## Related Backend Config
35
+
36
+ ```yaml
37
+ costInsights:
38
+ environments: [dev, stg, prod]
39
+ defaultEnvironment: prod
40
+ auth:
41
+ allowUnauthenticated: false
42
+ s3:
43
+ region: ap-northeast-2
44
+ bucket: your-cur-result-bucket
45
+ # Optional for local SSO
46
+ # profile: your-aws-profile
47
+ # Optional if your CUR prefix differs
48
+ dailyPrefixTemplate: '{environment}/daily/{yearMonth}/'
49
+ ```
@@ -0,0 +1,322 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { Page, Header, Content, Progress, TabbedLayout } from '@backstage/core-components';
3
+ import { useApi, discoveryApiRef, fetchApiRef } from '@backstage/core-plugin-api';
4
+ import { Box, Typography, ToggleButtonGroup, ToggleButton, Card, CardContent, Button, Alert } from '@mui/material';
5
+ import { DatePicker } from '@mui/x-date-pickers/DatePicker';
6
+ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
7
+ import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
8
+ import { DateTime } from 'luxon';
9
+ import { useState, useEffect, useMemo } from 'react';
10
+ import { EC2Overview } from './EC2Overview.esm.js';
11
+ import { EC2ResourceTable } from './EC2ResourceTable.esm.js';
12
+
13
+ function EC2CostPage() {
14
+ const discoveryApi = useApi(discoveryApiRef);
15
+ const fetchApi = useApi(fetchApiRef);
16
+ const [environments, setEnvironments] = useState([]);
17
+ const [environment, setEnvironment] = useState("");
18
+ const [loading, setLoading] = useState(true);
19
+ const [error, setError] = useState(null);
20
+ const [resources, setResources] = useState([]);
21
+ const [allResources, setAllResources] = useState([]);
22
+ const [monthlyData, setMonthlyData] = useState([]);
23
+ const [startDate, setStartDate] = useState(
24
+ DateTime.now().startOf("month")
25
+ );
26
+ const [endDate, setEndDate] = useState(DateTime.now());
27
+ const handleStartDateChange = (newDate) => {
28
+ setStartDate(newDate);
29
+ if (newDate && endDate && endDate < newDate) {
30
+ setEndDate(newDate);
31
+ }
32
+ };
33
+ const handleEndDateChange = (newDate) => {
34
+ if (!newDate) {
35
+ setEndDate(newDate);
36
+ return;
37
+ }
38
+ if (startDate && newDate < startDate) {
39
+ return;
40
+ }
41
+ setEndDate(newDate);
42
+ };
43
+ useEffect(() => {
44
+ const fetchPluginConfig = async () => {
45
+ try {
46
+ const baseUrl = await discoveryApi.getBaseUrl("cost-insights");
47
+ const response = await fetchApi.fetch(`${baseUrl}/config`);
48
+ if (!response.ok) {
49
+ throw new Error(
50
+ `Failed to fetch Cost Insights config: ${response.statusText}`
51
+ );
52
+ }
53
+ const data = await response.json();
54
+ const envs = data.environments && data.environments.length > 0 ? data.environments : ["prd"];
55
+ const defaultEnv = envs.includes(data.defaultEnvironment) ? data.defaultEnvironment : envs[0];
56
+ setEnvironments(envs);
57
+ setEnvironment(defaultEnv);
58
+ } catch (err) {
59
+ setError(err instanceof Error ? err.message : "Unknown error");
60
+ } finally {
61
+ setLoading(false);
62
+ }
63
+ };
64
+ fetchPluginConfig();
65
+ }, [discoveryApi, fetchApi]);
66
+ useEffect(() => {
67
+ if (!environment) {
68
+ return;
69
+ }
70
+ const fetchEC2Data = async () => {
71
+ try {
72
+ setLoading(true);
73
+ setError(null);
74
+ const baseUrl = await discoveryApi.getBaseUrl("cost-insights");
75
+ const from = (startDate ?? DateTime.now().startOf("month")).startOf(
76
+ "day"
77
+ );
78
+ const to = (endDate ?? DateTime.now()).endOf("day");
79
+ if (from > to) {
80
+ throw new Error("From date must be before or equal to To date");
81
+ }
82
+ const intervals = `${from.toISO()}/${to.toISO()}`;
83
+ const response = await fetchApi.fetch(
84
+ `${baseUrl}/product/ec2/insights?intervals=${encodeURIComponent(
85
+ intervals
86
+ )}&environment=${encodeURIComponent(environment)}`
87
+ );
88
+ if (!response.ok) {
89
+ throw new Error(`Failed to fetch EC2 data: ${response.statusText}`);
90
+ }
91
+ const data = await response.json();
92
+ const ec2Resources = data.entities?.ec2 || [];
93
+ const monthlyDataFromAPI = data.monthlyData || [];
94
+ setAllResources(ec2Resources);
95
+ setResources(ec2Resources);
96
+ setMonthlyData(monthlyDataFromAPI);
97
+ } catch (err) {
98
+ setError(err instanceof Error ? err.message : "Unknown error");
99
+ } finally {
100
+ setLoading(false);
101
+ }
102
+ };
103
+ fetchEC2Data();
104
+ }, [discoveryApi, fetchApi, environment, startDate, endDate]);
105
+ useEffect(() => {
106
+ if (!startDate || !endDate) {
107
+ setResources(allResources);
108
+ return;
109
+ }
110
+ const filtered = allResources.map((resource) => {
111
+ const filteredDailyCosts = resource.dailyCosts.filter(({ date }) => {
112
+ const dailyDate = DateTime.fromISO(date);
113
+ return dailyDate >= startDate && dailyDate <= endDate;
114
+ });
115
+ const totalCost = filteredDailyCosts.reduce(
116
+ (sum, { cost }) => sum + cost,
117
+ 0
118
+ );
119
+ return {
120
+ ...resource,
121
+ dailyCosts: filteredDailyCosts,
122
+ totalCost
123
+ };
124
+ });
125
+ setResources(filtered);
126
+ }, [allResources, startDate, endDate]);
127
+ const instanceResources = useMemo(() => {
128
+ return resources.filter((r) => r.resourceType === "instance");
129
+ }, [resources]);
130
+ const volumeResources = useMemo(() => {
131
+ return resources.filter(
132
+ (r) => r.resourceType === "snapshot" || r.resourceType === "volume"
133
+ );
134
+ }, [resources]);
135
+ const elasticIpResources = useMemo(() => {
136
+ return resources.filter((r) => r.resourceType === "elastic-ip");
137
+ }, [resources]);
138
+ const natGatewayResources = useMemo(() => {
139
+ return resources.filter((r) => r.resourceType === "nat-gateway");
140
+ }, [resources]);
141
+ const dataTransferResources = useMemo(() => {
142
+ return resources.filter((r) => r.resourceType === "data-transfer");
143
+ }, [resources]);
144
+ const vpcResources = useMemo(() => {
145
+ return resources.filter((r) => r.resourceType === "vpc");
146
+ }, [resources]);
147
+ return /* @__PURE__ */ jsxs(Page, { themeId: "tool", children: [
148
+ /* @__PURE__ */ jsx(
149
+ Header,
150
+ {
151
+ title: "EC2 Cost Analysis",
152
+ subtitle: "Detailed EC2 resource cost breakdown and trends"
153
+ }
154
+ ),
155
+ /* @__PURE__ */ jsxs(Content, { children: [
156
+ /* @__PURE__ */ jsxs(Box, { sx: { mb: 3, display: "flex", alignItems: "center", gap: 2 }, children: [
157
+ /* @__PURE__ */ jsx(
158
+ Typography,
159
+ {
160
+ variant: "subtitle1",
161
+ sx: { fontWeight: 600, color: "text.primary" }
162
+ }
163
+ ),
164
+ /* @__PURE__ */ jsx(
165
+ ToggleButtonGroup,
166
+ {
167
+ value: environment,
168
+ exclusive: true,
169
+ onChange: (_, newEnv) => {
170
+ if (newEnv !== null) {
171
+ setEnvironment(newEnv);
172
+ }
173
+ },
174
+ "aria-label": "environment selection",
175
+ sx: {
176
+ "& .MuiToggleButton-root": {
177
+ px: 3,
178
+ py: 1,
179
+ fontWeight: 600,
180
+ textTransform: "none",
181
+ border: "1px solid",
182
+ borderColor: "divider",
183
+ "&.Mui-selected": {
184
+ backgroundColor: "primary.main",
185
+ color: "primary.contrastText",
186
+ borderColor: "primary.main",
187
+ "&:hover": {
188
+ backgroundColor: "primary.dark",
189
+ borderColor: "primary.dark"
190
+ }
191
+ }
192
+ }
193
+ },
194
+ children: environments.map((env) => /* @__PURE__ */ jsx(
195
+ ToggleButton,
196
+ {
197
+ value: env,
198
+ "aria-label": `${env} environment`,
199
+ children: env
200
+ },
201
+ env
202
+ ))
203
+ }
204
+ )
205
+ ] }),
206
+ /* @__PURE__ */ jsx(Card, { sx: { mb: 3 }, children: /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs(
207
+ Box,
208
+ {
209
+ sx: {
210
+ display: "flex",
211
+ alignItems: "center",
212
+ gap: 2,
213
+ flexWrap: "wrap"
214
+ },
215
+ children: [
216
+ /* @__PURE__ */ jsx(Typography, { variant: "subtitle1", sx: { fontWeight: 600 }, children: "Date Range:" }),
217
+ /* @__PURE__ */ jsxs(LocalizationProvider, { dateAdapter: AdapterLuxon, children: [
218
+ /* @__PURE__ */ jsx(
219
+ DatePicker,
220
+ {
221
+ label: "From",
222
+ value: startDate,
223
+ onChange: handleStartDateChange,
224
+ slotProps: {
225
+ textField: { size: "small", sx: { minWidth: 180 } }
226
+ }
227
+ }
228
+ ),
229
+ /* @__PURE__ */ jsx(Typography, { sx: { mx: 1 }, children: "\u2014" }),
230
+ /* @__PURE__ */ jsx(
231
+ DatePicker,
232
+ {
233
+ label: "To",
234
+ value: endDate,
235
+ onChange: handleEndDateChange,
236
+ minDate: startDate || void 0,
237
+ slotProps: {
238
+ textField: { size: "small", sx: { minWidth: 180 } }
239
+ }
240
+ }
241
+ )
242
+ ] }),
243
+ /* @__PURE__ */ jsx(
244
+ Button,
245
+ {
246
+ variant: "outlined",
247
+ size: "small",
248
+ onClick: () => {
249
+ setStartDate(DateTime.now().startOf("month"));
250
+ setEndDate(DateTime.now());
251
+ },
252
+ children: "Reset to Current Month"
253
+ }
254
+ )
255
+ ]
256
+ }
257
+ ) }) }),
258
+ loading && /* @__PURE__ */ jsx(Progress, {}),
259
+ error && /* @__PURE__ */ jsxs(Alert, { severity: "error", children: [
260
+ "Error loading EC2 cost data: ",
261
+ error
262
+ ] }),
263
+ !loading && !error && resources.length === 0 && monthlyData.length === 0 && /* @__PURE__ */ jsx(Alert, { severity: "info", children: "No EC2 cost data found" }),
264
+ !loading && !error && (resources.length > 0 || monthlyData.length > 0) && /* @__PURE__ */ jsxs(TabbedLayout, { children: [
265
+ /* @__PURE__ */ jsx(TabbedLayout.Route, { path: "/", title: "Overview", children: /* @__PURE__ */ jsx(
266
+ EC2Overview,
267
+ {
268
+ resources,
269
+ monthlyData,
270
+ startDate,
271
+ endDate
272
+ }
273
+ ) }),
274
+ instanceResources.length > 0 && /* @__PURE__ */ jsx(TabbedLayout.Route, { path: "/instances", title: "EC2 Instances", children: /* @__PURE__ */ jsx(
275
+ EC2ResourceTable,
276
+ {
277
+ resources: instanceResources,
278
+ title: "EC2 Instances"
279
+ }
280
+ ) }),
281
+ volumeResources.length > 0 && /* @__PURE__ */ jsx(TabbedLayout.Route, { path: "/volumes", title: "Volumes", children: /* @__PURE__ */ jsx(
282
+ EC2ResourceTable,
283
+ {
284
+ resources: volumeResources,
285
+ title: "Volumes & Snapshots"
286
+ }
287
+ ) }),
288
+ elasticIpResources.length > 0 && /* @__PURE__ */ jsx(TabbedLayout.Route, { path: "/elastic-ip", title: "Elastic IP", children: /* @__PURE__ */ jsx(
289
+ EC2ResourceTable,
290
+ {
291
+ resources: elasticIpResources,
292
+ title: "Elastic IP Addresses"
293
+ }
294
+ ) }),
295
+ natGatewayResources.length > 0 && /* @__PURE__ */ jsx(TabbedLayout.Route, { path: "/nat-gateway", title: "NAT Gateway", children: /* @__PURE__ */ jsx(
296
+ EC2ResourceTable,
297
+ {
298
+ resources: natGatewayResources,
299
+ title: "NAT Gateway"
300
+ }
301
+ ) }),
302
+ dataTransferResources.length > 0 && /* @__PURE__ */ jsx(TabbedLayout.Route, { path: "/data-transfer", title: "Data Transfer", children: /* @__PURE__ */ jsx(
303
+ EC2ResourceTable,
304
+ {
305
+ resources: dataTransferResources,
306
+ title: "Data Transfer"
307
+ }
308
+ ) }),
309
+ vpcResources.length > 0 && /* @__PURE__ */ jsx(TabbedLayout.Route, { path: "/vpc", title: "VPC", children: /* @__PURE__ */ jsx(
310
+ EC2ResourceTable,
311
+ {
312
+ resources: vpcResources,
313
+ title: "VPC Endpoints"
314
+ }
315
+ ) })
316
+ ] })
317
+ ] })
318
+ ] });
319
+ }
320
+
321
+ export { EC2CostPage };
322
+ //# sourceMappingURL=EC2CostPage.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EC2CostPage.esm.js","sources":["../../src/components/EC2CostPage.tsx"],"sourcesContent":["import {\n Content,\n Header,\n Page,\n Progress,\n TabbedLayout,\n} from '@backstage/core-components';\nimport {\n discoveryApiRef,\n fetchApiRef,\n useApi,\n} from '@backstage/core-plugin-api';\nimport {\n Alert,\n Box,\n Button,\n Card,\n CardContent,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n} from '@mui/material';\nimport { DatePicker } from '@mui/x-date-pickers/DatePicker';\nimport { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';\nimport { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';\nimport { DateTime } from 'luxon';\nimport { useEffect, useMemo, useState } from 'react';\nimport { EC2Overview } from './EC2Overview';\nimport { EC2ResourceTable } from './EC2ResourceTable';\n\ninterface EC2Resource {\n resourceId: string;\n resourceType:\n | 'instance'\n | 'elastic-ip'\n | 'other'\n | 'snapshot'\n | 'volume'\n | 'nat-gateway'\n | 'data-transfer'\n | 'vpc';\n instanceType?: string;\n volumeType?: string;\n volumeSize?: number;\n totalCost: number;\n usageAmount: number;\n dailyCosts: Array<{\n date: string;\n cost: number;\n }>;\n}\n\ninterface CostInsightsPluginConfig {\n environments: string[];\n defaultEnvironment: string;\n}\n\nexport function EC2CostPage() {\n const discoveryApi = useApi(discoveryApiRef);\n const fetchApi = useApi(fetchApiRef);\n\n const [environments, setEnvironments] = useState<string[]>([]);\n const [environment, setEnvironment] = useState<string>('');\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [resources, setResources] = useState<EC2Resource[]>([]);\n const [allResources, setAllResources] = useState<EC2Resource[]>([]);\n const [monthlyData, setMonthlyData] = useState<\n Array<{\n month: string;\n instances: number;\n volume: number;\n elasticIp: number;\n natGateway: number;\n dataTransfer: number;\n vpc: number;\n total: number;\n }>\n >([]);\n\n const [startDate, setStartDate] = useState<DateTime | null>(\n DateTime.now().startOf('month'),\n );\n const [endDate, setEndDate] = useState<DateTime | null>(DateTime.now());\n\n const handleStartDateChange = (newDate: DateTime | null) => {\n setStartDate(newDate);\n if (newDate && endDate && endDate < newDate) {\n setEndDate(newDate);\n }\n };\n\n const handleEndDateChange = (newDate: DateTime | null) => {\n if (!newDate) {\n setEndDate(newDate);\n return;\n }\n\n if (startDate && newDate < startDate) {\n return;\n }\n\n setEndDate(newDate);\n };\n\n useEffect(() => {\n const fetchPluginConfig = async () => {\n try {\n const baseUrl = await discoveryApi.getBaseUrl('cost-insights');\n const response = await fetchApi.fetch(`${baseUrl}/config`);\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch Cost Insights config: ${response.statusText}`,\n );\n }\n\n const data = (await response.json()) as CostInsightsPluginConfig;\n const envs =\n data.environments && data.environments.length > 0\n ? data.environments\n : ['prd'];\n const defaultEnv = envs.includes(data.defaultEnvironment)\n ? data.defaultEnvironment\n : envs[0];\n\n setEnvironments(envs);\n setEnvironment(defaultEnv);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Unknown error');\n } finally {\n setLoading(false);\n }\n };\n\n fetchPluginConfig();\n }, [discoveryApi, fetchApi]);\n\n useEffect(() => {\n if (!environment) {\n return;\n }\n\n const fetchEC2Data = async () => {\n try {\n setLoading(true);\n setError(null);\n\n const baseUrl = await discoveryApi.getBaseUrl('cost-insights');\n\n const from = (startDate ?? DateTime.now().startOf('month')).startOf(\n 'day',\n );\n const to = (endDate ?? DateTime.now()).endOf('day');\n if (from > to) {\n throw new Error('From date must be before or equal to To date');\n }\n const intervals = `${from.toISO()}/${to.toISO()}`;\n\n const response = await fetchApi.fetch(\n `${baseUrl}/product/ec2/insights?intervals=${encodeURIComponent(\n intervals,\n )}&environment=${encodeURIComponent(environment)}`,\n );\n\n if (!response.ok) {\n throw new Error(`Failed to fetch EC2 data: ${response.statusText}`);\n }\n\n const data = await response.json();\n const ec2Resources = data.entities?.ec2 || [];\n const monthlyDataFromAPI = data.monthlyData || [];\n\n setAllResources(ec2Resources);\n setResources(ec2Resources);\n setMonthlyData(monthlyDataFromAPI);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Unknown error');\n } finally {\n setLoading(false);\n }\n };\n\n fetchEC2Data();\n }, [discoveryApi, fetchApi, environment, startDate, endDate]);\n\n useEffect(() => {\n if (!startDate || !endDate) {\n setResources(allResources);\n return;\n }\n\n const filtered = allResources.map((resource) => {\n const filteredDailyCosts = resource.dailyCosts.filter(({ date }) => {\n const dailyDate = DateTime.fromISO(date);\n return dailyDate >= startDate && dailyDate <= endDate;\n });\n\n const totalCost = filteredDailyCosts.reduce(\n (sum, { cost }) => sum + cost,\n 0,\n );\n\n return {\n ...resource,\n dailyCosts: filteredDailyCosts,\n totalCost,\n };\n });\n\n setResources(filtered);\n }, [allResources, startDate, endDate]);\n\n const instanceResources = useMemo(() => {\n return resources.filter((r) => r.resourceType === 'instance');\n }, [resources]);\n\n const volumeResources = useMemo(() => {\n return resources.filter(\n (r) => r.resourceType === 'snapshot' || r.resourceType === 'volume',\n );\n }, [resources]);\n\n const elasticIpResources = useMemo(() => {\n return resources.filter((r) => r.resourceType === 'elastic-ip');\n }, [resources]);\n\n const natGatewayResources = useMemo(() => {\n return resources.filter((r) => r.resourceType === 'nat-gateway');\n }, [resources]);\n\n const dataTransferResources = useMemo(() => {\n return resources.filter((r) => r.resourceType === 'data-transfer');\n }, [resources]);\n\n const vpcResources = useMemo(() => {\n return resources.filter((r) => r.resourceType === 'vpc');\n }, [resources]);\n\n return (\n <Page themeId=\"tool\">\n <Header\n title=\"EC2 Cost Analysis\"\n subtitle=\"Detailed EC2 resource cost breakdown and trends\"\n />\n <Content>\n <Box sx={{ mb: 3, display: 'flex', alignItems: 'center', gap: 2 }}>\n <Typography\n variant=\"subtitle1\"\n sx={{ fontWeight: 600, color: 'text.primary' }}\n ></Typography>\n <ToggleButtonGroup\n value={environment}\n exclusive\n onChange={(_, newEnv: string | null) => {\n if (newEnv !== null) {\n setEnvironment(newEnv);\n }\n }}\n aria-label=\"environment selection\"\n sx={{\n '& .MuiToggleButton-root': {\n px: 3,\n py: 1,\n fontWeight: 600,\n textTransform: 'none',\n border: '1px solid',\n borderColor: 'divider',\n '&.Mui-selected': {\n backgroundColor: 'primary.main',\n color: 'primary.contrastText',\n borderColor: 'primary.main',\n '&:hover': {\n backgroundColor: 'primary.dark',\n borderColor: 'primary.dark',\n },\n },\n },\n }}\n >\n {environments.map((env) => (\n <ToggleButton\n key={env}\n value={env}\n aria-label={`${env} environment`}\n >\n {env}\n </ToggleButton>\n ))}\n </ToggleButtonGroup>\n </Box>\n\n <Card sx={{ mb: 3 }}>\n <CardContent>\n <Box\n sx={{\n display: 'flex',\n alignItems: 'center',\n gap: 2,\n flexWrap: 'wrap',\n }}\n >\n <Typography variant=\"subtitle1\" sx={{ fontWeight: 600 }}>\n Date Range:\n </Typography>\n <LocalizationProvider dateAdapter={AdapterLuxon}>\n <DatePicker\n label=\"From\"\n value={startDate}\n onChange={handleStartDateChange}\n slotProps={{\n textField: { size: 'small', sx: { minWidth: 180 } },\n }}\n />\n <Typography sx={{ mx: 1 }}>—</Typography>\n <DatePicker\n label=\"To\"\n value={endDate}\n onChange={handleEndDateChange}\n minDate={startDate || undefined}\n slotProps={{\n textField: { size: 'small', sx: { minWidth: 180 } },\n }}\n />\n </LocalizationProvider>\n <Button\n variant=\"outlined\"\n size=\"small\"\n onClick={() => {\n setStartDate(DateTime.now().startOf('month'));\n setEndDate(DateTime.now());\n }}\n >\n Reset to Current Month\n </Button>\n </Box>\n </CardContent>\n </Card>\n {loading && <Progress />}\n\n {error && (\n <Alert severity=\"error\">Error loading EC2 cost data: {error}</Alert>\n )}\n\n {!loading &&\n !error &&\n resources.length === 0 &&\n monthlyData.length === 0 && (\n <Alert severity=\"info\">No EC2 cost data found</Alert>\n )}\n\n {!loading &&\n !error &&\n (resources.length > 0 || monthlyData.length > 0) && (\n <TabbedLayout>\n <TabbedLayout.Route path=\"/\" title=\"Overview\">\n <EC2Overview\n resources={resources}\n monthlyData={monthlyData}\n startDate={startDate}\n endDate={endDate}\n />\n </TabbedLayout.Route>\n\n {instanceResources.length > 0 && (\n <TabbedLayout.Route path=\"/instances\" title=\"EC2 Instances\">\n <EC2ResourceTable\n resources={instanceResources}\n title=\"EC2 Instances\"\n />\n </TabbedLayout.Route>\n )}\n\n {volumeResources.length > 0 && (\n <TabbedLayout.Route path=\"/volumes\" title=\"Volumes\">\n <EC2ResourceTable\n resources={volumeResources}\n title=\"Volumes & Snapshots\"\n />\n </TabbedLayout.Route>\n )}\n\n {elasticIpResources.length > 0 && (\n <TabbedLayout.Route path=\"/elastic-ip\" title=\"Elastic IP\">\n <EC2ResourceTable\n resources={elasticIpResources}\n title=\"Elastic IP Addresses\"\n />\n </TabbedLayout.Route>\n )}\n\n {natGatewayResources.length > 0 && (\n <TabbedLayout.Route path=\"/nat-gateway\" title=\"NAT Gateway\">\n <EC2ResourceTable\n resources={natGatewayResources}\n title=\"NAT Gateway\"\n />\n </TabbedLayout.Route>\n )}\n\n {dataTransferResources.length > 0 && (\n <TabbedLayout.Route path=\"/data-transfer\" title=\"Data Transfer\">\n <EC2ResourceTable\n resources={dataTransferResources}\n title=\"Data Transfer\"\n />\n </TabbedLayout.Route>\n )}\n\n {vpcResources.length > 0 && (\n <TabbedLayout.Route path=\"/vpc\" title=\"VPC\">\n <EC2ResourceTable\n resources={vpcResources}\n title=\"VPC Endpoints\"\n />\n </TabbedLayout.Route>\n )}\n </TabbedLayout>\n )}\n </Content>\n </Page>\n );\n}\n"],"names":[],"mappings":"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"}