Skip to content

Commit 4905390

Browse files
abhizipstackclaude
andcommitted
feat: track model execution status and show failure icon in explorer
Implement model-level execution status tracking (NOT_STARTED, RUNNING, SUCCESS, FAILED) at DAG node level with visual indicators in the file explorer. Status is tracked per individual DAG node, ensuring accurate status for each model even when downstream dependencies fail. Backend changes: - Add RunStatus enum and 4 new fields to ConfigModels (run_status, failure_reason, last_run_at, run_duration) - Migration 0003_add_model_run_status - Update file_explorer.load_models to return status fields - Add _update_model_status to DAG executor — called before execution (RUNNING), after success (SUCCESS), and on exception (FAILED with reason) - Update execute/views.py to return 400 on DAG execution failures - Fix clear_cache decorator to re-raise exceptions instead of swallowing them silently Frontend changes: - Add getModelRunStatus helper to render colored dot badges next to model names in the explorer tree - Running: blue, Success: green, Failed: red - Show Popover on hover over failed status with full error message and last run timestamp - Trigger explorer refresh via setRefreshModels after runTransformation succeeds or fails Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b1dd61d commit 4905390

8 files changed

Lines changed: 277 additions & 38 deletions

File tree

backend/backend/application/file_explorer/file_explorer.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,18 +93,25 @@ def load_models(self, session: Session):
9393
# Sort models by execution order (DAG order)
9494
sorted_model_names = topological_sort_models(models_with_refs)
9595

96+
# Build a lookup from model name -> model object for status fields
97+
model_lookup = {m.model_name: m for m in all_models}
98+
9699
# Build the model structure in sorted order
97100
no_code_model_structure = []
98101
for no_code_model_name in sorted_model_names:
99-
no_code_model_structure.append(
100-
{
101-
"extension": no_code_model_name,
102-
"title": no_code_model_name,
103-
"key": f"{self.project_name}/models/no_code/{no_code_model_name}",
104-
"is_folder": False,
105-
"type": "NO_CODE_MODEL",
106-
}
107-
)
102+
model = model_lookup.get(no_code_model_name)
103+
model_data = {
104+
"extension": no_code_model_name,
105+
"title": no_code_model_name,
106+
"key": f"{self.project_name}/models/no_code/{no_code_model_name}",
107+
"is_folder": False,
108+
"type": "NO_CODE_MODEL",
109+
"run_status": getattr(model, "run_status", None),
110+
"failure_reason": getattr(model, "failure_reason", None),
111+
"last_run_at": model.last_run_at.isoformat() if getattr(model, "last_run_at", None) else None,
112+
"run_duration": getattr(model, "run_duration", None),
113+
}
114+
no_code_model_structure.append(model_data)
108115
model_structure: dict[str, Any] = {
109116
"title": "models",
110117
"key": f"{self.project_name}/models",
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from django.db import migrations, models
2+
3+
4+
class Migration(migrations.Migration):
5+
6+
dependencies = [
7+
("core", "0002_seed_data"),
8+
]
9+
10+
operations = [
11+
migrations.AddField(
12+
model_name="configmodels",
13+
name="run_status",
14+
field=models.CharField(
15+
choices=[
16+
("NOT_STARTED", "Not Started"),
17+
("RUNNING", "Running"),
18+
("SUCCESS", "Success"),
19+
("FAILED", "Failed"),
20+
],
21+
default="NOT_STARTED",
22+
help_text="Current execution status of the model",
23+
max_length=20,
24+
),
25+
),
26+
migrations.AddField(
27+
model_name="configmodels",
28+
name="failure_reason",
29+
field=models.TextField(
30+
blank=True,
31+
help_text="Error message if the model execution failed",
32+
null=True,
33+
),
34+
),
35+
migrations.AddField(
36+
model_name="configmodels",
37+
name="last_run_at",
38+
field=models.DateTimeField(
39+
blank=True,
40+
help_text="Timestamp of the last execution",
41+
null=True,
42+
),
43+
),
44+
migrations.AddField(
45+
model_name="configmodels",
46+
name="run_duration",
47+
field=models.FloatField(
48+
blank=True,
49+
help_text="Duration of last execution in seconds",
50+
null=True,
51+
),
52+
),
53+
]

backend/backend/core/models/config_models.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ class ConfigModels(DefaultOrganizationMixin, BaseModel):
1919
This model is used to store the no code models.
2020
"""
2121

22+
class RunStatus(models.TextChoices):
23+
NOT_STARTED = "NOT_STARTED", "Not Started"
24+
RUNNING = "RUNNING", "Running"
25+
SUCCESS = "SUCCESS", "Success"
26+
FAILED = "FAILED", "Failed"
27+
2228
def get_model_upload_path(self, filename: str) -> str:
2329
"""
2430
This returns the file path based on the org and project dynamically.
@@ -94,6 +100,29 @@ class Meta:
94100
last_modified_by = models.JSONField(default=dict)
95101
last_modified_at = models.DateTimeField(auto_now=True)
96102

103+
# Execution status tracking
104+
run_status = models.CharField(
105+
max_length=20,
106+
choices=RunStatus.choices,
107+
default=RunStatus.NOT_STARTED,
108+
help_text="Current execution status of the model",
109+
)
110+
failure_reason = models.TextField(
111+
null=True,
112+
blank=True,
113+
help_text="Error message if the model execution failed",
114+
)
115+
last_run_at = models.DateTimeField(
116+
null=True,
117+
blank=True,
118+
help_text="Timestamp of the last execution",
119+
)
120+
run_duration = models.FloatField(
121+
null=True,
122+
blank=True,
123+
help_text="Duration of last execution in seconds",
124+
)
125+
97126
# Current Manager
98127
config_objects = models.Manager()
99128

backend/backend/core/routers/execute/views.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,17 @@ def execute_run_command(request: Request, project_id: str) -> Response:
5757
)
5858
logger.info(f"[execute_run_command] API called - project_id={project_id}, file_name={file_name}, environment_id={environment_id}")
5959
app = ApplicationContext(project_id=project_id)
60-
app.execute_visitran_run_command(current_model=file_name, environment_id=environment_id)
61-
app.visitran_context.close_db_connection()
62-
app.backup_current_no_code_model()
63-
logger.info(f"[execute_run_command] Completed successfully for file_name={file_name}")
64-
_data = {"status": "success"}
65-
return Response(data=_data)
60+
try:
61+
app.execute_visitran_run_command(current_model=file_name, environment_id=environment_id)
62+
app.visitran_context.close_db_connection()
63+
app.backup_current_no_code_model()
64+
logger.info(f"[execute_run_command] Completed successfully for file_name={file_name}")
65+
_data = {"status": "success"}
66+
return Response(data=_data)
67+
except Exception as e:
68+
logger.error(f"[execute_run_command] DAG execution failed for file_name={file_name}: {e}")
69+
_data = {"status": "failed", "error_message": str(e)}
70+
return Response(data=_data, status=status.HTTP_400_BAD_REQUEST)
6671

6772

6873

backend/backend/utils/cache_service/decorators/cache_decorator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ def wrapped(view_or_request, *args, **kwargs):
9696

9797
except Exception as e:
9898
Logger.exception("Error executing view function")
99+
raise
99100

100101
return response
101102

backend/visitran/visitran.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import ibis
1717
import networkx as nx
18+
from django.utils import timezone
1819
from visitran import utils
1920
from visitran.adapters.adapter import BaseAdapter
2021
from visitran.adapters.seed import BaseSeed
@@ -71,6 +72,12 @@
7172
from visitran.templates.model import VisitranModel
7273
from visitran.templates.snapshot import VisitranSnapshot
7374

75+
# Import Django models for status tracking
76+
try:
77+
from backend.core.models.config_models import ConfigModels
78+
except ImportError:
79+
ConfigModels = None
80+
7481
warnings.filterwarnings("ignore", message=".*?pkg_resources.*?")
7582
from matplotlib import pyplot as plt # noqa: E402
7683

@@ -228,6 +235,38 @@ def sort_func(node_key: str):
228235
self.sorted_dag_nodes = list(nx.lexicographical_topological_sort(self.dag, key=sort_func))
229236
fire_event(SortedDAGNodes(sorted_dag_nodes=str(self.sorted_dag_nodes)))
230237

238+
def _update_model_status(self, model_name: str, run_status: str, failure_reason: str = None) -> None:
239+
"""Update the run status of a model in the database."""
240+
if ConfigModels is None:
241+
return
242+
243+
try:
244+
class_name = model_name.split("'")[1].split(".")[-2] if "'" in model_name else model_name
245+
246+
session = getattr(self.context, "session", None)
247+
if not session:
248+
return
249+
250+
project_id = session.project_id
251+
if not project_id:
252+
return
253+
254+
model_instance = ConfigModels.objects.get(
255+
project_instance__project_uuid=project_id,
256+
model_name=class_name,
257+
)
258+
model_instance.run_status = run_status
259+
model_instance.last_run_at = timezone.now()
260+
261+
if run_status == ConfigModels.RunStatus.FAILED:
262+
model_instance.failure_reason = failure_reason
263+
elif run_status == ConfigModels.RunStatus.SUCCESS:
264+
model_instance.failure_reason = None
265+
266+
model_instance.save(update_fields=["run_status", "last_run_at", "failure_reason"])
267+
except Exception as e:
268+
logging.warning(f"Failed to update model status for {model_name}: {e}")
269+
231270
def execute_graph(self) -> None:
232271
"""Executes the sorted DAG elements one by one."""
233272
dag_nodes = self.sorted_dag_nodes
@@ -237,6 +276,9 @@ def execute_graph(self) -> None:
237276
node = self.dag.nodes[node_name]["model_object"]
238277
is_executable = self.dag.nodes[node_name].get("executable", True)
239278
try:
279+
# Set status to RUNNING before execution
280+
self._update_model_status(str(node_name), ConfigModels.RunStatus.RUNNING if ConfigModels else "RUNNING")
281+
240282
# Apply model_configs override from deployment configuration
241283
self._apply_model_config_override(node)
242284

@@ -270,6 +312,9 @@ def execute_graph(self) -> None:
270312
self.db_adapter.db_connection.create_schema(node.destination_schema_name) # create if not exists
271313
self.db_adapter.run_model(visitran_model=node)
272314

315+
# Set status to SUCCESS after successful execution
316+
self._update_model_status(str(node_name), ConfigModels.RunStatus.SUCCESS if ConfigModels else "SUCCESS")
317+
273318
base_result = BaseResult(
274319
node_name=str(node_name),
275320
sequence_num=sequence_number,
@@ -282,11 +327,22 @@ def execute_graph(self) -> None:
282327
sequence_number += 1
283328
BASE_RESULT.append(base_result)
284329
except VisitranBaseExceptions as visitran_err:
330+
self._update_model_status(
331+
str(node_name),
332+
ConfigModels.RunStatus.FAILED if ConfigModels else "FAILED",
333+
failure_reason=str(visitran_err),
334+
)
285335
raise visitran_err
286336
except Exception as err:
287337
dest_table = node.destination_table_name
288338
sch_name = node.destination_schema_name
289339
err_trace = repr(err)
340+
341+
self._update_model_status(
342+
str(node_name),
343+
ConfigModels.RunStatus.FAILED if ConfigModels else "FAILED",
344+
failure_reason=err_trace,
345+
)
290346
base_result = BaseResult(
291347
node_name=str(node_name),
292348
sequence_num=sequence_number,

frontend/src/ide/editor/no-code-model/no-code-model.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1602,6 +1602,7 @@ function NoCodeModel({ nodeData }) {
16021602
axios(requestOptions)
16031603
.then(() => {
16041604
getSampleData(undefined, undefined, spec);
1605+
setRefreshModels(true);
16051606
})
16061607
.catch((error) => {
16071608
const notifKey = notify({
@@ -1636,6 +1637,7 @@ function NoCodeModel({ nodeData }) {
16361637
});
16371638
setTransformationErrorFlag(true);
16381639
setIsLoading(false);
1640+
setRefreshModels(true);
16391641
});
16401642
};
16411643

0 commit comments

Comments
 (0)