Skip to content

Commit

Permalink
save
Browse files Browse the repository at this point in the history
  • Loading branch information
levkk committed Nov 2, 2024
1 parent fb20815 commit 3a11f6c
Show file tree
Hide file tree
Showing 28 changed files with 578 additions and 70 deletions.
69 changes: 63 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion rwf-admin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ rwf = { path = "../rwf" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
time = { version = "0.3", features = ["serde"] }
time = { version = "0.3", features = ["formatting", "serde", "parsing", "macros"] }
once_cell = "1"
uuid = { version = "*", features = ["v4"]}
2 changes: 2 additions & 0 deletions rwf-admin/src/controllers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use rwf::job::JobModel;
use rwf::prelude::*;
use rwf::serde::Serialize;

pub mod models;

#[derive(Default)]
pub struct Index;

Expand Down
182 changes: 182 additions & 0 deletions rwf-admin/src/controllers/models.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
use std::collections::HashMap;

use rwf::model::{Escape, Row};
use rwf::prelude::*;

use uuid::Uuid;

#[derive(Clone, macros::Model)]
struct Table {
table_name: String,
}

impl Table {
async fn load() -> Result<Vec<Table>, Error> {
Ok(Pool::pool()
.with_connection(|mut conn| async move {
Table::find_by_sql(
"
SELECT
relname::text AS table_name
FROM
pg_class
WHERE
pg_table_is_visible(oid)
AND reltype != 0
AND relname NOT LIKE 'pg_%'
ORDER BY oid",
&[],
)
.fetch_all(&mut conn)
.await
})
.await?)
}
}

#[derive(Clone, macros::Model)]
struct TableColumn {
table_name: String,
column_name: String,
data_type: String,
column_default: String,
}

impl TableColumn {
pub async fn for_table(name: &str) -> Result<Vec<TableColumn>, Error> {
Ok(Pool::pool()
.with_connection(|mut conn| async move {
TableColumn::find_by_sql(
"SELECT
table_name::text,
column_name::text,
data_type::text,
COALESCE(column_default::text, '')::text AS column_default
FROM information_schema.columns
INNER JOIN pg_class ON table_name = relname
INNER JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace
WHERE pg_class.relname = $1::text AND pg_table_is_visible(pg_class.oid)
ORDER BY ordinal_position",
&[name.to_value()],
)
.fetch_all(&mut conn)
.await
})
.await?
.into_iter()
.map(|c| c.transform_default())
.collect())
}

pub fn transform_default(mut self) -> Self {
if self.column_default == "now()" {
let format =
time::macros::format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
self.column_default = OffsetDateTime::now_utc().format(format).unwrap();
} else if self.column_default == "gen_random_uuid()" {
self.column_default = Uuid::new_v4().to_string();
} else if self.column_default.ends_with("::character varying") {
self.column_default = self
.column_default
.replace("::character varying", "")
.replace("'", "");
} else if self.column_default.ends_with("::jsonb") {
self.column_default = self.column_default.replace("::jsonb", "").replace("'", "");
}

self
}

pub fn skip(&self) -> bool {
if self.column_default.starts_with("nextval(") {
true
} else {
false
}
}
}

#[derive(Default)]
pub struct ModelsController;

#[async_trait]
impl Controller for ModelsController {
async fn handle(&self, _request: &Request) -> Result<Response, Error> {
let tables = Table::load().await?;
render!("templates/rwf_admin/models.html", "models" => tables)
}
}

#[derive(Default)]
pub struct ModelController;

#[async_trait]
impl Controller for ModelController {
async fn handle(&self, request: &Request) -> Result<Response, Error> {
let model = request.query().get::<String>("name");
let selected_columns = request
.query()
.get::<String>("columns")
.unwrap_or("".to_string())
.split(",")
.into_iter()
.map(|c| c.trim().to_string())
.filter(|c| !c.is_empty())
.collect::<Vec<_>>();

if let Some(model) = model {
let columns = TableColumn::for_table(&model)
.await?
.into_iter()
.filter(|c| {
selected_columns.contains(&c.column_name) || selected_columns.is_empty()
})
.collect::<Vec<_>>();
let create_columns = columns
.clone()
.into_iter()
.filter(|c| !c.skip())
.collect::<Vec<_>>();
let order_by = if columns
.iter()
.find(|c| c.column_name == "id")
.take()
.is_some()
{
"ORDER BY id DESC "
} else {
""
};

if !columns.is_empty() {
let table_name = model.clone();
let rows = Pool::pool()
.with_connection(|mut conn| async move {
Row::find_by_sql(
format!(
"SELECT * FROM \"{}\" {}LIMIT 25",
table_name.escape(),
order_by
),
&[],
)
.fetch_all(&mut conn)
.await
})
.await?;
let mut data = vec![];
for row in rows {
data.push(row.values()?);
}

render!("templates/rwf_admin/model.html",
"table_name" => model,
"columns" => columns,
"rows" => data,
"create_columns" => create_columns)
}
}

Ok(Response::not_found())
}
}
3 changes: 3 additions & 0 deletions rwf-admin/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use models::{ModelController, ModelsController};
use rwf::controller::Engine;
use rwf::prelude::*;

Expand All @@ -9,5 +10,7 @@ pub fn engine() -> Engine {
route!("/" => Index),
route!("/jobs" => Jobs),
route!("/requests" => Requests),
route!("/models" => ModelsController),
route!("/models/model" => ModelController),
])
}
Binary file not shown.
9 changes: 9 additions & 0 deletions rwf-admin/static/js/model_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Controller } from "hotwired/stimulus";

export default class extends Controller {
connect() {
const elems = this.element.querySelectorAll("select");
M.FormSelect.init(elems);
M.updateTextFields();
}
}
1 change: 1 addition & 0 deletions rwf-admin/static/js/requests_controller.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Controller } from "hotwired/stimulus";
import "https://cdn.jsdelivr.net/npm/chart.js";

export default class extends Controller {
static targets = ["requestsOk", "chart"];
Expand Down
8 changes: 5 additions & 3 deletions rwf-admin/templates/rwf_admin/head.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,25 @@
<meta name="turbo-cache-control" content="no-cache">
<%- rwf_head() %>

<script async src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- <script async src="https://cdn.jsdelivr.net/npm/chart.js"></script> -->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css"
/>
<script
async
src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"
></script>

<script type="module" async>
import { Application } from 'hotwired/stimulus'
const application = Application.start();
import Requests from '/static/js/requests_controller.js'
import Model from '/static/js/model_controller.js'
application.register('requests', Requests)
application.register('model', Model)
</script>

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="grey darken-4 grey-text text-lighten-4">
<body class="">
<%- rwf_turbo_stream("/turbo-stream") %>
3 changes: 3 additions & 0 deletions rwf-admin/templates/rwf_admin/jobs.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ <h3 class="center-align"><%= latency %>s</h3>
</div>
</div>
<div>
<h3>Last jobs</h3>
<% if jobs %>
<table>
<thead>
Expand Down Expand Up @@ -66,6 +67,8 @@ <h3 class="center-align"><%= latency %>s</h3>
<% end %>
</tbody>
</table>
<% else %>
<p class="center-align">There are currently no jobs.</p>
<% end %>
</div>
</div>
Expand Down
Loading

0 comments on commit 3a11f6c

Please sign in to comment.