diff --git a/backend/backend/application/file_explorer/file_explorer.py b/backend/backend/application/file_explorer/file_explorer.py
index 79402ba..e3a461c 100644
--- a/backend/backend/application/file_explorer/file_explorer.py
+++ b/backend/backend/application/file_explorer/file_explorer.py
@@ -93,18 +93,25 @@ def load_models(self, session: Session):
# Sort models by execution order (DAG order)
sorted_model_names = topological_sort_models(models_with_refs)
+ # Build a lookup from model name -> model object for status fields
+ model_lookup = {m.model_name: m for m in all_models}
+
# Build the model structure in sorted order
no_code_model_structure = []
for no_code_model_name in sorted_model_names:
- no_code_model_structure.append(
- {
- "extension": no_code_model_name,
- "title": no_code_model_name,
- "key": f"{self.project_name}/models/no_code/{no_code_model_name}",
- "is_folder": False,
- "type": "NO_CODE_MODEL",
- }
- )
+ model = model_lookup.get(no_code_model_name)
+ model_data = {
+ "extension": no_code_model_name,
+ "title": no_code_model_name,
+ "key": f"{self.project_name}/models/no_code/{no_code_model_name}",
+ "is_folder": False,
+ "type": "NO_CODE_MODEL",
+ "run_status": getattr(model, "run_status", None),
+ "failure_reason": getattr(model, "failure_reason", None),
+ "last_run_at": model.last_run_at.isoformat() if getattr(model, "last_run_at", None) else None,
+ "run_duration": getattr(model, "run_duration", None),
+ }
+ no_code_model_structure.append(model_data)
model_structure: dict[str, Any] = {
"title": "models",
"key": f"{self.project_name}/models",
diff --git a/backend/backend/core/migrations/0003_add_model_run_status.py b/backend/backend/core/migrations/0003_add_model_run_status.py
new file mode 100644
index 0000000..c2e3ddd
--- /dev/null
+++ b/backend/backend/core/migrations/0003_add_model_run_status.py
@@ -0,0 +1,53 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("core", "0002_seed_data"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="configmodels",
+ name="run_status",
+ field=models.CharField(
+ choices=[
+ ("NOT_STARTED", "Not Started"),
+ ("RUNNING", "Running"),
+ ("SUCCESS", "Success"),
+ ("FAILED", "Failed"),
+ ],
+ default="NOT_STARTED",
+ help_text="Current execution status of the model",
+ max_length=20,
+ ),
+ ),
+ migrations.AddField(
+ model_name="configmodels",
+ name="failure_reason",
+ field=models.TextField(
+ blank=True,
+ help_text="Error message if the model execution failed",
+ null=True,
+ ),
+ ),
+ migrations.AddField(
+ model_name="configmodels",
+ name="last_run_at",
+ field=models.DateTimeField(
+ blank=True,
+ help_text="Timestamp of the last execution",
+ null=True,
+ ),
+ ),
+ migrations.AddField(
+ model_name="configmodels",
+ name="run_duration",
+ field=models.FloatField(
+ blank=True,
+ help_text="Duration of last execution in seconds",
+ null=True,
+ ),
+ ),
+ ]
diff --git a/backend/backend/core/models/config_models.py b/backend/backend/core/models/config_models.py
index e7b7aa2..f699cac 100644
--- a/backend/backend/core/models/config_models.py
+++ b/backend/backend/core/models/config_models.py
@@ -19,6 +19,12 @@ class ConfigModels(DefaultOrganizationMixin, BaseModel):
This model is used to store the no code models.
"""
+ class RunStatus(models.TextChoices):
+ NOT_STARTED = "NOT_STARTED", "Not Started"
+ RUNNING = "RUNNING", "Running"
+ SUCCESS = "SUCCESS", "Success"
+ FAILED = "FAILED", "Failed"
+
def get_model_upload_path(self, filename: str) -> str:
"""
This returns the file path based on the org and project dynamically.
@@ -94,6 +100,29 @@ class Meta:
last_modified_by = models.JSONField(default=dict)
last_modified_at = models.DateTimeField(auto_now=True)
+ # Execution status tracking
+ run_status = models.CharField(
+ max_length=20,
+ choices=RunStatus.choices,
+ default=RunStatus.NOT_STARTED,
+ help_text="Current execution status of the model",
+ )
+ failure_reason = models.TextField(
+ null=True,
+ blank=True,
+ help_text="Error message if the model execution failed",
+ )
+ last_run_at = models.DateTimeField(
+ null=True,
+ blank=True,
+ help_text="Timestamp of the last execution",
+ )
+ run_duration = models.FloatField(
+ null=True,
+ blank=True,
+ help_text="Duration of last execution in seconds",
+ )
+
# Current Manager
config_objects = models.Manager()
diff --git a/backend/backend/core/routers/execute/views.py b/backend/backend/core/routers/execute/views.py
index 742d277..648a952 100644
--- a/backend/backend/core/routers/execute/views.py
+++ b/backend/backend/core/routers/execute/views.py
@@ -57,12 +57,17 @@ def execute_run_command(request: Request, project_id: str) -> Response:
)
logger.info(f"[execute_run_command] API called - project_id={project_id}, file_name={file_name}, environment_id={environment_id}")
app = ApplicationContext(project_id=project_id)
- app.execute_visitran_run_command(current_model=file_name, environment_id=environment_id)
- app.visitran_context.close_db_connection()
- app.backup_current_no_code_model()
- logger.info(f"[execute_run_command] Completed successfully for file_name={file_name}")
- _data = {"status": "success"}
- return Response(data=_data)
+ try:
+ app.execute_visitran_run_command(current_model=file_name, environment_id=environment_id)
+ app.visitran_context.close_db_connection()
+ app.backup_current_no_code_model()
+ logger.info(f"[execute_run_command] Completed successfully for file_name={file_name}")
+ _data = {"status": "success"}
+ return Response(data=_data)
+ except Exception as e:
+ logger.error(f"[execute_run_command] DAG execution failed for file_name={file_name}: {e}")
+ _data = {"status": "failed", "error_message": str(e)}
+ return Response(data=_data, status=status.HTTP_400_BAD_REQUEST)
diff --git a/backend/backend/utils/cache_service/decorators/cache_decorator.py b/backend/backend/utils/cache_service/decorators/cache_decorator.py
index 32f8d4e..6b5ba18 100644
--- a/backend/backend/utils/cache_service/decorators/cache_decorator.py
+++ b/backend/backend/utils/cache_service/decorators/cache_decorator.py
@@ -96,6 +96,7 @@ def wrapped(view_or_request, *args, **kwargs):
except Exception as e:
Logger.exception("Error executing view function")
+ raise
return response
diff --git a/backend/visitran/visitran.py b/backend/visitran/visitran.py
index c75b20e..1d8d1f4 100644
--- a/backend/visitran/visitran.py
+++ b/backend/visitran/visitran.py
@@ -15,6 +15,7 @@
import ibis
import networkx as nx
+from django.utils import timezone
from visitran import utils
from visitran.adapters.adapter import BaseAdapter
from visitran.adapters.seed import BaseSeed
@@ -71,6 +72,12 @@
from visitran.templates.model import VisitranModel
from visitran.templates.snapshot import VisitranSnapshot
+# Import Django models for status tracking
+try:
+ from backend.core.models.config_models import ConfigModels
+except ImportError:
+ ConfigModels = None
+
warnings.filterwarnings("ignore", message=".*?pkg_resources.*?")
from matplotlib import pyplot as plt # noqa: E402
@@ -228,6 +235,38 @@ def sort_func(node_key: str):
self.sorted_dag_nodes = list(nx.lexicographical_topological_sort(self.dag, key=sort_func))
fire_event(SortedDAGNodes(sorted_dag_nodes=str(self.sorted_dag_nodes)))
+ def _update_model_status(self, model_name: str, run_status: str, failure_reason: str = None) -> None:
+ """Update the run status of a model in the database."""
+ if ConfigModels is None:
+ return
+
+ try:
+ class_name = model_name.split("'")[1].split(".")[-2] if "'" in model_name else model_name
+
+ session = getattr(self.context, "session", None)
+ if not session:
+ return
+
+ project_id = session.project_id
+ if not project_id:
+ return
+
+ model_instance = ConfigModels.objects.get(
+ project_instance__project_uuid=project_id,
+ model_name=class_name,
+ )
+ model_instance.run_status = run_status
+ model_instance.last_run_at = timezone.now()
+
+ if run_status == ConfigModels.RunStatus.FAILED:
+ model_instance.failure_reason = failure_reason
+ elif run_status == ConfigModels.RunStatus.SUCCESS:
+ model_instance.failure_reason = None
+
+ model_instance.save(update_fields=["run_status", "last_run_at", "failure_reason"])
+ except Exception as e:
+ logging.warning(f"Failed to update model status for {model_name}: {e}")
+
def execute_graph(self) -> None:
"""Executes the sorted DAG elements one by one."""
dag_nodes = self.sorted_dag_nodes
@@ -237,6 +276,9 @@ def execute_graph(self) -> None:
node = self.dag.nodes[node_name]["model_object"]
is_executable = self.dag.nodes[node_name].get("executable", True)
try:
+ # Set status to RUNNING before execution
+ self._update_model_status(str(node_name), ConfigModels.RunStatus.RUNNING if ConfigModels else "RUNNING")
+
# Apply model_configs override from deployment configuration
self._apply_model_config_override(node)
@@ -270,6 +312,9 @@ def execute_graph(self) -> None:
self.db_adapter.db_connection.create_schema(node.destination_schema_name) # create if not exists
self.db_adapter.run_model(visitran_model=node)
+ # Set status to SUCCESS after successful execution
+ self._update_model_status(str(node_name), ConfigModels.RunStatus.SUCCESS if ConfigModels else "SUCCESS")
+
base_result = BaseResult(
node_name=str(node_name),
sequence_num=sequence_number,
@@ -282,11 +327,22 @@ def execute_graph(self) -> None:
sequence_number += 1
BASE_RESULT.append(base_result)
except VisitranBaseExceptions as visitran_err:
+ self._update_model_status(
+ str(node_name),
+ ConfigModels.RunStatus.FAILED if ConfigModels else "FAILED",
+ failure_reason=str(visitran_err),
+ )
raise visitran_err
except Exception as err:
dest_table = node.destination_table_name
sch_name = node.destination_schema_name
err_trace = repr(err)
+
+ self._update_model_status(
+ str(node_name),
+ ConfigModels.RunStatus.FAILED if ConfigModels else "FAILED",
+ failure_reason=err_trace,
+ )
base_result = BaseResult(
node_name=str(node_name),
sequence_num=sequence_number,
diff --git a/frontend/src/ide/editor/no-code-model/no-code-model.jsx b/frontend/src/ide/editor/no-code-model/no-code-model.jsx
index 80304d0..1546469 100644
--- a/frontend/src/ide/editor/no-code-model/no-code-model.jsx
+++ b/frontend/src/ide/editor/no-code-model/no-code-model.jsx
@@ -1602,6 +1602,7 @@ function NoCodeModel({ nodeData }) {
axios(requestOptions)
.then(() => {
getSampleData(undefined, undefined, spec);
+ setRefreshModels(true);
})
.catch((error) => {
const notifKey = notify({
@@ -1636,6 +1637,7 @@ function NoCodeModel({ nodeData }) {
});
setTransformationErrorFlag(true);
setIsLoading(false);
+ setRefreshModels(true);
});
};
diff --git a/frontend/src/ide/explorer/explorer-component.jsx b/frontend/src/ide/explorer/explorer-component.jsx
index 83ef24a..8bdc5c4 100644
--- a/frontend/src/ide/explorer/explorer-component.jsx
+++ b/frontend/src/ide/explorer/explorer-component.jsx
@@ -16,6 +16,7 @@ import {
Badge,
theme,
Checkbox,
+ Popover,
} from "antd";
import {
CaretDownOutlined,
@@ -460,29 +461,47 @@ const IdeExplorer = ({
);
- // Add checkboxes to model children when in delete mode
- item.children = item.children.map((child) => ({
- ...child,
- title: (
-
+ {failureReason}
+
+ )}
+ {lastRunAt && (
+