diff --git a/backend/migrations/20220610181005_add_script_hub.down.sql b/backend/migrations/20220610181005_add_script_hub.down.sql new file mode 100644 index 0000000000..d2f607c5b8 --- /dev/null +++ b/backend/migrations/20220610181005_add_script_hub.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/backend/migrations/20220610181005_add_script_hub.up.sql b/backend/migrations/20220610181005_add_script_hub.up.sql new file mode 100644 index 0000000000..897b84bc5d --- /dev/null +++ b/backend/migrations/20220610181005_add_script_hub.up.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +ALTER TYPE JOB_KIND ADD VALUE 'script_hub'; diff --git a/backend/openapi.yaml b/backend/openapi.yaml index 214db1d37d..1859d7ed94 100644 --- a/backend/openapi.yaml +++ b/backend/openapi.yaml @@ -1210,6 +1210,52 @@ paths: items: type: string + /scripts/hub/list: + get: + summary: list all available hub scripts + operationId: listHubScripts + tags: + - script + responses: + "200": + description: hub scripts list + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: number + summary: + type: string + app: + type: string + approved: + type: boolean + required: + - id + - summary + - app + - approved + + /scripts/hub/get/{path}: + get: + summary: get hub script content by path + operationId: getHubScriptContentByPath + tags: + - script + parameters: + - $ref: "#/components/parameters/ScriptPath" + responses: + "200": + description: script details + content: + text/plain: + schema: + type: string + /w/{workspace}/scripts/list: get: summary: list all available scripts diff --git a/backend/src/jobs.rs b/backend/src/jobs.rs index 6079c10641..c4bb474e78 100644 --- a/backend/src/jobs.rs +++ b/backend/src/jobs.rs @@ -12,7 +12,7 @@ use sqlx::{query_scalar, Postgres, Transaction}; use std::collections::HashMap; use crate::js_eval::eval_timeout; -use crate::scripts::ScriptLang; +use crate::scripts::{get_hub_script_by_path, ScriptLang}; use crate::users::create_token_for_owner; use crate::{ audit::{audit_log, ActionKind}, @@ -167,14 +167,11 @@ pub async fn run_job_by_path( ) -> error::Result<(StatusCode, String)> { let script_path = script_path.to_path(); let mut tx = user_db.begin(&authed).await?; - let script_hash = get_latest_hash_for_path(&mut tx, &w_id, script_path).await?; + let job_payload = script_path_to_payload(script_path, &mut tx, &w_id).await?; let (uuid, tx) = push( tx, &w_id, - JobPayload::ScriptHash { - hash: script_hash, - path: script_path.to_owned(), - }, + job_payload, args, &authed.username, owner_to_token_owner(&authed.username, false), @@ -188,6 +185,25 @@ pub async fn run_job_by_path( Ok((StatusCode::CREATED, uuid.to_string())) } +async fn script_path_to_payload<'c>( + script_path: &str, + db: &mut Transaction<'c, Postgres>, + w_id: &String, +) -> Result { + let job_payload = if script_path.starts_with("hub/") { + JobPayload::ScriptHub { + path: script_path.to_owned(), + } + } else { + let script_hash = get_latest_hash_for_path(db, w_id, script_path).await?; + JobPayload::ScriptHash { + hash: script_hash, + path: script_path.to_owned(), + } + }; + Ok(job_payload) +} + pub async fn get_latest_hash_for_path<'c>( db: &mut Transaction<'c, Postgres>, w_id: &str, @@ -816,6 +832,7 @@ enum Job { #[serde(rename_all(serialize = "lowercase"))] pub enum JobKind { Script, + Script_Hub, Preview, Dependencies, Flow, @@ -954,6 +971,9 @@ struct PreviewFlow { } pub enum JobPayload { + ScriptHub { + path: String, + }, ScriptHash { hash: ScriptHash, path: String, @@ -1030,6 +1050,25 @@ pub async fn push<'c>( Some(language), ) } + JobPayload::ScriptHub { path } => ( + None, + Some(path.clone()), + Some( + get_hub_script_by_path( + Authed { + email: Some("".to_string()), + username: user.to_string(), + is_admin: false, + groups: vec![], + }, + Path(StripPath(path)), + ) + .await?, + ), + JobKind::Script_Hub, + None, + Some(ScriptLang::Deno), + ), JobPayload::Code(RawCode { content, path, @@ -1446,12 +1485,7 @@ async fn push_next_flow_job( let mut tx = db.begin().await?; let job_payload = match &module.value { FlowModuleValue::Script { path: script_path } => { - let script_hash = - get_latest_hash_for_path(&mut tx, &job.workspace_id, script_path).await?; - JobPayload::ScriptHash { - hash: script_hash, - path: script_path.to_owned(), - } + script_path_to_payload(script_path, &mut tx, &job.workspace_id).await? } a @ _ => { tracing::info!("Unrecognized module values {:?}", a); diff --git a/backend/src/scripts.rs b/backend/src/scripts.rs index 068ddbdc30..e0026ff391 100644 --- a/backend/src/scripts.rs +++ b/backend/src/scripts.rs @@ -11,7 +11,7 @@ use sql_builder::prelude::*; use crate::{ audit::{audit_log, ActionKind}, db::{UserDB, DB}, - error::{Error, JsonResult, Result}, + error::{to_anyhow, Error, JsonResult, Result}, jobs, parser, users::{owner_to_token_owner, truncate_token, Authed, Tokened}, utils::{require_admin, Pagination, StripPath}, @@ -41,6 +41,8 @@ pub fn global_service() -> Router { post(parse_python_code_to_jsonschema), ) .route("/deno/tojsonschema", post(parse_deno_code_to_jsonschema)) + .route("/hub/list", get(list_hub_scripts)) + .route("/hub/get/*path", get(get_hub_script_by_path)) } pub fn workspaced_service() -> Router { @@ -241,6 +243,41 @@ async fn list_scripts( Ok(Json(rows)) } +#[derive(Deserialize, Serialize)] +struct SearchData { + asks: Vec, +} +#[derive(Deserialize, Serialize)] +struct ScriptSearch { + id: i32, + summary: String, + app: String, + approved: bool, +} + +async fn list_hub_scripts( + Authed { + email, username, .. + }: Authed, +) -> JsonResult> { + let http_client = reqwest::ClientBuilder::new() + .user_agent("windmill/beta") + .build() + .map_err(to_anyhow)?; + let rows = http_client + .get("https://hub.windmill.dev/searchData?approved=true") + .header("X-email", email.unwrap_or_else(|| "".to_string())) + .header("X-username", username) + .send() + .await + .map_err(to_anyhow)? + .json::() + .await + .map_err(to_anyhow)? + .asks; + Ok(Json(rows)) +} + fn hash_script(ns: &NewScript) -> i64 { let mut dh = DefaultHasher::new(); ns.hash(&mut dh); @@ -448,6 +485,34 @@ async fn create_script( Ok((StatusCode::CREATED, format!("{}", hash))) } +pub async fn get_hub_script_by_path( + Authed { + email, username, .. + }: Authed, + Path(path): Path, +) -> Result { + let path = path + .to_path() + .strip_prefix("hub/") + .ok_or_else(|| Error::BadRequest("Impossible to remove prefix hex".to_string()))?; + + let http_client = reqwest::ClientBuilder::new() + .user_agent("windmill/beta") + .build() + .map_err(to_anyhow)?; + let content = http_client + .get(format!("https://hub.windmill.dev/raw/{path}.ts")) + .header("X-email", email.unwrap_or_else(|| "".to_string())) + .header("X-username", username) + .send() + .await + .map_err(to_anyhow)? + .text() + .await + .map_err(to_anyhow)?; + Ok(content) +} + async fn get_script_by_path( authed: Authed, Extension(user_db): Extension, diff --git a/backend/src/utils.rs b/backend/src/utils.rs index 99ec8f0bf4..7615bc4e9f 100644 --- a/backend/src/utils.rs +++ b/backend/src/utils.rs @@ -20,11 +20,15 @@ pub struct Pagination { pub per_page: Option, } #[derive(Deserialize)] -pub struct StripPath(String); +pub struct StripPath(pub String); impl StripPath { pub fn to_path(&self) -> &str { - self.0.strip_prefix('/').unwrap() + if self.0.starts_with('/') { + self.0.strip_prefix('/').unwrap() + } else { + &self.0 + } } } diff --git a/backend/src/worker.rs b/backend/src/worker.rs index 3fc6581430..0d265c71c2 100644 --- a/backend/src/worker.rs +++ b/backend/src/worker.rs @@ -345,6 +345,7 @@ async fn handle_job( } } else { let (inner_content, requirements_o, language) = if matches!(job.job_kind, JobKind::Preview) + || matches!(job.job_kind, JobKind::Script_Hub) { let code = (job.raw_code.as_ref().unwrap_or(&"no raw code".to_owned())).to_owned(); let reqs = if job @@ -549,7 +550,7 @@ print(res_json) let wrapper_content: String = format!( r#" import {{ main }} from "./inner.ts"; -const {{{spread}}}= JSON.parse(`{ser_args}`); +const {{{spread}}} = JSON.parse(`{ser_args}`); async function run() {{ let res: any = await main({spread}); diff --git a/deno-client/index.ts b/deno-client/index.ts index 81309e9dc7..1bdfdcd890 100644 --- a/deno-client/index.ts +++ b/deno-client/index.ts @@ -13,7 +13,7 @@ export { */ export function createConf(): Configuration & { workspace_id: string } { const token = Deno.env.get("WM_TOKEN") ?? 'no_token' - const base_url = Deno.env.get("BASE_URL") ?? 'http://localhost:8000' + const base_url = Deno.env.get("BASE_INTERNAL_URL") ?? 'http://localhost:8000' return { ...createConfiguration({ baseServer: new ServerConfiguration(`${base_url}/api`, {}), diff --git a/frontend/src/routes/__layout-root.svelte b/frontend/src/routes/__layout-root.svelte index 3678ed90e2..495bbeabad 100644 --- a/frontend/src/routes/__layout-root.svelte +++ b/frontend/src/routes/__layout-root.svelte @@ -3,15 +3,15 @@ import { page } from '$app/stores' import { SvelteToast } from '@zerodevx/svelte-toast' import { onMount } from 'svelte' - import { UserService, WorkspaceService } from '../gen' + import { WorkspaceService } from '../gen' + import { superadmin, userStore, usersWorkspaceStore, workspaceStore } from '../stores' import { - clearStores, - superadmin, - usernameStore, - usersWorkspaceStore, - workspaceStore - } from '../stores' - import { getUser, logout, logoutWithRedirect, refreshSuperadmin, sendUserToast } from '../utils' + getUserExt, + logout, + logoutWithRedirect, + refreshSuperadmin, + sendUserToast + } from '../utils' // Default toast options const toastOptions = { @@ -31,15 +31,20 @@ 'Connection got disposed.' ] - async function loadData() { + async function loadUser() { try { $usersWorkspaceStore = await WorkspaceService.listUserWorkspaces() await refreshSuperadmin() - if ($workspaceStore && $usernameStore) { - await getUser($workspaceStore) - } else if ($superadmin) { - console.log('You are a superadmin, you can go wherever you please') + if ($workspaceStore) { + if ($userStore) { + console.log(`Welcome ${$userStore.email}`) + } else if ($superadmin) { + console.log('You are a superadmin, you can go wherever you please') + } else { + $userStore = await getUserExt($workspaceStore) + throw Error('Not logged in') + } } else { goto('/user/workspaces') } @@ -49,7 +54,7 @@ } onMount(() => { - loadData() + loadUser() window.onunhandledrejection = (event: PromiseRejectionEvent) => { event.preventDefault() diff --git a/frontend/src/routes/__layout@root.svelte b/frontend/src/routes/__layout@root.svelte index 34dd16a4a3..f4e71bac08 100644 --- a/frontend/src/routes/__layout@root.svelte +++ b/frontend/src/routes/__layout@root.svelte @@ -21,14 +21,8 @@ import { onMount } from 'svelte' import Icon from 'svelte-awesome' import '../app.css' - import { OpenAPI } from '../gen' - import { - superadmin, - usernameStore, - userStore, - usersWorkspaceStore, - workspaceStore - } from '../stores' + import { OpenAPI, ScriptService } from '../gen' + import { hubScripts, superadmin, userStore, usersWorkspaceStore, workspaceStore } from '../stores' import { clickOutside, logout } from '../utils' OpenAPI.WITH_CREDENTIALS = true @@ -56,10 +50,20 @@ workspacePickerOpen = false } + async function loadSearchData() { + const scripts = await ScriptService.listHubScripts() + $hubScripts = scripts.map((x) => ({ + path: `hub/${x.id}/${x.summary.toLowerCase().replaceAll(/\s+/g, '_')}`, + summary: `${x.summary} (${x.app})`, + approved: x.approved + })) + } + onMount(() => { isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) //Mobile isCollapsed = isMobile + loadSearchData() }) @@ -179,7 +183,7 @@
- {$usernameStore ?? $superadmin ?? '___'} + {$userStore?.username ?? $superadmin ?? '___'} {#if $userStore?.is_admin} {/if} diff --git a/frontend/src/routes/audit_logs.svelte b/frontend/src/routes/audit_logs.svelte index 25c24b534b..543bd5e11f 100644 --- a/frontend/src/routes/audit_logs.svelte +++ b/frontend/src/routes/audit_logs.svelte @@ -5,7 +5,7 @@ import { displayDate, sendUserToast } from '../utils' import { goto } from '$app/navigation' import PageHeader from './components/PageHeader.svelte' - import { usernameStore, userStore, workspaceStore } from '../stores' + import { userStore, workspaceStore } from '../stores' import TableCustom from './components/TableCustom.svelte' import CenteredPage from './components/CenteredPage.svelte' import Icon from 'svelte-awesome' @@ -81,9 +81,7 @@ loadUsers() loadLogs(username, pageIndex) } - if ($usernameStore) { - username = $usernameStore - } + username = $userStore?.username } diff --git a/frontend/src/routes/components/GroupModal.svelte b/frontend/src/routes/components/GroupModal.svelte index 0e65396c86..fa8e6b6480 100644 --- a/frontend/src/routes/components/GroupModal.svelte +++ b/frontend/src/routes/components/GroupModal.svelte @@ -1,12 +1,12 @@ diff --git a/frontend/src/routes/components/ItemPicker.svelte b/frontend/src/routes/components/ItemPicker.svelte index e4abc21a5d..20b592cbb7 100644 --- a/frontend/src/routes/components/ItemPicker.svelte +++ b/frontend/src/routes/components/ItemPicker.svelte @@ -40,7 +40,7 @@
-
    +
      {#each filteredItems as obj}
    • (it[x] = { type: 'static', @@ -37,7 +46,7 @@ ) schemaForms[i]?.setArgs(it) } - schemas[i] = script.schema ?? emptySchema() + schemas[i] = schema ?? emptySchema() } else { schemaForms[i]?.setArgs({}) schemas[i] = emptySchema() @@ -66,7 +75,7 @@

Step script

- +

Step inputs

-
+
{label}
{#each options as [label, val]} diff --git a/frontend/src/routes/components/ScriptPicker.svelte b/frontend/src/routes/components/ScriptPicker.svelte index afadaddbae..e6ea4ab4f1 100644 --- a/frontend/src/routes/components/ScriptPicker.svelte +++ b/frontend/src/routes/components/ScriptPicker.svelte @@ -1,10 +1,10 @@ @@ -62,38 +73,23 @@ bind:this={itemPicker} pickCallback={(path, _) => { scriptPath = path + dispatch('select', { path: scriptPath }) }} - itemName={isFlow ? 'Flow' : 'Script'} + itemName={itemKind == 'flow' ? 'Flow' : 'Script'} extraField="summary" loadItems={async () => { return items }} /> -
- {#if allowFlow} - +
+ {#if options.length > 1} + {/if} - - + {#if scriptPath != undefined && scriptPath != ''}