mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 16:02:28 +00:00
feat: add support for full fleged apps (react, svelte, vue) (#1536)
* add react support * use whoami * update * update * update * create react app * all * sqlx
This commit is contained in:
Generated
+1
@@ -5184,6 +5184,7 @@ dependencies = [
|
||||
"async_zip",
|
||||
"axum",
|
||||
"base64 0.21.0",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"cookie",
|
||||
|
||||
+1
-1
@@ -162,5 +162,5 @@ async_zip = { version = "0.0.11", features = ["full"] }
|
||||
once_cell = "1.17.1"
|
||||
rsmq_async = { version = "5.1.5" }
|
||||
gosyn = "0.2.2"
|
||||
bytes = "1.4.0"
|
||||
|
||||
[patch.crates-io]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,33 @@
|
||||
-- Add up migration script here
|
||||
CREATE TABLE raw_app (
|
||||
path varchar(255) PRIMARY KEY,
|
||||
version INTEGER NOT NULL DEFAULT 0,
|
||||
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id),
|
||||
summary VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
edited_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
data TEXT NOT NULL,
|
||||
extra_perms JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE POLICY see_own ON raw_app FOR ALL
|
||||
USING (SPLIT_PART(raw_app.path, '/', 1) = 'u' AND SPLIT_PART(raw_app.path, '/', 2) = current_setting('session.user'));
|
||||
|
||||
CREATE POLICY see_member ON raw_app FOR ALL
|
||||
USING (SPLIT_PART(raw_app.path, '/', 1) = 'g' AND SPLIT_PART(raw_app.path, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[]));
|
||||
|
||||
CREATE POLICY see_extra_perms_user ON raw_app FOR ALL
|
||||
USING (extra_perms ? CONCAT('u/', current_setting('session.user')))
|
||||
WITH CHECK ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
|
||||
|
||||
CREATE POLICY see_extra_perms_groups ON raw_app FOR ALL
|
||||
USING (extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
|
||||
WITH CHECK (exists(
|
||||
SELECT key, value FROM jsonb_each_text(extra_perms)
|
||||
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
|
||||
AND value::boolean));
|
||||
|
||||
CREATE POLICY see_folder_extra_perms_user ON raw_app FOR ALL
|
||||
USING (SPLIT_PART(raw_app.path, '/', 1) = 'f' AND SPLIT_PART(raw_app.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[]))
|
||||
WITH CHECK (SPLIT_PART(raw_app.path, '/', 1) = 'f' AND SPLIT_PART(raw_app.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
|
||||
|
||||
ALTER TYPE FAVORITE_KIND ADD VALUE 'raw_app';
|
||||
+74
-2
@@ -1101,7 +1101,8 @@
|
||||
"Enum": [
|
||||
"app",
|
||||
"script",
|
||||
"flow"
|
||||
"flow",
|
||||
"raw_app"
|
||||
]
|
||||
},
|
||||
"name": "favorite_kind"
|
||||
@@ -2308,6 +2309,27 @@
|
||||
},
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM app WHERE path = $1 AND workspace_id = $2)"
|
||||
},
|
||||
"5a0333e29280c0814d62473b5e5e7b187c4d8cbf2caeb645bfef82f6880467a7": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "data",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT data FROM raw_app\n WHERE path = $1 AND workspace_id = $2"
|
||||
},
|
||||
"5b9b58612ca0f703a5d154a76fab82ac2329aef965fa937bfab2810b6e1336a4": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -2489,7 +2511,8 @@
|
||||
"Enum": [
|
||||
"app",
|
||||
"script",
|
||||
"flow"
|
||||
"flow",
|
||||
"raw_app"
|
||||
]
|
||||
},
|
||||
"name": "favorite_kind"
|
||||
@@ -3212,6 +3235,21 @@
|
||||
},
|
||||
"query": "UPDATE group_ SET summary = $1 WHERE name = $2 AND workspace_id = $3"
|
||||
},
|
||||
"84bbd11f7bb0c65dcfb2e12038b8a9cb51d303480ac3c5f8a5e833a53c40ed9b": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO raw_app\n (workspace_id, path, summary, extra_perms, data)\n VALUES ($1, $2, $3, '{}', $4)"
|
||||
},
|
||||
"8543f029d9784234e4c6a6dcd7b03e62d544b98be261334ee210594e0bb839f2": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -4722,6 +4760,19 @@
|
||||
},
|
||||
"query": "UPDATE usr SET is_admin = $1 WHERE username = $2 AND workspace_id = $3"
|
||||
},
|
||||
"b2b2b7251be2b80207f47b10eeff78d61c84161caaf16b8fd25d82b97aac1186": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "DELETE FROM raw_app WHERE path = $1 AND workspace_id = $2"
|
||||
},
|
||||
"b3b80de52d0931a2fdb5d38b7603a2d69cc25ab1cda413228c363a5ffd777113": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -5232,6 +5283,27 @@
|
||||
},
|
||||
"query": "\n UPDATE queue\n SET flow_status = JSONB_SET(\n JSONB_SET(flow_status, ARRAY['failure_module'], $1),\n ARRAY['step'], $2)\n WHERE id = $3\n "
|
||||
},
|
||||
"c4b3ab3d87f158b20f0557f78c6307c50620425aeb43636e19931c142b5ce0d7": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "exists",
|
||||
"ordinal": 0,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
null
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM raw_app WHERE path = $1 AND workspace_id = $2)"
|
||||
},
|
||||
"c4f1c14c3aae145b52ff39bdecd779cd3eba27a869fda68516dc68dc2abefd38": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
|
||||
@@ -72,3 +72,4 @@ prometheus.workspace = true
|
||||
async_zip.workspace = true
|
||||
rsmq_async.workspace = true
|
||||
regex.workspace = true
|
||||
bytes.workspace = true
|
||||
|
||||
@@ -2888,6 +2888,80 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/Input"
|
||||
|
||||
/w/{workspace}/raw_apps/list:
|
||||
get:
|
||||
summary: list all available raw apps
|
||||
operationId: listRawApps
|
||||
tags:
|
||||
- raw_app
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
- $ref: "#/components/parameters/OrderDesc"
|
||||
- $ref: "#/components/parameters/CreatedBy"
|
||||
- name: path_start
|
||||
description: mask to filter matching starting path
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: path_exact
|
||||
description: mask to filter exact matching path
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: starred_only
|
||||
description: |
|
||||
(default false)
|
||||
show only the starred items
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: All available raw apps
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/ListableRawApp"
|
||||
|
||||
/w/{workspace}/raw_apps/exists/{path}:
|
||||
get:
|
||||
summary: does an app exisst at path
|
||||
operationId: existsRawApp
|
||||
tags:
|
||||
- raw_app
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
responses:
|
||||
"200":
|
||||
description: app exists
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: boolean
|
||||
|
||||
/w/{workspace}/apps/get_data/{version}/{path}:
|
||||
get:
|
||||
summary: get app by path
|
||||
operationId: getRawAppData
|
||||
tags:
|
||||
- raw_app
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/VersionId"
|
||||
- $ref: "#/components/parameters/ScriptPath"
|
||||
responses:
|
||||
"200":
|
||||
description: app details
|
||||
content:
|
||||
text/javascript:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/apps/list:
|
||||
get:
|
||||
summary: list all available apps
|
||||
@@ -2927,6 +3001,44 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/ListableApp"
|
||||
|
||||
/w/{workspace}/apps/create:
|
||||
post:
|
||||
summary: create app
|
||||
operationId: createApp
|
||||
tags:
|
||||
- app
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: new app
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
value: {}
|
||||
summary:
|
||||
type: string
|
||||
policy:
|
||||
$ref: "#/components/schemas/Policy"
|
||||
draft_only:
|
||||
type: boolean
|
||||
required:
|
||||
- path
|
||||
- value
|
||||
- summary
|
||||
- policy
|
||||
responses:
|
||||
"201":
|
||||
description: app created
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/apps/exists/{path}:
|
||||
get:
|
||||
summary: does an app exisst at path
|
||||
@@ -3030,16 +3142,16 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AppWithLastVersion"
|
||||
|
||||
/w/{workspace}/apps/create:
|
||||
/w/{workspace}/raw_apps/create:
|
||||
post:
|
||||
summary: create app
|
||||
operationId: createApp
|
||||
summary: create raw app
|
||||
operationId: createRawApp
|
||||
tags:
|
||||
- app
|
||||
- raw_app
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: new app
|
||||
description: new raw app
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
@@ -3048,21 +3160,65 @@ paths:
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
value: {}
|
||||
value:
|
||||
type: string
|
||||
summary:
|
||||
type: string
|
||||
policy:
|
||||
$ref: "#/components/schemas/Policy"
|
||||
draft_only:
|
||||
type: boolean
|
||||
required:
|
||||
- path
|
||||
- value
|
||||
- summary
|
||||
- policy
|
||||
responses:
|
||||
"201":
|
||||
description: app created
|
||||
description: raw app created
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/raw_apps/update/{path}:
|
||||
post:
|
||||
summary: update app
|
||||
operationId: updateRawApp
|
||||
tags:
|
||||
- raw_app
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/ScriptPath"
|
||||
requestBody:
|
||||
description: updateraw app
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
summary:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: app updated
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/raw_apps/delete/{path}:
|
||||
delete:
|
||||
summary: delete raw app
|
||||
operationId: deleteRawApp
|
||||
tags:
|
||||
- raw_app
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
responses:
|
||||
"200":
|
||||
description: app deleted
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
@@ -4499,7 +4655,7 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
[script, group_, resource, schedule, variable, flow, folder, app]
|
||||
[script, group_, resource, schedule, variable, flow, folder, app, raw_app]
|
||||
responses:
|
||||
"200":
|
||||
description: acls
|
||||
@@ -4525,7 +4681,7 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
[script, group_, resource, schedule, variable, flow, folder, app]
|
||||
[script, group_, resource, schedule, variable, flow, folder, app, raw_app]
|
||||
requestBody:
|
||||
description: acl to add
|
||||
required: true
|
||||
@@ -4562,7 +4718,7 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
[script, group_, resource, schedule, variable, flow, folder, app]
|
||||
[script, group_, resource, schedule, variable, flow, folder, app, raw_app]
|
||||
requestBody:
|
||||
description: acl to add
|
||||
required: true
|
||||
@@ -4642,7 +4798,7 @@ paths:
|
||||
type: string
|
||||
favorite_kind:
|
||||
type: string
|
||||
enum: [flow, app, script]
|
||||
enum: [flow, app, script, raw_app]
|
||||
responses:
|
||||
"200":
|
||||
description: star item
|
||||
@@ -4665,7 +4821,7 @@ paths:
|
||||
type: string
|
||||
favorite_kind:
|
||||
type: string
|
||||
enum: [flow, app, script]
|
||||
enum: [flow, app, script, raw_app]
|
||||
responses:
|
||||
"200":
|
||||
description: unstar item
|
||||
@@ -4799,6 +4955,12 @@ components:
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
VersionId:
|
||||
name: version
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: number
|
||||
Token:
|
||||
name: token
|
||||
in: path
|
||||
@@ -6269,6 +6431,34 @@ components:
|
||||
- edited_at
|
||||
- execution_mode
|
||||
|
||||
ListableRawApp:
|
||||
type: object
|
||||
properties:
|
||||
workspace_id:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
summary:
|
||||
type: string
|
||||
extra_perms:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
starred:
|
||||
type: boolean
|
||||
version:
|
||||
type: number
|
||||
edited_at:
|
||||
type: string
|
||||
format: date-time
|
||||
required:
|
||||
- workspace_id
|
||||
- path
|
||||
- summary
|
||||
- extra_perms
|
||||
- version
|
||||
- edited_at
|
||||
|
||||
AppWithLastVersion:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -384,6 +384,22 @@ async fn create_app(
|
||||
return Err(Error::BadRequest("App path cannot be empty".to_string()));
|
||||
}
|
||||
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM raw_app WHERE path = $1 AND workspace_id = $2)",
|
||||
&app.path,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
|
||||
if exists {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"App with path {} already exists",
|
||||
&app.path
|
||||
)));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'app'",
|
||||
&app.path,
|
||||
@@ -536,6 +552,22 @@ async fn update_app(
|
||||
if let Some(npath) = &ns.path {
|
||||
if npath != path {
|
||||
require_owner_of_path(&authed, path)?;
|
||||
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM raw_app WHERE path = $1 AND workspace_id = $2)",
|
||||
npath,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
|
||||
if exists {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"App with path {} already exists",
|
||||
npath
|
||||
)));
|
||||
}
|
||||
}
|
||||
sqlb.set_str("path", npath);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ pub enum FavoriteKind {
|
||||
Script,
|
||||
Flow,
|
||||
App,
|
||||
Raw_App,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct Favorite {
|
||||
|
||||
@@ -1270,7 +1270,6 @@ pub async fn run_flow_by_path(
|
||||
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
|
||||
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?;
|
||||
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
|
||||
|
||||
let (uuid, tx) = push(
|
||||
tx,
|
||||
&w_id,
|
||||
|
||||
@@ -42,6 +42,7 @@ mod groups;
|
||||
mod inputs;
|
||||
pub mod jobs;
|
||||
mod oauth2;
|
||||
mod raw_apps;
|
||||
mod resources;
|
||||
mod schedule;
|
||||
mod scripts;
|
||||
@@ -123,6 +124,7 @@ pub async fn run_server(
|
||||
// Reordered alphabetically
|
||||
.nest("/acls", granular_acls::workspaced_service())
|
||||
.nest("/apps", apps::workspaced_service())
|
||||
.nest("/raw_apps", raw_apps::workspaced_service())
|
||||
.nest("/audit", audit::workspaced_service())
|
||||
.nest("/capture", capture::workspaced_service())
|
||||
.nest("/favorites", favorite::workspaced_service())
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* Author: Ruben Fiszel
|
||||
* Copyright: Windmill Labs, Inc 2022
|
||||
* 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::{UserDB, DB},
|
||||
users::{require_owner_of_path, Authed},
|
||||
webhook_util::{WebhookMessage, WebhookShared},
|
||||
};
|
||||
use axum::{
|
||||
body,
|
||||
extract::{Extension, Json, Path, Query},
|
||||
response::Response,
|
||||
routing::{delete, get, post},
|
||||
Router,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use hyper::{header, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sql_builder::{bind::Bind, SqlBuilder};
|
||||
use sqlx::FromRow;
|
||||
use std::str;
|
||||
use windmill_audit::{audit_log, ActionKind};
|
||||
use windmill_common::{
|
||||
apps::ListAppQuery,
|
||||
error::{Error, JsonResult, Result},
|
||||
utils::{not_found_if_none, paginate, Pagination, StripPath},
|
||||
};
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new()
|
||||
.route("/list", get(list_apps))
|
||||
.route("/get_data/:version/*path", get(get_data))
|
||||
.route("/exists/*path", get(exists_app))
|
||||
.route("/update/*path", post(update_app))
|
||||
.route("/delete/*path", delete(delete_app))
|
||||
.route("/create", post(create_app))
|
||||
}
|
||||
|
||||
#[derive(FromRow, Deserialize, Serialize)]
|
||||
pub struct ListableApp {
|
||||
pub path: String,
|
||||
pub workspace_id: String,
|
||||
pub summary: String,
|
||||
pub edited_at: chrono::DateTime<chrono::Utc>,
|
||||
pub extra_perms: serde_json::Value,
|
||||
pub starred: bool,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateApp {
|
||||
pub path: String,
|
||||
pub summary: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EditApp {
|
||||
pub path: Option<String>,
|
||||
pub summary: Option<String>,
|
||||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
async fn list_apps(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Query(pagination): Query<Pagination>,
|
||||
Query(lq): Query<ListAppQuery>,
|
||||
) -> JsonResult<Vec<ListableApp>> {
|
||||
let (per_page, offset) = paginate(pagination);
|
||||
|
||||
let mut sqlb = SqlBuilder::select_from("raw_app as app")
|
||||
.fields(&[
|
||||
"app.workspace_id",
|
||||
"app.path",
|
||||
"app.edited_at",
|
||||
"app.summary",
|
||||
"app.extra_perms",
|
||||
"app.version",
|
||||
"favorite.path IS NOT NULL as starred",
|
||||
])
|
||||
.left()
|
||||
.join("favorite")
|
||||
.on(
|
||||
"favorite.favorite_kind = 'raw_app' AND favorite.workspace_id = app.workspace_id AND favorite.path = app.path AND favorite.usr = ?"
|
||||
.bind(&authed.username),
|
||||
)
|
||||
.order_desc("favorite.path IS NOT NULL")
|
||||
.order_by("app.edited_at", true)
|
||||
.and_where("app.workspace_id = ?".bind(&w_id))
|
||||
.offset(offset)
|
||||
.limit(per_page)
|
||||
.clone();
|
||||
|
||||
if lq.starred_only.unwrap_or(false) {
|
||||
sqlb.and_where_is_not_null("favorite.path");
|
||||
}
|
||||
|
||||
let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?;
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let rows = sqlx::query_as::<_, ListableApp>(&sql)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
async fn get_data(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, _version, path)): Path<(String, u16, StripPath)>,
|
||||
) -> Result<Response> {
|
||||
let path = path.to_path();
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let app_o = sqlx::query_scalar!(
|
||||
"SELECT data FROM raw_app
|
||||
WHERE path = $1 AND workspace_id = $2",
|
||||
path.to_owned(),
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
let app = not_found_if_none(app_o, "App", path)?;
|
||||
let res = Response::builder().header(header::CONTENT_TYPE, "text/javascript");
|
||||
|
||||
Ok(res
|
||||
.body(body::boxed(body::Full::from(Bytes::from(app))))
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
async fn create_app(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(app): Json<CreateApp>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
if &app.path == "" {
|
||||
return Err(Error::BadRequest("App path cannot be empty".to_string()));
|
||||
}
|
||||
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM app WHERE path = $1 AND workspace_id = $2)",
|
||||
app.path,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
|
||||
if exists {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"App with path {} already exists",
|
||||
&app.path
|
||||
)));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO raw_app
|
||||
(workspace_id, path, summary, extra_perms, data)
|
||||
VALUES ($1, $2, $3, '{}', $4)",
|
||||
w_id,
|
||||
app.path,
|
||||
app.summary,
|
||||
app.value,
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
"apps.create",
|
||||
ActionKind::Create,
|
||||
&w_id,
|
||||
Some(&app.path),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
webhook.send_message(
|
||||
w_id.clone(),
|
||||
WebhookMessage::CreateApp { workspace: w_id, path: app.path.clone() },
|
||||
);
|
||||
|
||||
Ok((StatusCode::CREATED, app.path))
|
||||
}
|
||||
|
||||
async fn delete_app(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> Result<String> {
|
||||
let path = path.to_path();
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM raw_app WHERE path = $1 AND workspace_id = $2",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
"apps.delete",
|
||||
ActionKind::Delete,
|
||||
&w_id,
|
||||
Some(path),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
webhook.send_message(
|
||||
w_id.clone().clone(),
|
||||
WebhookMessage::DeleteApp { workspace: w_id, path: path.to_owned() },
|
||||
);
|
||||
|
||||
Ok(format!("app {} deleted", path))
|
||||
}
|
||||
|
||||
async fn update_app(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(app): Json<EditApp>,
|
||||
) -> Result<String> {
|
||||
use sql_builder::prelude::*;
|
||||
|
||||
let path = path.to_path();
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let npath = &app.path;
|
||||
let npath = if npath.is_some() || app.summary.is_some() {
|
||||
let mut sqlb = SqlBuilder::update_table("raw_app");
|
||||
sqlb.and_where_eq("path", "?".bind(&path));
|
||||
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
|
||||
|
||||
if let Some(npath) = &app.path {
|
||||
if npath != path {
|
||||
require_owner_of_path(&authed, path)?;
|
||||
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM app WHERE path = $1 AND workspace_id = $2)",
|
||||
npath,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
|
||||
if exists {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"App with path {} already exists",
|
||||
npath
|
||||
)));
|
||||
}
|
||||
}
|
||||
sqlb.set_str("path", npath);
|
||||
}
|
||||
|
||||
if let Some(nsummary) = &app.summary {
|
||||
sqlb.set_str("summary", nsummary);
|
||||
}
|
||||
|
||||
if let Some(value) = &app.value {
|
||||
sqlb.set_str("data", value);
|
||||
sqlb.set("version", "version + 1");
|
||||
}
|
||||
|
||||
sqlb.returning("path");
|
||||
|
||||
let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?;
|
||||
let npath_o: Option<String> = sqlx::query_scalar(&sql).fetch_optional(&mut tx).await?;
|
||||
not_found_if_none(npath_o, "Raw App", path)?
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
"apps.update",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&npath),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
webhook.send_message(
|
||||
w_id.clone(),
|
||||
WebhookMessage::UpdateApp {
|
||||
workspace: w_id,
|
||||
old_path: path.to_owned(),
|
||||
new_path: npath.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
Ok(format!("app {} updated (npath: {:?})", path, npath))
|
||||
}
|
||||
|
||||
async fn exists_app(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> JsonResult<bool> {
|
||||
let path = path.to_path();
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM raw_app WHERE path = $1 AND workspace_id = $2)",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(Json(exists))
|
||||
}
|
||||
@@ -630,7 +630,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
if job.id == Uuid::nil() {
|
||||
tracing::info!(worker = %worker_name, "running warmup job");
|
||||
} else {
|
||||
tracing::info!(worker = %worker_name, id = %job.id, root_id = %job_root, "fetched job {}, root job: {}", job.id, job_root);
|
||||
tracing::info!(worker = %worker_name, workspace_id = %job.workspace_id, id = %job.id, root_id = %job_root, "fetched job {}, root job: {}", job.id, job_root);
|
||||
}
|
||||
|
||||
let job_dir = format!("{worker_dir}/{}", job.id);
|
||||
|
||||
@@ -20,7 +20,7 @@ The default proxy is setup to use the remote backend: <https://app.windmill.dev>
|
||||
You can configure another proxy to use like so:
|
||||
|
||||
```bash
|
||||
REMOTE=http://localhost:8000 REMOTE_LSP=http://localhost:3000 npm run dev
|
||||
REMOTE=http://127.0.0.1:8000 REMOTE_LSP=http://127.0.0.1:3001 npm run dev
|
||||
```
|
||||
|
||||
## Use a Local backend
|
||||
@@ -80,7 +80,7 @@ DATABASE_URL=postgres://postgres:changeme@127.0.0.1:5433/windmill?sslmode=disabl
|
||||
In the frontend folder:
|
||||
|
||||
```bash
|
||||
REMOTE=http://localhost:8000 REMOTE_LSP=http://localhost:3000 npm run dev
|
||||
REMOTE=http://127.0.0.1:8000 REMOTE_LSP=http://127.0.0.1:3001 npm run dev
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
import { Alert, Button, Drawer } from './common'
|
||||
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
||||
import Path from './Path.svelte'
|
||||
import { AppService, FlowService, ScriptService } from '$lib/gen'
|
||||
import { AppService, FlowService, RawAppService, ScriptService } from '$lib/gen'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
type Kind = 'script' | 'resource' | 'schedule' | 'variable' | 'flow' | 'app'
|
||||
type Kind = 'script' | 'resource' | 'schedule' | 'variable' | 'flow' | 'app' | 'raw_app'
|
||||
|
||||
let kind: Kind
|
||||
let initialPath: string = ''
|
||||
@@ -25,6 +25,7 @@
|
||||
kind_l: Kind
|
||||
) {
|
||||
kind = kind_l
|
||||
path = undefined
|
||||
initialPath = initialPath_l
|
||||
summary = summary_l
|
||||
loadOwner()
|
||||
@@ -77,6 +78,15 @@
|
||||
summary
|
||||
}
|
||||
})
|
||||
} else if (kind == 'raw_app') {
|
||||
await RawAppService.updateRawApp({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath,
|
||||
requestBody: {
|
||||
path: path != initialPath ? path : undefined,
|
||||
summary
|
||||
}
|
||||
})
|
||||
}
|
||||
dispatch('update', path)
|
||||
drawer.closeDrawer()
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
import Required from './Required.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule' | 'app'
|
||||
type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule' | 'app' | 'raw_app'
|
||||
let meta: Meta | undefined = undefined
|
||||
export let namePlaceholder = ''
|
||||
export let initialPath: string
|
||||
|
||||
@@ -13,7 +13,15 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
type Kind = 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'app'
|
||||
type Kind =
|
||||
| 'script'
|
||||
| 'group_'
|
||||
| 'resource'
|
||||
| 'schedule'
|
||||
| 'variable'
|
||||
| 'flow'
|
||||
| 'app'
|
||||
| 'raw_app'
|
||||
let kind: Kind
|
||||
|
||||
let path: string = ''
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Star, StarOff } from 'lucide-svelte'
|
||||
|
||||
export let path: string
|
||||
export let kind: 'flow' | 'app' | 'script'
|
||||
export let kind: 'flow' | 'app' | 'script' | 'raw_app'
|
||||
export let starred = false
|
||||
export let workspace_id: string
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { Policy } from "$lib/gen";
|
||||
import { writable } from "svelte/store";
|
||||
import type {
|
||||
App,
|
||||
import type { Policy } from '$lib/gen'
|
||||
import { writable } from 'svelte/store'
|
||||
import type { App } from './types'
|
||||
|
||||
} from './types'
|
||||
|
||||
export const importStore = writable<{ summary: string, value: App, policy: Policy } | undefined>(undefined)
|
||||
export const importStore = writable<{ summary: string; value: App; policy: Policy } | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<script lang="ts">
|
||||
import Dropdown from '$lib/components/Dropdown.svelte'
|
||||
import type MoveDrawer from '$lib/components/MoveDrawer.svelte'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import type ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import { RawAppService, type ListableRawApp } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import {
|
||||
faEdit,
|
||||
faEye,
|
||||
faFileExport,
|
||||
faShare,
|
||||
faTrashAlt
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Button from '../button/Button.svelte'
|
||||
import Row from './Row.svelte'
|
||||
import Drawer from '../drawer/Drawer.svelte'
|
||||
import DrawerContent from '../drawer/DrawerContent.svelte'
|
||||
import FileInput from '../fileInput/FileInput.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
|
||||
export let app: ListableRawApp & { canWrite: boolean }
|
||||
export let marked: string | undefined
|
||||
export let starred: boolean
|
||||
export let shareModal: ShareModal
|
||||
export let moveDrawer: MoveDrawer
|
||||
export let deleteConfirmedCallback: (() => void) | undefined
|
||||
|
||||
let updateAppDrawer: Drawer
|
||||
|
||||
let { summary, version, path, extra_perms, workspace_id, canWrite } = app
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={updateAppDrawer} size="800px">
|
||||
<DrawerContent title="Update app" on:close={() => updateAppDrawer?.toggleDrawer?.()}>
|
||||
<FileInput
|
||||
accept={'.js'}
|
||||
multiple={false}
|
||||
convertTo={'text'}
|
||||
iconSize={24}
|
||||
class="text-sm py-4"
|
||||
on:change={async ({ detail }) => {
|
||||
await RawAppService.updateRawApp({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path,
|
||||
requestBody: { value: detail?.[0] }
|
||||
})
|
||||
goto(`/apps/get_raw/${version + 1}/${path}`)
|
||||
}}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<Row
|
||||
href="/apps/get_raw/{version}/{path}"
|
||||
kind="raw_app"
|
||||
{marked}
|
||||
{path}
|
||||
{summary}
|
||||
workspaceId={workspace_id ?? $workspaceStore ?? ''}
|
||||
{starred}
|
||||
on:change
|
||||
canFavorite={true}
|
||||
>
|
||||
<svelte:fragment slot="badges">
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<span class="hidden md:inline-flex gap-x-1">
|
||||
{#if !$userStore?.operator}
|
||||
{#if canWrite}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faEdit }}
|
||||
on:click={() => updateAppDrawer?.toggleDrawer?.()}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
<Button
|
||||
href="/apps/get_raw/{version}/{path}"
|
||||
color="dark"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
startIcon={{ icon: faEye }}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
</span>
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
dropdownItems={() => {
|
||||
return [
|
||||
{
|
||||
displayName: 'View',
|
||||
icon: faEye,
|
||||
href: `/apps/get/${path}`
|
||||
},
|
||||
{
|
||||
displayName: 'Move/Rename',
|
||||
icon: faFileExport,
|
||||
action: () => {
|
||||
moveDrawer.openDrawer(path, summary, 'raw_app')
|
||||
},
|
||||
disabled: !canWrite
|
||||
},
|
||||
{
|
||||
displayName: canWrite ? 'Share' : 'See Permissions',
|
||||
icon: faShare,
|
||||
action: () => {
|
||||
shareModal.openDrawer && shareModal.openDrawer(path, 'raw_app')
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Delete',
|
||||
icon: faTrashAlt,
|
||||
action: async (event) => {
|
||||
if (event?.shiftKey) {
|
||||
await RawAppService.deleteRawApp({ workspace: $workspaceStore ?? '', path })
|
||||
dispatch('change')
|
||||
} else {
|
||||
deleteConfirmedCallback = async () => {
|
||||
await RawAppService.deleteRawApp({ workspace: $workspaceStore ?? '', path })
|
||||
dispatch('change')
|
||||
}
|
||||
}
|
||||
},
|
||||
type: 'delete',
|
||||
disabled: !canWrite
|
||||
}
|
||||
]
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Row>
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let kind: 'script' | 'flow' | 'app' = 'script'
|
||||
export let kind: 'script' | 'flow' | 'app' | 'raw_app' = 'script'
|
||||
export let summary: string | undefined = undefined
|
||||
export let path: string
|
||||
export let href: string
|
||||
|
||||
@@ -4,19 +4,21 @@
|
||||
import { Code2, LayoutDashboard } from 'lucide-svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
|
||||
export let kind: 'script' | 'flow' | 'app'
|
||||
export let kind: 'script' | 'flow' | 'app' | 'raw_app'
|
||||
export let href: string = '#'
|
||||
|
||||
const color = {
|
||||
script: 'bg-blue-50 border-blue-200',
|
||||
flow: 'bg-[#f0fdfa] border-[#99f6e4]',
|
||||
app: 'bg-[#fff7ed] border-orange-300'
|
||||
app: 'bg-[#fff7ed] border-orange-300',
|
||||
raw_app: 'bg-[#fff7ed] border-orange-300'
|
||||
}[kind]
|
||||
|
||||
const iconColor = {
|
||||
script: '#60A5FA',
|
||||
flow: '#14b8a6',
|
||||
app: '#fb923c'
|
||||
app: '#fb923c',
|
||||
raw_app: '#fb923c'
|
||||
}[kind]
|
||||
</script>
|
||||
|
||||
@@ -25,7 +27,7 @@
|
||||
<span class="ml-1 mb-0.5 -mt-0.5">
|
||||
<Icon data={faBarsStaggered} scale={1.1} class="mr-0.5 text-[#14b8a6]" />
|
||||
</span>
|
||||
{:else if kind === 'app'}
|
||||
{:else if kind === 'app' || kind === 'raw_app'}
|
||||
<LayoutDashboard size={24} color={iconColor} />
|
||||
{:else if kind === 'script'}
|
||||
<Code2 size={24} color={iconColor} />
|
||||
|
||||
@@ -2,21 +2,42 @@
|
||||
import { goto } from '$app/navigation'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { Button } from '$lib/components/common'
|
||||
import { Button, FileInput } from '$lib/components/common'
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import { LayoutDashboard } from 'lucide-svelte'
|
||||
import { importStore } from '../apps/store'
|
||||
import { RawAppService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import Path from '../Path.svelte'
|
||||
|
||||
let drawer: Drawer | undefined = undefined
|
||||
let pendingJson: string
|
||||
let rawAppDrawer: Drawer | undefined = undefined
|
||||
let pendingJson: string = ''
|
||||
let pendingCode: string = ''
|
||||
let summary: string = ''
|
||||
let path: string = ''
|
||||
let pathError: string = ''
|
||||
|
||||
async function importJson() {
|
||||
$importStore = JSON.parse(pendingJson)
|
||||
await goto('/apps/add?nodraft=true')
|
||||
drawer?.closeDrawer?.()
|
||||
}
|
||||
|
||||
async function importRawApp() {
|
||||
await RawAppService.createRawApp({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path,
|
||||
summary,
|
||||
value: pendingCode
|
||||
}
|
||||
})
|
||||
await goto(`/apps/get_raw/0/${path}`)
|
||||
rawAppDrawer?.closeDrawer?.()
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Buttons -->
|
||||
@@ -28,8 +49,12 @@
|
||||
href="/apps/add?nodraft=true"
|
||||
dropdownItems={[
|
||||
{
|
||||
label: 'Import from raw JSON',
|
||||
label: 'Import low-code app from JSON',
|
||||
onClick: () => drawer?.toggleDrawer?.()
|
||||
},
|
||||
{
|
||||
label: 'Import app in React/Vue/Svelte',
|
||||
onClick: () => rawAppDrawer?.toggleDrawer?.()
|
||||
}
|
||||
]}
|
||||
>
|
||||
@@ -41,10 +66,43 @@
|
||||
|
||||
<!-- Raw JSON -->
|
||||
<Drawer bind:this={drawer} size="800px">
|
||||
<DrawerContent title="Import app from JSON" on:close={() => drawer?.toggleDrawer?.()}>
|
||||
<DrawerContent title="Import low-code app from JSON" on:close={() => drawer?.toggleDrawer?.()}>
|
||||
<SimpleEditor bind:code={pendingJson} lang="json" class="h-full" fixedOverflowWidgets={false} />
|
||||
<svelte:fragment slot="actions">
|
||||
<Button size="sm" on:click={importJson}>Import</Button>
|
||||
</svelte:fragment>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<!-- Raw JSON -->
|
||||
<Drawer bind:this={rawAppDrawer} size="800px">
|
||||
<DrawerContent
|
||||
title="Import app in React/Vue/Svelte"
|
||||
on:close={() => rawAppDrawer?.toggleDrawer?.()}
|
||||
>
|
||||
<Path bind:error={pathError} bind:path initialPath="" namePlaceholder={'app'} kind="resource" />
|
||||
<h2 class="border-b pb-1 mt-10 mb-4">Summary</h2>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
bind:value={summary}
|
||||
placeholder="Short summary to be displayed when listed"
|
||||
/>
|
||||
|
||||
<h2 class="border-b pb-1 mt-10 mb-4">IIFE JS code</h2>
|
||||
<FileInput
|
||||
accept={'.js'}
|
||||
multiple={false}
|
||||
convertTo={'text'}
|
||||
iconSize={24}
|
||||
class="text-sm py-4"
|
||||
on:change={({ detail }) => {
|
||||
pendingCode = detail?.[0]
|
||||
}}
|
||||
/>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={pathError != ''} size="sm" on:click={importRawApp}>Import</Button>
|
||||
</svelte:fragment>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
href="/flows/add?nodraft=true"
|
||||
dropdownItems={[
|
||||
{
|
||||
label: 'Import from raw JSON',
|
||||
label: 'Import from JSON',
|
||||
onClick: () => drawer?.toggleDrawer?.()
|
||||
}
|
||||
]}
|
||||
|
||||
@@ -3,7 +3,16 @@
|
||||
import { Alert, Badge, Skeleton } from '$lib/components/common'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { AppService, FlowService, ListableApp, Script, ScriptService, type Flow } from '$lib/gen'
|
||||
import {
|
||||
AppService,
|
||||
FlowService,
|
||||
ListableApp,
|
||||
Script,
|
||||
ScriptService,
|
||||
type Flow,
|
||||
type ListableRawApp,
|
||||
RawAppService
|
||||
} from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { canWrite } from '$lib/utils'
|
||||
import type uFuzzy from '@leeoniya/ufuzzy'
|
||||
@@ -25,8 +34,9 @@
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
import FlowIcon from './FlowIcon.svelte'
|
||||
import RawAppRow from '../common/table/RawAppRow.svelte'
|
||||
|
||||
type TableItem<T, U extends 'script' | 'flow' | 'app'> = T & {
|
||||
type TableItem<T, U extends 'script' | 'flow' | 'app' | 'raw_app'> = T & {
|
||||
canWrite: boolean
|
||||
marked?: string
|
||||
type?: U
|
||||
@@ -37,12 +47,14 @@
|
||||
type TableScript = TableItem<Script, 'script'>
|
||||
type TableFlow = TableItem<Flow, 'flow'>
|
||||
type TableApp = TableItem<ListableApp, 'app'>
|
||||
type TableRawApp = TableItem<ListableRawApp, 'raw_app'>
|
||||
|
||||
let scripts: TableScript[] | undefined
|
||||
let flows: TableFlow[] | undefined
|
||||
let apps: TableApp[] | undefined
|
||||
let raw_apps: TableRawApp[] | undefined
|
||||
|
||||
let filteredItems: (TableScript | TableFlow | TableApp)[] = []
|
||||
let filteredItems: (TableScript | TableFlow | TableApp | TableRawApp)[] = []
|
||||
|
||||
let itemKind: 'script' | 'flow' | 'app' | 'all' = 'all'
|
||||
|
||||
@@ -102,14 +114,29 @@
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function loadRawApps(): Promise<void> {
|
||||
raw_apps = (await RawAppService.listRawApps({ workspace: $workspaceStore! })).map(
|
||||
(app: ListableRawApp) => {
|
||||
return {
|
||||
canWrite:
|
||||
canWrite(app.path!, app.extra_perms!, $userStore) &&
|
||||
app.workspace_id == $workspaceStore &&
|
||||
!$userStore?.operator,
|
||||
...app
|
||||
}
|
||||
}
|
||||
)
|
||||
loading = false
|
||||
}
|
||||
|
||||
$: owners = Array.from(
|
||||
new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
|
||||
).sort()
|
||||
|
||||
let combinedItems: (TableScript | TableFlow | TableApp)[] | undefined = undefined
|
||||
let combinedItems: (TableScript | TableFlow | TableApp | TableRawApp)[] | undefined = undefined
|
||||
|
||||
$: combinedItems =
|
||||
flows == undefined || scripts == undefined || apps == undefined
|
||||
flows == undefined || scripts == undefined || apps == undefined || raw_apps == undefined
|
||||
? undefined
|
||||
: [
|
||||
...flows.map((x) => ({
|
||||
@@ -126,6 +153,11 @@
|
||||
...x,
|
||||
type: 'app' as 'app',
|
||||
time: new Date(x.edited_at).getTime()
|
||||
})),
|
||||
...raw_apps.map((x) => ({
|
||||
...x,
|
||||
type: 'raw_app' as 'raw_app',
|
||||
time: new Date(x.edited_at).getTime()
|
||||
}))
|
||||
].sort((a, b) =>
|
||||
a.starred != b.starred ? (a.starred ? -1 : 1) : a.time - b.time > 0 ? -1 : 1
|
||||
@@ -150,8 +182,10 @@
|
||||
loadFlows()
|
||||
if (!archived) {
|
||||
loadApps()
|
||||
loadRawApps()
|
||||
} else {
|
||||
apps = []
|
||||
raw_apps = []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,6 +264,7 @@
|
||||
loadScripts()
|
||||
loadApps()
|
||||
loadFlows()
|
||||
loadRawApps()
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -239,6 +274,7 @@
|
||||
loadScripts()
|
||||
loadApps()
|
||||
loadFlows()
|
||||
loadRawApps()
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -321,37 +357,51 @@
|
||||
<div class="border rounded-md divide-y divide-gray-200">
|
||||
<!-- <VirtualList {items} let:item bind:start bind:end> -->
|
||||
{#each (items ?? []).slice(0, nbDisplayed) as item (item.type + '/' + item.path)}
|
||||
{#if item.type == 'script'}
|
||||
<ScriptRow
|
||||
bind:deleteConfirmedCallback
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={loadScripts}
|
||||
script={item}
|
||||
{shareModal}
|
||||
{moveDrawer}
|
||||
/>
|
||||
{:else if item.type == 'flow'}
|
||||
<FlowRow
|
||||
bind:deleteConfirmedCallback
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={loadFlows}
|
||||
flow={item}
|
||||
{shareModal}
|
||||
{moveDrawer}
|
||||
/>
|
||||
{:else if item.type == 'app'}
|
||||
<AppRow
|
||||
bind:deleteConfirmedCallback
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={loadApps}
|
||||
app={item}
|
||||
{moveDrawer}
|
||||
{shareModal}
|
||||
/>
|
||||
{/if}
|
||||
{#key item.summary}
|
||||
{#key item.starred}
|
||||
{#if item.type == 'script'}
|
||||
<ScriptRow
|
||||
bind:deleteConfirmedCallback
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={loadScripts}
|
||||
script={item}
|
||||
{shareModal}
|
||||
{moveDrawer}
|
||||
/>
|
||||
{:else if item.type == 'flow'}
|
||||
<FlowRow
|
||||
bind:deleteConfirmedCallback
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={loadFlows}
|
||||
flow={item}
|
||||
{shareModal}
|
||||
{moveDrawer}
|
||||
/>
|
||||
{:else if item.type == 'app'}
|
||||
<AppRow
|
||||
bind:deleteConfirmedCallback
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={loadApps}
|
||||
app={item}
|
||||
{moveDrawer}
|
||||
{shareModal}
|
||||
/>
|
||||
{:else if item.type == 'raw_app'}
|
||||
<RawAppRow
|
||||
bind:deleteConfirmedCallback
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={loadRawApps}
|
||||
app={item}
|
||||
{moveDrawer}
|
||||
{shareModal}
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
{/key}
|
||||
{/each}
|
||||
<!-- </VirtualList> -->
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
export let favoriteLinks = [] as {
|
||||
label: string
|
||||
href: string
|
||||
kind: 'script' | 'flow' | 'app'
|
||||
kind: 'script' | 'flow' | 'app' | 'raw_app'
|
||||
}[]
|
||||
</script>
|
||||
|
||||
@@ -42,13 +42,17 @@
|
||||
{:else}
|
||||
<div class="py-1 w-full max-w-full">
|
||||
{#each favoriteLinks ?? [] as favorite (favorite.href)}
|
||||
<a href={favorite.href} on:click={close} class="w-full inline-flex flex-row px-4 py-2 hover:bg-gray-100">
|
||||
<a
|
||||
href={favorite.href}
|
||||
on:click={close}
|
||||
class="w-full inline-flex flex-row px-4 py-2 hover:bg-gray-100"
|
||||
>
|
||||
<span class="center-center">
|
||||
{#if favorite.kind == 'script'}
|
||||
<Code2 size={16} />
|
||||
{:else if favorite.kind == 'flow'}
|
||||
<Icon data={faBarsStaggered} />
|
||||
{:else if favorite.kind == 'app'}
|
||||
{:else if favorite.kind == 'app' || favorite.kind == 'raw_app'}
|
||||
<LayoutDashboard size={16} />
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
import Icon from 'svelte-awesome'
|
||||
|
||||
import UserMenu from '$lib/components/sidebar/UserMenu.svelte'
|
||||
import { AppService, FlowService, OpenAPI, ScriptService, UserService } from '$lib/gen'
|
||||
import {
|
||||
AppService,
|
||||
FlowService,
|
||||
OpenAPI,
|
||||
RawAppService,
|
||||
ScriptService,
|
||||
UserService
|
||||
} from '$lib/gen'
|
||||
import { classNames, isCloudHosted } from '$lib/utils'
|
||||
import { browser } from '$app/environment'
|
||||
|
||||
@@ -48,7 +55,11 @@
|
||||
|
||||
let innerWidth = browser ? window.innerWidth : 2000
|
||||
|
||||
let favoriteLinks = [] as { label: string; href: string; kind: 'app' | 'script' | 'flow' }[]
|
||||
let favoriteLinks = [] as {
|
||||
label: string
|
||||
href: string
|
||||
kind: 'app' | 'script' | 'flow' | 'raw_app'
|
||||
}[]
|
||||
$: $workspaceStore && $starStore && onLoad()
|
||||
|
||||
function onLoad() {
|
||||
@@ -75,6 +86,10 @@
|
||||
workspace: $workspaceStore ?? '',
|
||||
starredOnly: true
|
||||
})
|
||||
const raw_apps = await RawAppService.listRawApps({
|
||||
workspace: $workspaceStore ?? '',
|
||||
starredOnly: true
|
||||
})
|
||||
favoriteLinks = [
|
||||
...scripts.map((s) => ({
|
||||
label: s.summary || s.path,
|
||||
@@ -90,6 +105,11 @@
|
||||
label: f.summary || f.path,
|
||||
href: `/apps/get/${f.path}`,
|
||||
kind: 'app' as 'app'
|
||||
})),
|
||||
...raw_apps.map((f) => ({
|
||||
label: f.summary || f.path,
|
||||
href: `/apps/get_raw/${f.version}/${f.path}`,
|
||||
kind: 'raw_app' as 'raw_app'
|
||||
}))
|
||||
]
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
async function loadApp() {
|
||||
if (importJson) {
|
||||
sendUserToast('Loaded from raw JSON')
|
||||
sendUserToast('Loaded from JSON')
|
||||
if ('value' in importJson) {
|
||||
summary = importJson.summary
|
||||
value = importJson.value
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export function load({ params }) {
|
||||
return {
|
||||
stuff: { title: `App ${params.path}` }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import { Skeleton } from '$lib/components/common'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
let loaded = false
|
||||
|
||||
onMount(async () => {
|
||||
globalThis.windmill = {
|
||||
username: $userStore?.username,
|
||||
email: $userStore?.email,
|
||||
workspace: $workspaceStore
|
||||
}
|
||||
|
||||
// //@ts-ignore
|
||||
// await import('http://localhost:3000/app.iife.js')
|
||||
/* @vite-ignore */
|
||||
await import(
|
||||
`/api/w/${$workspaceStore}/raw_apps/get_data/${$page.params.version}/${$page.params.path}`
|
||||
)
|
||||
try {
|
||||
globalThis.render()
|
||||
} catch (e) {
|
||||
sendUserToast('App seem to be ill-defined', true)
|
||||
console.error(e)
|
||||
}
|
||||
loaded = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<div id="root" />
|
||||
|
||||
{#if !loaded}
|
||||
<Skeleton layout={[10]} />
|
||||
{/if}
|
||||
File diff suppressed because one or more lines are too long
@@ -7,7 +7,6 @@ const config = {
|
||||
'hljs',
|
||||
'splitpanes__pane',
|
||||
'splitpanes__splitter',
|
||||
|
||||
...(process.env.NODE_ENV === 'production'
|
||||
? [
|
||||
{ pattern: /^m(\w?)-.*$/ },
|
||||
|
||||
Reference in New Issue
Block a user