From 6218cc01dd180d539b51c2348775185a86a9fca2 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Mon, 12 Jun 2023 12:54:48 -0700 Subject: [PATCH 1/6] Local state --- pgml-dashboard/src/api/docs.rs | 8 +- pgml-dashboard/src/guards.rs | 100 ++++++++++++------------- pgml-dashboard/src/lib.rs | 133 +++++++-------------------------- pgml-dashboard/src/main.rs | 40 +--------- 4 files changed, 82 insertions(+), 199 deletions(-) diff --git a/pgml-dashboard/src/api/docs.rs b/pgml-dashboard/src/api/docs.rs index c93601551..390de2ae9 100644 --- a/pgml-dashboard/src/api/docs.rs +++ b/pgml-dashboard/src/api/docs.rs @@ -25,7 +25,7 @@ async fn search(query: &str, index: &State) -> ResponseOk } #[get("/docs/", rank = 10)] -async fn doc_handler<'a>(path: PathBuf, cluster: Cluster) -> Result { +async fn doc_handler<'a>(path: PathBuf, cluster: &Cluster) -> Result { let guides = vec![ NavLink::new("Setup").children(vec![ NavLink::new("Installation").children(vec![ @@ -75,7 +75,7 @@ async fn doc_handler<'a>(path: PathBuf, cluster: Cluster) -> Result", rank = 10)] -async fn blog_handler<'a>(path: PathBuf, cluster: Cluster) -> Result { +async fn blog_handler<'a>(path: PathBuf, cluster: &Cluster) -> Result { render( cluster, &path, @@ -123,7 +123,7 @@ async fn blog_handler<'a>(path: PathBuf, cluster: Cluster) -> Result( - cluster: Cluster, + cluster: &Cluster, path: &'a PathBuf, mut nav_links: Vec, nav_title: &'a str, @@ -201,7 +201,7 @@ async fn render<'a>( let user = if cluster.context.user.is_anonymous() { None } else { - Some(cluster.context.user) + Some(cluster.context.user.clone()) }; let mut layout = crate::templates::Layout::new(&title); diff --git a/pgml-dashboard/src/guards.rs b/pgml-dashboard/src/guards.rs index b3380daab..20f3c8a02 100644 --- a/pgml-dashboard/src/guards.rs +++ b/pgml-dashboard/src/guards.rs @@ -1,12 +1,11 @@ +use std::collections::HashMap; use std::env::var; -use rocket::http::CookieJar; -use rocket::request::{FromRequest, Outcome, Request}; -use rocket::State; -use sqlx::PgPool; +use rocket::request::{self, FromRequest, Request}; +use sqlx::{postgres::PgPoolOptions, Executor, PgPool}; -use crate::models::User; -use crate::{Clusters, Context}; +use crate::models; +use crate::{ClustersSettings, Context}; pub fn default_database_url() -> String { match var("DATABASE_URL") { @@ -17,63 +16,56 @@ pub fn default_database_url() -> String { #[derive(Debug)] pub struct Cluster { - pool: Option, + pool: PgPool, pub context: Context, } -impl<'a> Cluster { - pub fn pool(&'a self) -> &'a PgPool { - self.pool.as_ref().unwrap() - } -} - -#[rocket::async_trait] -impl<'r> FromRequest<'r> for Cluster { - type Error = (); +impl Default for Cluster { + fn default() -> Self { + let max_connections = 5; + let min_connections = 1; + let idle_timeout = 15_000; - async fn from_request(request: &'r Request<'_>) -> Outcome { - // Using `State` as a request guard. Use `inner()` to get an `'r`. - let cookies = match request.guard::<&CookieJar<'r>>().await { - Outcome::Success(cookies) => cookies, - _ => return Outcome::Forward(()), + let settings = ClustersSettings { + max_connections, + idle_timeout, + min_connections, }; - let cluster_id = match cookies.get_private("cluster_id") { - Some(cluster_id) => match cluster_id.value().parse::() { - Ok(cluster_id) => cluster_id, - Err(_) => -1, + Cluster { + pool: PgPoolOptions::new() + .max_connections(settings.max_connections) + .idle_timeout(std::time::Duration::from_millis(settings.idle_timeout)) + .min_connections(settings.min_connections) + .after_connect(|conn, _meta| { + Box::pin(async move { + conn.execute("SET application_name = 'pgml_dashboard';") + .await?; + Ok(()) + }) + }) + .connect_lazy(&default_database_url()) + .expect("Default database URL is malformed"), + context: Context { + user: models::User::default(), + cluster: models::Cluster::default(), + visible_clusters: HashMap::default(), }, + } + } +} - None => -1, - }; - - let user_id: i64 = match cookies.get_private("user_id") { - Some(user_id) => match user_id.value().parse::() { - Ok(user_id) => user_id, - Err(_) => -1, - }, - - None => -1, - }; - - let clusters_shared_state = match request.guard::<&State>().await { - Outcome::Success(pool) => pool, - _ => return Outcome::Forward(()), - }; - - let pool = clusters_shared_state.get(cluster_id); +#[rocket::async_trait] +impl<'r> FromRequest<'r> for &'r Cluster { + type Error = (); - let context = Context { - user: User { - id: user_id, - email: "".to_string(), - }, - cluster: clusters_shared_state.get_context(cluster_id).cluster, - visible_clusters: clusters_shared_state - .get_context(cluster_id) - .visible_clusters, - }; + async fn from_request(request: &'r Request<'_>) -> request::Outcome { + request::Outcome::Success(request.local_cache(|| Cluster::default())) + } +} - Outcome::Success(Cluster { pool, context }) +impl<'a> Cluster { + pub fn pool(&'a self) -> &'a PgPool { + &self.pool } } diff --git a/pgml-dashboard/src/lib.rs b/pgml-dashboard/src/lib.rs index 7b6385a4f..eb88b99fa 100644 --- a/pgml-dashboard/src/lib.rs +++ b/pgml-dashboard/src/lib.rs @@ -1,15 +1,12 @@ #[macro_use] extern crate rocket; -use std::collections::HashMap; -use std::sync::Arc; - -use parking_lot::Mutex; use rocket::form::Form; use rocket::response::Redirect; use rocket::route::Route; use sailfish::TemplateOnce; -use sqlx::{postgres::PgPoolOptions, PgPool}; +use sqlx::PgPool; +use std::collections::HashMap; pub mod api; pub mod fairings; @@ -20,13 +17,12 @@ pub mod responses; pub mod templates; pub mod utils; -use crate::templates::{ - DeploymentsTab, Layout, ModelsTab, NotebooksTab, ProjectsTab, SnapshotsTab, UploaderTab, -}; -use crate::utils::tabs; use guards::Cluster; use responses::{BadRequest, Error, ResponseOk}; -use sqlx::Executor; +use templates::{ + DeploymentsTab, Layout, ModelsTab, NotebooksTab, ProjectsTab, SnapshotsTab, UploaderTab, +}; +use utils::tabs; #[derive(Debug, Default, Clone)] pub struct ClustersSettings { @@ -46,81 +42,8 @@ pub struct Context { pub visible_clusters: HashMap, } -/// Globally shared state, saved in memory. -/// -/// If this state is reset, it should be trivial to rebuild it from a persistent medium, e.g. the database. -#[derive(Debug)] -pub struct Clusters { - pools: Arc>>, - contexts: Arc>>, -} - -impl Clusters { - pub fn add( - &self, - cluster_id: i64, - database_url: &str, - settings: ClustersSettings, - ) -> anyhow::Result { - let mut pools = self.pools.lock(); - - let pool = PgPoolOptions::new() - .max_connections(settings.max_connections) - .idle_timeout(std::time::Duration::from_millis(settings.idle_timeout)) - .min_connections(settings.min_connections) - .after_connect(|conn, _meta| { - Box::pin(async move { - conn.execute("SET application_name = 'pgml_dashboard';") - .await?; - Ok(()) - }) - }) - .connect_lazy(database_url)?; - - pools.insert(cluster_id, pool.clone()); - - Ok(pool) - } - - /// Set the context for a cluster_id. - /// - /// This ideally should be set - /// on every request to avoid stale cache. - pub fn set_context(&self, cluster_id: i64, context: Context) { - self.contexts.lock().insert(cluster_id, context); - } - - /// Retrieve cluster context for the request. - pub fn get_context(&self, cluster_id: i64) -> Context { - match self.contexts.lock().get(&cluster_id) { - Some(context) => context.clone(), - None => Context::default(), - } - } - - /// Retrieve cluster connection pool reference. - pub fn get(&self, cluster_id: i64) -> Option { - match self.pools.lock().get(&cluster_id) { - Some(pool) => Some(pool.clone()), - None => None, - } - } - - /// Delete a cluster connection pool reference. - pub fn delete(&self, cluster_id: i64) { - let _ = self.pools.lock().remove(&cluster_id); - } - - pub fn new() -> Clusters { - Clusters { - pools: Arc::new(Mutex::new(HashMap::new())), - contexts: Arc::new(Mutex::new(HashMap::new())), - } - } -} - #[get("/projects")] -pub async fn project_index(cluster: Cluster) -> Result { +pub async fn project_index(cluster: &Cluster) -> Result { Ok(ResponseOk( templates::Projects { projects: models::Project::all(cluster.pool()).await?, @@ -131,7 +54,7 @@ pub async fn project_index(cluster: Cluster) -> Result { } #[get("/projects/")] -pub async fn project_get(cluster: Cluster, id: i64) -> Result { +pub async fn project_get(cluster: &Cluster, 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?; @@ -143,7 +66,7 @@ pub async fn project_get(cluster: Cluster, id: i64) -> Result } #[get("/notebooks")] -pub async fn notebook_index(cluster: Cluster) -> Result { +pub async fn notebook_index(cluster: &Cluster) -> Result { Ok(ResponseOk( templates::Notebooks { notebooks: models::Notebook::all(&cluster.pool()).await?, @@ -155,7 +78,7 @@ pub async fn notebook_index(cluster: Cluster) -> Result { #[post("/notebooks", data = "")] pub async fn notebook_create( - cluster: Cluster, + cluster: &Cluster, data: Form>, ) -> Result { let notebook = crate::models::Notebook::create(cluster.pool(), data.name).await?; @@ -167,7 +90,7 @@ pub async fn notebook_create( } #[get("/notebooks/")] -pub async fn notebook_get(cluster: Cluster, notebook_id: i64) -> Result { +pub async fn notebook_get(cluster: &Cluster, notebook_id: i64) -> Result { let notebook = models::Notebook::get_by_id(cluster.pool(), notebook_id).await?; Ok(ResponseOk(Layout::new("Notebooks").render( @@ -179,7 +102,7 @@ pub async fn notebook_get(cluster: Cluster, notebook_id: i64) -> Result/reset")] -pub async fn notebook_reset(cluster: Cluster, notebook_id: i64) -> Result { +pub async fn notebook_reset(cluster: &Cluster, notebook_id: i64) -> Result { let notebook = models::Notebook::get_by_id(cluster.pool(), notebook_id).await?; notebook.reset(cluster.pool()).await?; @@ -191,7 +114,7 @@ pub async fn notebook_reset(cluster: Cluster, notebook_id: i64) -> Result/cell", data = "")] pub async fn cell_create( - cluster: Cluster, + cluster: &Cluster, notebook_id: i64, cell: Form>, ) -> Result { @@ -213,7 +136,7 @@ pub async fn cell_create( #[get("/notebooks//cell/")] pub async fn cell_get( - cluster: Cluster, + cluster: &Cluster, notebook_id: i64, cell_id: i64, ) -> Result { @@ -240,7 +163,7 @@ pub async fn cell_get( #[post("/notebooks//cell//edit", data = "")] pub async fn cell_edit( - cluster: Cluster, + cluster: &Cluster, notebook_id: i64, cell_id: i64, data: Form>, @@ -276,7 +199,7 @@ pub async fn cell_edit( #[get("/notebooks//cell//edit")] pub async fn cell_trigger_edit( - cluster: Cluster, + cluster: &Cluster, notebook_id: i64, cell_id: i64, ) -> Result { @@ -302,7 +225,7 @@ pub async fn cell_trigger_edit( #[post("/notebooks//cell//play")] pub async fn cell_play( - cluster: Cluster, + cluster: &Cluster, notebook_id: i64, cell_id: i64, ) -> Result { @@ -329,7 +252,7 @@ pub async fn cell_play( #[post("/notebooks//cell//remove")] pub async fn cell_remove( - cluster: Cluster, + cluster: &Cluster, notebook_id: i64, cell_id: i64, ) -> Result { @@ -352,7 +275,7 @@ pub async fn cell_remove( #[post("/notebooks//cell//delete")] pub async fn cell_delete( - cluster: Cluster, + cluster: &Cluster, notebook_id: i64, cell_id: i64, ) -> Result { @@ -368,7 +291,7 @@ pub async fn cell_delete( } #[get("/models")] -pub async fn models_index(cluster: Cluster) -> Result { +pub async fn models_index(cluster: &Cluster) -> Result { let projects = models::Project::all(cluster.pool()).await?; let mut models = HashMap::new(); // let mut max_scores = HashMap::new(); @@ -401,7 +324,7 @@ pub async fn models_index(cluster: Cluster) -> Result { } #[get("/models/")] -pub async fn models_get(cluster: Cluster, id: i64) -> Result { +pub async fn models_get(cluster: &Cluster, id: i64) -> Result { let model = models::Model::get_by_id(cluster.pool(), id).await?; let snapshot = models::Snapshot::get_by_id(cluster.pool(), model.snapshot_id).await?; let project = models::Project::get_by_id(cluster.pool(), model.project_id).await?; @@ -419,7 +342,7 @@ pub async fn models_get(cluster: Cluster, id: i64) -> Result } #[get("/snapshots")] -pub async fn snapshots_index(cluster: Cluster) -> Result { +pub async fn snapshots_index(cluster: &Cluster) -> Result { let snapshots = models::Snapshot::all(cluster.pool()).await?; Ok(ResponseOk( @@ -428,7 +351,7 @@ pub async fn snapshots_index(cluster: Cluster) -> Result { } #[get("/snapshots/")] -pub async fn snapshots_get(cluster: Cluster, id: i64) -> Result { +pub async fn snapshots_get(cluster: &Cluster, id: i64) -> Result { let snapshot = models::Snapshot::get_by_id(cluster.pool(), id).await?; let samples = snapshot.samples(cluster.pool(), 500).await?; @@ -452,7 +375,7 @@ pub async fn snapshots_get(cluster: Cluster, id: i64) -> Result Result { +pub async fn deployments_index(cluster: &Cluster) -> Result { let projects = models::Project::all(cluster.pool()).await?; let mut deployments = HashMap::new(); @@ -474,7 +397,7 @@ pub async fn deployments_index(cluster: Cluster) -> Result { } #[get("/deployments/")] -pub async fn deployments_get(cluster: Cluster, id: i64) -> Result { +pub async fn deployments_get(cluster: &Cluster, 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?; @@ -497,7 +420,7 @@ pub async fn uploader_index() -> ResponseOk { #[post("/uploader", data = "
")] pub async fn uploader_upload( - cluster: Cluster, + cluster: &Cluster, form: Form>, ) -> Result { let mut uploaded_file = models::UploadedFile::create(cluster.pool()).await.unwrap(); @@ -519,7 +442,7 @@ pub async fn uploader_upload( } #[get("/uploader/done?")] -pub async fn uploaded_index(cluster: Cluster, table_name: &str) -> ResponseOk { +pub async fn uploaded_index(cluster: &Cluster, table_name: &str) -> ResponseOk { let sql = templates::Sql::new( cluster.pool(), &format!("SELECT * FROM {} LIMIT 10", table_name), @@ -540,7 +463,7 @@ pub async fn uploaded_index(cluster: Cluster, table_name: &str) -> ResponseOk { #[get("/?&&&&&&")] pub async fn dashboard( - cluster: Cluster, + cluster: &Cluster, tab: Option<&str>, notebook_id: Option, model_id: Option, diff --git a/pgml-dashboard/src/main.rs b/pgml-dashboard/src/main.rs index 0c29b5ed7..1719c9339 100644 --- a/pgml-dashboard/src/main.rs +++ b/pgml-dashboard/src/main.rs @@ -4,6 +4,7 @@ use rocket::{ }; use pgml_dashboard::{ + guards, responses::{self, BadRequest, Response}, utils::{config, markdown}, }; @@ -117,27 +118,11 @@ async fn main() { markdown::SearchIndex::build().await.unwrap(); - let clusters = pgml_dashboard::Clusters::new(); - let settings = pgml_dashboard::ClustersSettings { - min_connections: 0, - max_connections: 5, - idle_timeout: 15_000, - }; - - clusters - .add( - -1, - &pgml_dashboard::guards::default_database_url(), - settings, - ) - .unwrap(); - - pgml_dashboard::migrate(&clusters.get(-1).unwrap()) + pgml_dashboard::migrate(&guards::Cluster::default().pool()) .await .unwrap(); let _ = rocket::build() - .manage(clusters) .manage(markdown::SearchIndex::open().unwrap()) .mount("/", rocket::routes![index, error]) .mount("/dashboard/static", FileServer::from(&config::static_dir())) @@ -159,8 +144,8 @@ async fn main() { #[cfg(test)] mod test { use crate::{error, index}; + use pgml_dashboard::guards::Cluster; use pgml_dashboard::utils::{config, markdown}; - use pgml_dashboard::Clusters; use rocket::fs::FileServer; use rocket::local::asynchronous::Client; use rocket::{Build, Rocket}; @@ -169,29 +154,12 @@ mod test { async fn rocket() -> Rocket { dotenv::dotenv().ok(); - let max_connections = 5; - let min_connections = 1; - let idle_timeout = 15_000; - - let clusters = Clusters::new(); - clusters - .add( - -1, - &pgml_dashboard::guards::default_database_url(), - pgml_dashboard::ClustersSettings { - max_connections, - idle_timeout, - min_connections, - }, - ) - .unwrap(); - pgml_dashboard::migrate(&clusters.get(-1).unwrap()) + pgml_dashboard::migrate(Cluster::default().pool()) .await .unwrap(); rocket::build() - .manage(clusters) .manage(markdown::SearchIndex::open().unwrap()) .mount("/", rocket::routes![index, error]) .mount("/dashboard/static", FileServer::from(&config::static_dir())) From 8a6d92c0dbe4eef0c845913f389c79ec7f3a93f8 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Mon, 12 Jun 2023 13:04:51 -0700 Subject: [PATCH 2/6] public --- pgml-dashboard/src/guards.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgml-dashboard/src/guards.rs b/pgml-dashboard/src/guards.rs index 20f3c8a02..55cf63c03 100644 --- a/pgml-dashboard/src/guards.rs +++ b/pgml-dashboard/src/guards.rs @@ -16,7 +16,7 @@ pub fn default_database_url() -> String { #[derive(Debug)] pub struct Cluster { - pool: PgPool, + pub pool: PgPool, pub context: Context, } From 5eff6ac6d6614d9b6b877b43709585ae506bae3a Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Mon, 12 Jun 2023 15:35:27 -0700 Subject: [PATCH 3/6] Fixes --- pgml-dashboard/src/api/docs.rs | 2 ++ pgml-dashboard/src/models.rs | 11 ++++++++++- pgml-dashboard/src/utils/markdown.rs | 4 ++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pgml-dashboard/src/api/docs.rs b/pgml-dashboard/src/api/docs.rs index 390de2ae9..dcd6c3fee 100644 --- a/pgml-dashboard/src/api/docs.rs +++ b/pgml-dashboard/src/api/docs.rs @@ -131,6 +131,8 @@ async fn render<'a>( ) -> Result { let url = path.clone(); + println!("Path: {:?}", url); + // Get the document content let path = Path::new(&config::content_dir()) .join(folder) diff --git a/pgml-dashboard/src/models.rs b/pgml-dashboard/src/models.rs index 87af09c80..11f2e9563 100644 --- a/pgml-dashboard/src/models.rs +++ b/pgml-dashboard/src/models.rs @@ -933,12 +933,21 @@ impl UploadedFile { } // Shared context models. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] pub struct User { pub id: i64, pub email: String, } +impl Default for User { + fn default() -> User { + User { + id: -1, + email: "".to_string(), + } + } +} + impl User { pub fn is_anonymous(&self) -> bool { self.id == 0 diff --git a/pgml-dashboard/src/utils/markdown.rs b/pgml-dashboard/src/utils/markdown.rs index 7c38a06e3..13fa36974 100644 --- a/pgml-dashboard/src/utils/markdown.rs +++ b/pgml-dashboard/src/utils/markdown.rs @@ -1043,8 +1043,8 @@ impl SearchIndex { pub fn documents() -> Vec { let guides = - glob::glob(&(config::static_dir() + "/docs/guides/**/*.md")).expect("glob failed"); - let blogs = glob::glob(&(config::static_dir() + "/blog/**/*.md")).expect("glob failed"); + glob::glob(&(config::content_dir() + "/docs/guides/**/*.md")).expect("glob failed"); + let blogs = glob::glob(&(config::content_dir() + "/blog/**/*.md")).expect("glob failed"); guides .chain(blogs) .map(|path| path.expect("glob path failed")) From 275905aac10c0986613b308deae1d2f92d17393c Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Mon, 12 Jun 2023 15:37:14 -0700 Subject: [PATCH 4/6] remove debug --- pgml-dashboard/src/api/docs.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/pgml-dashboard/src/api/docs.rs b/pgml-dashboard/src/api/docs.rs index dcd6c3fee..390de2ae9 100644 --- a/pgml-dashboard/src/api/docs.rs +++ b/pgml-dashboard/src/api/docs.rs @@ -131,8 +131,6 @@ async fn render<'a>( ) -> Result { let url = path.clone(); - println!("Path: {:?}", url); - // Get the document content let path = Path::new(&config::content_dir()) .join(folder) From 8255fdd0f86f507ac19d713f5b6b9d4fb5d06e69 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Mon, 12 Jun 2023 15:56:00 -0700 Subject: [PATCH 5/6] optional pool --- pgml-dashboard/src/guards.rs | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/pgml-dashboard/src/guards.rs b/pgml-dashboard/src/guards.rs index 55cf63c03..ffb2db8d9 100644 --- a/pgml-dashboard/src/guards.rs +++ b/pgml-dashboard/src/guards.rs @@ -16,7 +16,7 @@ pub fn default_database_url() -> String { #[derive(Debug)] pub struct Cluster { - pub pool: PgPool, + pub pool: Option, pub context: Context, } @@ -33,19 +33,21 @@ impl Default for Cluster { }; Cluster { - pool: PgPoolOptions::new() - .max_connections(settings.max_connections) - .idle_timeout(std::time::Duration::from_millis(settings.idle_timeout)) - .min_connections(settings.min_connections) - .after_connect(|conn, _meta| { - Box::pin(async move { - conn.execute("SET application_name = 'pgml_dashboard';") - .await?; - Ok(()) + pool: Some( + PgPoolOptions::new() + .max_connections(settings.max_connections) + .idle_timeout(std::time::Duration::from_millis(settings.idle_timeout)) + .min_connections(settings.min_connections) + .after_connect(|conn, _meta| { + Box::pin(async move { + conn.execute("SET application_name = 'pgml_dashboard';") + .await?; + Ok(()) + }) }) - }) - .connect_lazy(&default_database_url()) - .expect("Default database URL is malformed"), + .connect_lazy(&default_database_url()) + .expect("Default database URL is alformed"), + ), context: Context { user: models::User::default(), cluster: models::Cluster::default(), @@ -66,6 +68,6 @@ impl<'r> FromRequest<'r> for &'r Cluster { impl<'a> Cluster { pub fn pool(&'a self) -> &'a PgPool { - &self.pool + self.pool.as_ref().unwrap() } } From f2e8467df7603422d347fee424691c2873ebe157 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Mon, 12 Jun 2023 16:10:05 -0700 Subject: [PATCH 6/6] Only open source --- pgml-dashboard/templates/layout/nav/top.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgml-dashboard/templates/layout/nav/top.html b/pgml-dashboard/templates/layout/nav/top.html index f12ff183c..a35c883a8 100644 --- a/pgml-dashboard/templates/layout/nav/top.html +++ b/pgml-dashboard/templates/layout/nav/top.html @@ -12,7 +12,7 @@