clean up old files
This commit is contained in:
parent
c9b755521e
commit
9bb7dcca7c
21 changed files with 320 additions and 1516 deletions
|
|
@ -3,14 +3,29 @@ use axum::{
|
|||
http::request::Parts,
|
||||
response::{IntoResponse as _, Redirect, Response},
|
||||
};
|
||||
use derive_builder::Builder;
|
||||
use interim_models::portal::Portal;
|
||||
use sqlx::postgres::types::Oid;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{app::App, errors::AppError};
|
||||
|
||||
pub(crate) trait NavigatorPage {
|
||||
/// Returns the path component of the URL to the corresonding web page.
|
||||
/// Starts with a "/" character and the `root_path` if one is specified for
|
||||
/// [`crate::settings::Settings`].
|
||||
fn get_path(&self) -> String;
|
||||
|
||||
/// Wrap [`Self::get_path`] in an Axum HTTP 303 redirection response for
|
||||
/// convenience.
|
||||
fn redirect_to(&self) -> Response {
|
||||
Redirect::to(&self.get_path()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper type for semantically generating URI paths, e.g. for redirects.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Navigator {
|
||||
pub(crate) struct Navigator {
|
||||
root_path: String,
|
||||
sub_path: String,
|
||||
}
|
||||
|
|
@ -35,6 +50,15 @@ impl Navigator {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a [`NavigatorPage`] builder for navigating to a relation's
|
||||
/// "settings" page.
|
||||
pub(crate) fn rel_settings_page(&self) -> RelSettingsPageBuilder {
|
||||
RelSettingsPageBuilder {
|
||||
root_path: Some(self.get_root_path()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_root_path(&self) -> String {
|
||||
self.root_path.to_owned()
|
||||
}
|
||||
|
|
@ -58,3 +82,31 @@ impl FromRequestParts<App> for Navigator {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Builder, Clone, Debug, PartialEq)]
|
||||
pub(crate) struct RelSettingsPage {
|
||||
rel_oid: Oid,
|
||||
|
||||
#[builder(setter(custom))]
|
||||
root_path: String,
|
||||
|
||||
/// Any value provided for `suffix` will be appended (without %-encoding) to
|
||||
/// the final path value. This may be used for sub-paths and/or search
|
||||
/// parameters.
|
||||
#[builder(default, setter(strip_option))]
|
||||
suffix: Option<String>,
|
||||
|
||||
workspace_id: Uuid,
|
||||
}
|
||||
|
||||
impl NavigatorPage for RelSettingsPage {
|
||||
fn get_path(&self) -> String {
|
||||
format!(
|
||||
"{root_path}/w/{workspace_id}/r/{rel_oid}/settings/{suffix}",
|
||||
root_path = self.root_path,
|
||||
workspace_id = self.workspace_id.simple(),
|
||||
rel_oid = self.rel_oid.0,
|
||||
suffix = self.suffix.clone().unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,269 +0,0 @@
|
|||
use std::net::SocketAddr;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{ConnectInfo, WebSocketUpgrade, ws::WebSocket},
|
||||
http::{HeaderValue, header::CACHE_CONTROL},
|
||||
response::Response,
|
||||
routing::{any, get, post},
|
||||
};
|
||||
use axum_extra::routing::RouterExt as _;
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::{
|
||||
services::{ServeDir, ServeFile},
|
||||
set_header::SetResponseHeaderLayer,
|
||||
};
|
||||
|
||||
use crate::{app_state::App, auth, routes};
|
||||
|
||||
pub fn new_router(state: App) -> Router<()> {
|
||||
let root_path = state.settings.root_path.clone();
|
||||
let app = Router::new()
|
||||
.route_with_tsr("/workspaces/", get(routes::bases::list_bases_page))
|
||||
.route_with_tsr("/workspaces/add/", post(routes::bases::add_base_page))
|
||||
.route_with_tsr(
|
||||
"/d/{base_id}/config/",
|
||||
get(routes::bases::base_config_page_get),
|
||||
)
|
||||
.route(
|
||||
"/d/{base_id}/config/",
|
||||
post(routes::bases::base_config_page_post),
|
||||
)
|
||||
.route_with_tsr(
|
||||
"/d/{base_id}/relations/",
|
||||
get(routes::relations::list_relations_page),
|
||||
)
|
||||
.route_with_tsr(
|
||||
"/d/{base_id}/r/{class_oid}/",
|
||||
get(routes::relations::rel_index_page),
|
||||
)
|
||||
.route_with_tsr(
|
||||
"/d/{base_id}/r/{class_oid}/rbac/",
|
||||
get(routes::relations::rel_rbac_page),
|
||||
)
|
||||
.route_with_tsr(
|
||||
"/d/{base_id}/r/{class_oid}/rbac/invite/",
|
||||
get(routes::relations::rel_rbac_invite_page_get),
|
||||
)
|
||||
.route(
|
||||
"/d/{base_id}/r/{class_oid}/rbac/invite",
|
||||
post(routes::relations::rel_rbac_invite_page_post),
|
||||
)
|
||||
.route_with_tsr(
|
||||
"/d/{base_id}/r/{class_oid}/lenses/",
|
||||
get(routes::lenses::lenses_page),
|
||||
)
|
||||
.route_with_tsr(
|
||||
"/d/{base_id}/r/{class_oid}/lenses/add/",
|
||||
get(routes::lenses::add_lens_page_get),
|
||||
)
|
||||
.route(
|
||||
"/d/{base_id}/r/{class_oid}/lenses/add/",
|
||||
post(routes::lenses::add_lens_page_post),
|
||||
)
|
||||
.route_with_tsr(
|
||||
"/d/{base_id}/r/{class_oid}/l/{lens_id}/",
|
||||
get(routes::lens_index::lens_page_get),
|
||||
)
|
||||
.route(
|
||||
"/d/{base_id}/r/{class_oid}/l/{lens_id}/get-data",
|
||||
get(routes::lenses::get_data_page_get),
|
||||
)
|
||||
.route(
|
||||
"/d/{base_id}/r/{class_oid}/l/{lens_id}/add-column",
|
||||
post(routes::lenses::add_column_page_post),
|
||||
)
|
||||
.route(
|
||||
"/d/{base_id}/r/{class_oid}/l/{lens_id}/update-value",
|
||||
post(routes::lenses::update_value_page_post),
|
||||
)
|
||||
.route(
|
||||
"/d/{base_id}/r/{class_oid}/l/{lens_id}/set-filter",
|
||||
post(routes::lens_set_filter::lens_set_filter_page_post),
|
||||
)
|
||||
.route(
|
||||
"/d/{base_id}/r/{class_oid}/l/{lens_id}/insert",
|
||||
post(routes::lens_insert::insert_page_post),
|
||||
)
|
||||
.route_with_tsr(
|
||||
"/d/{base_id}/r/{class_oid}/l/{lens_id}/viewer/",
|
||||
get(routes::lenses::viewer_page),
|
||||
)
|
||||
.route("/__dev-healthz", any(dev_healthz_handler))
|
||||
.nest("/auth", auth::new_router())
|
||||
.layer(SetResponseHeaderLayer::if_not_present(
|
||||
CACHE_CONTROL,
|
||||
HeaderValue::from_static("no-cache"),
|
||||
))
|
||||
.nest_service(
|
||||
"/js_dist",
|
||||
ServiceBuilder::new()
|
||||
.layer(SetResponseHeaderLayer::if_not_present(
|
||||
CACHE_CONTROL,
|
||||
// FIXME: restore production value
|
||||
// HeaderValue::from_static("max-age=21600, stale-while-revalidate=86400"),
|
||||
HeaderValue::from_static("no-cache"),
|
||||
))
|
||||
.service(
|
||||
ServeDir::new("js_dist").not_found_service(
|
||||
ServiceBuilder::new()
|
||||
.layer(SetResponseHeaderLayer::if_not_present(
|
||||
CACHE_CONTROL,
|
||||
HeaderValue::from_static("no-cache"),
|
||||
))
|
||||
.service(ServeFile::new("static/_404.html")),
|
||||
),
|
||||
),
|
||||
)
|
||||
.nest_service(
|
||||
"/glm_dist",
|
||||
ServiceBuilder::new()
|
||||
.layer(SetResponseHeaderLayer::if_not_present(
|
||||
CACHE_CONTROL,
|
||||
// FIXME: restore production value
|
||||
// HeaderValue::from_static("max-age=21600, stale-while-revalidate=86400"),
|
||||
HeaderValue::from_static("no-cache"),
|
||||
))
|
||||
.service(
|
||||
ServeDir::new("glm_dist").not_found_service(
|
||||
ServiceBuilder::new()
|
||||
.layer(SetResponseHeaderLayer::if_not_present(
|
||||
CACHE_CONTROL,
|
||||
HeaderValue::from_static("no-cache"),
|
||||
))
|
||||
.service(ServeFile::new("static/_404.html")),
|
||||
),
|
||||
),
|
||||
)
|
||||
.nest_service(
|
||||
"/css_dist",
|
||||
ServiceBuilder::new()
|
||||
.layer(SetResponseHeaderLayer::if_not_present(
|
||||
CACHE_CONTROL,
|
||||
// FIXME: restore production value
|
||||
// HeaderValue::from_static("max-age=21600, stale-while-revalidate=86400"),
|
||||
HeaderValue::from_static("no-cache"),
|
||||
))
|
||||
.service(
|
||||
ServeDir::new("css_dist").not_found_service(
|
||||
ServiceBuilder::new()
|
||||
.layer(SetResponseHeaderLayer::if_not_present(
|
||||
CACHE_CONTROL,
|
||||
HeaderValue::from_static("no-cache"),
|
||||
))
|
||||
.service(ServeFile::new("static/_404.html")),
|
||||
),
|
||||
),
|
||||
)
|
||||
.fallback_service(
|
||||
ServiceBuilder::new()
|
||||
.layer(SetResponseHeaderLayer::if_not_present(
|
||||
CACHE_CONTROL,
|
||||
HeaderValue::from_static("max-age=21600, stale-while-revalidate=86400"),
|
||||
))
|
||||
.service(
|
||||
ServeDir::new("static").not_found_service(
|
||||
ServiceBuilder::new()
|
||||
.layer(SetResponseHeaderLayer::if_not_present(
|
||||
CACHE_CONTROL,
|
||||
HeaderValue::from_static("no-cache"),
|
||||
))
|
||||
.service(ServeFile::new("static/_404.html")),
|
||||
),
|
||||
),
|
||||
)
|
||||
.with_state(state);
|
||||
if root_path.is_empty() {
|
||||
app
|
||||
} else {
|
||||
Router::new().nest(&root_path, app).fallback_service(
|
||||
ServeDir::new("static").not_found_service(ServeFile::new("static/_404.html")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async fn dev_healthz_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
) -> Response {
|
||||
tracing::info!("{addr} connected");
|
||||
ws.on_upgrade(move |socket| handle_dev_healthz_socket(socket, addr))
|
||||
}
|
||||
|
||||
async fn handle_dev_healthz_socket(mut socket: WebSocket, _: SocketAddr) {
|
||||
// Keep socket open indefinitely until the entire server exits
|
||||
while let Some(Ok(_)) = socket.recv().await {}
|
||||
}
|
||||
|
||||
// #[derive(Deserialize)]
|
||||
// struct RbacIndexPath {
|
||||
// oid: u32,
|
||||
// }
|
||||
//
|
||||
// async fn rbac_index(
|
||||
// State(Settings {
|
||||
// base_path,
|
||||
// pg_user_role_prefix: role_prefix,
|
||||
// ..
|
||||
// }): State<Settings>,
|
||||
// DieselConn(diesel_conn): DieselConn,
|
||||
// PgConn(pg_client): PgConn,
|
||||
// CurrentUser(current_user): CurrentUser,
|
||||
// Path(params): Path<RbacIndexPath>,
|
||||
// ) -> Result<Response, AppError> {
|
||||
// pg_set_role(&role_prefix, ¤t_user.id, &pg_client, &diesel_conn)
|
||||
// .await
|
||||
// .context("failed to set tokio_postgres role")?;
|
||||
//
|
||||
// struct UserDetails {
|
||||
// user: User,
|
||||
// roles: Vec<String>,
|
||||
// }
|
||||
// let all_users = {
|
||||
// let role_prefix = role_prefix.clone();
|
||||
// diesel_conn
|
||||
// .interact(move |conn| -> Result<_> {
|
||||
// let pg_users: Vec<PgRole> =
|
||||
// .get_results(conn)
|
||||
// .context("failed to query pg users with database access")?;
|
||||
// let user_ids: Vec<Uuid> = pg_users
|
||||
// .iter()
|
||||
// .filter_map(|role| {
|
||||
// let mut rolname = role.rolname.clone();
|
||||
// rolname.replace_range(0..role_prefix.len(), "");
|
||||
// Uuid::parse_str(&rolname).ok()
|
||||
// })
|
||||
// .collect();
|
||||
// let all_users: Vec<User> = users::table
|
||||
// .filter(users::dsl::id.eq_any(user_ids))
|
||||
// .get_results(conn)
|
||||
// .context("failed to query users with database access")?;
|
||||
// Ok(all_users)
|
||||
// })
|
||||
// .await
|
||||
// .unwrap()?
|
||||
// };
|
||||
// #[derive(Template)]
|
||||
// #[template(path = "rbac.html")]
|
||||
// struct ResponseTemplate {
|
||||
// base_path: String,
|
||||
// role_prefix: String,
|
||||
// users: Vec<UserDetails>,
|
||||
// }
|
||||
//
|
||||
// Ok(Html(
|
||||
// ResponseTemplate {
|
||||
// base_path,
|
||||
// role_prefix,
|
||||
// users: all_users
|
||||
// .into_iter()
|
||||
// .map(|user| UserDetails {
|
||||
// user,
|
||||
// roles: vec![],
|
||||
// })
|
||||
// .collect(),
|
||||
// }
|
||||
// .render()?,
|
||||
// )
|
||||
// .into_response())
|
||||
// }
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
use anyhow::Context as _;
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::{Html, IntoResponse as _, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::Form;
|
||||
use interim_models::base::Base;
|
||||
use interim_pgtypes::escape_identifier;
|
||||
use serde::Deserialize;
|
||||
use sqlx::{query, query_scalar};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
app_error::AppError,
|
||||
app_state::AppDbConn,
|
||||
base_pooler::{self, WorkspacePooler},
|
||||
base_user_perms::sync_perms_for_base,
|
||||
settings::Settings,
|
||||
user::CurrentUser,
|
||||
};
|
||||
|
||||
pub async fn list_bases_page(
|
||||
State(settings): State<Settings>,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
) -> Result<Response, AppError> {
|
||||
let bases = Base::with_permission_in(["configure", "connect"])
|
||||
.for_user(current_user.id)
|
||||
.fetch_all(&mut app_db)
|
||||
.await?;
|
||||
#[derive(Template)]
|
||||
#[template(path = "list_bases.html")]
|
||||
struct ResponseTemplate {
|
||||
bases: Vec<Base>,
|
||||
settings: Settings,
|
||||
}
|
||||
Ok(Html(ResponseTemplate { bases, settings }.render()?).into_response())
|
||||
}
|
||||
|
||||
pub async fn add_base_page(
|
||||
State(settings): State<Settings>,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
) -> Result<Response, AppError> {
|
||||
// FIXME: CSRF
|
||||
let base = Base::insertable_builder()
|
||||
.url("".to_owned())
|
||||
.owner_id(current_user.id)
|
||||
.build()?
|
||||
.insert(&mut app_db)
|
||||
.await?;
|
||||
query!(
|
||||
"
|
||||
insert into base_user_perms
|
||||
(id, base_id, user_id, perm)
|
||||
values ($1, $2, $3, 'configure')",
|
||||
Uuid::now_v7(),
|
||||
base.id,
|
||||
current_user.id
|
||||
)
|
||||
.execute(app_db.get_conn())
|
||||
.await?;
|
||||
Ok(Redirect::to(&format!("{}/d/{}/config", settings.root_path, base.id)).into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct BaseConfigPagePath {
|
||||
base_id: Uuid,
|
||||
}
|
||||
|
||||
pub async fn base_config_page_get(
|
||||
State(settings): State<Settings>,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(_current_user): CurrentUser,
|
||||
Path(params): Path<BaseConfigPagePath>,
|
||||
) -> Result<Response, AppError> {
|
||||
// FIXME: auth
|
||||
let base = Base::with_id(params.base_id).fetch_one(&mut app_db).await?;
|
||||
#[derive(Template)]
|
||||
#[template(path = "base_config.html")]
|
||||
struct ResponseTemplate {
|
||||
base: Base,
|
||||
settings: Settings,
|
||||
}
|
||||
Ok(Html(ResponseTemplate { base, settings }.render()?).into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct BaseConfigPageForm {
|
||||
name: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
pub async fn base_config_page_post(
|
||||
State(settings): State<Settings>,
|
||||
State(mut base_pooler): State<WorkspacePooler>,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
Path(BaseConfigPagePath { base_id }): Path<BaseConfigPagePath>,
|
||||
Form(form): Form<BaseConfigPageForm>,
|
||||
) -> Result<Response, AppError> {
|
||||
// FIXME: CSRF
|
||||
// FIXME: auth
|
||||
let base = Base::with_id(base_id).fetch_one(&mut app_db).await?;
|
||||
query!(
|
||||
"update bases set name = $1, url = $2 where id = $3",
|
||||
&form.name,
|
||||
&form.url,
|
||||
&base_id
|
||||
)
|
||||
.execute(app_db.get_conn())
|
||||
.await?;
|
||||
if form.url != base.url {
|
||||
base_pooler.close_for(base_id).await?;
|
||||
let rolname = format!("{}{}", base.user_role_prefix, current_user.id.simple());
|
||||
// Bootstrap user role with database connect privilege. If the user was
|
||||
// able to successfully authenticate a connection string, it should be
|
||||
// safe to say that they should be allowed to connect as an Interim
|
||||
// user.
|
||||
let mut root_client = base_pooler
|
||||
.acquire_for(base.id, base_pooler::RoleAssignment::Root)
|
||||
.await?;
|
||||
let db_name: String = query_scalar!("select current_database()")
|
||||
.fetch_one(root_client.get_conn())
|
||||
.await?
|
||||
.context("unable to select current_database()")?;
|
||||
|
||||
query(&format!(
|
||||
"grant connect on database {} to {}",
|
||||
escape_identifier(&db_name),
|
||||
escape_identifier(&rolname)
|
||||
))
|
||||
.execute(root_client.get_conn())
|
||||
.await?;
|
||||
sync_perms_for_base(base.id, &mut app_db, &mut root_client).await?;
|
||||
}
|
||||
Ok(Redirect::to(&format!("{}/d/{}/config", settings.root_path, base_id)).into_response())
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
use askama::Template;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::{Html, IntoResponse as _, Response},
|
||||
};
|
||||
use interim_models::{base::Base, expression::PgExpressionAny, lens::Lens};
|
||||
use interim_pgtypes::pg_attribute::PgAttribute;
|
||||
use sqlx::postgres::types::Oid;
|
||||
|
||||
use crate::{
|
||||
app_error::AppError,
|
||||
app_state::AppDbConn,
|
||||
base_pooler::{RoleAssignment, WorkspacePooler},
|
||||
navbar::{NavLocation, Navbar, RelLocation},
|
||||
settings::Settings,
|
||||
user::CurrentUser,
|
||||
};
|
||||
|
||||
use super::LensPagePath;
|
||||
|
||||
pub async fn lens_page_get(
|
||||
State(settings): State<Settings>,
|
||||
State(mut base_pooler): State<WorkspacePooler>,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
Path(LensPagePath {
|
||||
lens_id,
|
||||
base_id,
|
||||
class_oid,
|
||||
}): Path<LensPagePath>,
|
||||
) -> Result<Response, AppError> {
|
||||
// FIXME auth
|
||||
let base = Base::with_id(base_id).fetch_one(&mut app_db).await?;
|
||||
let lens = Lens::with_id(lens_id).fetch_one(&mut app_db).await?;
|
||||
|
||||
let mut base_client = base_pooler
|
||||
.acquire_for(lens.base_id, RoleAssignment::User(current_user.id))
|
||||
.await?;
|
||||
|
||||
let attrs = PgAttribute::all_for_rel(lens.class_oid)
|
||||
.fetch_all(&mut base_client)
|
||||
.await?;
|
||||
let attr_names: Vec<String> = attrs.iter().map(|attr| attr.attname.clone()).collect();
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "lens.html")]
|
||||
struct ResponseTemplate {
|
||||
attr_names: Vec<String>,
|
||||
filter: Option<PgExpressionAny>,
|
||||
settings: Settings,
|
||||
navbar: Navbar,
|
||||
}
|
||||
Ok(Html(
|
||||
ResponseTemplate {
|
||||
attr_names,
|
||||
filter: lens.filter.0,
|
||||
navbar: Navbar::builder()
|
||||
.root_path(settings.root_path.clone())
|
||||
.base(base.clone())
|
||||
.populate_rels(&mut app_db, &mut base_client)
|
||||
.await?
|
||||
.current(NavLocation::Rel(
|
||||
Oid(class_oid),
|
||||
Some(RelLocation::Lens(lens.id)),
|
||||
))
|
||||
.build()?,
|
||||
settings,
|
||||
}
|
||||
.render()?,
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::Response,
|
||||
};
|
||||
use axum_extra::extract::Form;
|
||||
use interim_models::{encodable::Encodable, lens::Lens};
|
||||
use interim_pgtypes::{escape_identifier, pg_class::PgClass};
|
||||
use sqlx::{postgres::types::Oid, query};
|
||||
|
||||
use crate::{
|
||||
app_error::AppError,
|
||||
app_state::AppDbConn,
|
||||
base_pooler::{RoleAssignment, WorkspacePooler},
|
||||
navigator::Navigator,
|
||||
user::CurrentUser,
|
||||
};
|
||||
|
||||
use super::LensPagePath;
|
||||
|
||||
pub async fn insert_page_post(
|
||||
State(mut base_pooler): State<WorkspacePooler>,
|
||||
navigator: Navigator,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
Path(LensPagePath {
|
||||
base_id,
|
||||
class_oid,
|
||||
lens_id,
|
||||
}): Path<LensPagePath>,
|
||||
Form(body): Form<HashMap<String, Vec<String>>>,
|
||||
) -> Result<Response, AppError> {
|
||||
// FIXME auth, csrf
|
||||
|
||||
let lens = Lens::with_id(lens_id).fetch_one(&mut app_db).await?;
|
||||
let mut base_client = base_pooler
|
||||
.acquire_for(base_id, RoleAssignment::User(current_user.id))
|
||||
.await?;
|
||||
|
||||
let rel = PgClass::with_oid(Oid(class_oid))
|
||||
.fetch_one(&mut base_client)
|
||||
.await?;
|
||||
|
||||
let col_names: Vec<String> = body.keys().cloned().collect();
|
||||
let col_list_sql = col_names
|
||||
.iter()
|
||||
.map(|value| escape_identifier(value))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
|
||||
let n_rows = body.values().map(|value| value.len()).max().unwrap_or(0);
|
||||
if n_rows > 0 {
|
||||
let mut param_index = 1;
|
||||
let mut params: Vec<Encodable> = vec![];
|
||||
let mut row_list: Vec<String> = vec![];
|
||||
for i in 0..n_rows {
|
||||
let mut param_slots: Vec<String> = vec![];
|
||||
for col in col_names.iter() {
|
||||
let maybe_value: Option<Encodable> = body
|
||||
.get(col)
|
||||
.and_then(|col_values| col_values.get(i))
|
||||
.map(|value_raw| serde_json::from_str(value_raw))
|
||||
.transpose()?;
|
||||
if let Some(value) = maybe_value.filter(|value| !value.is_none()) {
|
||||
params.push(value);
|
||||
param_slots.push(format!("${param_index}"));
|
||||
param_index += 1;
|
||||
} else {
|
||||
param_slots.push("default".to_owned());
|
||||
}
|
||||
}
|
||||
row_list.push(format!("({})", param_slots.join(", ")));
|
||||
}
|
||||
let row_list_sql = row_list.join(",\n");
|
||||
|
||||
let query_sql = &format!(
|
||||
r#"
|
||||
insert into {0}.{1}
|
||||
({col_list_sql})
|
||||
values
|
||||
{row_list_sql}
|
||||
"#,
|
||||
escape_identifier(&rel.regnamespace),
|
||||
escape_identifier(&rel.relname)
|
||||
);
|
||||
let mut q = query(query_sql);
|
||||
for param in params {
|
||||
q = param.bind_onto(q);
|
||||
}
|
||||
q.execute(base_client.get_conn()).await?;
|
||||
}
|
||||
|
||||
Ok(navigator.lens_page(&lens).redirect_to())
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
use axum::{extract::Path, response::Response};
|
||||
use axum_extra::extract::Form;
|
||||
use interim_models::{expression::PgExpressionAny, lens::Lens};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{app_error::AppError, app_state::AppDbConn, navigator::Navigator, user::CurrentUser};
|
||||
|
||||
use super::LensPagePath;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct FormBody {
|
||||
filter_expression: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn lens_set_filter_page_post(
|
||||
navigator: Navigator,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(_): CurrentUser,
|
||||
Path(LensPagePath { lens_id, .. }): Path<LensPagePath>,
|
||||
Form(body): Form<FormBody>,
|
||||
) -> Result<Response, AppError> {
|
||||
// FIXME auth, csrf
|
||||
|
||||
let lens = Lens::with_id(lens_id).fetch_one(&mut app_db).await?;
|
||||
|
||||
let filter: Option<PgExpressionAny> =
|
||||
serde_json::from_str(&body.filter_expression.unwrap_or("null".to_owned()))?;
|
||||
Lens::update()
|
||||
.id(lens.id)
|
||||
.filter(filter)
|
||||
.build()?
|
||||
.execute(&mut app_db)
|
||||
.await?;
|
||||
|
||||
Ok(navigator.lens_page(&lens).redirect_to())
|
||||
}
|
||||
|
|
@ -1,440 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use axum_extra::extract::Form;
|
||||
use interim_models::{
|
||||
base::Base,
|
||||
encodable::Encodable,
|
||||
field::{Field, InsertableFieldBuilder},
|
||||
lens::{Lens, LensDisplayType},
|
||||
presentation::{Presentation, RFC_3339_S},
|
||||
};
|
||||
use interim_pgtypes::{escape_identifier, pg_attribute::PgAttribute, pg_class::PgClass};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sqlx::{
|
||||
postgres::{PgRow, types::Oid},
|
||||
query,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
app_error::{AppError, bad_request},
|
||||
app_state::AppDbConn,
|
||||
base_pooler::{RoleAssignment, WorkspacePooler},
|
||||
field_info::FieldInfo,
|
||||
navigator::Navigator,
|
||||
settings::Settings,
|
||||
user::CurrentUser,
|
||||
};
|
||||
|
||||
use super::LensPagePath;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct LensesPagePath {
|
||||
base_id: Uuid,
|
||||
class_oid: u32,
|
||||
}
|
||||
|
||||
pub async fn lenses_page(
|
||||
State(settings): State<Settings>,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
Path(LensesPagePath { base_id, class_oid }): Path<LensesPagePath>,
|
||||
) -> Result<Response, AppError> {
|
||||
// FIXME auth
|
||||
let lenses = Lens::belonging_to_base(base_id)
|
||||
.belonging_to_rel(Oid(class_oid))
|
||||
.fetch_all(&mut app_db)
|
||||
.await?;
|
||||
#[derive(Template)]
|
||||
#[template(path = "lenses.html")]
|
||||
struct ResponseTemplate {
|
||||
base_id: Uuid,
|
||||
class_oid: u32,
|
||||
lenses: Vec<Lens>,
|
||||
settings: Settings,
|
||||
}
|
||||
|
||||
Ok(Html(
|
||||
ResponseTemplate {
|
||||
base_id,
|
||||
class_oid,
|
||||
lenses,
|
||||
settings,
|
||||
}
|
||||
.render()?,
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn add_lens_page_get(
|
||||
State(settings): State<Settings>,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
Path(LensesPagePath { base_id, class_oid }): Path<LensesPagePath>,
|
||||
) -> Result<Response, AppError> {
|
||||
// FIXME auth
|
||||
#[derive(Template)]
|
||||
#[template(path = "add_lens.html")]
|
||||
struct ResponseTemplate {
|
||||
base_id: Uuid,
|
||||
class_oid: u32,
|
||||
settings: Settings,
|
||||
}
|
||||
|
||||
Ok(Html(
|
||||
ResponseTemplate {
|
||||
base_id,
|
||||
class_oid,
|
||||
settings,
|
||||
}
|
||||
.render()?,
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AddLensPagePostForm {
|
||||
name: String,
|
||||
}
|
||||
|
||||
pub async fn add_lens_page_post(
|
||||
State(settings): State<Settings>,
|
||||
State(mut base_pooler): State<WorkspacePooler>,
|
||||
navigator: Navigator,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
Path(LensesPagePath { base_id, class_oid }): Path<LensesPagePath>,
|
||||
Form(AddLensPagePostForm { name }): Form<AddLensPagePostForm>,
|
||||
) -> Result<Response, AppError> {
|
||||
// FIXME auth
|
||||
// FIXME csrf
|
||||
|
||||
let mut client = base_pooler
|
||||
.acquire_for(base_id, RoleAssignment::User(current_user.id))
|
||||
.await?;
|
||||
|
||||
let attrs = PgAttribute::all_for_rel(Oid(class_oid))
|
||||
.fetch_all(&mut client)
|
||||
.await?;
|
||||
|
||||
let lens = Lens::insertable_builder()
|
||||
.base_id(base_id)
|
||||
.class_oid(Oid(class_oid))
|
||||
.name(name)
|
||||
.display_type(LensDisplayType::Table)
|
||||
.build()?
|
||||
.insert(&mut app_db)
|
||||
.await?;
|
||||
|
||||
for attr in attrs {
|
||||
InsertableFieldBuilder::default_from_attr(&attr)
|
||||
.lens_id(lens.id)
|
||||
.build()?
|
||||
.insert(&mut app_db)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(navigator.lens_page(&lens).redirect_to())
|
||||
}
|
||||
|
||||
pub async fn get_data_page_get(
|
||||
State(settings): State<Settings>,
|
||||
State(mut base_pooler): State<WorkspacePooler>,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
Path(LensPagePath {
|
||||
lens_id,
|
||||
base_id,
|
||||
class_oid,
|
||||
}): Path<LensPagePath>,
|
||||
) -> Result<Response, AppError> {
|
||||
// FIXME auth
|
||||
let base = Base::with_id(base_id).fetch_one(&mut app_db).await?;
|
||||
let lens = Lens::with_id(lens_id).fetch_one(&mut app_db).await?;
|
||||
|
||||
let mut base_client = base_pooler
|
||||
.acquire_for(lens.base_id, RoleAssignment::User(current_user.id))
|
||||
.await?;
|
||||
let rel = PgClass::with_oid(lens.class_oid)
|
||||
.fetch_one(&mut base_client)
|
||||
.await?;
|
||||
|
||||
let attrs = PgAttribute::all_for_rel(lens.class_oid)
|
||||
.fetch_all(&mut base_client)
|
||||
.await?;
|
||||
let pkey_attrs = PgAttribute::pkeys_for_rel(lens.class_oid)
|
||||
.fetch_all(&mut base_client)
|
||||
.await?;
|
||||
|
||||
let fields: Vec<FieldInfo> = {
|
||||
let fields: Vec<Field> = Field::belonging_to_portal(lens.id)
|
||||
.fetch_all(&mut app_db)
|
||||
.await?;
|
||||
let mut field_info: Vec<FieldInfo> = Vec::with_capacity(fields.len());
|
||||
for field in fields {
|
||||
if let Some(attr) = attrs.iter().find(|attr| attr.attname == field.name) {
|
||||
field_info.push(FieldInfo {
|
||||
field,
|
||||
has_default: attr.atthasdef,
|
||||
not_null: attr.attnotnull.unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
field_info
|
||||
};
|
||||
|
||||
const FRONTEND_ROW_LIMIT: i64 = 1000;
|
||||
let mut sql_raw = format!(
|
||||
"select {0} from {1}.{2}",
|
||||
pkey_attrs
|
||||
.iter()
|
||||
.chain(attrs.iter())
|
||||
.map(|attr| escape_identifier(&attr.attname))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
escape_identifier(&rel.regnamespace),
|
||||
escape_identifier(&rel.relname),
|
||||
);
|
||||
let rows: Vec<PgRow> = if let Some(filter_expr) = lens.filter.0 {
|
||||
let filter_fragment = filter_expr.into_query_fragment();
|
||||
let filter_params = filter_fragment.to_params();
|
||||
sql_raw = format!(
|
||||
"{sql_raw} where {0} limit ${1}",
|
||||
filter_fragment.to_sql(1),
|
||||
filter_params.len() + 1
|
||||
);
|
||||
let mut q = query(&sql_raw);
|
||||
for param in filter_params {
|
||||
q = param.bind_onto(q);
|
||||
}
|
||||
q = q.bind(FRONTEND_ROW_LIMIT);
|
||||
q.fetch_all(base_client.get_conn()).await?
|
||||
} else {
|
||||
sql_raw = format!("{sql_raw} limit $1");
|
||||
query(&sql_raw)
|
||||
.bind(FRONTEND_ROW_LIMIT)
|
||||
.fetch_all(base_client.get_conn())
|
||||
.await?
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DataRow {
|
||||
pkey: String,
|
||||
data: Vec<Encodable>,
|
||||
}
|
||||
|
||||
let mut data_rows: Vec<DataRow> = vec![];
|
||||
let mut pkeys: Vec<String> = vec![];
|
||||
for row in rows.iter() {
|
||||
let mut pkey_values: HashMap<String, Encodable> = HashMap::new();
|
||||
for attr in pkey_attrs.clone() {
|
||||
let field = Field::default_from_attr(&attr)
|
||||
.ok_or(anyhow::anyhow!("unsupported primary key column type"))?;
|
||||
pkey_values.insert(field.name.clone(), field.get_value_encodable(row)?);
|
||||
}
|
||||
let pkey = serde_json::to_string(&pkey_values)?;
|
||||
pkeys.push(pkey.clone());
|
||||
let mut row_data: Vec<Encodable> = vec![];
|
||||
for field in fields.iter() {
|
||||
row_data.push(field.field.get_value_encodable(row)?);
|
||||
}
|
||||
data_rows.push(DataRow {
|
||||
pkey,
|
||||
data: row_data,
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ResponseBody {
|
||||
rows: Vec<DataRow>,
|
||||
fields: Vec<FieldInfo>,
|
||||
pkeys: Vec<String>,
|
||||
}
|
||||
Ok(Json(ResponseBody {
|
||||
rows: data_rows,
|
||||
fields,
|
||||
pkeys,
|
||||
})
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AddColumnPageForm {
|
||||
name: String,
|
||||
label: String,
|
||||
presentation_tag: String,
|
||||
timestamp_format: Option<String>,
|
||||
}
|
||||
|
||||
fn try_presentation_from_form(form: &AddColumnPageForm) -> Result<Presentation, AppError> {
|
||||
let serialized = match form.presentation_tag.as_str() {
|
||||
"Timestamp" => {
|
||||
json!({
|
||||
"t": form.presentation_tag,
|
||||
"c": {
|
||||
"format": form.timestamp_format.clone().unwrap_or(RFC_3339_S.to_owned()),
|
||||
},
|
||||
})
|
||||
}
|
||||
_ => json!({"t": form.presentation_tag}),
|
||||
};
|
||||
serde_json::from_value(serialized).or(Err(bad_request!("unable to parse field type")))
|
||||
}
|
||||
|
||||
pub async fn add_column_page_post(
|
||||
State(mut base_pooler): State<WorkspacePooler>,
|
||||
navigator: Navigator,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
Path(LensPagePath { lens_id, .. }): Path<LensPagePath>,
|
||||
Form(form): Form<AddColumnPageForm>,
|
||||
) -> Result<Response, AppError> {
|
||||
// FIXME auth
|
||||
// FIXME csrf
|
||||
// FIXME validate column name length is less than 64
|
||||
|
||||
let lens = Lens::with_id(lens_id).fetch_one(&mut app_db).await?;
|
||||
let base = Base::with_id(lens.base_id).fetch_one(&mut app_db).await?;
|
||||
|
||||
let mut base_client = base_pooler
|
||||
.acquire_for(base.id, RoleAssignment::User(current_user.id))
|
||||
.await?;
|
||||
|
||||
let class = PgClass::with_oid(lens.class_oid)
|
||||
.fetch_one(&mut base_client)
|
||||
.await?;
|
||||
|
||||
let presentation = try_presentation_from_form(&form)?;
|
||||
let data_type_fragment = presentation.attr_data_type_fragment();
|
||||
|
||||
query(&format!(
|
||||
r#"
|
||||
alter table {0}
|
||||
add column if not exists {1} {2}
|
||||
"#,
|
||||
class.get_identifier(),
|
||||
escape_identifier(&form.name),
|
||||
data_type_fragment
|
||||
))
|
||||
.execute(base_client.get_conn())
|
||||
.await?;
|
||||
|
||||
Field::insert()
|
||||
.lens_id(lens.id)
|
||||
.name(form.name)
|
||||
.label(if form.label.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(form.label)
|
||||
})
|
||||
.presentation(presentation)
|
||||
.build()?
|
||||
.insert(&mut app_db)
|
||||
.await?;
|
||||
|
||||
Ok(navigator.lens_page(&lens).redirect_to())
|
||||
}
|
||||
|
||||
// #[derive(Debug, Deserialize)]
|
||||
// pub struct AddSelectionPageForm {
|
||||
// column: String,
|
||||
// }
|
||||
//
|
||||
// pub async fn add_selection_page_post(
|
||||
// State(settings): State<Settings>,
|
||||
// AppDbConn(mut app_db): AppDbConn,
|
||||
// CurrentUser(current_user): CurrentUser,
|
||||
// Path(LensPagePath {
|
||||
// base_id,
|
||||
// class_oid,
|
||||
// lens_id,
|
||||
// }): Path<LensPagePath>,
|
||||
// Form(form): Form<AddSelectionPageForm>,
|
||||
// ) -> Result<Response, AppError> {
|
||||
// dbg!(&form);
|
||||
// // FIXME auth
|
||||
// // FIXME csrf
|
||||
//
|
||||
// let lens = Lens::fetch_by_id(lens_id, &mut *app_db)
|
||||
// .await?
|
||||
// .ok_or(not_found!("lens not found"))?;
|
||||
// Selection::insertable_builder()
|
||||
// .lens_id(lens.id)
|
||||
// .attr_filters(vec![AttrFilter::NameEq(form.column)])
|
||||
// .build()?
|
||||
// .insert(&mut *app_db)
|
||||
// .await?;
|
||||
//
|
||||
// Ok(Redirect::to(&format!(
|
||||
// "{0}/d/{base_id}/r/{class_oid}/l/{lens_id}/",
|
||||
// settings.root_path
|
||||
// ))
|
||||
// .into_response())
|
||||
// }
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateValuePageForm {
|
||||
column: String,
|
||||
pkeys: HashMap<String, Encodable>,
|
||||
value: Encodable,
|
||||
}
|
||||
|
||||
pub async fn update_value_page_post(
|
||||
State(mut base_pooler): State<WorkspacePooler>,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
Path(LensPagePath {
|
||||
base_id, class_oid, ..
|
||||
}): Path<LensPagePath>,
|
||||
Json(body): Json<UpdateValuePageForm>,
|
||||
) -> Result<Response, AppError> {
|
||||
// FIXME auth
|
||||
// FIXME csrf
|
||||
|
||||
let mut base_client = base_pooler
|
||||
.acquire_for(base_id, RoleAssignment::User(current_user.id))
|
||||
.await?;
|
||||
let rel = PgClass::with_oid(Oid(class_oid))
|
||||
.fetch_one(&mut base_client)
|
||||
.await?;
|
||||
let pkey_attrs = PgAttribute::pkeys_for_rel(rel.oid)
|
||||
.fetch_all(&mut base_client)
|
||||
.await?;
|
||||
|
||||
body.pkeys
|
||||
.get(&pkey_attrs.first().unwrap().attname)
|
||||
.unwrap()
|
||||
.clone()
|
||||
.bind_onto(body.value.bind_onto(query(&format!(
|
||||
r#"update {0}.{1} set {2} = $1 where {3} = $2"#,
|
||||
escape_identifier(&rel.regnamespace),
|
||||
escape_identifier(&rel.relname),
|
||||
escape_identifier(&body.column),
|
||||
escape_identifier(&pkey_attrs.first().unwrap().attname),
|
||||
))))
|
||||
.execute(base_client.get_conn())
|
||||
.await?;
|
||||
|
||||
Ok(Json(json!({ "ok": true })).into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ViewerPagePath {
|
||||
base_id: Uuid,
|
||||
class_oid: u32,
|
||||
lens_id: Uuid,
|
||||
}
|
||||
|
||||
pub async fn viewer_page(
|
||||
State(settings): State<Settings>,
|
||||
State(mut base_pooler): State<WorkspacePooler>,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
Path(params): Path<ViewerPagePath>,
|
||||
) -> Result<Response, AppError> {
|
||||
todo!("not yet implemented");
|
||||
}
|
||||
|
|
@ -1,251 +0,0 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::{Html, IntoResponse as _, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::Form;
|
||||
use interim_models::{base::Base, rel_invitation::RelInvitation};
|
||||
use interim_pgtypes::{
|
||||
pg_acl::{PgAclItem, PgPrivilegeType},
|
||||
pg_class::{PgClass, PgRelKind},
|
||||
pg_role::{PgRole, RoleTree},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use sqlx::postgres::types::Oid;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
app_error::{AppError, forbidden},
|
||||
app_state::AppDbConn,
|
||||
base_pooler::{self, WorkspacePooler},
|
||||
navbar::{NavLocation, Navbar, RelLocation},
|
||||
renderable_role_tree::RenderableRoleTree,
|
||||
settings::Settings,
|
||||
user::CurrentUser,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ListRelationsPagePath {
|
||||
base_id: Uuid,
|
||||
}
|
||||
|
||||
pub async fn list_relations_page(
|
||||
State(settings): State<Settings>,
|
||||
State(mut base_pooler): State<WorkspacePooler>,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
Path(ListRelationsPagePath { base_id }): Path<ListRelationsPagePath>,
|
||||
) -> Result<Response, AppError> {
|
||||
// FIXME auth
|
||||
let base = Base::with_id(base_id).fetch_one(&mut app_db).await?;
|
||||
let rolname = format!("{}{}", &base.user_role_prefix, current_user.id.simple());
|
||||
let mut client = base_pooler
|
||||
.acquire_for(base_id, base_pooler::RoleAssignment::User(current_user.id))
|
||||
.await?;
|
||||
|
||||
let roles = PgRole::with_name_in(vec![rolname])
|
||||
.fetch_all(&mut client)
|
||||
.await?;
|
||||
let role = roles.first().context("role not found in pg_roles")?;
|
||||
let granted_role_tree = RoleTree::granted_to(role.oid)
|
||||
.fetch_tree(&mut client)
|
||||
.await?
|
||||
.context("unable to construct role tree")?;
|
||||
let granted_roles: HashSet<String> = granted_role_tree
|
||||
.flatten_inherited()
|
||||
.into_iter()
|
||||
.map(|role| role.rolname.clone())
|
||||
.collect();
|
||||
|
||||
let all_rels = PgClass::with_kind_in([PgRelKind::OrdinaryTable])
|
||||
.fetch_all(&mut client)
|
||||
.await?;
|
||||
dbg!(&all_rels);
|
||||
let accessible_rels: Vec<PgClass> = all_rels
|
||||
.into_iter()
|
||||
.filter(|rel| {
|
||||
let privileges: HashSet<PgPrivilegeType> = rel
|
||||
.relacl
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|item| granted_roles.contains(&item.grantee))
|
||||
.flat_map(|item| item.privileges)
|
||||
.map(|privilege| privilege.privilege)
|
||||
.collect();
|
||||
privileges.contains(&PgPrivilegeType::Select)
|
||||
})
|
||||
.collect();
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "list_rels.html")]
|
||||
struct ResponseTemplate {
|
||||
base: Base,
|
||||
rels: Vec<PgClass>,
|
||||
settings: Settings,
|
||||
}
|
||||
|
||||
Ok(Html(
|
||||
ResponseTemplate {
|
||||
base,
|
||||
rels: accessible_rels,
|
||||
settings,
|
||||
}
|
||||
.render()?,
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RelPagePath {
|
||||
base_id: Uuid,
|
||||
class_oid: u32,
|
||||
}
|
||||
|
||||
pub async fn rel_index_page(
|
||||
State(settings): State<Settings>,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
Path(RelPagePath { base_id, class_oid }): Path<RelPagePath>,
|
||||
) -> Result<Response, AppError> {
|
||||
todo!();
|
||||
}
|
||||
|
||||
pub async fn rel_rbac_page(
|
||||
State(settings): State<Settings>,
|
||||
State(mut base_pooler): State<WorkspacePooler>,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
Path(RelPagePath { base_id, class_oid }): Path<RelPagePath>,
|
||||
) -> Result<Response, AppError> {
|
||||
// FIXME: auth
|
||||
let base = Base::with_id(base_id).fetch_one(&mut app_db).await?;
|
||||
let mut base_client = base_pooler
|
||||
.acquire_for(base_id, base_pooler::RoleAssignment::User(current_user.id))
|
||||
.await?;
|
||||
let class = PgClass::with_oid(Oid(class_oid))
|
||||
.fetch_one(&mut base_client)
|
||||
.await?;
|
||||
|
||||
let owners: RenderableRoleTree = RoleTree::members_of_oid(class.relowner)
|
||||
.fetch_tree(&mut base_client)
|
||||
.await?
|
||||
.ok_or(forbidden!(
|
||||
"user does not have permission to determine relation owner"
|
||||
))?
|
||||
.into();
|
||||
|
||||
let all_invites = RelInvitation::belonging_to_rel(Oid(class_oid))
|
||||
.fetch_all(&mut app_db)
|
||||
.await?;
|
||||
let mut invites_by_email: HashMap<String, Vec<RelInvitation>> = HashMap::new();
|
||||
for invite in all_invites {
|
||||
let entry = invites_by_email.entry(invite.email.clone()).or_default();
|
||||
entry.push(invite);
|
||||
}
|
||||
|
||||
struct AclTree {
|
||||
acl_item: PgAclItem,
|
||||
grantees: RenderableRoleTree,
|
||||
}
|
||||
let mut acl_trees: Vec<AclTree> = vec![];
|
||||
for item in class.relacl.clone().unwrap_or_default() {
|
||||
acl_trees.push(AclTree {
|
||||
acl_item: item.clone(),
|
||||
grantees: RoleTree::members_of_rolname(&item.grantee)
|
||||
.fetch_tree(&mut base_client)
|
||||
.await?
|
||||
.ok_or(forbidden!(
|
||||
"unable to construct full acl tree for role {0}",
|
||||
&item.grantee
|
||||
))?
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "rel_rbac.html")]
|
||||
struct ResponseTemplate {
|
||||
acl_trees: Vec<AclTree>,
|
||||
base: Base,
|
||||
invites_by_email: HashMap<String, Vec<RelInvitation>>,
|
||||
navbar: Navbar,
|
||||
owners: RenderableRoleTree,
|
||||
pg_class: PgClass,
|
||||
settings: Settings,
|
||||
}
|
||||
|
||||
Ok(Html(
|
||||
ResponseTemplate {
|
||||
acl_trees,
|
||||
invites_by_email,
|
||||
pg_class: class,
|
||||
navbar: Navbar::builder()
|
||||
.root_path(settings.root_path.clone())
|
||||
.base(base.clone())
|
||||
.populate_rels(&mut app_db, &mut base_client)
|
||||
.await?
|
||||
.current(NavLocation::Rel(Oid(class_oid), Some(RelLocation::Rbac)))
|
||||
.build()?,
|
||||
owners,
|
||||
base,
|
||||
settings,
|
||||
}
|
||||
.render()?,
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn rel_rbac_invite_page_get(
|
||||
State(settings): State<Settings>,
|
||||
) -> Result<Response, AppError> {
|
||||
#[derive(Template)]
|
||||
#[template(path = "rbac_invite.html")]
|
||||
struct ResponseTemplate {
|
||||
settings: Settings,
|
||||
}
|
||||
Ok(Html(ResponseTemplate { settings }.render()?).into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RbacInvitePagePostForm {
|
||||
email: String,
|
||||
}
|
||||
|
||||
pub async fn rel_rbac_invite_page_post(
|
||||
State(settings): State<Settings>,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
Path(RelPagePath { base_id, class_oid }): Path<RelPagePath>,
|
||||
Form(form): Form<RbacInvitePagePostForm>,
|
||||
) -> Result<Response, AppError> {
|
||||
// FIXME auth
|
||||
// FIXME form validation
|
||||
for privilege in [
|
||||
PgPrivilegeType::Select,
|
||||
PgPrivilegeType::Insert,
|
||||
PgPrivilegeType::Update,
|
||||
PgPrivilegeType::Delete,
|
||||
PgPrivilegeType::Truncate,
|
||||
PgPrivilegeType::References,
|
||||
PgPrivilegeType::Trigger,
|
||||
] {
|
||||
RelInvitation::upsertable()
|
||||
.email(form.email.clone())
|
||||
.base_id(base_id)
|
||||
.class_oid(Oid(class_oid))
|
||||
.privilege(privilege)
|
||||
.created_by(current_user.id)
|
||||
.build()?
|
||||
.upsert(&mut app_db)
|
||||
.await?;
|
||||
}
|
||||
Ok(Redirect::to(&format!(
|
||||
"{0}/d/{base_id}/r/{class_oid}/rbac",
|
||||
settings.root_path
|
||||
))
|
||||
.into_response())
|
||||
}
|
||||
|
|
@ -11,12 +11,21 @@ mod add_portal_handler;
|
|||
mod get_data_handler;
|
||||
mod insert_handler;
|
||||
mod portal_handler;
|
||||
mod set_filter_handler;
|
||||
mod settings_invite_handler;
|
||||
mod update_value_handler;
|
||||
|
||||
pub(super) fn new_router() -> Router<App> {
|
||||
Router::<App>::new()
|
||||
.route("/settings/invite", post(settings_invite_handler::post))
|
||||
.route("/add-portal", post(add_portal_handler::post))
|
||||
.route_with_tsr("/p/{portal_id}/", get(portal_handler::get))
|
||||
.route_with_tsr("/p/{portal_id}/get-data/", get(get_data_handler::get))
|
||||
.route("/p/{portal_id}/add-field", post(add_field_handler::post))
|
||||
.route("/p/{portal_id}/insert", post(insert_handler::post))
|
||||
.route(
|
||||
"/p/{portal_id}/update-value",
|
||||
post(update_value_handler::post),
|
||||
)
|
||||
.route("/p/{portal_id}/set-filter", post(set_filter_handler::post))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
use axum::{debug_handler, extract::Path, response::Response};
|
||||
// [`axum_extra`]'s form extractor is preferred:
|
||||
// https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform
|
||||
use axum_extra::extract::Form;
|
||||
use interim_models::{
|
||||
expression::PgExpressionAny,
|
||||
portal::Portal,
|
||||
workspace_user_perm::{self, WorkspaceUserPerm},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
app::{App, AppDbConn},
|
||||
errors::{AppError, forbidden},
|
||||
navigator::Navigator,
|
||||
user::CurrentUser,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct PathParams {
|
||||
portal_id: Uuid,
|
||||
rel_oid: u32,
|
||||
workspace_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct FormBody {
|
||||
filter_expression: Option<String>,
|
||||
}
|
||||
|
||||
/// HTTP POST handler for applying a [`PgExpressionAny`] filter to a portal's
|
||||
/// table viewer.
|
||||
///
|
||||
/// This handler expects 3 path parameters with the structure described by
|
||||
/// [`PathParams`].
|
||||
#[debug_handler(state = App)]
|
||||
pub(super) async fn post(
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(user): CurrentUser,
|
||||
navigator: Navigator,
|
||||
Path(PathParams {
|
||||
portal_id,
|
||||
workspace_id,
|
||||
..
|
||||
}): Path<PathParams>,
|
||||
Form(form): Form<FormBody>,
|
||||
) -> Result<Response, AppError> {
|
||||
// Check workspace authorization.
|
||||
let workspace_perms = WorkspaceUserPerm::belonging_to_user(user.id)
|
||||
.fetch_all(&mut app_db)
|
||||
.await?;
|
||||
if workspace_perms.iter().all(|p| {
|
||||
p.workspace_id != workspace_id || p.perm != workspace_user_perm::PermissionValue::Connect
|
||||
}) {
|
||||
return Err(forbidden!("access denied to workspace"));
|
||||
}
|
||||
// FIXME ensure workspace corresponds to rel/portal, and that user has
|
||||
// permission to access/alter both as needed.
|
||||
|
||||
let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?;
|
||||
let filter: Option<PgExpressionAny> =
|
||||
serde_json::from_str(&form.filter_expression.unwrap_or("null".to_owned()))?;
|
||||
Portal::update()
|
||||
.id(portal.id)
|
||||
.filter(filter)
|
||||
.build()?
|
||||
.execute(&mut app_db)
|
||||
.await?;
|
||||
|
||||
Ok(navigator.portal_page(&portal).redirect_to())
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
use axum::{debug_handler, extract::Path, response::Response};
|
||||
// [`axum_extra`]'s form extractor is preferred:
|
||||
// https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform
|
||||
use axum_extra::extract::Form;
|
||||
use interim_models::{
|
||||
rel_invitation::RelInvitation,
|
||||
workspace_user_perm::{self, WorkspaceUserPerm},
|
||||
};
|
||||
use interim_pgtypes::pg_acl::PgPrivilegeType;
|
||||
use serde::Deserialize;
|
||||
use sqlx::postgres::types::Oid;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
app::{App, AppDbConn},
|
||||
errors::{AppError, forbidden},
|
||||
navigator::{Navigator, NavigatorPage as _},
|
||||
user::CurrentUser,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct PathParams {
|
||||
rel_oid: u32,
|
||||
workspace_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct FormBody {
|
||||
email: String,
|
||||
}
|
||||
|
||||
/// HTTP POST handler for inviting another Phonograph user to collaborate on a
|
||||
/// relation, by email address.
|
||||
#[debug_handler(state = App)]
|
||||
pub(super) async fn post(
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(user): CurrentUser,
|
||||
navigator: Navigator,
|
||||
Path(PathParams {
|
||||
rel_oid,
|
||||
workspace_id,
|
||||
}): Path<PathParams>,
|
||||
Form(form): Form<FormBody>,
|
||||
) -> Result<Response, AppError> {
|
||||
// Check workspace authorization.
|
||||
let workspace_perms = WorkspaceUserPerm::belonging_to_user(user.id)
|
||||
.fetch_all(&mut app_db)
|
||||
.await?;
|
||||
if workspace_perms.iter().all(|p| {
|
||||
p.workspace_id != workspace_id || p.perm != workspace_user_perm::PermissionValue::Connect
|
||||
}) {
|
||||
return Err(forbidden!("access denied to workspace"));
|
||||
}
|
||||
// FIXME ensure workspace corresponds to rel/portal, and that user has
|
||||
// permission to access/alter both as needed.
|
||||
// FIXME form validation
|
||||
for privilege in [
|
||||
PgPrivilegeType::Select,
|
||||
PgPrivilegeType::Insert,
|
||||
PgPrivilegeType::Update,
|
||||
PgPrivilegeType::Delete,
|
||||
PgPrivilegeType::Truncate,
|
||||
PgPrivilegeType::References,
|
||||
PgPrivilegeType::Trigger,
|
||||
] {
|
||||
RelInvitation::upsertable()
|
||||
.email(form.email.clone())
|
||||
.workspace_id(workspace_id)
|
||||
.class_oid(Oid(rel_oid))
|
||||
.privilege(privilege)
|
||||
.created_by(user.id)
|
||||
.build()?
|
||||
.upsert(&mut app_db)
|
||||
.await?;
|
||||
}
|
||||
Ok(navigator
|
||||
.rel_settings_page()
|
||||
.workspace_id(workspace_id)
|
||||
.rel_oid(Oid(rel_oid))
|
||||
.build()?
|
||||
.redirect_to())
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use axum::{
|
||||
Json, debug_handler,
|
||||
extract::{Path, State},
|
||||
response::{IntoResponse as _, Response},
|
||||
};
|
||||
// [`axum_extra`]'s form extractor is preferred:
|
||||
// https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform
|
||||
use axum_extra::extract::Form;
|
||||
use interim_models::{
|
||||
datum::Datum,
|
||||
portal::Portal,
|
||||
workspace::Workspace,
|
||||
workspace_user_perm::{self, WorkspaceUserPerm},
|
||||
};
|
||||
use interim_pgtypes::{escape_identifier, pg_attribute::PgAttribute, pg_class::PgClass};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use sqlx::{postgres::types::Oid, query};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
app::{App, AppDbConn},
|
||||
errors::{AppError, forbidden},
|
||||
user::CurrentUser,
|
||||
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct PathParams {
|
||||
portal_id: Uuid,
|
||||
rel_oid: u32,
|
||||
workspace_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct FormBody {
|
||||
column: String,
|
||||
pkeys: HashMap<String, Datum>,
|
||||
value: Datum,
|
||||
}
|
||||
|
||||
/// HTTP POST handler for updating a single value in a backing Postgres table.
|
||||
///
|
||||
/// This handler expects 3 path parameters with the structure described by
|
||||
/// [`PathParams`].
|
||||
#[debug_handler(state = App)]
|
||||
pub(super) async fn post(
|
||||
State(mut workspace_pooler): State<WorkspacePooler>,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(user): CurrentUser,
|
||||
Path(PathParams {
|
||||
portal_id,
|
||||
rel_oid,
|
||||
workspace_id,
|
||||
}): Path<PathParams>,
|
||||
Form(form): Form<FormBody>,
|
||||
) -> Result<Response, AppError> {
|
||||
// Check workspace authorization.
|
||||
let workspace_perms = WorkspaceUserPerm::belonging_to_user(user.id)
|
||||
.fetch_all(&mut app_db)
|
||||
.await?;
|
||||
if workspace_perms.iter().all(|p| {
|
||||
p.workspace_id != workspace_id || p.perm != workspace_user_perm::PermissionValue::Connect
|
||||
}) {
|
||||
return Err(forbidden!("access denied to workspace"));
|
||||
}
|
||||
// FIXME ensure workspace corresponds to rel/portal, and that user has
|
||||
// permission to access/alter both as needed.
|
||||
|
||||
let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?;
|
||||
let workspace = Workspace::with_id(portal.workspace_id)
|
||||
.fetch_one(&mut app_db)
|
||||
.await?;
|
||||
|
||||
let mut workspace_client = workspace_pooler
|
||||
.acquire_for(workspace.id, RoleAssignment::User(user.id))
|
||||
.await?;
|
||||
|
||||
let rel = PgClass::with_oid(portal.class_oid)
|
||||
.fetch_one(&mut workspace_client)
|
||||
.await?;
|
||||
|
||||
let pkey_attrs = PgAttribute::pkeys_for_rel(Oid(rel_oid))
|
||||
.fetch_all(&mut workspace_client)
|
||||
.await?;
|
||||
|
||||
// TODO: simplify pkey management
|
||||
form.pkeys
|
||||
.get(&pkey_attrs.first().unwrap().attname)
|
||||
.unwrap()
|
||||
.clone()
|
||||
.bind_onto(form.value.bind_onto(query(&format!(
|
||||
"update {ident} set {value_col} = $1 where {pkey_col} = $2",
|
||||
ident = rel.get_identifier(),
|
||||
value_col = escape_identifier(&form.column),
|
||||
pkey_col = escape_identifier(&pkey_attrs.first().unwrap().attname),
|
||||
))))
|
||||
.execute(workspace_client.get_conn())
|
||||
.await?;
|
||||
|
||||
Ok(Json(json!({ "ok": true })).into_response())
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block main %}
|
||||
<form method="post" action="">
|
||||
<div>
|
||||
<label for="lens-name-input">Lens name:</label>
|
||||
<input type="text" name="name" id="lens-name-input">
|
||||
</div>
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block main %}
|
||||
<form method="post" action="">
|
||||
<div>
|
||||
<label for="input-name">Name:</label>
|
||||
<input type="text" name="name" value="{{ base.name }}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="input-url">Database URL:</label>
|
||||
<input autocomplete="off" type="text" name="url" value="{{ base.url }}">
|
||||
</div>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block main %}
|
||||
<script type="module" src="{{ settings.root_path }}/js_dist/cells.mjs"></script>
|
||||
<script type="module" src="{{ settings.root_path }}/js_dist/lens-controls.mjs"></script>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{% for field in fields %}
|
||||
<th>
|
||||
<div>{{ field.label.clone().unwrap_or(field.name.clone()) }}</div>
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
{% for field in fields %}
|
||||
<td>
|
||||
{% match Value::get_from_row(row, field.name.as_str()) %}
|
||||
{% when Ok with (value) %}
|
||||
{{ value.to_html_string(&field.display_type) | safe }}
|
||||
{% when Err with (err) %}
|
||||
<span class="pg-value-error">{{ err }}</span>
|
||||
{% endmatch %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<lens-controls selections="{{ selections_json }}"></lens-controls>
|
||||
{% endblock %}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block main %}
|
||||
<table>
|
||||
<tbody>
|
||||
{% for lens in lenses %}
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="{{ settings.root_path }}/d/{{ base_id.simple() }}/r/{{ class_oid }}/l/{{ lens.id.simple() }}"
|
||||
>
|
||||
{{ lens.name }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block main %}
|
||||
<form method="post" action="{{ settings.root_path }}/databases/add">
|
||||
<button type="submit">Add Database</button>
|
||||
</form>
|
||||
<table>
|
||||
<tbody>
|
||||
{% for base in bases %}
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block main %}
|
||||
<table>
|
||||
<tbody>
|
||||
{% for rel in rels %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ settings.root_path }}/d/{{ base.id.simple() }}/r/{{ rel.oid.0 }}">
|
||||
{{ rel.relname }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{ settings.root_path }}/auth/login">Login</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
<link rel="stylesheet" href="{{ root_path }}/css_dist/navbar.css">
|
||||
<nav class="navbar">
|
||||
<button type="button" class="base-switcher">
|
||||
<div>{{ base.name }}</div>
|
||||
{#- TODO: icon #}
|
||||
</button>
|
||||
<section>
|
||||
<h2 class="navbar__heading">Tables</h2>
|
||||
<menu class="navbar__menu">
|
||||
{%- for schema in namespaces %}
|
||||
<li class="navbar__menu-item">
|
||||
<collapsible-menu class="navbar__collapsible-menu" root-path="{{ root_path }}">
|
||||
<h3 slot="summary" class="navbar__heading navbar__heading--entity">
|
||||
{{ schema.name }}
|
||||
</h3>
|
||||
<menu slot="content" class="navbar__menu">
|
||||
{%- for rel in schema.rels %}
|
||||
<li class="navbar__menu-item
|
||||
{%- if current == Some(NavLocation::Rel(rel.class_oid.to_owned(), None)) -%}
|
||||
{# preserve space #} navbar__menu-item--active
|
||||
{%- endif -%}
|
||||
">
|
||||
<collapsible-menu
|
||||
class="navbar__collapsible-menu"
|
||||
root-path="{{ root_path }}"
|
||||
expanded="
|
||||
{%- if let Some(NavLocation::Rel(rel_oid, _)) = current -%}
|
||||
{%- if rel_oid.to_owned() == rel.class_oid -%}
|
||||
true
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
"
|
||||
>
|
||||
<h4 slot="summary" class="navbar__heading navbar__heading--entity">
|
||||
{{ rel.name }}
|
||||
</h4>
|
||||
<menu slot="content" class="navbar__menu">
|
||||
<li class="navbar__menu-item">
|
||||
<a
|
||||
href="{{ root_path }}/d/{{ base.id.simple() }}/r/{{ rel.class_oid.0 }}/rbac"
|
||||
class="navbar__menu-link"
|
||||
>
|
||||
Sharing
|
||||
</a>
|
||||
</li>
|
||||
<li class="navbar__menu-item">
|
||||
<collapsible-menu class="navbar__collapsible-menu" root-path="{{ root_path }}">
|
||||
<h5 slot="summary" class="navbar__heading">Tabs</h5>
|
||||
<menu slot="content" class="navbar__menu">
|
||||
{% for lens in rel.lenses %}
|
||||
<li class="navbar__menu-item
|
||||
">
|
||||
<a
|
||||
href="
|
||||
{{- root_path -}}
|
||||
/d/
|
||||
{{- base.id.simple() -}}
|
||||
/r/
|
||||
{{- rel.class_oid.0 -}}
|
||||
/l/
|
||||
{{- lens.id.simple() -}}
|
||||
"
|
||||
class="navbar__menu-link navbar__menu-link--entity
|
||||
{%- if current == Some(NavLocation::Rel(rel.class_oid.to_owned(), Some(RelLocation::Lens(lens.id.to_owned())))) -%}
|
||||
{# preserve space #} navbar__menu-link--current
|
||||
{%- endif -%}
|
||||
"
|
||||
>
|
||||
{{ lens.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</menu>
|
||||
</collapsible-menu>
|
||||
</li>
|
||||
</menu>
|
||||
</collapsible-menu>
|
||||
</li>
|
||||
{% endfor -%}
|
||||
</menu>
|
||||
</collapsible-menu>
|
||||
</li>
|
||||
{% endfor -%}
|
||||
</menu>
|
||||
</section>
|
||||
<script type="module" src="{{ root_path }}/js_dist/collapsible-menu.webc.mjs"></script>
|
||||
</nav>
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block main %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>OID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for relation in relations %}
|
||||
<tr>
|
||||
<td>{{ relation.relname }}</td>
|
||||
<td>{{ relation.oid }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Reference in a new issue