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:
@@ -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
@@ -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
|
||||
|
||||

|
||||
|
||||
Vendored
+4
-3
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]];
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
@@ -13,4 +13,5 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { openCostPlugin, OpenCostPage } from './plugin';
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { openCostPlugin } from './plugin';
|
||||
|
||||
describe('opencost', () => {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
createPlugin,
|
||||
createRoutableExtension,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user