Files
windmill/backend/windmill-api/src/workspace_shared_ui.rs
T
Ruben Fiszel de0b6b1528 feat: workspace-shared ui/ folder reusable across raw apps (#8974)
* feat: add workspace-shared ui/ folder reusable across raw apps

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: add shared ui/ drawer in raw app editor sidebar

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: forward workspace shared ui/ to raw app editor iframe

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* all

* all

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 18:54:55 +00:00

201 lines
5.3 KiB
Rust

/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2026
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use crate::db::{ApiAuthed, DB};
use axum::{
extract::{Extension, Json, Path},
routing::{get, put},
Router,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use windmill_audit::audit_oss::audit_log;
use windmill_audit::ActionKind;
use windmill_common::{
db::UserDB,
error::{Error, JsonResult, Result},
utils::require_admin,
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
pub fn workspaced_service() -> Router {
Router::new()
.route("/get", get(get_shared_ui))
.route("/list", get(list_shared_ui))
.route("/version", get(get_version))
.route("/", put(update_shared_ui))
}
#[derive(Serialize, Deserialize)]
pub struct SharedUi {
pub files: HashMap<String, String>,
pub version: i64,
pub edited_at: chrono::DateTime<chrono::Utc>,
pub edited_by: String,
}
#[derive(Serialize)]
pub struct SharedUiListing {
pub paths: Vec<String>,
pub sizes: HashMap<String, i64>,
pub version: i64,
pub edited_at: chrono::DateTime<chrono::Utc>,
pub edited_by: String,
}
#[derive(Serialize)]
pub struct SharedUiVersion {
pub version: i64,
}
#[derive(Deserialize)]
pub struct UpdateSharedUi {
pub files: HashMap<String, String>,
}
async fn get_shared_ui(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
) -> JsonResult<SharedUi> {
let mut tx = user_db.begin(&authed).await?;
let row = sqlx::query!(
"SELECT files, version, edited_at, edited_by FROM workspace_shared_ui WHERE workspace_id = $1",
&w_id
)
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
let result = match row {
Some(r) => SharedUi {
files: serde_json::from_value(r.files).unwrap_or_default(),
version: r.version,
edited_at: r.edited_at,
edited_by: r.edited_by,
},
None => SharedUi {
files: HashMap::new(),
version: 0,
edited_at: chrono::Utc::now(),
edited_by: String::new(),
},
};
Ok(Json(result))
}
async fn list_shared_ui(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
) -> JsonResult<SharedUiListing> {
let mut tx = user_db.begin(&authed).await?;
let row = sqlx::query!(
"SELECT files, version, edited_at, edited_by FROM workspace_shared_ui WHERE workspace_id = $1",
&w_id
)
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
let (files, version, edited_at, edited_by) = match row {
Some(r) => {
let files: HashMap<String, String> =
serde_json::from_value(r.files).unwrap_or_default();
(files, r.version, r.edited_at, r.edited_by)
}
None => (HashMap::new(), 0, chrono::Utc::now(), String::new()),
};
let mut paths: Vec<String> = files.keys().cloned().collect();
paths.sort();
let sizes: HashMap<String, i64> = files
.iter()
.map(|(k, v)| (k.clone(), v.len() as i64))
.collect();
Ok(Json(SharedUiListing {
paths,
sizes,
version,
edited_at,
edited_by,
}))
}
async fn get_version(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
) -> JsonResult<SharedUiVersion> {
let mut tx = user_db.begin(&authed).await?;
let version = sqlx::query_scalar!(
"SELECT version FROM workspace_shared_ui WHERE workspace_id = $1",
&w_id
)
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(SharedUiVersion { version: version.unwrap_or(0) }))
}
async fn update_shared_ui(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(payload): Json<UpdateSharedUi>,
) -> Result<String> {
require_admin(authed.is_admin, &authed.username)?;
let files_json = serde_json::to_value(&payload.files)
.map_err(|e| Error::internal_err(format!("serializing files: {e}")))?;
let mut tx = db.begin().await?;
sqlx::query!(
r#"INSERT INTO workspace_shared_ui (workspace_id, files, version, edited_at, edited_by)
VALUES ($1, $2, 1, now(), $3)
ON CONFLICT (workspace_id) DO UPDATE
SET files = EXCLUDED.files,
version = workspace_shared_ui.version + 1,
edited_at = now(),
edited_by = EXCLUDED.edited_by"#,
&w_id,
files_json,
&authed.username,
)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
&authed,
"workspace_shared_ui.update",
ActionKind::Update,
&w_id,
Some(&authed.email),
Some([("file_count", &payload.files.len().to_string()[..])].into()),
)
.await?;
tx.commit().await?;
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
DeployedObject::Settings { setting_type: "shared_ui".to_string() },
None,
false,
None,
)
.await?;
Ok(format!("Updated shared UI for workspace {}", &w_id))
}