diff --git a/src/components/app-overview/index.tsx b/src/components/app-overview/index.tsx
index 151254b7d..e045612e3 100644
--- a/src/components/app-overview/index.tsx
+++ b/src/components/app-overview/index.tsx
@@ -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;
@@ -50,6 +51,7 @@ export function AppOverview({ appName }: { appName: string }) {
{appAlias && }
diff --git a/src/components/replica-list/replica-name.tsx b/src/components/replica-list/replica-name.tsx
index f07d1884a..5d9c575ce 100644
--- a/src/components/replica-list/replica-name.tsx
+++ b/src/components/replica-list/replica-name.tsx
@@ -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}
/>
diff --git a/src/components/resources/index.tsx b/src/components/resources/index.tsx
new file mode 100644
index 000000000..d82ab9ef7
--- /dev/null
+++ b/src/components/resources/index.tsx
@@ -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 = ({
+ 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 (
+
+
+ Used resources
+
+
+
+
+
+
+
+ {resources ? (
+
+
+ Period
+
+ {getPeriod(resources)}
+
+
+
+
+
+
+ CPU{' '}
+
+ min {formatCpuUsage(resources?.cpu?.min)}, avg{' '}
+ {formatCpuUsage(resources?.cpu?.avg)}, max{' '}
+ {formatCpuUsage(resources?.cpu?.max)}
+
+
+
+ Memory{' '}
+
+ min {formatMemoryUsage(resources?.memory?.min)}, avg{' '}
+ {formatMemoryUsage(resources?.memory?.avg)}, max{' '}
+ {formatMemoryUsage(resources?.memory?.max)}
+
+
+
+
+
+ ) : (
+ No data
+ )}
+
+
+ );
+};
+
+UsedResources.propTypes = {
+ appName: PropTypes.string.isRequired,
+};
diff --git a/src/components/resources/style.css b/src/components/resources/style.css
new file mode 100644
index 000000000..55a8a74a4
--- /dev/null
+++ b/src/components/resources/style.css
@@ -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;
+}
diff --git a/src/externalUrls.ts b/src/externalUrls.ts
index d887540b9..ab910cdcf 100644
--- a/src/externalUrls.ts
+++ b/src/externalUrls.ts
@@ -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}/`,
diff --git a/src/store/radix-api.ts b/src/store/radix-api.ts
index 5411cad91..45d94b92d 100644
--- a/src/store/radix-api.ts
+++ b/src/store/radix-api.ts
@@ -1046,6 +1046,21 @@ const injectedRtkApi = api.injectEndpoints({
},
}),
}),
+ getResources: build.query({
+ 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
@@ -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 */
@@ -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,
@@ -3452,6 +3503,7 @@ export const {
useUpdatePrivateImageHubsSecretValueMutation,
useRegenerateDeployKeyMutation,
useResetManuallyScaledComponentsInApplicationMutation,
+ useGetResourcesQuery,
useRestartApplicationMutation,
useStartApplicationMutation,
useStopApplicationMutation,