diff --git a/frontend/src/api.ts b/frontend/src/api.ts
index f7c9e20d51..6615381260 100644
--- a/frontend/src/api.ts
+++ b/frontend/src/api.ts
@@ -171,6 +171,8 @@ export const API = {
INSTANCES: {
BASE: () => `${API.BASE()}/instances`,
LIST: () => `${API.INSTANCES.BASE()}/list`,
+ DETAILS: (projectName: IProject['project_name']) =>
+ `${API.BASE()}/project/${projectName}/instances/get`,
},
SERVER: {
diff --git a/frontend/src/libs/fleet.ts b/frontend/src/libs/fleet.ts
index 6b4c0693ec..c3a7912353 100644
--- a/frontend/src/libs/fleet.ts
+++ b/frontend/src/libs/fleet.ts
@@ -1,6 +1,12 @@
import { isEqual } from 'lodash';
import { StatusIndicatorProps } from '@cloudscape-design/components';
+export const formatBackend = (backend: TBackendType | string | null | undefined): string => {
+ if (!backend) return '-';
+ if (backend === 'remote') return 'ssh';
+ return backend;
+};
+
export const getStatusIconType = (status: IInstance['status']): StatusIndicatorProps['type'] => {
switch (status) {
case 'pending':
@@ -55,6 +61,90 @@ const getInstanceFields = (instance: IInstance) => ({
spot: instance.instance_type?.resources.spot,
});
+const formatRange = (min: unknown, max: unknown, suffix = ''): string => {
+ if (min == null && max == null) return '';
+ if (min === max) return `${min}${suffix}`;
+ if (max == null) return `${min}${suffix}..`;
+ if (min == null) return `..${max}${suffix}`;
+ return `${min}${suffix}..${max}${suffix}`;
+};
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const formatCpu = (cpu: any): string | null => {
+ if (!cpu) return null;
+ if (typeof cpu === 'number') return `cpu=${cpu}`;
+ if (cpu.min != null || cpu.max != null) return `cpu=${formatRange(cpu.min, cpu.max)}`;
+ const arch = cpu.arch;
+ const count = cpu.count;
+ if (!count) return null;
+ const prefix = arch === 'arm' ? 'arm:' : '';
+ return `cpu=${prefix}${formatRange(count.min, count.max)}`;
+};
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const formatGpu = (gpu: any): string | null => {
+ if (!gpu) return null;
+ const count = gpu.count;
+ if (!count || (count.min === 0 && (count.max == null || count.max === 0))) return null;
+
+ const gpuParts: string[] = [];
+
+ if (gpu.memory) {
+ const memStr = formatRange(gpu.memory.min, gpu.memory.max, 'GB');
+ if (memStr) gpuParts.push(memStr);
+ }
+
+ const countStr = formatRange(count.min, count.max);
+ if (countStr) gpuParts.push(countStr);
+
+ if (gpu.total_memory) {
+ const tmStr = formatRange(gpu.total_memory.min, gpu.total_memory.max, 'GB');
+ if (tmStr) gpuParts.push(tmStr);
+ }
+
+ let label: string;
+ if (gpu.name && gpu.name.length > 0) {
+ label = gpu.name.join(',');
+ } else if (gpu.vendor) {
+ label = gpu.vendor;
+ } else {
+ label = '';
+ }
+
+ return 'gpu=' + [label, ...gpuParts].filter(Boolean).join(':');
+};
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const formatFleetResources = (resources: any): string => {
+ if (!resources) return '-';
+
+ const parts: string[] = [];
+
+ const cpuStr = formatCpu(resources.cpu);
+ if (cpuStr) parts.push(cpuStr);
+
+ if (resources.memory) {
+ const memStr = formatRange(resources.memory.min, resources.memory.max, 'GB');
+ if (memStr) parts.push(`mem=${memStr}`);
+ }
+
+ if (resources.disk?.size) {
+ const diskStr = formatRange(resources.disk.size.min, resources.disk.size.max, 'GB');
+ if (diskStr) parts.push(`disk=${diskStr}`);
+ }
+
+ const gpuStr = formatGpu(resources.gpu);
+ if (gpuStr) parts.push(gpuStr);
+
+ return parts.length > 0 ? parts.join(' ') : '-';
+};
+
+export const formatFleetBackend = (config: IFleetConfigurationRequest): string => {
+ if (config.ssh_config) return 'ssh';
+ if (!config.backends || config.backends.length === 0) return '-';
+ return config.backends.map((b) => formatBackend(b)).join(', ');
+};
+
export const getFleetInstancesLinkText = (fleet: IFleet): string => {
const instances = fleet.instances.filter((i) => i.status !== 'terminated');
const hasPending = instances.some((i) => i.status === 'pending');
@@ -68,7 +158,7 @@ export const getFleetInstancesLinkText = (fleet: IFleet): string => {
if (isSameInstances)
return `${instances.length}x ${instances[0].instance_type?.name}${
instances[0].instance_type?.resources.spot ? ' (spot)' : ''
- } @ ${instances[0].backend} (${instances[0].region})`;
+ } @ ${formatBackend(instances[0].backend)} (${instances[0].region})`;
return `${instances.length} instances`;
};
diff --git a/frontend/src/libs/instance.ts b/frontend/src/libs/instance.ts
new file mode 100644
index 0000000000..fae5c60b71
--- /dev/null
+++ b/frontend/src/libs/instance.ts
@@ -0,0 +1,32 @@
+import { StatusIndicatorProps } from '@cloudscape-design/components';
+
+export const prettyEnumValue = (value: string): string => {
+ return value.replace(/_/g, ' ').replace(/^\w/, (c) => c.toUpperCase());
+};
+
+export const getHealthStatusIconType = (healthStatus: THealthStatus): StatusIndicatorProps['type'] => {
+ switch (healthStatus) {
+ case 'healthy':
+ return 'success';
+ case 'warning':
+ return 'warning';
+ case 'failure':
+ return 'error';
+ default:
+ return 'info';
+ }
+};
+
+export const formatInstanceStatusText = (instance: IInstance): string => {
+ const status = instance.status;
+
+ if (
+ (status === 'idle' || status === 'busy') &&
+ instance.total_blocks !== null &&
+ instance.total_blocks > 1
+ ) {
+ return `${instance.busy_blocks}/${instance.total_blocks} Busy`;
+ }
+
+ return prettyEnumValue(status);
+};
diff --git a/frontend/src/libs/resources.ts b/frontend/src/libs/resources.ts
new file mode 100644
index 0000000000..17ec638740
--- /dev/null
+++ b/frontend/src/libs/resources.ts
@@ -0,0 +1,39 @@
+const mibToGB = (mib: number): string => `${Math.round(mib / 1024)}GB`;
+
+export const formatResources = (resources: IResources, includeSpot = true): string => {
+ const parts: string[] = [];
+
+ if (resources.cpus > 0) {
+ const archPrefix = resources.cpu_arch === 'arm' ? 'arm:' : '';
+ parts.push(`cpu=${archPrefix}${resources.cpus}`);
+ }
+
+ if (resources.memory_mib > 0) {
+ parts.push(`mem=${mibToGB(resources.memory_mib)}`);
+ }
+
+ if (resources.disk && resources.disk.size_mib > 0) {
+ parts.push(`disk=${mibToGB(resources.disk.size_mib)}`);
+ }
+
+ if (resources.gpus.length > 0) {
+ const gpu = resources.gpus[0];
+ const gpuParts: string[] = [];
+
+ if (gpu.memory_mib > 0) {
+ gpuParts.push(mibToGB(gpu.memory_mib));
+ }
+
+ gpuParts.push(String(resources.gpus.length));
+
+ parts.push('gpu=' + [gpu.name, ...gpuParts].filter(Boolean).join(':'));
+ }
+
+ let output = parts.join(' ');
+
+ if (includeSpot && resources.spot) {
+ output += ' (spot)';
+ }
+
+ return output || '-';
+};
diff --git a/frontend/src/libs/run.ts b/frontend/src/libs/run.ts
index 1c528b8b5f..60f4a51083 100644
--- a/frontend/src/libs/run.ts
+++ b/frontend/src/libs/run.ts
@@ -2,6 +2,7 @@ import { get as _get } from 'lodash';
import { StatusIndicatorProps } from '@cloudscape-design/components';
import { capitalize } from 'libs';
+import { formatResources } from 'libs/resources';
import { finishedRunStatuses } from '../pages/Runs/constants';
import { getJobProbesStatuses } from '../pages/Runs/Details/Jobs/List/helpers';
@@ -99,7 +100,9 @@ export const getExtendedModelFromRun = (run: IRun): IModelExtended | null => {
project_name: run.project_name,
run_name: run?.run_spec.run_name ?? 'No run name',
user: run.user,
- resources: run.latest_job_submission?.job_provisioning_data?.instance_type?.resources?.description ?? null,
+ resources: run.latest_job_submission?.job_provisioning_data?.instance_type?.resources
+ ? formatResources(run.latest_job_submission.job_provisioning_data.instance_type.resources)
+ : null,
price: run.latest_job_submission?.job_provisioning_data?.price ?? null,
submitted_at: run.submitted_at,
repository: getRepoNameFromRun(run),
diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json
index 0b6bbc08c9..3134782e76 100644
--- a/frontend/src/locale/en.json
+++ b/frontend/src/locale/en.json
@@ -584,6 +584,7 @@
"button_title": "Create a fleet"
},
"fleet": "Fleet",
+ "fleet_column_name": "Name",
"fleet_placeholder": "Filtering by fleet",
"fleet_name": "Fleet name",
"total_instances": "Number of instances",
@@ -613,12 +614,12 @@
"empty_message_text": "No instances to display.",
"nomatch_message_title": "No matches",
"nomatch_message_text": "We can't find a match.",
- "instance_name": "Instance",
- "instance_num": "Instance num",
+ "instance_name": "Name",
+ "instance_num": "Num",
"created": "Created",
"status": "Status",
"project": "Project",
- "hostname": "Host name",
+ "hostname": "Hostname",
"instance_type": "Type",
"statuses": {
"pending": "Pending",
@@ -633,7 +634,12 @@
"region": "Region",
"spot": "Spot",
"started": "Started",
- "price": "Price"
+ "finished_at": "Finished",
+ "price": "Price",
+ "termination_reason": "Termination reason",
+ "health": "Health",
+ "blocks": "Blocks",
+ "inspect": "Inspect"
},
"edit": {
"name": "Name",
diff --git a/frontend/src/pages/Events/List/hooks/useColumnDefinitions.tsx b/frontend/src/pages/Events/List/hooks/useColumnDefinitions.tsx
index bc9a07aa0c..fce75101ff 100644
--- a/frontend/src/pages/Events/List/hooks/useColumnDefinitions.tsx
+++ b/frontend/src/pages/Events/List/hooks/useColumnDefinitions.tsx
@@ -77,7 +77,12 @@ export const useColumnsDefinitions = () => {
{target.project_name}
)}
- /{target.name}
+ /
+