Skip to content

Commit e5cf2cf

Browse files
[UX] Make "No fleets" run status more explicit #3405
Open
1 parent b2be6a7 commit e5cf2cf

8 files changed

Lines changed: 78 additions & 16 deletions

File tree

frontend/src/libs/run.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ export const getStatusIconType = (
3939
export const getStatusIconColor = (
4040
status: IRun['status'] | TJobStatus,
4141
terminationReason: string | null | undefined,
42+
statusMessage: string,
4243
): StatusIndicatorProps.Color | undefined => {
44+
if (statusMessage === 'No fleets') {
45+
return 'red';
46+
}
4347
if (terminationReason === 'failed_to_start_due_to_no_capacity' || terminationReason === 'interrupted_by_no_capacity') {
4448
return 'yellow';
4549
}

frontend/src/pages/Runs/Details/RunDetails/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ export const RunDetails = () => {
6262

6363
const finishedAt = getRunListFinishedAt(runData);
6464

65+
const statusMessage = getRunStatusMessage(runData);
66+
6567
return (
6668
<>
6769
<Container header={<Header variant="h2">{t('common.general')}</Header>}>
@@ -112,9 +114,9 @@ export const RunDetails = () => {
112114
<div>
113115
<StatusIndicator
114116
type={getStatusIconType(status, terminationReason)}
115-
colorOverride={getStatusIconColor(status, terminationReason)}
117+
colorOverride={getStatusIconColor(status, terminationReason, statusMessage)}
116118
>
117-
{getRunStatusMessage(runData)}
119+
{statusMessage}
118120
</StatusIndicator>
119121
</div>
120122
</div>

frontend/src/pages/Runs/List/hooks/useColumnsDefinitions.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,14 @@ export const useColumnsDefinitions = () => {
8484
const terminationReason = finishedRunStatuses.includes(item.status)
8585
? item.latest_job_submission?.termination_reason
8686
: null;
87+
const statusMessage = getRunStatusMessage(item);
8788

8889
return (
8990
<StatusIndicator
9091
type={getStatusIconType(status, terminationReason)}
91-
colorOverride={getStatusIconColor(status, terminationReason)}
92+
colorOverride={getStatusIconColor(status, terminationReason, statusMessage)}
9293
>
93-
{getRunStatusMessage(item)}
94+
{statusMessage}
9495
</StatusIndicator>
9596
);
9697
},

src/dstack/_internal/cli/services/configurators/run.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,12 @@ def apply_configuration(
106106
ssh_identity_file=configurator_args.ssh_identity_file,
107107
)
108108

109-
print_run_plan(run_plan, max_offers=configurator_args.max_offers)
109+
no_fleets = False
110+
if len(run_plan.job_plans[0].offers) == 0:
111+
if len(self.api.client.fleets.list(self.api.project)) == 0:
112+
no_fleets = True
113+
114+
print_run_plan(run_plan, max_offers=configurator_args.max_offers, no_fleets=True)
110115

111116
confirm_message = "Submit a new run?"
112117
if conf.name:

src/dstack/_internal/cli/utils/common.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@
3232
" https://dstack.ai/docs/guides/troubleshooting/#no-offers"
3333
"[/]\n"
3434
)
35+
NO_FLEETS_WARNING = (
36+
"[warning]"
37+
"The project has no fleets. Create one before submitting a run:"
38+
" https://dstack.ai/docs/concepts/fleets"
39+
"[/]\n"
40+
)
3541

3642

3743
def cli_error(e: DstackError) -> CLIError:

src/dstack/_internal/cli/utils/run.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66

77
from dstack._internal.cli.models.offers import OfferCommandOutput, OfferRequirements
88
from dstack._internal.cli.models.runs import PsCommandOutput
9-
from dstack._internal.cli.utils.common import NO_OFFERS_WARNING, add_row_from_dict, console
9+
from dstack._internal.cli.utils.common import (
10+
NO_FLEETS_WARNING,
11+
NO_OFFERS_WARNING,
12+
add_row_from_dict,
13+
console,
14+
)
1015
from dstack._internal.core.models.backends.base import BackendType
1116
from dstack._internal.core.models.configurations import DevEnvironmentConfiguration
1217
from dstack._internal.core.models.instances import (
@@ -75,7 +80,10 @@ def print_runs_json(project: str, runs: List[Run]) -> None:
7580

7681

7782
def print_run_plan(
78-
run_plan: RunPlan, max_offers: Optional[int] = None, include_run_properties: bool = True
83+
run_plan: RunPlan,
84+
max_offers: Optional[int] = None,
85+
include_run_properties: bool = True,
86+
no_fleets: bool = False,
7987
):
8088
run_spec = run_plan.get_effective_run_spec()
8189
job_plan = run_plan.job_plans[0]
@@ -195,7 +203,7 @@ def th(s: str) -> str:
195203
)
196204
console.print()
197205
else:
198-
console.print(NO_OFFERS_WARNING)
206+
console.print(NO_FLEETS_WARNING if no_fleets else NO_OFFERS_WARNING)
199207

200208

201209
def _format_run_status(run) -> str:
@@ -215,8 +223,10 @@ def _format_run_status(run) -> str:
215223
RunStatus.FAILED: "indian_red1",
216224
RunStatus.DONE: "grey",
217225
}
218-
if status_text == "no offers" or status_text == "interrupted":
226+
if status_text in ("no offers", "interrupted"):
219227
color = "gold1"
228+
elif status_text == "no fleets":
229+
color = "indian_red1"
220230
elif status_text == "pulling":
221231
color = "sea_green3"
222232
else:
@@ -230,6 +240,8 @@ def _format_job_submission_status(job_submission: JobSubmission, verbose: bool)
230240
job_status = job_submission.status
231241
if status_message in ("no offers", "interrupted"):
232242
color = "gold1"
243+
elif status_message == "no fleets":
244+
color = "indian_red1"
233245
elif status_message == "stopped":
234246
color = "grey"
235247
else:

src/dstack/_internal/server/services/jobs/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,11 @@ def _get_job_status_message(job_model: JobModel) -> str:
804804
elif (
805805
job_model.termination_reason == JobTerminationReason.FAILED_TO_START_DUE_TO_NO_CAPACITY
806806
):
807+
if (
808+
job_model.termination_reason_message
809+
and "No fleet found" in job_model.termination_reason_message
810+
):
811+
return "no fleets"
807812
return "no offers"
808813
elif job_model.termination_reason == JobTerminationReason.INTERRUPTED_BY_NO_CAPACITY:
809814
return "interrupted"

src/tests/_internal/cli/utils/test_run.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ async def create_run_with_job(
9696
job_provisioning_data: Optional[JobProvisioningData] = None,
9797
termination_reason: Optional[JobTerminationReason] = None,
9898
exit_status: Optional[int] = None,
99+
termination_reason_message: Optional[str] = None,
99100
submitted_at: Optional[datetime] = None,
100101
) -> Run:
101102
if submitted_at is None:
@@ -178,6 +179,9 @@ async def create_run_with_job(
178179

179180
if exit_status is not None:
180181
job_model.exit_status = exit_status
182+
if termination_reason_message is not None:
183+
job_model.termination_reason_message = termination_reason_message
184+
if exit_status is not None or termination_reason_message is not None:
181185
await session.commit()
182186

183187
await session.refresh(run_model_db)
@@ -226,56 +230,77 @@ async def test_simple_run(self, session: AsyncSession):
226230
assert status_style == "bold sea_green3"
227231

228232
@pytest.mark.parametrize(
229-
"job_status,termination_reason,exit_status,expected_status,expected_style",
233+
"job_status,termination_reason,exit_status,termination_reason_message,expected_status,expected_style",
230234
[
231-
(JobStatus.DONE, None, None, "exited (0)", "grey"),
235+
(JobStatus.DONE, None, None, None, "exited (0)", "grey"),
232236
(
233237
JobStatus.FAILED,
234238
JobTerminationReason.CONTAINER_EXITED_WITH_ERROR,
235239
1,
240+
None,
236241
"exited (1)",
237242
"indian_red1",
238243
),
239244
(
240245
JobStatus.FAILED,
241246
JobTerminationReason.CONTAINER_EXITED_WITH_ERROR,
242247
42,
248+
None,
243249
"exited (42)",
244250
"indian_red1",
245251
),
246252
(
247253
JobStatus.FAILED,
248254
JobTerminationReason.FAILED_TO_START_DUE_TO_NO_CAPACITY,
249255
None,
256+
None,
250257
"no offers",
251258
"gold1",
252259
),
260+
(
261+
JobStatus.FAILED,
262+
JobTerminationReason.FAILED_TO_START_DUE_TO_NO_CAPACITY,
263+
None,
264+
"No fleet found. Create it before submitting a run: https://dstack.ai/docs/concepts/fleets",
265+
"no fleets",
266+
"indian_red1",
267+
),
253268
(
254269
JobStatus.FAILED,
255270
JobTerminationReason.INTERRUPTED_BY_NO_CAPACITY,
256271
None,
272+
None,
257273
"interrupted",
258274
"gold1",
259275
),
260276
(
261277
JobStatus.FAILED,
262278
JobTerminationReason.INSTANCE_UNREACHABLE,
263279
None,
280+
None,
264281
"error",
265282
"indian_red1",
266283
),
267284
(
268285
JobStatus.TERMINATED,
269286
JobTerminationReason.TERMINATED_BY_USER,
270287
None,
288+
None,
271289
"stopped",
272290
"grey",
273291
),
274-
(JobStatus.TERMINATED, JobTerminationReason.ABORTED_BY_USER, None, "aborted", "grey"),
275-
(JobStatus.RUNNING, None, None, "running", "bold sea_green3"),
276-
(JobStatus.PROVISIONING, None, None, "provisioning", "bold deep_sky_blue1"),
277-
(JobStatus.PULLING, None, None, "pulling", "bold sea_green3"),
278-
(JobStatus.TERMINATING, None, None, "terminating", "bold deep_sky_blue1"),
292+
(
293+
JobStatus.TERMINATED,
294+
JobTerminationReason.ABORTED_BY_USER,
295+
None,
296+
None,
297+
"aborted",
298+
"grey",
299+
),
300+
(JobStatus.RUNNING, None, None, None, "running", "bold sea_green3"),
301+
(JobStatus.PROVISIONING, None, None, None, "provisioning", "bold deep_sky_blue1"),
302+
(JobStatus.PULLING, None, None, None, "pulling", "bold sea_green3"),
303+
(JobStatus.TERMINATING, None, None, None, "terminating", "bold deep_sky_blue1"),
279304
],
280305
)
281306
async def test_status_messages(
@@ -284,6 +309,7 @@ async def test_status_messages(
284309
job_status: JobStatus,
285310
termination_reason: Optional[JobTerminationReason],
286311
exit_status: Optional[int],
312+
termination_reason_message: Optional[str],
287313
expected_status: str,
288314
expected_style: str,
289315
):
@@ -292,6 +318,7 @@ async def test_status_messages(
292318
job_status=job_status,
293319
termination_reason=termination_reason,
294320
exit_status=exit_status,
321+
termination_reason_message=termination_reason_message,
295322
)
296323

297324
table = get_runs_table([api_run], verbose=False)

0 commit comments

Comments
 (0)