Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 at",
"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
27 changes: 12 additions & 15 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, 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,14 +83,9 @@ 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>
</ColumnLayout>
</Container>
)}
Expand Down
29 changes: 14 additions & 15 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, 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,21 +92,10 @@ 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 { columns } as const;
Expand Down
Loading