Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2d7a030
Adding default_currency varible to use when no location parameter is …
Manas23601 May 29, 2025
c9eb848
default is USD
Manas23601 May 29, 2025
21f812f
feat: Add support for multiple aggregations in cost allocation view
maxvishy May 14, 2025
e890dc3
add default conversation rate
Manas23601 Jun 1, 2025
3db42a1
add necessary components to support currency conversion
Manas23601 Jun 1, 2025
d4fe46d
add constants for allocationData(now RawData first) and conversionrate
Manas23601 Jun 1, 2025
3162ed6
applying conversion rate function
Manas23601 Jun 1, 2025
d8c4f70
fix import errors due to rebase
Manas23601 Jun 1, 2025
19fa131
Fix generateTitle function which was erroring out
Manas23601 Jun 1, 2025
82506c0
mention DEFAULT_CURRENCY variable whereabouts
Manas23601 Jun 1, 2025
8f334fc
fix rebasing issues
Manas23601 Jun 1, 2025
0e4156b
Use free API to do currency conversion
Manas23601 Aug 13, 2025
29b7c03
remove commented code
Manas23601 Aug 13, 2025
effc249
Fix build and publish script
mittal-ishaan Jun 4, 2025
64f11fb
fix rebase
Manas23601 Aug 13, 2025
ebc7c69
reference step outputs as env
mittal-ishaan Jun 4, 2025
8177c7d
fix rebase
Manas23601 Aug 13, 2025
3eab98e
add automation to promote to demo
ameijer Jun 9, 2025
9a2e5d9
debugging builds, refactor
ameijer Jun 9, 2025
89da9bd
bugfix
ameijer Jun 9, 2025
9aeebf1
Bump tspascoal/get-user-teams-membership from 2 to 3
dependabot[bot] Jun 16, 2025
78e48c7
Bump @date-io/core from 1.3.13 to 3.2.0
dependabot[bot] Apr 23, 2025
d9e519e
Bump @babel/core from 7.26.8 to 7.28.0
dependabot[bot] Jul 28, 2025
661e84f
fix images and loading of external costs
reschandreas Jul 27, 2025
f14bf88
Update README.md
SharmaVansh1910 Aug 1, 2025
a4fc663
Remove working directory from build and publish container step
cpetersen5 Aug 7, 2025
bad98ee
Add step to checkout opencost-ui in Build and Publish Release workflow
cpetersen5 Aug 7, 2025
e23b8db
Change order of checkout steps
cpetersen5 Aug 7, 2025
157c760
Merge branch 'main' into default_currency
Manas23601 Aug 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,7 @@ For some use cases such as the case of [OpenCost deployed behind an ingress cont
```sh
$ docker run -p 9091:9090 -e BASE_URL_OVERRIDE=anything -d opencost-ui:latest
```

## Overriding the Default Currency

The UI by default assumes all metrics are in the "USD" currency. if you wish to set the default currency to something else, edit [DEFAULT_CURRENCY](src\constants\defaults.js)
8 changes: 7 additions & 1 deletion src/components/Controls/Edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import InputLabel from '@material-ui/core/InputLabel'
import MenuItem from '@material-ui/core/MenuItem'
import Select from '@material-ui/core/Select'

import React from 'react';
// import React from 'react';
import React, { useState, useEffect } from 'react';

import SelectWindow from '../SelectWindow';

Expand All @@ -25,6 +26,11 @@ function EditControl({
currencyOptions, currency, setCurrency,
}) {
const classes = useStyles();
// Handle multiple aggregations
const handleAggregationChange = (event) => {
const value = event.target.value;
setAggregateBy(value);
};
return (
<div className={classes.wrapper}>
<SelectWindow
Expand Down
4 changes: 4 additions & 0 deletions src/components/Controls/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const Controls = ({
currency,
currencyOptions,
setCurrency,
conversionRate,
setConversionRate,
}) => {

return (
Expand All @@ -35,6 +37,8 @@ const Controls = ({
currency={currency}
currencyOptions={currencyOptions}
setCurrency={setCurrency}
conversionRate={conversionRate}
setConversionRate={setConversionRate}
/>

<DownloadControl
Expand Down
3 changes: 2 additions & 1 deletion src/components/cloudCost/cloudCost.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ import {
import { toCurrency } from "../../util";
import CloudCostChart from "./cloudCostChart";
import { CloudCostRow } from "./cloudCostRow";
import { DEFAULT_CURRENCY } from "../../constants/defaults";

const CloudCost = ({
cumulativeData = [],
totalData: totalsRow = {},
graphData = [],
currency = "USD",
currency = DEFAULT_CURRENCY,
drilldown,
sampleData = false,
}) => {
Expand Down
3 changes: 2 additions & 1 deletion src/components/externalCosts/externalCostsTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ import { useLocation, useHistory } from "react-router";
import { toCurrency } from "../../util";
import { ExternalCostRow } from "./externalCostRow";
import { aggToKeyMapExternalCosts } from "./tokens";
import { DEFAULT_CURRENCY } from "../../constants/defaults";

const ExternalCostsTable = ({
tableData,
currency = "USD",
currency = DEFAULT_CURRENCY,
aggregateBy = "usageUnit",
drilldown,
}) => {
Expand Down
2 changes: 2 additions & 0 deletions src/constants/defaults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const DEFAULT_CURRENCY = "USD";
export const DEFAULT_CONVERSION_RATE = 1.0;
89 changes: 68 additions & 21 deletions src/pages/Allocations.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
toArray,
trim,
} from "lodash";
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useMemo } from "react";
import ReactDOM from "react-dom";
import { useLocation, useHistory } from "react-router";

Expand All @@ -25,14 +25,17 @@ import Page from "../components/Page";
import Footer from "../components/Footer";
import Subtitle from "../components/Subtitle";
import Warnings from "../components/Warnings";
import { getCurrencyConversionRate } from "../services/currency";
import AllocationService from "../services/allocation";
import { currencyCodes } from "../constants/currencyCodes";
import {
checkCustomWindow,
cumulativeToTotals,
rangeToCumulative,
toVerboseTimeRange,
applyConversionRate,
} from "../util";
import { currencyCodes } from "../constants/currencyCodes";
import { DEFAULT_CURRENCY, DEFAULT_CONVERSION_RATE } from "../constants/defaults";

const windowOptions = [
{ name: "Today", value: "today" },
Expand Down Expand Up @@ -87,34 +90,62 @@ function generateTitle({ window, aggregateBy, accumulate }) {
}
}

let aggregationName = get(
find(aggregationOptions, { value: aggregateBy }),
"name",
""
).toLowerCase();
if (aggregationName === "") {
console.warn(`unknown aggregation: ${aggregateBy}`);
let aggregationName = "";
if (Array.isArray(aggregateBy) && aggregateBy.length > 0) {
// If aggregateBy is an array, get names for all selected values
const selectedAggregationNames = aggregateBy.map(val =>
get(find(aggregationOptions, { value: val }), "name", val).toLowerCase()
);

if (selectedAggregationNames.length === 1) {
aggregationName = selectedAggregationNames[0];
} else if (selectedAggregationNames.length === 2) {
aggregationName = selectedAggregationNames.join(" and "); // "namespace and cluster"
} else {
// For three or more, use commas with "and" before the last item
const last = selectedAggregationNames.pop();
aggregationName = `${selectedAggregationNames.join(", ")} and ${last}`; // "namespace, cluster, and node"
}

} else {
// Fallback for single string (if it ever occurs or for initial default)
aggregationName = get(
find(aggregationOptions, { value: aggregateBy }),
"name",
""
).toLowerCase();
if (aggregationName === "") {
console.warn(`unknown aggregation: ${aggregateBy}`);
}
}

let str = `${windowName} by ${aggregationName}`;

if (!accumulate) {
str = `${str} daily`;
}

return str;
}

const ReportsPage = () => {
const classes = useStyles();

// Allocation data state
const [allocationData, setAllocationData] = useState([]);
// Raw data state which will set allocationData when changed
const [rawData, setRawData] = useState([])
const [cumulativeData, setCumulativeData] = useState({});
const [totalData, setTotalData] = useState({});


const [baseCurrency, setBaseCurrency] = useState(DEFAULT_CURRENCY);
const [targetCurrency, setTargetCurrency] = useState(DEFAULT_CURRENCY);
const [conversionRate, setConversionRate] = useState(DEFAULT_CONVERSION_RATE);

const allocationData = useMemo(() => {
return applyConversionRate(rawData, conversionRate);
}, [rawData, conversionRate]);

// When allocation data changes, create a cumulative version of it
useEffect(() => {
useEffect(() => {
const cumulative = rangeToCumulative(allocationData, aggregateBy);
setCumulativeData(toArray(cumulative));
setTotalData(cumulativeToTotals(cumulative));
Expand All @@ -125,8 +156,7 @@ const ReportsPage = () => {
const [window, setWindow] = useState(windowOptions[0].value);
const [aggregateBy, setAggregateBy] = useState(aggregationOptions[0].value);
const [accumulate, setAccumulate] = useState(accumulateOptions[0].value);
const [currency, setCurrency] = useState("USD");


// Report state, including current report and saved options
const [title, setTitle] = useState("Last 7 days by namespace daily");

Expand Down Expand Up @@ -161,8 +191,25 @@ const ReportsPage = () => {
setWindow(searchParams.get("window") || "7d");
setAggregateBy(searchParams.get("agg") || "namespace");
setAccumulate(searchParams.get("acc") === "true" || false);
setCurrency(searchParams.get("currency") || "USD");
setTargetCurrency(searchParams.get("currency") || DEFAULT_CURRENCY);
}, [routerLocation]);

// NEW: Effect to fetch conversion rate whenever targetCurrency changes
useEffect(() => {
const fetchRate = async () => {
if (targetCurrency === baseCurrency || !targetCurrency) {
setConversionRate(1);
return;
}
const rate = await getCurrencyConversionRate(baseCurrency, targetCurrency);
console.log(rate)
if (rate) {
setConversionRate(rate);
setBaseCurrency(targetCurrency); // Update base to new currency after successful conversion
}
};
fetchRate();
}, [targetCurrency]);

async function initialize() {
setInit(true);
Expand All @@ -184,7 +231,7 @@ const ReportsPage = () => {
// update cluster aggregations to use clusterName/clusterId names
allocationRange[i] = sortBy(allocationRange[i], (a) => a.totalCost);
}
setAllocationData(allocationRange);
setRawData(allocationRange);
} else {
if (resp.message && resp.message.indexOf("boundary error") >= 0) {
let match = resp.message.match(/(ETL is \d+\.\d+% complete)/);
Expand All @@ -199,7 +246,7 @@ const ReportsPage = () => {
},
]);
}
setAllocationData([]);
setRawData([]);
}
} catch (err) {
if (err.message.indexOf("404") === 0) {
Expand All @@ -223,7 +270,7 @@ const ReportsPage = () => {
},
]);
}
setAllocationData([]);
setRawData([]);
}

setLoading(false);
Expand Down Expand Up @@ -278,7 +325,7 @@ const ReportsPage = () => {
}}
title={title}
cumulativeData={cumulativeData}
currency={currency}
currency={targetCurrency}
currencyOptions={currencyCodes}
setCurrency={(curr) => {
searchParams.set("currency", curr);
Expand All @@ -301,7 +348,7 @@ const ReportsPage = () => {
allocationData={allocationData}
cumulativeData={cumulativeData}
totalData={totalData}
currency={currency}
currency={targetCurrency}
/>
)}
</Paper>
Expand Down
5 changes: 3 additions & 2 deletions src/pages/CloudCosts.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from "../components/cloudCost/tokens";

import { currencyCodes } from "../constants/currencyCodes";
import { DEFAULT_CURRENCY } from "../constants/defaults";
import CloudCost from "../components/cloudCost/cloudCost";
import { CloudCostDetails } from "../components/cloudCost/cloudCostDetails";

Expand Down Expand Up @@ -53,7 +54,7 @@ const CloudCosts = () => {
costMetricOptions[0].value
);
const [filters, setFilters] = React.useState([]);
const [currency, setCurrency] = React.useState("USD");
const [currency, setCurrency] = React.useState(DEFAULT_CURRENCY);
const [selectedProviderId, setSelectedProviderId] = React.useState("");
const [selectedItemName, setselectedItemName] = React.useState("");
const sampleData = aggregateBy.includes("item");
Expand Down Expand Up @@ -187,7 +188,7 @@ const CloudCosts = () => {
setWindow(searchParams.get("window") || "7d");
setAggregateBy(searchParams.get("agg") || "provider");
setCostMetric(searchParams.get("costMetric") || "AmortizedNetCost");
setCurrency(searchParams.get("currency") || "USD");
setCurrency(searchParams.get("currency") || DEFAULT_CURRENCY);
}, [routerLocation]);

// Initialize once, then fetch report each time setFetch(true) is called
Expand Down
5 changes: 3 additions & 2 deletions src/pages/ExternalCosts.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
aggregationOptions,
costTypeOptions,
} from "../components/externalCosts/tokens";
import { DEFAULT_CURRENCY } from "../constants/defaults";
import ExternalCostsControls from "../components/externalCosts/externalCostsControls";
import ExternalCostsChart from "../components/externalCosts/externalCostsChart";
import ExternalCostsTable from "../components/externalCosts/externalCostsTable";
Expand All @@ -43,7 +44,7 @@ const ExternalCosts = () => {
aggregationOptions[0].value
);
const [filters, setFilters] = React.useState([]);
const [currency, setCurrency] = React.useState("USD");
const [currency, setCurrency] = React.useState(DEFAULT_CURRENCY);

// page and settings state
const [init, setInit] = React.useState(false);
Expand Down Expand Up @@ -213,7 +214,7 @@ const ExternalCosts = () => {
React.useEffect(() => {
setWindow(searchParams.get("window") || "7d");
setAggregateBy(searchParams.get("agg") || "domain");
setCurrency(searchParams.get("currency") || "USD");
setCurrency(searchParams.get("currency") || DEFAULT_CURRENCY);
setCostType(searchParams.get("costType") || "blended");
setSortBy(searchParams.get("sortBy") || "cost");
setSortDirection(searchParams.get("sortDirection") || "desc");
Expand Down
Loading