Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
92 changes: 91 additions & 1 deletion frontend/src/libs/fleet.ts
Original file line number Diff line number Diff line change
@@ -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':
Expand Down Expand Up @@ -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');
Expand All @@ -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`;
};
32 changes: 32 additions & 0 deletions frontend/src/libs/instance.ts
Original file line number Diff line number Diff line change
@@ -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);
};
39 changes: 39 additions & 0 deletions frontend/src/libs/resources.ts
Original file line number Diff line number Diff line change
@@ -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 || '-';
};
5 changes: 4 additions & 1 deletion frontend/src/libs/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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),
Expand Down
14 changes: 10 additions & 4 deletions frontend/src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,12 @@ export const useColumnsDefinitions = () => {
{target.project_name}
</NavigateLink>
)}
/{target.name}
/
<NavigateLink
href={ROUTES.INSTANCES.DETAILS.FORMAT(target.project_name ?? '', target.id)}
>
{target.name}
</NavigateLink>
</div>
);

Expand Down
24 changes: 13 additions & 11 deletions frontend/src/pages/Fleets/Details/FleetDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { format } from 'date-fns';
import { Box, ColumnLayout, Container, Header, Loader, NavigateLink, StatusIndicator } from 'components';

import { DATE_TIME_FORMAT } from 'consts';
import { getFleetInstancesLinkText, getFleetPrice, getFleetStatusIconType } from 'libs/fleet';
import { formatFleetBackend, formatFleetResources, getFleetInstancesLinkText, getFleetPrice, getFleetStatusIconType } from 'libs/fleet';
import { ROUTES } from 'routes';
import { useGetFleetDetailsQuery } from 'services/fleet';

Expand All @@ -26,14 +26,6 @@ export const FleetDetails = () => {
},
);

const renderPrice = (fleet: IFleet) => {
const price = getFleetPrice(fleet);

if (typeof price === 'number') return `$${price}`;

return '-';
};

return (
<>
{isLoading && (
Expand Down Expand Up @@ -70,6 +62,16 @@ export const FleetDetails = () => {
</div>
</div>

<div>
<Box variant="awsui-key-label">{t('fleets.instances.backend')}</Box>
<div>{formatFleetBackend(data.spec.configuration)}</div>
</div>

<div>
<Box variant="awsui-key-label">{t('fleets.instances.resources')}</Box>
<div>{formatFleetResources(data.spec.configuration.resources)}</div>
</div>

<div>
<Box variant="awsui-key-label">{t('fleets.instances.title')}</Box>

Expand All @@ -81,13 +83,13 @@ export const FleetDetails = () => {
</div>

<div>
<Box variant="awsui-key-label">{t('fleets.instances.started')}</Box>
<Box variant="awsui-key-label">{t('fleets.instances.created')}</Box>
<div>{format(new Date(data.created_at), DATE_TIME_FORMAT)}</div>
</div>

<div>
<Box variant="awsui-key-label">{t('fleets.instances.price')}</Box>
<div>{renderPrice(data)}</div>
<div>{(() => { const p = getFleetPrice(data); return typeof p === 'number' ? `$${p}` : '-'; })()}</div>
</div>
</ColumnLayout>
</Container>
Expand Down
23 changes: 15 additions & 8 deletions frontend/src/pages/Fleets/List/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Button, ListEmptyMessage, NavigateLink, StatusIndicator, TableProps } f
import { DATE_TIME_FORMAT } from 'consts';
import { useProjectFilter } from 'hooks/useProjectFilter';
import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters';
import { getFleetInstancesLinkText, getFleetPrice, getFleetStatusIconType } from 'libs/fleet';
import { formatFleetBackend, formatFleetResources, getFleetInstancesLinkText, getFleetPrice, getFleetStatusIconType } from 'libs/fleet';
import { ROUTES } from 'routes';

export const useEmptyMessages = ({
Expand Down Expand Up @@ -51,7 +51,7 @@ export const useColumnsDefinitions = () => {
const columns: TableProps.ColumnDefinition<IFleet>[] = [
{
id: 'fleet_name',
header: t('fleets.fleet'),
header: t('fleets.fleet_column_name'),
cell: (item) => (
<NavigateLink href={ROUTES.FLEETS.DETAILS.FORMAT(item.project_name, item.id)}>{item.name}</NavigateLink>
),
Expand All @@ -72,6 +72,16 @@ export const useColumnsDefinitions = () => {
<NavigateLink href={ROUTES.PROJECT.DETAILS.FORMAT(item.project_name)}>{item.project_name}</NavigateLink>
),
},
{
id: 'backend',
header: t('fleets.instances.backend'),
cell: (item) => formatFleetBackend(item.spec.configuration),
},
{
id: 'resources',
header: t('fleets.instances.resources'),
cell: (item) => formatFleetResources(item.spec.configuration.resources),
},
{
id: 'instances',
header: t('fleets.instances.title'),
Expand All @@ -82,19 +92,16 @@ export const useColumnsDefinitions = () => {
),
},
{
id: 'started',
header: t('fleets.instances.started'),
id: 'created',
header: t('fleets.instances.created'),
cell: (item) => format(new Date(item.created_at), DATE_TIME_FORMAT),
},
{
id: 'price',
header: t('fleets.instances.price'),
cell: (item) => {
const price = getFleetPrice(item);

if (typeof price === 'number') return `$${price}`;

return '-';
return typeof price === 'number' ? `$${price}` : '-';
},
},
];
Expand Down
Loading