Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show used resources #1097

Merged
merged 16 commits into from
Oct 1, 2024
Merged
2 changes: 2 additions & 0 deletions src/components/app-overview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { DefaultAppAlias } from '../component/default-app-alias';
import { DNSAliases } from '../component/dns-aliases';
import { EnvironmentsSummary } from '../environments-summary';
import { JobsList } from '../jobs-list';
import { UsedResources } from '../resources';

const LATEST_JOBS_LIMIT = 5;

Expand Down Expand Up @@ -50,6 +51,7 @@ export function AppOverview({ appName }: { appName: string }) {
<div className="grid grid--gap-medium grid--overview-columns">
<ApplicationCost appName={appName} />
<FutureApplicationCost appName={appName} />
<UsedResources appName={appName} />
</div>

{appAlias && <DefaultAppAlias appName={appName} appAlias={appAlias} />}
Expand Down
2 changes: 1 addition & 1 deletion src/components/replica-list/replica-name.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const ReplicaName: FunctionComponent<{
displayName={'Job Manager'}
replicaName={replica.name}
description={
'Job Manager creates, gets, deletes singe jobs and batch jobs with Job API'
'Job Manager creates, gets, deletes single jobs and batch jobs with Job API'
}
replicaUrlFunc={replicaUrlFunc}
/>
Expand Down
165 changes: 165 additions & 0 deletions src/components/resources/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { Icon, Tooltip, Typography } from '@equinor/eds-core-react';
import { library_books } from '@equinor/eds-icons';
import * as PropTypes from 'prop-types';
import type { FunctionComponent } from 'react';
import { externalUrls } from '../../externalUrls';
import {
type GetResourcesApiResponse,
useGetResourcesQuery,
} from '../../store/radix-api';
import { formatDateTimeYear } from '../../utils/datetime';
import AsyncResource from '../async-resource/async-resource';

import './style.css';

function getPeriod({ from, to }: GetResourcesApiResponse): string {
return `${formatDateTimeYear(new Date(from))} - ${formatDateTimeYear(
new Date(to)
)}`;
}

export interface UsedResourcesProps {
appName: string;
}

export const UsedResources: FunctionComponent<UsedResourcesProps> = ({
appName,
}) => {
const { data: resources, ...state } = useGetResourcesQuery(
{ appName },
{ skip: !appName }
);
const formatCpuUsage = (value?: number): string => {
if (!value) {
return '-';
}
if (value >= 1) {
return parseFloat(value.toPrecision(3)).toString();
}

const millicores = value * 1000.0;
let formattedValue: string;
if (millicores >= 1.0) {
formattedValue = parseFloat(millicores.toPrecision(3)).toString();
return `${formattedValue}m`;
}
let mcStr = millicores.toFixed(20); // Use 20 decimal places to ensure precision
mcStr = mcStr.replace(/0+$/, '');
// Find the position of the decimal point
const decimalIndex = mcStr.indexOf('.');
// Find the index of the first non-zero digit after the decimal point
let firstNonZeroIndex = -1;
for (let i = decimalIndex + 1; i < mcStr.length; i++) {
if (mcStr[i] !== '0') {
firstNonZeroIndex = i;
break;
}
}
if (firstNonZeroIndex === -1) {
return '0m';
}
// Create a new number where the digit at firstNonZeroIndex becomes the first decimal digit
const digits = `0.${mcStr.substring(firstNonZeroIndex)}`;
let num = parseFloat(digits);
// Round the number to one digit
num = Math.round(num * 10) / 10;
// Handle rounding that results in num >= 1
if (num >= 1) {
num = 1;
}
let numStr = num.toString();
// Remove the decimal point and any following zeros
numStr = numStr.replace('0.', '').replace(/0+$/, '');
// Replace the part of mcStr starting from firstNonZeroIndex - 1
let zerosCount = firstNonZeroIndex - decimalIndex - 1;
// Adjust zerosCount, when num is 1
if (num === 1) {
zerosCount -= 1;
}
const leadingDigitalZeros = '0'.repeat(Math.max(zerosCount, 0));
const output = `0.${leadingDigitalZeros}${numStr}`;
return `${output}m`;
};

const formatMemoryUsage = (value?: number): string => {
if (!value) {
return '-';
}
const units = [
{ unit: 'P', size: 1e15 },
{ unit: 'T', size: 1e12 },
{ unit: 'G', size: 1e9 },
{ unit: 'M', size: 1e6 },
{ unit: 'k', size: 1e3 },
];

let unit = ''; // Default to bytes

// Determine the appropriate unit
for (const u of units) {
if (value >= u.size) {
value = value / u.size;
unit = u.unit;
break;
}
}
const formattedValue = parseFloat(value.toPrecision(3)).toString();
return formattedValue + unit;
};

return (
<div className="grid grid--gap-medium">
<div className="grid grid--gap-medium grid--auto-columns">
<Typography variant="h6"> Used resources</Typography>
<Typography
link
href={externalUrls.resourcesDocs}
rel="noopener noreferrer"
>
<Tooltip title="Read more in the documentation">
<Icon data={library_books} />
</Tooltip>
</Typography>
</div>
<AsyncResource asyncState={state}>
{resources ? (
<div className="resources-section grid grid--gap-medium">
<div className="grid grid--gap-small">
<Typography variant="overline">Period</Typography>
<Typography group="input" variant="text">
{getPeriod(resources)}
</Typography>
</div>

<div className="grid grid--gap-small grid--auto-columns">
<div>
<Typography>
CPU{' '}
<strong>
min {formatCpuUsage(resources?.cpu?.min)}, avg{' '}
{formatCpuUsage(resources?.cpu?.avg)}, max{' '}
{formatCpuUsage(resources?.cpu?.max)}
</strong>
</Typography>
<Typography>
Memory{' '}
<strong>
min {formatMemoryUsage(resources?.memory?.min)}, avg{' '}
{formatMemoryUsage(resources?.memory?.avg)}, max{' '}
{formatMemoryUsage(resources?.memory?.max)}
</strong>
</Typography>
</div>
</div>
</div>
) : (
<Typography variant="caption">No data</Typography>
)}
</AsyncResource>
</div>
);
};

UsedResources.propTypes = {
appName: PropTypes.string.isRequired,
};
26 changes: 26 additions & 0 deletions src/components/resources/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.resources-section > div {
grid-auto-rows: min-content;
}

@media (min-width: 30rem) {
.resources-section {
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
}
}

.icon-justify-end {
justify-self: end;
}

.resources__scrim .resources__scrim-content {
margin: var(--eds_spacing_medium);
margin-top: initial;
align-content: center;
min-width: 500px;
}

.resources-content {
padding: var(--eds_spacing_medium);
padding-top: 0;
overflow: auto;
}
3 changes: 3 additions & 0 deletions src/externalUrls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export const externalUrls = {
externalDNSGuide: 'https://www.radix.equinor.com/guides/external-alias/',
workloadIdentityGuide: 'https://www.radix.equinor.com/guides/workload-identity/',
uptimeDocs: 'https://radix.equinor.com/docs/topic-uptime/',
resourcesDocs: 'https://radix.equinor.com/guides/resource-request/',
kubernetesResourcesCpuUnits: 'https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-cpu',
kubernetesResourcesMemoryUnits: 'https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-memory',
radixPlatformWebConsole: `https://console.${clusterBases.radixPlatformWebConsole}/`,
radixPlatform2WebConsole: `https://console.${clusterBases.radixPlatform2WebConsole}/`,
playgroundWebConsole: `https://console.${clusterBases.playgroundWebConsole}/`,
Expand Down
52 changes: 52 additions & 0 deletions src/store/radix-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,21 @@ const injectedRtkApi = api.injectEndpoints({
},
}),
}),
getResources: build.query<GetResourcesApiResponse, GetResourcesApiArg>({
query: (queryArg) => ({
url: `/applications/${queryArg.appName}/resources`,
headers: {
'Impersonate-User': queryArg['Impersonate-User'],
'Impersonate-Group': queryArg['Impersonate-Group'],
},
params: {
environment: queryArg.environment,
component: queryArg.component,
duration: queryArg.duration,
since: queryArg.since,
},
}),
}),
restartApplication: build.mutation<
RestartApplicationApiResponse,
RestartApplicationApiArg
Expand Down Expand Up @@ -2205,6 +2220,24 @@ export type ResetManuallyScaledComponentsInApplicationApiArg = {
/** Works only with custom setup of cluster. Allow impersonation of a comma-seperated list of test groups (Required if Impersonate-User is set) */
'Impersonate-Group'?: string;
};
export type GetResourcesApiResponse =
/** status 200 Successful trigger pipeline */ UsedResources;
export type GetResourcesApiArg = {
/** Name of the application */
appName: string;
/** Name of the application environment */
environment?: string;
/** Name of the application component in an environment */
component?: string;
/** Duration of the period, default is 30d (30 days). Example 10m, 1h, 2d, 3w, where m-minutes, h-hours, d-days, w-weeks */
duration?: string;
/** End time-point of the period in the past, default is now. Example 10m, 1h, 2d, 3w, where m-minutes, h-hours, d-days, w-weeks */
since?: string;
/** Works only with custom setup of cluster. Allow impersonation of test users (Required if Impersonate-Group is set) */
'Impersonate-User'?: string;
/** Works only with custom setup of cluster. Allow impersonation of a comma-seperated list of test groups (Required if Impersonate-User is set) */
'Impersonate-Group'?: string;
};
export type RestartApplicationApiResponse = unknown;
export type RestartApplicationApiArg = {
/** Name of application */
Expand Down Expand Up @@ -3367,6 +3400,24 @@ export type RegenerateDeployKeyAndSecretData = {
/** SharedSecret of the shared secret */
sharedSecret?: string;
};
export type UsedResource = {
/** Avg Average resource used */
avg?: number;
/** Max resource used */
max?: number;
/** Min resource used */
min?: number;
};
export type UsedResources = {
cpu?: UsedResource;
/** From timestamp */
from: string;
memory?: UsedResource;
/** To timestamp */
to: string;
/** Warning messages */
warnings?: string[];
};
export const {
useShowApplicationsQuery,
useRegisterApplicationMutation,
Expand Down Expand Up @@ -3452,6 +3503,7 @@ export const {
useUpdatePrivateImageHubsSecretValueMutation,
useRegenerateDeployKeyMutation,
useResetManuallyScaledComponentsInApplicationMutation,
useGetResourcesQuery,
useRestartApplicationMutation,
useStartApplicationMutation,
useStopApplicationMutation,
Expand Down