From e054d6d542356be644c35b66116e7e2b3e0107e0 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Fri, 17 Oct 2025 13:36:40 +0500 Subject: [PATCH 1/3] Add Run.next_triggered_at --- src/dstack/_internal/core/models/runs.py | 1 + src/dstack/_internal/server/services/runs.py | 2 ++ src/tests/_internal/server/routers/test_runs.py | 4 ++++ 3 files changed, 7 insertions(+) diff --git a/src/dstack/_internal/core/models/runs.py b/src/dstack/_internal/core/models/runs.py index 765888e349..0a5b174d23 100644 --- a/src/dstack/_internal/core/models/runs.py +++ b/src/dstack/_internal/core/models/runs.py @@ -553,6 +553,7 @@ class Run(CoreModel): deployment_num: int = 0 # default for compatibility with pre-0.19.14 servers error: Optional[str] = None deleted: Optional[bool] = None + next_triggered_at: Optional[datetime] = None def is_deployment_in_progress(self) -> bool: return any( diff --git a/src/dstack/_internal/server/services/runs.py b/src/dstack/_internal/server/services/runs.py index c43f802fc8..1b9d18e4b1 100644 --- a/src/dstack/_internal/server/services/runs.py +++ b/src/dstack/_internal/server/services/runs.py @@ -715,6 +715,7 @@ def run_model_to_run( status_message = _get_run_status_message(run_model) error = _get_run_error(run_model) fleet = _get_run_fleet(run_model) + next_triggered_at = _get_next_triggered_at(run_spec) run = Run( id=run_model.id, project_name=run_model.project.name, @@ -734,6 +735,7 @@ def run_model_to_run( deployment_num=run_model.deployment_num, error=error, deleted=run_model.deleted, + next_triggered_at=next_triggered_at, ) run.cost = _get_run_cost(run) return run diff --git a/src/tests/_internal/server/routers/test_runs.py b/src/tests/_internal/server/routers/test_runs.py index be868f1e55..f4e481f539 100644 --- a/src/tests/_internal/server/routers/test_runs.py +++ b/src/tests/_internal/server/routers/test_runs.py @@ -517,6 +517,7 @@ def get_dev_env_run_dict( "termination_reason": None, "error": None, "deleted": deleted, + "next_triggered_at": None, } @@ -665,6 +666,7 @@ async def test_lists_runs(self, test_db, session: AsyncSession, client: AsyncCli "termination_reason": None, "error": None, "deleted": False, + "next_triggered_at": None, }, { "id": str(run2.id), @@ -687,6 +689,7 @@ async def test_lists_runs(self, test_db, session: AsyncSession, client: AsyncCli "termination_reason": None, "error": None, "deleted": False, + "next_triggered_at": None, }, ] @@ -853,6 +856,7 @@ async def test_limits_job_submissions( "termination_reason": None, "error": None, "deleted": False, + "next_triggered_at": None, }, ] From c9993e33855e43dcee478dae2077e15cac1f5a43 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Fri, 17 Oct 2025 14:18:01 +0500 Subject: [PATCH 2/3] Return next_triggered_at=None for finished runs --- src/dstack/_internal/server/services/runs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/dstack/_internal/server/services/runs.py b/src/dstack/_internal/server/services/runs.py index 1b9d18e4b1..25ac750aa3 100644 --- a/src/dstack/_internal/server/services/runs.py +++ b/src/dstack/_internal/server/services/runs.py @@ -715,7 +715,9 @@ def run_model_to_run( status_message = _get_run_status_message(run_model) error = _get_run_error(run_model) fleet = _get_run_fleet(run_model) - next_triggered_at = _get_next_triggered_at(run_spec) + next_triggered_at = None + if not run_model.status.is_finished(): + next_triggered_at = _get_next_triggered_at(run_spec) run = Run( id=run_model.id, project_name=run_model.project.name, From 0d4f86c303d67e8b498eec515cb68da92ef2e584 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Fri, 17 Oct 2025 14:19:17 +0500 Subject: [PATCH 3/3] Show Schedule and Next run on Run page --- frontend/src/locale/en.json | 2 ++ .../pages/Runs/Details/RunDetails/index.tsx | 28 +++++++++++++++---- frontend/src/pages/Runs/List/helpers.ts | 18 ++++++++---- frontend/src/types/run.d.ts | 7 +++++ 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 7d32a7545d..b5239fe60b 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -417,6 +417,8 @@ "backend": "Backend", "region": "Region", "instance_id": "Instance ID", + "schedule": "Schedule", + "next_run": "Next run", "resources": "Resources", "spot": "Spot", "termination_reason": "Termination reason", diff --git a/frontend/src/pages/Runs/Details/RunDetails/index.tsx b/frontend/src/pages/Runs/Details/RunDetails/index.tsx index aab63a3edd..fae251effa 100644 --- a/frontend/src/pages/Runs/Details/RunDetails/index.tsx +++ b/frontend/src/pages/Runs/Details/RunDetails/index.tsx @@ -20,6 +20,7 @@ import { getRunListItemPrice, getRunListItemRegion, getRunListItemResources, + getRunListItemSchedule, getRunListItemServiceUrl, getRunListItemSpot, } from '../../List/helpers'; @@ -38,6 +39,8 @@ export const RunDetails = () => { }); const serviceUrl = runData ? getRunListItemServiceUrl(runData) : null; + const schedule = runData ? getRunListItemSchedule(runData) : null; + const nextTriggeredAt = runData ? runData.next_triggered_at : null; if (isLoadingRun) return ( @@ -115,7 +118,7 @@ export const RunDetails = () => {
{t('projects.run.error')} -
{getRunError(runData)}
+
{getRunError(runData) ?? '-'}
@@ -138,6 +141,11 @@ export const RunDetails = () => {
{getRunListItemResources(runData)}
+
+ {t('projects.run.backend')} +
{getRunListItemBackend(runData)}
+
+
{t('projects.run.region')}
{getRunListItemRegion(runData)}
@@ -152,11 +160,6 @@ export const RunDetails = () => { {t('projects.run.spot')}
{getRunListItemSpot(runData)}
- -
- {t('projects.run.backend')} -
{getRunListItemBackend(runData)}
-
{serviceUrl && ( @@ -169,6 +172,19 @@ export const RunDetails = () => { )} + + {schedule && ( + +
+ {t('projects.run.schedule')} +
{schedule}
+
+
+ {t('projects.run.next_run')} +
{nextTriggeredAt ? format(new Date(nextTriggeredAt), DATE_TIME_FORMAT) : '-'}
+
+
+ )} {runData.run_spec.configuration.type === 'dev-environment' && !runIsStopped(runData.status) && ( diff --git a/frontend/src/pages/Runs/List/helpers.ts b/frontend/src/pages/Runs/List/helpers.ts index e66bd69319..893a5fdbcb 100644 --- a/frontend/src/pages/Runs/List/helpers.ts +++ b/frontend/src/pages/Runs/List/helpers.ts @@ -14,7 +14,7 @@ export const getRunListItemResources = (run: IRun) => { return '-'; } - return run.latest_job_submission?.job_provisioning_data?.instance_type?.resources?.description; + return run.latest_job_submission?.job_provisioning_data?.instance_type?.resources?.description ?? '-'; }; export const getRunListItemSpotLabelKey = (run: IRun) => { @@ -31,7 +31,7 @@ export const getRunListItemSpotLabelKey = (run: IRun) => { export const getRunListItemSpot = (run: IRun) => { if (run.jobs.length > 1) { - return ''; + return '-'; } return run.latest_job_submission?.job_provisioning_data?.instance_type?.resources?.spot?.toString() ?? '-'; @@ -57,7 +57,7 @@ export const getRunListItemPrice = (run: IRun) => { export const getRunListItemInstance = (run: IRun) => { if (run.jobs.length > 1) { - return ''; + return '-'; } return run.latest_job_submission?.job_provisioning_data?.instance_type?.name; @@ -65,7 +65,7 @@ export const getRunListItemInstance = (run: IRun) => { export const getRunListItemInstanceId = (run: IRun) => { if (run.jobs.length > 1) { - return ''; + return '-'; } return run.latest_job_submission?.job_provisioning_data?.instance_id ?? '-'; @@ -73,7 +73,7 @@ export const getRunListItemInstanceId = (run: IRun) => { export const getRunListItemRegion = (run: IRun) => { if (run.jobs.length > 1) { - return ''; + return '-'; } return run.latest_job_submission?.job_provisioning_data?.region ?? '-'; @@ -81,7 +81,7 @@ export const getRunListItemRegion = (run: IRun) => { export const getRunListItemBackend = (run: IRun) => { if (run.jobs.length > 1) { - return ''; + return '-'; } return run.latest_job_submission?.job_provisioning_data?.backend ?? '-'; @@ -92,3 +92,9 @@ export const getRunListItemServiceUrl = (run: IRun) => { if (!url) return null; return url.startsWith('/') ? `${getBaseUrl()}${url}` : url; }; + +export const getRunListItemSchedule = (run: IRun) => { + if (run.run_spec.configuration.type != 'task' || !run.run_spec.configuration.schedule) return null; + + return run.run_spec.configuration.schedule.cron.join(', '); +}; diff --git a/frontend/src/types/run.d.ts b/frontend/src/types/run.d.ts index 79b20f14d0..b8e4640957 100644 --- a/frontend/src/types/run.d.ts +++ b/frontend/src/types/run.d.ts @@ -240,9 +240,14 @@ declare interface IJob { job_submissions: IJobSubmission[]; } +declare interface ISchedule { + cron: string[]; +} + declare interface ITaskConfiguration { type: 'task'; priority?: number | null; + schedule?: ISchedule | null; } declare interface IServiceConfiguration { @@ -250,6 +255,7 @@ declare interface IServiceConfiguration { gateway: string | null; priority?: number | null; } + declare interface IRunSpec { configuration: TDevEnvironmentConfiguration | ITaskConfiguration | IServiceConfiguration; configuration_path: string; @@ -286,6 +292,7 @@ declare interface IRun { cost: number; service: IRunService | null; status_message?: string | null; + next_triggered_at?: string | null; } declare interface IMetricsItem {