Updates based off of @freben's code review

* Reduced changeset to `minor`
* Fixed indentation and quoting in README
* yarn.lock update for rebased versions
* ran `prettier` against all .js and .ts files and fixed errors

Signed-off-by: Matt Ray <github@mattray.dev>
This commit is contained in:
Matt Ray
2023-08-02 17:16:49 +10:00
committed by Fredrik Adelöw
parent 1380a689ab
commit 6961967fb4
27 changed files with 753 additions and 415 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
---
'@backstage/plugin-opencost': major
'@backstage/plugin-opencost': minor
---
New OpenCost plugin provides an port of the latest OpenCost UI to Backstage with updated dependencies. The plugin's README covers installation and configuration
+10 -10
View File
@@ -15,18 +15,18 @@ All of the code was originally ported from https://github.com/opencost/opencost/
```
2. Add the `OpenCostPage` to your `packages/app/src/App.tsx`:
```tsx
import { OpenCostPage } from '@backstage/plugin-opencost';
```
```tsx
import { OpenCostPage } from '@backstage/plugin-opencost';
```
and
```tsx
<FlatRoutes>
<Route path="/opencost" element={<OpenCostPage />} />
</FlatRoutes>
```
```tsx
<FlatRoutes>
<Route path="/opencost" element={<OpenCostPage />} />
</FlatRoutes>
```
3. Add link to OpenCost to your sidebar
@@ -74,6 +74,6 @@ opencost:
- Allow for user-provided default reports and/or disabling controls
- Support multiple hard-coded reports
- clean up deprecation warnings and upgrade to all the latest React components
- Fork(?) to support Kubecost, which could provide Alerts and Recommendations, similar to the Cost Explorer plugin
- Fork(?) to support `Kubecost`, which could provide Alerts and Recommendations, similar to the Cost Explorer plugin
![Screenshot](screenshot.png)
+4 -3
View File
@@ -15,13 +15,14 @@
*/
export interface Config {
/** Configurations for the OpenCost plugin
* opencost config
/**
* Configurations for the OpenCost plugin
*
* @visibility frontend
*/
opencost?: {
/**
* The ppbase URL for the OpenCost API
* The base URL for the OpenCost API
* @visibility frontend
*/
baseUrl: string;
+2 -2
View File
@@ -31,10 +31,11 @@
"@material-ui/lab": "^4.0.0-alpha.60",
"@material-ui/pickers": "^3.3.10",
"@material-ui/styles": "^4.11.5",
"axios": "^1.4.0",
"date-fns": "^2.30.0",
"lodash": "^4.17.21",
"react-use": "^17.2.4",
"recharts": "^2.7.0"
"recharts": "^2.5.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0",
@@ -48,7 +49,6 @@
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"@types/node": "*",
"cross-fetch": "^3.1.5",
"msw": "^1.0.0"
},
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
BarChart,
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
import { primary, greyscale, browns } from '../../constants/colors';
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { isArray, filter, map, reduce, reverse, sortBy } from 'lodash';
@@ -13,84 +13,89 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import { get, forEach, reverse, round, sortBy } from 'lodash'
import ExportIcon from '@material-ui/icons/GetApp'
import IconButton from '@material-ui/core/IconButton'
import Tooltip from '@material-ui/core/Tooltip'
import React from 'react';
import { get, forEach, reverse, round, sortBy } from 'lodash';
import ExportIcon from '@material-ui/icons/GetApp';
import IconButton from '@material-ui/core/IconButton';
import Tooltip from '@material-ui/core/Tooltip';
const columns = [
{
head: "Name",
prop: "name",
head: 'Name',
prop: 'name',
currency: false,
}, {
head: "CPU",
prop: "cpuCost",
},
{
head: 'CPU',
prop: 'cpuCost',
currency: true,
}, {
head: "GPU",
prop: "gpuCost",
},
{
head: 'GPU',
prop: 'gpuCost',
currency: true,
}, {
head: "RAM",
prop: "ramCost",
},
{
head: 'RAM',
prop: 'ramCost',
currency: true,
}, {
head: "PV",
prop: "pvCost",
},
{
head: 'PV',
prop: 'pvCost',
currency: true,
}, {
head: "Network",
prop: "networkCost",
},
{
head: 'Network',
prop: 'networkCost',
currency: true,
}, {
head: "Shared",
prop: "sharedCost",
},
{
head: 'Shared',
prop: 'sharedCost',
currency: true,
}, {
head: "Total",
prop: "totalCost",
},
{
head: 'Total',
prop: 'totalCost',
currency: true,
}
]
},
];
const toCSVLine = (datum) => {
const cols = []
const toCSVLine = datum => {
const cols = [];
forEach(columns, c => {
if (c.currency) {
cols.push(round(get(datum, c.prop, 0.0), 2))
cols.push(round(get(datum, c.prop, 0.0), 2));
} else {
cols.push(`"${get(datum, c.prop, "")}"`)
cols.push(`"${get(datum, c.prop, '')}"`);
}
})
});
return cols.join(',')
}
return cols.join(',');
};
const DownloadControl = ({
cumulativeData,
title,
}) => {
const DownloadControl = ({ cumulativeData, title }) => {
// downloadReport downloads a CSV of the cumulative allocation data
function downloadReport() {
// Build CSV
const head = columns.map(c => c.head).join(',')
const body = reverse(sortBy(cumulativeData, 'totalCost')).map(toCSVLine).join('\r\n')
const csv = `${head}\r\n${body}`
const head = columns.map(c => c.head).join(',');
const body = reverse(sortBy(cumulativeData, 'totalCost'))
.map(toCSVLine)
.join('\r\n');
const csv = `${head}\r\n${body}`;
// Create download link
const a = document.createElement("a")
a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }))
const filename = title.toLocaleLowerCase('en-US').replace(/\s/gi, '-')
a.setAttribute("download", `${filename}-${Date.now()}.csv`)
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }));
const filename = title.toLocaleLowerCase('en-US').replace(/\s/gi, '-');
a.setAttribute('download', `${filename}-${Date.now()}.csv`);
// Click the link
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
return (
@@ -99,7 +104,7 @@ const DownloadControl = ({
<ExportIcon />
</IconButton>
</Tooltip>
)
}
);
};
export default React.memo(DownloadControl)
export default React.memo(DownloadControl);
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { makeStyles } from '@material-ui/styles';
import FormControl from '@material-ui/core/FormControl';
import InputLabel from '@material-ui/core/InputLabel';
@@ -13,10 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import DownloadControl from './Download'
import EditControl from './Edit'
import React from 'react';
import DownloadControl from './Download';
import EditControl from './Edit';
const Controls = ({
windowOptions,
@@ -34,7 +33,6 @@ const Controls = ({
currencyOptions,
setCurrency,
}) => {
return (
<div>
<EditControl
@@ -51,12 +49,9 @@ const Controls = ({
currencyOptions={currencyOptions}
setCurrency={setCurrency}
/>
<DownloadControl
cumulativeData={cumulativeData}
title={title}
/>
<DownloadControl cumulativeData={cumulativeData} title={title} />
</div>
)
}
);
};
export default React.memo(Controls)
export default React.memo(Controls);
@@ -37,6 +37,8 @@ describe('OpenCostPage', () => {
it('should render', async () => {
await renderInTestApp(<OpenCostPage />);
expect(screen.getByText('Open source Kubernetes cloud cost monitoring')).toBeInTheDocument();
expect(
screen.getByText('Open source Kubernetes cloud cost monitoring'),
).toBeInTheDocument();
});
});
@@ -38,10 +38,10 @@ export const OpenCostPage = () => (
/>
</a>
</Header>
<Content>
<Content>
<Grid container spacing={3} direction="column">
<Grid item>
<OpenCostReport />
<OpenCostReport />
</Grid>
</Grid>
</Content>
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// code ported from https://github.com/opencost/opencost/blob/develop/ui/src/Reports.js
import React, { useEffect, useState } from 'react';
+9 -11
View File
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import { makeStyles } from '@material-ui/styles'
import React from 'react'
import { makeStyles } from '@material-ui/styles';
import React from 'react';
const useStyles = makeStyles({
wrapper: {
@@ -29,21 +29,19 @@ const useStyles = makeStyles({
display: 'flex',
flexFlow: 'column',
flexGrow: 1,
}
})
},
});
const Page = props => {
const classes = useStyles()
const classes = useStyles();
return (
<div className={classes.flexGrow}>
<div className={classes.wrapper}>
<div className={classes.flexGrow}>
{props.children}
</div>
<div className={classes.flexGrow}>{props.children}</div>
</div>
</div>
)
}
);
};
export default Page
export default Page;
+114 -106
View File
@@ -14,19 +14,22 @@
* limitations under the License.
*/
import React, { useEffect, useState } from 'react'
import { makeStyles } from '@material-ui/styles'
import { endOfDay, startOfDay } from 'date-fns'
import { MuiPickersUtilsProvider, KeyboardDatePicker } from '@material-ui/pickers'
import Button from '@material-ui/core/Button'
import DateFnsUtils from '@date-io/date-fns'
import FormControl from '@material-ui/core/FormControl'
import Link from '@material-ui/core/Link'
import Popover from '@material-ui/core/Popover'
import TextField from '@material-ui/core/TextField'
import Typography from '@material-ui/core/Typography'
import { isValid } from 'date-fns'
import { find, get } from 'lodash'
import React, { useEffect, useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import { endOfDay, startOfDay } from 'date-fns';
import {
MuiPickersUtilsProvider,
KeyboardDatePicker,
} from '@material-ui/pickers';
import Button from '@material-ui/core/Button';
import DateFnsUtils from '@date-io/date-fns';
import FormControl from '@material-ui/core/FormControl';
import Link from '@material-ui/core/Link';
import Popover from '@material-ui/core/Popover';
import TextField from '@material-ui/core/TextField';
import Typography from '@material-ui/core/Typography';
import { isValid } from 'date-fns';
import { find, get } from 'lodash';
const useStyles = makeStyles({
dateContainer: {
@@ -45,96 +48,99 @@ const useStyles = makeStyles({
margin: 8,
width: 120,
},
})
});
const SelectWindow = ({ windowOptions, window, setWindow }) => {
const classes = useStyles()
const [anchorEl, setAnchorEl] = useState(null)
const classes = useStyles();
const [anchorEl, setAnchorEl] = useState(null);
const [startDate, setStartDate] = useState(null)
const [endDate, setEndDate] = useState(null)
const [intervalString, setIntervalString] = useState(null)
const [startDate, setStartDate] = useState(null);
const [endDate, setEndDate] = useState(null);
const [intervalString, setIntervalString] = useState(null);
const handleClick = (event) => {
setAnchorEl(event.currentTarget)
const handleClick = event => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleStartDateChange = date => {
if (isValid(date)) {
setStartDate(startOfDay(date));
}
};
const handleClose = () => {
setAnchorEl(null)
const handleEndDateChange = date => {
if (isValid(date)) {
setEndDate(endOfDay(date));
}
};
const handleStartDateChange = (date) => {
if (isValid(date)) {
setStartDate(startOfDay(date))
}
const handleSubmitPresetDates = dateString => {
setWindow(dateString);
setStartDate(null);
setEndDate(null);
handleClose();
};
const handleSubmitCustomDates = () => {
if (intervalString !== null) {
setWindow(intervalString);
handleClose();
}
};
const handleEndDateChange = (date) => {
if (isValid(date)) {
setEndDate(endOfDay(date))
}
useEffect(() => {
if (startDate !== null && endDate !== null) {
// Note: getTimezoneOffset() is calculated based on current system locale, NOT date object
const adjustedStartDate = new Date(
startDate - startDate.getTimezoneOffset() * 60000,
);
const adjustedEndDate = new Date(
endDate - endDate.getTimezoneOffset() * 60000,
);
setIntervalString(
`${adjustedStartDate.toISOString().split('.')[0]}Z` +
`,${adjustedEndDate.toISOString().split('.')[0]}Z`,
);
}
}, [startDate, endDate]);
const handleSubmitPresetDates = (dateString) => {
setWindow(dateString)
setStartDate(null)
setEndDate(null)
handleClose()
}
const open = Boolean(anchorEl);
const id = open ? 'date-range-popover' : undefined;
const handleSubmitCustomDates = () => {
if (intervalString !== null) {
setWindow(intervalString)
handleClose()
}
}
useEffect(() => {
if (startDate !== null && endDate !== null) {
// Note: getTimezoneOffset() is calculated based on current system locale, NOT date object
const adjustedStartDate = new Date(startDate - startDate.getTimezoneOffset() * 60000)
const adjustedEndDate = new Date(endDate - endDate.getTimezoneOffset() * 60000)
setIntervalString(
`${adjustedStartDate.toISOString().split('.')[0] }Z`
+ `,${
adjustedEndDate.toISOString().split('.')[0] }Z`
)
}
}, [startDate, endDate])
const open = Boolean(anchorEl)
const id = open ? 'date-range-popover' : undefined
return (
<>
<FormControl className={classes.formControl}>
<TextField
return (
<>
<FormControl className={classes.formControl}>
<TextField
id="filled-read-only-input"
label="Date Range"
value={get(find(windowOptions, { value: window }), "name", "Custom")}
value={get(find(windowOptions, { value: window }), 'name', 'Custom')}
onClick={e => handleClick(e)}
inputProps={{
readOnly: true,
style: { cursor: 'pointer' },
}}
/>
</FormControl>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
>
<div className={classes.dateContainer}>
<div className={classes.dateContainerColumn}>
/>
</FormControl>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
>
<div className={classes.dateContainer}>
<div className={classes.dateContainerColumn}>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardDatePicker
style={{ width: '144px' }}
@@ -171,36 +177,38 @@ const SelectWindow = ({ windowOptions, window, setWindow }) => {
}}
/>
</MuiPickersUtilsProvider>
<div>
<Button
style={{ marginTop: 16 }}
variant="contained"
color="default"
onClick={handleSubmitCustomDates}
>
Apply
</Button>
</div>
</div>
<div className={classes.dateContainerColumn} style={{ paddingTop: 12, marginLeft: 18 }}>
{windowOptions.map(opt =>
(<Typography key={opt.value}
<div>
<Button
style={{ marginTop: 16 }}
variant="contained"
color="default"
onClick={handleSubmitCustomDates}
>
Apply
</Button>
</div>
</div>
<div
className={classes.dateContainerColumn}
style={{ paddingTop: 12, marginLeft: 18 }}
>
{windowOptions.map(opt => (
<Typography key={opt.value}>
<Link
style={{ cursor: "pointer" }}
style={{ cursor: 'pointer' }}
key={opt.value}
value={opt.value}
onClick={() => handleSubmitPresetDates(opt.value)}
>
{opt.name}
</Link>
</Typography>)
)}
</div>
</Typography>
))}
</div>
</Popover>
</>
)
}
</div>
</Popover>
</>
);
};
export default React.memo(SelectWindow)
export default React.memo(SelectWindow);
+16 -14
View File
@@ -14,12 +14,12 @@
* limitations under the License.
*/
import React from 'react'
import { makeStyles } from '@material-ui/styles'
import { upperFirst } from 'lodash'
import Breadcrumbs from '@material-ui/core/Breadcrumbs'
import NavigateNextIcon from '@material-ui/icons/NavigateNext'
import Typography from '@material-ui/core/Typography'
import React from 'react';
import { makeStyles } from '@material-ui/styles';
import { upperFirst } from 'lodash';
import Breadcrumbs from '@material-ui/core/Breadcrumbs';
import NavigateNextIcon from '@material-ui/icons/NavigateNext';
import Typography from '@material-ui/core/Typography';
import { toVerboseTimeRange } from '../util';
const useStyles = makeStyles({
@@ -29,14 +29,14 @@ const useStyles = makeStyles({
},
},
link: {
cursor: "pointer",
cursor: 'pointer',
},
})
});
const Subtitle = ({ report }) => {
const classes = useStyles()
const classes = useStyles();
const { aggregateBy, window } = report
const { aggregateBy, window } = report;
return (
<div className={classes.root}>
@@ -45,13 +45,15 @@ const Subtitle = ({ report }) => {
aria-label="breadcrumb"
>
{aggregateBy && aggregateBy.length > 0 ? (
<Typography>{toVerboseTimeRange(window)} by {upperFirst(aggregateBy)}</Typography>
<Typography>
{toVerboseTimeRange(window)} by {upperFirst(aggregateBy)}
</Typography>
) : (
<Typography>{toVerboseTimeRange(window)}</Typography>
)}
</Breadcrumbs>
</div>
)
}
);
};
export default React.memo(Subtitle)
export default React.memo(Subtitle);
+15 -15
View File
@@ -14,24 +14,24 @@
* limitations under the License.
*/
import React from 'react'
import { makeStyles } from '@material-ui/styles'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import Paper from '@material-ui/core/Paper'
import WarningIcon from '@material-ui/icons/Warning'
import React from 'react';
import { makeStyles } from '@material-ui/styles';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import Paper from '@material-ui/core/Paper';
import WarningIcon from '@material-ui/icons/Warning';
const useStyles = makeStyles({
root: {},
})
});
const Warnings = ({warnings}) => {
const classes = useStyles()
const Warnings = ({ warnings }) => {
const classes = useStyles();
if (!warnings || warnings.length === 0) {
return null
return null;
}
return (
@@ -47,7 +47,7 @@ const Warnings = ({warnings}) => {
))}
</List>
</Paper>
)
}
);
};
export default Warnings
export default Warnings;
+31 -17
View File
@@ -1,15 +1,31 @@
import blue from '@material-ui/core/colors/blue'
import brown from '@material-ui/core/colors/brown'
import cyan from '@material-ui/core/colors/cyan'
import deepOrange from '@material-ui/core/colors/deepOrange'
import deepPurple from '@material-ui/core/colors/deepPurple'
import green from '@material-ui/core/colors/green'
import grey from '@material-ui/core/colors/grey'
import indigo from '@material-ui/core/colors/indigo'
import orange from '@material-ui/core/colors/orange'
import red from '@material-ui/core/colors/red'
import teal from '@material-ui/core/colors/teal'
import yellow from '@material-ui/core/colors/yellow'
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import blue from '@material-ui/core/colors/blue';
import brown from '@material-ui/core/colors/brown';
import cyan from '@material-ui/core/colors/cyan';
import deepOrange from '@material-ui/core/colors/deepOrange';
import deepPurple from '@material-ui/core/colors/deepPurple';
import green from '@material-ui/core/colors/green';
import grey from '@material-ui/core/colors/grey';
import indigo from '@material-ui/core/colors/indigo';
import orange from '@material-ui/core/colors/orange';
import red from '@material-ui/core/colors/red';
import teal from '@material-ui/core/colors/teal';
import yellow from '@material-ui/core/colors/yellow';
export const primary = [
blue[500],
@@ -22,7 +38,7 @@ export const primary = [
indigo[500],
deepOrange[500],
deepPurple[500],
]
];
export const greyscale = [
grey[300],
@@ -31,8 +47,6 @@ export const greyscale = [
grey[500],
grey[100],
grey[600],
]
];
export const browns = [
brown[500],
]
export const browns = [brown[500]];
+196 -1
View File
@@ -1 +1,196 @@
export const currencyCodes = ["AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BOV", "BRL", "BSD", "BTN", "BWP", "BYR", "BZD", "CAD", "CDF", "CHE", "CHF", "CHW", "CLF", "CLP", "CNY", "COP", "COU", "CRC", "CUC", "CUP", "CVE", "CZK", "DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HRK", "HTG", "HUF", "IDR", "ILS", "INR", "IQD", "IRR", "ISK", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LTL", "LVL", "LYD", "MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRO", "MUR", "MVR", "MWK", "MXN", "MXV", "MYR", "MZN", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG", "QAR", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLL", "SOS", "SRD", "SSP", "STD", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "USN", "USS", "UYI", "UYU", "UZS", "VEF", "VND", "VUV", "WST", "XAF", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XCD", "XDR", "XFU", "XOF", "XPD", "XPF", "XPT", "XTS", "XXX", "YER", "ZAR", "ZMW"];
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const currencyCodes = [
'AED',
'AFN',
'ALL',
'AMD',
'ANG',
'AOA',
'ARS',
'AUD',
'AWG',
'AZN',
'BAM',
'BBD',
'BDT',
'BGN',
'BHD',
'BIF',
'BMD',
'BND',
'BOB',
'BOV',
'BRL',
'BSD',
'BTN',
'BWP',
'BYR',
'BZD',
'CAD',
'CDF',
'CHE',
'CHF',
'CHW',
'CLF',
'CLP',
'CNY',
'COP',
'COU',
'CRC',
'CUC',
'CUP',
'CVE',
'CZK',
'DJF',
'DKK',
'DOP',
'DZD',
'EGP',
'ERN',
'ETB',
'EUR',
'FJD',
'FKP',
'GBP',
'GEL',
'GHS',
'GIP',
'GMD',
'GNF',
'GTQ',
'GYD',
'HKD',
'HNL',
'HRK',
'HTG',
'HUF',
'IDR',
'ILS',
'INR',
'IQD',
'IRR',
'ISK',
'JMD',
'JOD',
'JPY',
'KES',
'KGS',
'KHR',
'KMF',
'KPW',
'KRW',
'KWD',
'KYD',
'KZT',
'LAK',
'LBP',
'LKR',
'LRD',
'LSL',
'LTL',
'LVL',
'LYD',
'MAD',
'MDL',
'MGA',
'MKD',
'MMK',
'MNT',
'MOP',
'MRO',
'MUR',
'MVR',
'MWK',
'MXN',
'MXV',
'MYR',
'MZN',
'NAD',
'NGN',
'NIO',
'NOK',
'NPR',
'NZD',
'OMR',
'PAB',
'PEN',
'PGK',
'PHP',
'PKR',
'PLN',
'PYG',
'QAR',
'RON',
'RSD',
'RUB',
'RWF',
'SAR',
'SBD',
'SCR',
'SDG',
'SEK',
'SGD',
'SHP',
'SLL',
'SOS',
'SRD',
'SSP',
'STD',
'SYP',
'SZL',
'THB',
'TJS',
'TMT',
'TND',
'TOP',
'TRY',
'TTD',
'TWD',
'TZS',
'UAH',
'UGX',
'USD',
'USN',
'USS',
'UYI',
'UYU',
'UZS',
'VEF',
'VND',
'VUV',
'WST',
'XAF',
'XAG',
'XAU',
'XBA',
'XBB',
'XBC',
'XBD',
'XCD',
'XDR',
'XFU',
'XOF',
'XPD',
'XPF',
'XPT',
'XTS',
'XXX',
'YER',
'ZAR',
'ZMW',
];
+1
View File
@@ -13,4 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { openCostPlugin, OpenCostPage } from './plugin';
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { openCostPlugin } from './plugin';
describe('opencost', () => {
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createPlugin,
createRoutableExtension,
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
+16 -3
View File
@@ -1,8 +1,23 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import axios from 'axios';
class AllocationService {
async fetchAllocation(baseUrl, win, aggregate, options) {
const { accumulate, } = options;
const { accumulate } = options;
const params = {
window: win,
aggregate: aggregate,
@@ -12,10 +27,8 @@ class AllocationService {
params.accumulate = accumulate;
}
console.log(`${baseUrl}/allocation/compute`, { params });
const result = await axios.get(`${baseUrl}/allocation/compute`, { params });
console.log(result.data);
return result.data;
}
}
-17
View File
@@ -1,17 +0,0 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';
import 'cross-fetch/polyfill';
+209 -140
View File
@@ -1,18 +1,34 @@
import { forEach, get, round } from 'lodash'
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { forEach, get, round } from 'lodash';
// rangeToCumulative takes an AllocationSetRange (type: array[AllocationSet])
// and accumulates the values into a single AllocationSet (type: object)
export function rangeToCumulative(allocationSetRange, aggregateBy) {
if (allocationSetRange.length === 0) {
return null
return null;
}
const result = {}
const result = {};
forEach(allocationSetRange, (allocSet) => {
forEach(allocSet, (alloc) => {
forEach(allocationSetRange, allocSet => {
forEach(allocSet, alloc => {
if (result[alloc.name] === undefined) {
const hrs = get(alloc, 'minutes', 0) / 60.0
const hrs = get(alloc, 'minutes', 0) / 60.0;
result[alloc.name] = {
name: alloc.name,
@@ -32,70 +48,76 @@ export function rangeToCumulative(allocationSetRange, aggregateBy) {
cpuEfficiency: get(alloc, 'cpuEfficiency', 0),
ramEfficiency: get(alloc, 'ramEfficiency', 0),
totalEfficiency: get(alloc, 'totalEfficiency', 0),
}
};
} else {
const hrs = get(alloc, 'minutes', 0) / 60.0
const hrs = get(alloc, 'minutes', 0) / 60.0;
result[alloc.name].cpuCost += get(alloc, 'cpuCost', 0)
result[alloc.name].gpuCost += get(alloc, 'gpuCost', 0)
result[alloc.name].ramCost += get(alloc, 'ramCost', 0)
result[alloc.name].pvCost += get(alloc, 'pvCost', 0)
result[alloc.name].networkCost += get(alloc, 'networkCost', 0)
result[alloc.name].sharedCost += get(alloc, 'sharedCost', 0)
result[alloc.name].externalCost += get(alloc, 'externalCost', 0)
result[alloc.name].totalCost += get(alloc, 'totalCost', 0)
result[alloc.name].cpuUseCoreHrs += get(alloc, 'cpuCoreUsageAverage', 0) * hrs
result[alloc.name].cpuReqCoreHrs += get(alloc, 'cpuCoreRequestAverage', 0) * hrs
result[alloc.name].ramUseByteHrs += get(alloc, 'ramByteUsageAverage', 0) * hrs
result[alloc.name].ramReqByteHrs += get(alloc, 'ramByteRequestAverage', 0) * hrs
result[alloc.name].cpuCost += get(alloc, 'cpuCost', 0);
result[alloc.name].gpuCost += get(alloc, 'gpuCost', 0);
result[alloc.name].ramCost += get(alloc, 'ramCost', 0);
result[alloc.name].pvCost += get(alloc, 'pvCost', 0);
result[alloc.name].networkCost += get(alloc, 'networkCost', 0);
result[alloc.name].sharedCost += get(alloc, 'sharedCost', 0);
result[alloc.name].externalCost += get(alloc, 'externalCost', 0);
result[alloc.name].totalCost += get(alloc, 'totalCost', 0);
result[alloc.name].cpuUseCoreHrs +=
get(alloc, 'cpuCoreUsageAverage', 0) * hrs;
result[alloc.name].cpuReqCoreHrs +=
get(alloc, 'cpuCoreRequestAverage', 0) * hrs;
result[alloc.name].ramUseByteHrs +=
get(alloc, 'ramByteUsageAverage', 0) * hrs;
result[alloc.name].ramReqByteHrs +=
get(alloc, 'ramByteRequestAverage', 0) * hrs;
}
})
})
});
});
// If the range is of length > 1 (i.e. it is not just a single set) then
// compute efficiency for each result after accumulating.
if (allocationSetRange.length > 1) {
forEach(result, (alloc, name) => {
// If we can't compute total efficiency, it defaults to 0.0
let totalEfficiency = 0.0
let totalEfficiency = 0.0;
// CPU efficiency is defined as (usage/request). If request == 0.0 but
// usage > 0, then efficiency gets set to 1.0.
let cpuEfficiency = 0.0
let cpuEfficiency = 0.0;
if (alloc.cpuReqCoreHrs > 0) {
cpuEfficiency = alloc.cpuUseCoreHrs / alloc.cpuReqCoreHrs
cpuEfficiency = alloc.cpuUseCoreHrs / alloc.cpuReqCoreHrs;
} else if (alloc.cpuUseCoreHrs > 0) {
cpuEfficiency = 1.0
cpuEfficiency = 1.0;
}
// RAM efficiency is defined as (usage/request). If request == 0.0 but
// usage > 0, then efficiency gets set to 1.0.
let ramEfficiency = 0.0
let ramEfficiency = 0.0;
if (alloc.ramReqByteHrs > 0) {
ramEfficiency = alloc.ramUseByteHrs / alloc.ramReqByteHrs
ramEfficiency = alloc.ramUseByteHrs / alloc.ramReqByteHrs;
} else if (alloc.ramUseByteHrs > 0) {
ramEfficiency = 1.0
ramEfficiency = 1.0;
}
// Compute efficiency as the cost-weighted average of CPU and RAM
// efficiency
if ((alloc.cpuCost + alloc.ramCost) > 0.0) {
totalEfficiency = ((alloc.cpuCost*cpuEfficiency)+(alloc.ramCost*ramEfficiency)) / (alloc.cpuCost + alloc.ramCost)
if (alloc.cpuCost + alloc.ramCost > 0.0) {
totalEfficiency =
(alloc.cpuCost * cpuEfficiency + alloc.ramCost * ramEfficiency) /
(alloc.cpuCost + alloc.ramCost);
}
result[name].cpuEfficiency = cpuEfficiency
result[name].ramEfficiency = ramEfficiency
result[name].totalEfficiency = totalEfficiency
})
result[name].cpuEfficiency = cpuEfficiency;
result[name].ramEfficiency = ramEfficiency;
result[name].totalEfficiency = totalEfficiency;
});
}
return result
return result;
}
// cumulativeToTotals adds each entry in the given AllocationSet (type: object)
// and returns a single Allocation (type: object) representing the totals
export function cumulativeToTotals(allocationSet) {
let totals = {
const totals = {
name: 'Totals',
cpuCost: 0,
gpuCost: 0,
@@ -108,177 +130,225 @@ export function cumulativeToTotals(allocationSet) {
cpuEfficiency: 0,
ramEfficiency: 0,
totalEfficiency: 0,
}
};
// Use these for computing efficiency. As such, idle will not factor into
// these numbers, including CPU and RAM cost.
let cpuReqCoreHrs = 0
let cpuUseCoreHrs = 0
let ramReqByteHrs = 0
let ramUseByteHrs = 0
let cpuCost = 0
let ramCost = 0
let cpuReqCoreHrs = 0;
let cpuUseCoreHrs = 0;
let ramReqByteHrs = 0;
let ramUseByteHrs = 0;
let cpuCost = 0;
let ramCost = 0;
forEach(allocationSet, (alloc, name) => {
// Accumulate efficiency-related fields
if (name !== "__idle__") {
cpuReqCoreHrs += get(alloc, 'cpuReqCoreHrs', 0.0)
cpuUseCoreHrs += get(alloc, 'cpuUseCoreHrs', 0.0)
ramReqByteHrs += get(alloc, 'ramReqByteHrs', 0.0)
ramUseByteHrs += get(alloc, 'ramUseByteHrs', 0.0)
cpuCost += get(alloc, 'cpuCost', 0.0)
ramCost += get(alloc, 'ramCost', 0.0)
if (name !== '__idle__') {
cpuReqCoreHrs += get(alloc, 'cpuReqCoreHrs', 0.0);
cpuUseCoreHrs += get(alloc, 'cpuUseCoreHrs', 0.0);
ramReqByteHrs += get(alloc, 'ramReqByteHrs', 0.0);
ramUseByteHrs += get(alloc, 'ramUseByteHrs', 0.0);
cpuCost += get(alloc, 'cpuCost', 0.0);
ramCost += get(alloc, 'ramCost', 0.0);
}
// Sum cumulative fields
totals.cpuCost += get(alloc, 'cpuCost', 0)
totals.gpuCost += get(alloc, 'gpuCost', 0)
totals.ramCost += get(alloc, 'ramCost', 0)
totals.pvCost += get(alloc, 'pvCost', 0)
totals.networkCost += get(alloc, 'networkCost', 0)
totals.sharedCost += get(alloc, 'sharedCost', 0)
totals.externalCost += get(alloc, 'externalCost', 0)
totals.totalCost += get(alloc, 'totalCost', 0)
})
totals.cpuCost += get(alloc, 'cpuCost', 0);
totals.gpuCost += get(alloc, 'gpuCost', 0);
totals.ramCost += get(alloc, 'ramCost', 0);
totals.pvCost += get(alloc, 'pvCost', 0);
totals.networkCost += get(alloc, 'networkCost', 0);
totals.sharedCost += get(alloc, 'sharedCost', 0);
totals.externalCost += get(alloc, 'externalCost', 0);
totals.totalCost += get(alloc, 'totalCost', 0);
});
// Compute efficiency
if (cpuReqCoreHrs > 0) {
totals.cpuEfficiency = cpuUseCoreHrs / cpuReqCoreHrs
totals.cpuEfficiency = cpuUseCoreHrs / cpuReqCoreHrs;
} else if (cpuUseCoreHrs > 0) {
totals.cpuEfficiency = 1.0
totals.cpuEfficiency = 1.0;
}
if (ramReqByteHrs > 0) {
totals.ramEfficiency = ramUseByteHrs / ramReqByteHrs
totals.ramEfficiency = ramUseByteHrs / ramReqByteHrs;
} else if (ramUseByteHrs > 0) {
totals.ramEfficiency = 1.0
totals.ramEfficiency = 1.0;
}
if ((cpuCost + ramCost) > 0) {
totals.totalEfficiency = ((cpuCost*totals.cpuEfficiency) + (ramCost*totals.ramEfficiency)) / (cpuCost + ramCost)
if (cpuCost + ramCost > 0) {
totals.totalEfficiency =
(cpuCost * totals.cpuEfficiency + ramCost * totals.ramEfficiency) /
(cpuCost + ramCost);
}
totals.cpuReqCoreHrs = cpuReqCoreHrs
totals.cpuUseCoreHrs = cpuUseCoreHrs
totals.ramReqByteHrs = ramReqByteHrs
totals.ramUseByteHrs = ramUseByteHrs
totals.cpuReqCoreHrs = cpuReqCoreHrs;
totals.cpuUseCoreHrs = cpuUseCoreHrs;
totals.ramReqByteHrs = ramReqByteHrs;
totals.ramUseByteHrs = ramUseByteHrs;
return totals
return totals;
}
export function toVerboseTimeRange(window) {
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
const start = new Date()
start.setUTCHours(0, 0, 0, 0)
const start = new Date();
start.setUTCHours(0, 0, 0, 0);
const end = new Date()
end.setUTCHours(0, 0, 0, 0)
const end = new Date();
end.setUTCHours(0, 0, 0, 0);
switch (window) {
case 'today':
return `${start.getUTCDate()} ${months[start.getUTCMonth()]} ${start.getUTCFullYear()}`
return `${start.getUTCDate()} ${
months[start.getUTCMonth()]
} ${start.getUTCFullYear()}`;
case 'yesterday':
start.setUTCDate(start.getUTCDate()-1)
return `${start.getUTCDate()} ${months[start.getUTCMonth()]} ${start.getUTCFullYear()}`
start.setUTCDate(start.getUTCDate() - 1);
return `${start.getUTCDate()} ${
months[start.getUTCMonth()]
} ${start.getUTCFullYear()}`;
case 'week':
start.setUTCDate(start.getUTCDate()-start.getUTCDay())
return `${start.getUTCDate()} ${months[start.getUTCMonth()]} ${start.getUTCFullYear()} until now`
start.setUTCDate(start.getUTCDate() - start.getUTCDay());
return `${start.getUTCDate()} ${
months[start.getUTCMonth()]
} ${start.getUTCFullYear()} until now`;
case 'month':
start.setUTCDate(1)
return `${start.getUTCDate()} ${months[start.getUTCMonth()]} ${start.getUTCFullYear()} until now`
start.setUTCDate(1);
return `${start.getUTCDate()} ${
months[start.getUTCMonth()]
} ${start.getUTCFullYear()} until now`;
case 'lastweek':
start.setUTCDate(start.getUTCDate()-(start.getUTCDay()+7))
end.setUTCDate(end.getUTCDate()-(end.getUTCDay()+1))
return `${start.getUTCDate()} ${months[start.getUTCMonth()]} ${start.getUTCFullYear()} through ${end.getUTCDate()} ${months[end.getUTCMonth()]} ${end.getUTCFullYear()}`
start.setUTCDate(start.getUTCDate() - (start.getUTCDay() + 7));
end.setUTCDate(end.getUTCDate() - (end.getUTCDay() + 1));
return `${start.getUTCDate()} ${
months[start.getUTCMonth()]
} ${start.getUTCFullYear()} through ${end.getUTCDate()} ${
months[end.getUTCMonth()]
} ${end.getUTCFullYear()}`;
case 'lastmonth':
end.setUTCDate(1)
end.setUTCDate(end.getUTCDate()-1)
start.setUTCDate(1)
start.setUTCDate(start.getUTCDate()-1)
start.setUTCDate(1)
return `${start.getUTCDate()} ${months[start.getUTCMonth()]} ${start.getUTCFullYear()} through ${end.getUTCDate()} ${months[end.getUTCMonth()]} ${end.getUTCFullYear()}`
end.setUTCDate(1);
end.setUTCDate(end.getUTCDate() - 1);
start.setUTCDate(1);
start.setUTCDate(start.getUTCDate() - 1);
start.setUTCDate(1);
return `${start.getUTCDate()} ${
months[start.getUTCMonth()]
} ${start.getUTCFullYear()} through ${end.getUTCDate()} ${
months[end.getUTCMonth()]
} ${end.getUTCFullYear()}`;
case '6d':
start.setUTCDate(start.getUTCDate()-6)
return `${start.getUTCDate()} ${months[start.getUTCMonth()]} ${start.getUTCFullYear()} through now`
start.setUTCDate(start.getUTCDate() - 6);
return `${start.getUTCDate()} ${
months[start.getUTCMonth()]
} ${start.getUTCFullYear()} through now`;
case '29d':
start.setUTCDate(start.getUTCDate()-29)
return `${start.getUTCDate()} ${months[start.getUTCMonth()]} ${start.getUTCFullYear()} through now`
start.setUTCDate(start.getUTCDate() - 29);
return `${start.getUTCDate()} ${
months[start.getUTCMonth()]
} ${start.getUTCFullYear()} through now`;
case '59d':
start.setUTCDate(start.getUTCDate()-59)
return `${start.getUTCDate()} ${months[start.getUTCMonth()]} ${start.getUTCFullYear()} through now`
start.setUTCDate(start.getUTCDate() - 59);
return `${start.getUTCDate()} ${
months[start.getUTCMonth()]
} ${start.getUTCFullYear()} through now`;
case '89d':
start.setUTCDate(start.getUTCDate()-89)
return `${start.getUTCDate()} ${months[start.getUTCMonth()]} ${start.getUTCFullYear()} through now`
start.setUTCDate(start.getUTCDate() - 89);
return `${start.getUTCDate()} ${
months[start.getUTCMonth()]
} ${start.getUTCFullYear()} through now`;
// no default
}
const splitDates = window.split(",")
const splitDates = window.split(',');
if (checkCustomWindow(window) && splitDates.length > 1) {
let s = splitDates[0].split(/\D+/).slice(0, 3)
let e = splitDates[1].split(/\D+/).slice(0, 3)
const s = splitDates[0].split(/\D+/).slice(0, 3);
const e = splitDates[1].split(/\D+/).slice(0, 3);
if (s.length === 3 && e.length === 3) {
start.setUTCFullYear(s[0], s[1]-1, s[2])
end.setUTCFullYear(e[0], e[1]-1, e[2])
start.setUTCFullYear(s[0], s[1] - 1, s[2]);
end.setUTCFullYear(e[0], e[1] - 1, e[2]);
if (start === end) {
return `${start.getUTCDate()} ${months[start.getUTCMonth()]} ${start.getUTCFullYear()}`
} else {
return `${start.getUTCDate()} ${months[start.getUTCMonth()]} ${start.getUTCFullYear()} through ${end.getUTCDate()} ${months[end.getUTCMonth()]} ${end.getUTCFullYear()}`
return `${start.getUTCDate()} ${
months[start.getUTCMonth()]
} ${start.getUTCFullYear()}`;
}
return `${start.getUTCDate()} ${
months[start.getUTCMonth()]
} ${start.getUTCFullYear()} through ${end.getUTCDate()} ${
months[end.getUTCMonth()]
} ${end.getUTCFullYear()}`;
}
}
return null
return null;
}
export function bytesToString(bytes) {
const ei = Math.pow(1024, 6)
const ei = Math.pow(1024, 6);
if (bytes >= ei) {
return `${round(bytes/ei, 1)} EiB`
return `${round(bytes / ei, 1)} EiB`;
}
const pi = Math.pow(1024, 5)
const pi = Math.pow(1024, 5);
if (bytes >= pi) {
return `${round(bytes/pi, 1)} PiB`
return `${round(bytes / pi, 1)} PiB`;
}
const ti = Math.pow(1024, 4)
const ti = Math.pow(1024, 4);
if (bytes >= ti) {
return `${round(bytes/ti, 1)} TiB`
return `${round(bytes / ti, 1)} TiB`;
}
const gi = Math.pow(1024, 3)
const gi = Math.pow(1024, 3);
if (bytes >= gi) {
return `${round(bytes/gi, 1)} GiB`
return `${round(bytes / gi, 1)} GiB`;
}
const mi = Math.pow(1024, 2)
const mi = Math.pow(1024, 2);
if (bytes >= mi) {
return `${round(bytes/mi, 1)} MiB`
return `${round(bytes / mi, 1)} MiB`;
}
const ki = Math.pow(1024, 1)
const ki = Math.pow(1024, 1);
if (bytes >= ki) {
return `${round(bytes/ki, 1)} KiB`
return `${round(bytes / ki, 1)} KiB`;
}
return `${round(bytes, 1)} B`
return `${round(bytes, 1)} B`;
}
const currencyLocale = "en-US"
const currencyLocale = 'en-US';
export function toCurrency(amount, currency, precision) {
if (typeof amount !== "number") {
console.warn(`Tried to convert "${amount}" to currency, but it is not a number`)
return ""
if (typeof amount !== 'number') {
Console.warn(
`Tried to convert "${amount}" to currency, but it is not a number`,
);
return '';
}
if (currency === undefined || currency === "") {
currency = "USD"
let ctype = currency;
if (currency === undefined || currency === '') {
ctype = 'USD';
}
const opts = {
style: "currency",
currency: currency,
style: 'currency',
currency: ctype,
};
}
if (typeof precision === "number") {
opts.minimumFractionDigits = precision
opts.maximumFractionDigits = precision
if (typeof precision === 'number') {
opts.minimumFractionDigits = precision;
opts.maximumFractionDigits = precision;
}
return amount.toLocaleString(currencyLocale, opts);
@@ -286,12 +356,11 @@ export function toCurrency(amount, currency, precision) {
export function checkCustomWindow(window) {
// Example ISO interval string: 2020-12-02T00:00:00Z,2020-12-03T23:59:59Z
const customDateRegex = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z,\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/
return customDateRegex.test(window)
const customDateRegex =
/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z,\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/;
return customDateRegex.test(window);
}
export default {
rangeToCumulative,
cumulativeToTotals,
@@ -299,4 +368,4 @@ export default {
bytesToString,
toCurrency,
checkCustomWindow,
}
};
+54
View File
@@ -7482,6 +7482,39 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-opencost@workspace:plugins/opencost":
version: 0.0.0-use.local
resolution: "@backstage/plugin-opencost@workspace:plugins/opencost"
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/core-app-api": "workspace:^"
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/dev-utils": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@date-io/date-fns": ^2.16.0
"@material-ui/core": ^4.9.13
"@material-ui/icons": ^4.9.1
"@material-ui/lab": ^4.0.0-alpha.60
"@material-ui/pickers": ^3.3.10
"@material-ui/styles": ^4.11.5
"@testing-library/jest-dom": ^5.10.1
"@testing-library/react": ^12.1.3
"@testing-library/user-event": ^14.0.0
axios: ^1.4.0
cross-fetch: ^3.1.5
date-fns: ^2.30.0
lodash: ^4.17.21
msw: ^1.0.0
react-use: ^17.2.4
recharts: ^2.5.0
peerDependencies:
react: ^16.13.1 || ^17.0.0
react-router-dom: "*"
languageName: unknown
linkType: soft
"@backstage/plugin-org-react@workspace:plugins/org-react":
version: 0.0.0-use.local
resolution: "@backstage/plugin-org-react@workspace:plugins/org-react"
@@ -9948,6 +9981,13 @@ __metadata:
languageName: node
linkType: hard
"@date-io/core@npm:^2.17.0":
version: 2.17.0
resolution: "@date-io/core@npm:2.17.0"
checksum: 008dfc79eb54256805113d76feca82fe0b08a245ecbfb2d53809e6a129dc201f9dbd053c8ad63512203ab1a13ff7f76de0edc31829588ef507d53307974c29a8
languageName: node
linkType: hard
"@date-io/date-fns@npm:^1.3.13":
version: 1.3.13
resolution: "@date-io/date-fns@npm:1.3.13"
@@ -9959,6 +9999,20 @@ __metadata:
languageName: node
linkType: hard
"@date-io/date-fns@npm:^2.16.0":
version: 2.17.0
resolution: "@date-io/date-fns@npm:2.17.0"
dependencies:
"@date-io/core": ^2.17.0
peerDependencies:
date-fns: ^2.0.0
peerDependenciesMeta:
date-fns:
optional: true
checksum: bfab634c985a98dd44fdfc5f982b0348da2886c9ad79a8d71fd7bfa05b740af7f5cf340508cd7ce51d67c141fb8766998c8efdf50653252c68ec078471cd76e6
languageName: node
linkType: hard
"@date-io/luxon@npm:1.x, @date-io/luxon@npm:^1.3.13":
version: 1.3.13
resolution: "@date-io/luxon@npm:1.3.13"