diff --git a/pgml-dashboard/src/api/cms.rs b/pgml-dashboard/src/api/cms.rs index 608273ef0..c42329e4d 100644 --- a/pgml-dashboard/src/api/cms.rs +++ b/pgml-dashboard/src/api/cms.rs @@ -1123,7 +1123,7 @@ This is the end of the markdown } } - // Ensure Docs render and ther are no unparsed gitbook compnents. + // Ensure Docs render and there are no unparsed gitbook compnents. #[sqlx::test] async fn render_guides_test() { let client = Client::tracked(rocket().await).await.unwrap(); diff --git a/pgml-dashboard/src/api/deployment/deployment_models.rs b/pgml-dashboard/src/api/deployment/deployment_models.rs new file mode 100644 index 000000000..35e832b26 --- /dev/null +++ b/pgml-dashboard/src/api/deployment/deployment_models.rs @@ -0,0 +1,115 @@ +use rocket::route::Route; +use sailfish::TemplateOnce; + +use crate::{ + guards::ConnectedCluster, + responses::{Error, ResponseOk}, +}; + +use crate::templates::{components::NavLink, *}; + +use crate::models; +use crate::templates; +use crate::utils::tabs; +use crate::utils::urls; + +use std::collections::HashMap; + +// Returns models page +#[get("/models")] +pub async fn deployment_models(cluster: ConnectedCluster<'_>) -> Result { + let mut layout = crate::templates::WebAppBase::new("Dashboard", &cluster.inner.context); + layout.breadcrumbs(vec![NavLink::new("Models", &urls::deployment_models()).active()]); + + let tabs = vec![tabs::Tab { + name: "Models", + content: ModelsTab {}.render_once().unwrap(), + }]; + + let nav_tabs = tabs::Tabs::new(tabs, Some("Models"), Some("Models"))?; + + Ok(ResponseOk(layout.render(templates::Dashboard { tabs: nav_tabs }))) +} + +// Returns models page +#[get("/models/")] +pub async fn model(cluster: ConnectedCluster<'_>, model_id: i64) -> Result { + let model = models::Model::get_by_id(cluster.pool(), model_id).await?; + let project = models::Project::get_by_id(cluster.pool(), model.project_id).await?; + + let mut layout = crate::templates::WebAppBase::new("Dashboard", &cluster.inner.context); + layout.breadcrumbs(vec![ + NavLink::new("Models", &urls::deployment_models()), + NavLink::new(&project.name, &urls::deployment_project_by_id(project.id)), + NavLink::new(&model.algorithm, &urls::deployment_model_by_id(model.id)).active(), + ]); + + let tabs = vec![tabs::Tab { + name: "Model", + content: ModelTab { model_id }.render_once().unwrap(), + }]; + + let nav_tabs = tabs::Tabs::new(tabs, Some("Models"), Some("Models"))?; + + Ok(ResponseOk(layout.render(templates::Dashboard { tabs: nav_tabs }))) +} + +#[get("/models_turboframe")] +pub async fn models_index(cluster: ConnectedCluster<'_>) -> Result { + let projects = models::Project::all(cluster.pool()).await?; + let mut models = HashMap::new(); + // let mut max_scores = HashMap::new(); + // let mut min_scores = HashMap::new(); + + for project in &projects { + let project_models = models::Model::get_by_project_id(cluster.pool(), project.id).await?; + // let mut key_metrics = project_models + // .iter() + // .map(|m| m.key_metric(project).unwrap_or(0.)) + // .collect::>(); + // key_metrics.sort_by(|a, b| a.partial_cmp(b).unwrap()); + + // max_scores.insert(project.id, key_metrics.iter().last().unwrap_or(&0.).clone()); + // min_scores.insert(project.id, key_metrics.iter().next().unwrap_or(&0.).clone()); + + models.insert(project.id, project_models); + } + + Ok(ResponseOk( + templates::Models { + projects, + models, + // min_scores, + // max_scores, + } + .render_once() + .unwrap(), + )) +} + +#[get("/models_turboframe/")] +pub async fn models_get(cluster: ConnectedCluster<'_>, id: i64) -> Result { + let model = models::Model::get_by_id(cluster.pool(), id).await?; + let snapshot = if let Some(snapshot_id) = model.snapshot_id { + Some(models::Snapshot::get_by_id(cluster.pool(), snapshot_id).await?) + } else { + None + }; + + let project = models::Project::get_by_id(cluster.pool(), model.project_id).await?; + + Ok(ResponseOk( + templates::Model { + deployed: model.deployed(cluster.pool()).await?, + model, + snapshot, + project, + } + .render_once() + .unwrap(), + )) +} + +pub fn routes() -> Vec { + routes![deployment_models, model, models_index, models_get,] +} diff --git a/pgml-dashboard/src/api/deployment/mod.rs b/pgml-dashboard/src/api/deployment/mod.rs new file mode 100644 index 000000000..f7f4e02c6 --- /dev/null +++ b/pgml-dashboard/src/api/deployment/mod.rs @@ -0,0 +1,63 @@ +use rocket::route::Route; +use sailfish::TemplateOnce; + +use crate::{ + guards::ConnectedCluster, + responses::{Error, ResponseOk}, +}; + +use crate::models; +use crate::templates; + +use std::collections::HashMap; + +pub mod deployment_models; +pub mod notebooks; +pub mod projects; +pub mod snapshots; +pub mod uploader; + +#[get("/deployments")] +pub async fn deployments_index(cluster: ConnectedCluster<'_>) -> Result { + let projects = models::Project::all(cluster.pool()).await?; + let mut deployments = HashMap::new(); + + for project in projects.iter() { + deployments.insert( + project.id, + models::Deployment::get_by_project_id(cluster.pool(), project.id).await?, + ); + } + + Ok(ResponseOk( + templates::Deployments { projects, deployments }.render_once().unwrap(), + )) +} + +#[get("/deployments/")] +pub async fn deployments_get(cluster: ConnectedCluster<'_>, id: i64) -> Result { + let deployment = models::Deployment::get_by_id(cluster.pool(), id).await?; + let project = models::Project::get_by_id(cluster.pool(), deployment.project_id).await?; + let model = models::Model::get_by_id(cluster.pool(), deployment.model_id).await?; + + Ok(ResponseOk( + templates::Deployment { + project, + deployment, + model, + } + .render_once() + .unwrap(), + )) +} + +pub fn routes() -> Vec { + let mut routes = routes![deployments_index, deployments_get,]; + + routes.extend(deployment_models::routes()); + routes.extend(notebooks::routes()); + routes.extend(projects::routes()); + routes.extend(snapshots::routes()); + routes.extend(uploader::routes()); + routes +} diff --git a/pgml-dashboard/src/api/deployment/notebooks.rs b/pgml-dashboard/src/api/deployment/notebooks.rs new file mode 100644 index 000000000..f3d1f00ff --- /dev/null +++ b/pgml-dashboard/src/api/deployment/notebooks.rs @@ -0,0 +1,300 @@ +use crate::forms; +use rocket::form::Form; +use rocket::response::Redirect; +use rocket::route::Route; +use rocket::serde::json::Json; +use sailfish::TemplateOnce; + +use crate::{ + guards::Cluster, + guards::ConnectedCluster, + responses::{Error, ResponseOk}, +}; + +use crate::templates::{components::NavLink, *}; +use crate::utils::tabs; + +use crate::models; +use crate::templates; +use crate::utils::urls; + +// Returns notebook page +#[get("/notebooks")] +pub async fn notebooks(cluster: ConnectedCluster<'_>) -> Result { + let mut layout = crate::templates::WebAppBase::new("Dashboard", &cluster.inner.context); + layout.breadcrumbs(vec![NavLink::new("Notebooks", &urls::deployment_notebooks()).active()]); + + let tabs = vec![tabs::Tab { + name: "Notebooks", + content: NotebooksTab {}.render_once().unwrap(), + }]; + + let nav_tabs = tabs::Tabs::new(tabs, Some("Notebooks"), Some("Notebooks"))?; + + Ok(ResponseOk(layout.render(templates::Dashboard { tabs: nav_tabs }))) +} + +// Returns the specified notebook page. +#[get("/notebooks/")] +pub async fn notebook(cluster: ConnectedCluster<'_>, notebook_id: i64) -> Result { + let notebook = models::Notebook::get_by_id(cluster.pool(), notebook_id).await?; + + let mut layout = crate::templates::WebAppBase::new("Dashboard", &cluster.inner.context); + layout.breadcrumbs(vec![ + NavLink::new("Notebooks", &urls::deployment_notebooks()), + NavLink::new(notebook.name.as_str(), &urls::deployment_notebook_by_id(notebook_id)).active(), + ]); + + let tabs = vec![tabs::Tab { + name: "Notebook", + content: NotebookTab { id: notebook_id }.render_once().unwrap(), + }]; + + let nav_tabs = tabs::Tabs::new(tabs, Some("Notebooks"), Some("Notebooks"))?; + + Ok(ResponseOk(layout.render(templates::Dashboard { tabs: nav_tabs }))) +} + +// Returns all the notebooks for a deployment in a turbo frame. +#[get("/notebooks_turboframe?")] +pub async fn notebook_index(cluster: ConnectedCluster<'_>, new: Option<&str>) -> Result { + Ok(ResponseOk( + templates::Notebooks { + notebooks: models::Notebook::all(cluster.pool()).await?, + new: new.is_some(), + } + .render_once() + .unwrap(), + )) +} + +// Creates a new named notebook and redirects to that specific notebook. +#[post("/notebooks", data = "")] +pub async fn notebook_create(cluster: &Cluster, data: Form>) -> Result { + let notebook = crate::models::Notebook::create(cluster.pool(), data.name).await?; + + models::Cell::create(cluster.pool(), ¬ebook, models::CellType::Sql as i32, "").await?; + + Ok(Redirect::to(urls::deployment_notebook_by_id(notebook.id))) +} + +// Returns the notebook in a turbo frame. +#[get("/notebooks_turboframe/")] +pub async fn notebook_get(cluster: ConnectedCluster<'_>, notebook_id: i64) -> Result { + let notebook = models::Notebook::get_by_id(cluster.pool(), notebook_id).await?; + let cells = notebook.cells(cluster.pool()).await?; + + Ok(ResponseOk( + templates::Notebook { cells, notebook }.render_once().unwrap(), + )) +} + +#[post("/notebooks//reset")] +pub async fn notebook_reset(cluster: ConnectedCluster<'_>, notebook_id: i64) -> Result { + let notebook = models::Notebook::get_by_id(cluster.pool(), notebook_id).await?; + notebook.reset(cluster.pool()).await?; + + Ok(Redirect::to(format!( + "{}/{}", + urls::deployment_notebooks_turboframe(), + notebook_id + ))) +} + +#[post("/notebooks//cell", data = "")] +pub async fn cell_create( + cluster: ConnectedCluster<'_>, + notebook_id: i64, + cell: Form>, +) -> Result { + let notebook = models::Notebook::get_by_id(cluster.pool(), notebook_id).await?; + let mut cell = + models::Cell::create(cluster.pool(), ¬ebook, cell.cell_type.parse::()?, cell.contents).await?; + + if !cell.contents.is_empty() { + cell.render(cluster.pool()).await?; + } + + Ok(Redirect::to(format!( + "{}/{}", + urls::deployment_notebooks_turboframe(), + notebook_id + ))) +} + +#[post("/notebooks//reorder", data = "")] +pub async fn notebook_reorder( + cluster: ConnectedCluster<'_>, + notebook_id: i64, + cells: Json, +) -> Result { + let _notebook = models::Notebook::get_by_id(cluster.pool(), notebook_id).await?; + + let pool = cluster.pool(); + let mut transaction = pool.begin().await?; + + // Super bad n+1, but it's ok for now? + for (idx, cell_id) in cells.cells.iter().enumerate() { + let cell = models::Cell::get_by_id(&mut *transaction, *cell_id).await?; + cell.reorder(&mut *transaction, idx as i32 + 1).await?; + } + + transaction.commit().await?; + + Ok(Redirect::to(format!( + "{}/{}", + urls::deployment_notebooks_turboframe(), + notebook_id + ))) +} + +#[get("/notebooks//cell/")] +pub async fn cell_get(cluster: ConnectedCluster<'_>, notebook_id: i64, cell_id: i64) -> Result { + let notebook = models::Notebook::get_by_id(cluster.pool(), notebook_id).await?; + let cell = models::Cell::get_by_id(cluster.pool(), cell_id).await?; + + Ok(ResponseOk( + templates::Cell { + cell, + notebook, + selected: false, + edit: false, + } + .render_once() + .unwrap(), + )) +} + +#[post("/notebooks//cell//cancel")] +pub async fn cell_cancel(cluster: ConnectedCluster<'_>, notebook_id: i64, cell_id: i64) -> Result { + let cell = models::Cell::get_by_id(cluster.pool(), cell_id).await?; + cell.cancel(cluster.pool()).await?; + Ok(Redirect::to(format!( + "{}/{}/cell/{}", + urls::deployment_notebooks(), + notebook_id, + cell_id + ))) +} + +#[post("/notebooks//cell//edit", data = "")] +pub async fn cell_edit( + cluster: ConnectedCluster<'_>, + notebook_id: i64, + cell_id: i64, + data: Form>, +) -> Result { + let notebook = models::Notebook::get_by_id(cluster.pool(), notebook_id).await?; + let mut cell = models::Cell::get_by_id(cluster.pool(), cell_id).await?; + + cell.update(cluster.pool(), data.cell_type.parse::()?, data.contents) + .await?; + + debug!("Rendering cell id={}", cell.id); + cell.render(cluster.pool()).await?; + debug!("Rendering of cell id={} complete", cell.id); + + Ok(ResponseOk( + templates::Cell { + cell, + notebook, + selected: false, + edit: false, + } + .render_once() + .unwrap(), + )) +} + +#[get("/notebooks//cell//edit")] +pub async fn cell_trigger_edit( + cluster: ConnectedCluster<'_>, + notebook_id: i64, + cell_id: i64, +) -> Result { + let notebook = models::Notebook::get_by_id(cluster.pool(), notebook_id).await?; + let cell = models::Cell::get_by_id(cluster.pool(), cell_id).await?; + + Ok(ResponseOk( + templates::Cell { + cell, + notebook, + selected: true, + edit: true, + } + .render_once() + .unwrap(), + )) +} + +#[post("/notebooks//cell//play")] +pub async fn cell_play(cluster: ConnectedCluster<'_>, notebook_id: i64, cell_id: i64) -> Result { + let notebook = models::Notebook::get_by_id(cluster.pool(), notebook_id).await?; + let mut cell = models::Cell::get_by_id(cluster.pool(), cell_id).await?; + cell.render(cluster.pool()).await?; + + Ok(ResponseOk( + templates::Cell { + cell, + notebook, + selected: true, + edit: false, + } + .render_once() + .unwrap(), + )) +} + +#[post("/notebooks//cell//remove")] +pub async fn cell_remove(cluster: ConnectedCluster<'_>, notebook_id: i64, cell_id: i64) -> Result { + let notebook = models::Notebook::get_by_id(cluster.pool(), notebook_id).await?; + let cell = models::Cell::get_by_id(cluster.pool(), cell_id).await?; + let bust_cache = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH)? + .as_millis() + .to_string(); + + Ok(ResponseOk( + templates::Undo { + notebook, + cell, + bust_cache, + } + .render_once()?, + )) +} + +#[post("/notebooks//cell//delete")] +pub async fn cell_delete(cluster: ConnectedCluster<'_>, notebook_id: i64, cell_id: i64) -> Result { + let _notebook = models::Notebook::get_by_id(cluster.pool(), notebook_id).await?; + let cell = models::Cell::get_by_id(cluster.pool(), cell_id).await?; + + let _ = cell.delete(cluster.pool()).await?; + + Ok(Redirect::to(format!( + "{}/{}/cell/{}", + urls::deployment_notebooks(), + notebook_id, + cell_id + ))) +} + +pub fn routes() -> Vec { + routes![ + notebooks, + notebook, + notebook_index, + notebook_create, + notebook_get, + notebook_reset, + cell_create, + notebook_reorder, + cell_get, + cell_cancel, + cell_edit, + cell_trigger_edit, + cell_play, + cell_remove, + cell_delete + ] +} diff --git a/pgml-dashboard/src/api/deployment/projects.rs b/pgml-dashboard/src/api/deployment/projects.rs new file mode 100644 index 000000000..83b598005 --- /dev/null +++ b/pgml-dashboard/src/api/deployment/projects.rs @@ -0,0 +1,78 @@ +use rocket::route::Route; +use sailfish::TemplateOnce; + +use crate::{ + guards::ConnectedCluster, + responses::{Error, ResponseOk}, +}; + +use crate::templates::{components::NavLink, *}; + +use crate::models; +use crate::templates; +use crate::utils::tabs; +use crate::utils::urls; + +// Returns the deployments projects page. +#[get("/projects")] +pub async fn projects(cluster: ConnectedCluster<'_>) -> Result { + let mut layout = crate::templates::WebAppBase::new("Dashboard", &cluster.inner.context); + layout.breadcrumbs(vec![NavLink::new("Projects", &urls::deployment_projects()).active()]); + + let tabs = vec![tabs::Tab { + name: "Projects", + content: ProjectsTab {}.render_once().unwrap(), + }]; + + let nav_tabs = tabs::Tabs::new(tabs, Some("Notebooks"), Some("Projects"))?; + + Ok(ResponseOk(layout.render(templates::Dashboard { tabs: nav_tabs }))) +} + +// Return the specified project page. +#[get("/projects/")] +pub async fn project(cluster: ConnectedCluster<'_>, project_id: i64) -> Result { + let project = models::Project::get_by_id(cluster.pool(), project_id).await?; + + let mut layout = crate::templates::WebAppBase::new("Dashboard", &cluster.inner.context); + layout.breadcrumbs(vec![ + NavLink::new("Projects", &urls::deployment_projects()), + NavLink::new(project.name.as_str(), &urls::deployment_project_by_id(project_id)).active(), + ]); + + let tabs = vec![tabs::Tab { + name: "Project", + content: ProjectTab { project_id }.render_once().unwrap(), + }]; + + let nav_tabs = tabs::Tabs::new(tabs, Some("Projects"), Some("Projects"))?; + + Ok(ResponseOk(layout.render(templates::Dashboard { tabs: nav_tabs }))) +} + +// Returns all the deployments for the project in a turbo frame. +#[get("/projects_turboframe")] +pub async fn project_index(cluster: ConnectedCluster<'_>) -> Result { + Ok(ResponseOk( + templates::Projects { + projects: models::Project::all(cluster.pool()).await?, + } + .render_once() + .unwrap(), + )) +} + +// Returns the specified project page. +#[get("/projects_turboframe/")] +pub async fn project_get(cluster: ConnectedCluster<'_>, id: i64) -> Result { + let project = models::Project::get_by_id(cluster.pool(), id).await?; + let models = models::Model::get_by_project_id(cluster.pool(), id).await?; + + Ok(ResponseOk( + templates::Project { project, models }.render_once().unwrap(), + )) +} + +pub fn routes() -> Vec { + routes![projects, project, project_index, project_get,] +} diff --git a/pgml-dashboard/src/api/deployment/snapshots.rs b/pgml-dashboard/src/api/deployment/snapshots.rs new file mode 100644 index 000000000..9413ea1c3 --- /dev/null +++ b/pgml-dashboard/src/api/deployment/snapshots.rs @@ -0,0 +1,89 @@ +use rocket::route::Route; +use sailfish::TemplateOnce; + +use crate::{ + guards::ConnectedCluster, + responses::{Error, ResponseOk}, +}; + +use crate::templates::{components::NavLink, *}; + +use crate::models; +use crate::templates; +use crate::utils::tabs; +use crate::utils::urls; +use std::collections::HashMap; + +// Returns snapshots page +#[get("/snapshots")] +pub async fn snapshots(cluster: ConnectedCluster<'_>) -> Result { + let mut layout = crate::templates::WebAppBase::new("Dashboard", &cluster.inner.context); + layout.breadcrumbs(vec![NavLink::new("Snapshots", &urls::deployment_snapshots()).active()]); + + let tabs = vec![tabs::Tab { + name: "Snapshots", + content: SnapshotsTab {}.render_once().unwrap(), + }]; + + let nav_tabs = tabs::Tabs::new(tabs, Some("Snapshots"), Some("Snapshots"))?; + + Ok(ResponseOk(layout.render(templates::Dashboard { tabs: nav_tabs }))) +} + +// Returns the specific snapshot page +#[get("/snapshots/")] +pub async fn snapshot(cluster: ConnectedCluster<'_>, snapshot_id: i64) -> Result { + let snapshot = models::Snapshot::get_by_id(cluster.pool(), snapshot_id).await?; + + let mut layout = crate::templates::WebAppBase::new("Dashboard", &cluster.inner.context); + layout.breadcrumbs(vec![ + NavLink::new("Snapshots", &urls::deployment_snapshots()), + NavLink::new(&snapshot.relation_name, &urls::deployment_snapshot_by_id(snapshot.id)).active(), + ]); + + let tabs = vec![tabs::Tab { + name: "Snapshot", + content: SnapshotTab { snapshot_id }.render_once().unwrap(), + }]; + + let nav_tabs = tabs::Tabs::new(tabs, Some("Snapshots"), Some("Snapshots"))?; + + Ok(ResponseOk(layout.render(templates::Dashboard { tabs: nav_tabs }))) +} + +// Returns all snapshots for the deployment in a turboframe. +#[get("/snapshots_turboframe")] +pub async fn snapshots_index(cluster: ConnectedCluster<'_>) -> Result { + let snapshots = models::Snapshot::all(cluster.pool()).await?; + + Ok(ResponseOk(templates::Snapshots { snapshots }.render_once().unwrap())) +} + +// Returns a specific snapshot for the deployment in a turboframe. +#[get("/snapshots_turboframe/")] +pub async fn snapshots_get(cluster: ConnectedCluster<'_>, id: i64) -> Result { + let snapshot = models::Snapshot::get_by_id(cluster.pool(), id).await?; + let samples = snapshot.samples(cluster.pool(), 500).await?; + + let models = snapshot.models(cluster.pool()).await?; + let mut projects = HashMap::new(); + + for model in &models { + projects.insert(model.project_id, model.project(cluster.pool()).await?); + } + + Ok(ResponseOk( + templates::Snapshot { + snapshot, + models, + projects, + samples, + } + .render_once() + .unwrap(), + )) +} + +pub fn routes() -> Vec { + routes![snapshots, snapshot, snapshots_index, snapshots_get,] +} diff --git a/pgml-dashboard/src/api/deployment/uploader.rs b/pgml-dashboard/src/api/deployment/uploader.rs new file mode 100644 index 000000000..ef1347b04 --- /dev/null +++ b/pgml-dashboard/src/api/deployment/uploader.rs @@ -0,0 +1,85 @@ +use crate::forms; +use rocket::form::Form; +use rocket::response::Redirect; +use rocket::route::Route; +use sailfish::TemplateOnce; + +use crate::{ + guards::ConnectedCluster, + responses::{BadRequest, Error, ResponseOk}, +}; + +use crate::templates::{components::NavLink, *}; + +use crate::models; +use crate::templates; +use crate::utils::tabs; +use crate::utils::urls; + +// Returns the uploader page. +#[get("/uploader")] +pub async fn uploader(cluster: ConnectedCluster<'_>) -> Result { + let mut layout = crate::templates::WebAppBase::new("Dashboard", &cluster.inner.context); + layout.breadcrumbs(vec![NavLink::new("Upload Data", &urls::deployment_uploader()).active()]); + + let tabs = vec![tabs::Tab { + name: "Upload data", + content: UploaderTab { table_name: None }.render_once().unwrap(), + }]; + + let nav_tabs = tabs::Tabs::new(tabs, Some("Upload Data"), Some("Upload Data"))?; + + Ok(ResponseOk(layout.render(templates::Dashboard { tabs: nav_tabs }))) +} + +// Returns uploader module in a turboframe. +#[get("/uploader_turboframe")] +pub async fn uploader_index() -> ResponseOk { + ResponseOk(templates::Uploader { error: None }.render_once().unwrap()) +} + +#[post("/uploader", data = "
")] +pub async fn uploader_upload( + cluster: ConnectedCluster<'_>, + form: Form>, +) -> Result { + let mut uploaded_file = models::UploadedFile::create(cluster.pool()).await.unwrap(); + + match uploaded_file + .upload(cluster.pool(), form.file.path().unwrap(), form.has_header) + .await + { + Ok(()) => Ok(Redirect::to(format!( + "{}/done?table_name={}", + urls::deployment_uploader_turboframe(), + uploaded_file.table_name() + ))), + Err(err) => Err(BadRequest( + templates::Uploader { + error: Some(err.to_string()), + } + .render_once() + .unwrap(), + )), + } +} + +#[get("/uploader_turboframe/done?")] +pub async fn uploaded_index(cluster: ConnectedCluster<'_>, table_name: &str) -> ResponseOk { + let sql = templates::Sql::new(cluster.pool(), &format!("SELECT * FROM {} LIMIT 10", table_name)) + .await + .unwrap(); + ResponseOk( + templates::Uploaded { + table_name: table_name.to_string(), + columns: sql.columns.clone(), + sql, + } + .render_once() + .unwrap(), + ) +} + +pub fn routes() -> Vec { + routes![uploader, uploader_index, uploader_upload, uploaded_index,] +} diff --git a/pgml-dashboard/src/api/mod.rs b/pgml-dashboard/src/api/mod.rs index 5ea5df6cd..8bff8d7dd 100644 --- a/pgml-dashboard/src/api/mod.rs +++ b/pgml-dashboard/src/api/mod.rs @@ -2,6 +2,7 @@ use rocket::route::Route; pub mod chatbot; pub mod cms; +pub mod deployment; pub fn routes() -> Vec { let mut routes = Vec::new(); diff --git a/pgml-dashboard/src/components/breadcrumbs/template.html b/pgml-dashboard/src/components/breadcrumbs/template.html index 69b25a2c7..47c100909 100644 --- a/pgml-dashboard/src/components/breadcrumbs/template.html +++ b/pgml-dashboard/src/components/breadcrumbs/template.html @@ -1,14 +1,28 @@ +<% + use crate::utils::config; + use crate::utils::urls; + + let home_uri = if config::standalone_dashboard() { + urls::deployment_notebooks() + } else { + "/deployments".to_string() + }; +%> +