Skip to content

Commit

Permalink
Show used resources (#1097)
Browse files Browse the repository at this point in the history
* Added resources component

* Added resources popup with details

* Added resources args

* Added links to docs

* Added links to docs

* Fix and merge

* Cleanup

* Removed popup

* Adjusted cpu resources

* Adjusted cpu resources
  • Loading branch information
satr authored Oct 1, 2024
1 parent a2450d7 commit 8d0183e
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 1 deletion.
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

0 comments on commit 8d0183e

Please sign in to comment.