mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat: hide scripts with on function main from operators + badge/filter for devs (#3584)
* feat: save in DB whether script has not main function * fix: if cannot parse script args, clear schema * feat: no main function badge and filter * fix: sqlx * fix: no main
This commit is contained in:
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28)",
|
||||
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -66,10 +66,11 @@
|
||||
"Bool",
|
||||
"Int4",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "020d33ed5d47350b456783fd548422ea8dcf2d786d0e9fa849754db82c9fa378"
|
||||
"hash": "92d00c6a1f4c40f2a23c9ba758a59597deabd7ca93653ad72d2cdc37efefc9d4"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE script ADD COLUMN no_main_func BOOLEAN;
|
||||
@@ -3164,6 +3164,7 @@ async fn run_deployed_relative_imports(db: &Pool<Postgres>, script_content: Stri
|
||||
deployment_message: None,
|
||||
concurrency_key: None,
|
||||
visible_to_runner_only: None,
|
||||
no_main_func: None,
|
||||
},
|
||||
).await.unwrap();
|
||||
|
||||
|
||||
@@ -3401,6 +3401,13 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
- name: hide_without_main
|
||||
description: |
|
||||
(default false)
|
||||
hide the scripts without an exported main function
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
- name: is_template
|
||||
description: |
|
||||
(default regardless)
|
||||
@@ -8356,6 +8363,8 @@ components:
|
||||
type: boolean
|
||||
visible_to_runner_only:
|
||||
type: boolean
|
||||
no_main_func:
|
||||
type: boolean
|
||||
required:
|
||||
- hash
|
||||
- path
|
||||
@@ -8371,6 +8380,7 @@ components:
|
||||
- language
|
||||
- kind
|
||||
- starred
|
||||
- no_main_func
|
||||
|
||||
NewScript:
|
||||
type: object
|
||||
@@ -8444,6 +8454,8 @@ components:
|
||||
type: string
|
||||
visible_to_runner_only:
|
||||
type: boolean
|
||||
no_main_func:
|
||||
type: boolean
|
||||
required:
|
||||
- path
|
||||
- summary
|
||||
|
||||
@@ -91,6 +91,8 @@ pub struct ScriptWDraft {
|
||||
pub concurrency_key: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub visible_to_runner_only: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub no_main_func: Option<bool>,
|
||||
}
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
@@ -192,7 +194,8 @@ async fn list_scripts(
|
||||
"tag",
|
||||
"draft.path IS NOT NULL as has_draft",
|
||||
"draft_only",
|
||||
"ws_error_handler_muted"
|
||||
"ws_error_handler_muted",
|
||||
"no_main_func",
|
||||
])
|
||||
.left()
|
||||
.join("favorite")
|
||||
@@ -212,6 +215,10 @@ async fn list_scripts(
|
||||
.limit(per_page)
|
||||
.clone();
|
||||
|
||||
if authed.is_operator || lq.hide_without_main.unwrap_or(false) {
|
||||
sqlb.and_where("o.no_main_func IS NOT TRUE");
|
||||
}
|
||||
|
||||
if lq.show_archived.unwrap_or(false) {
|
||||
sqlb.and_where_eq(
|
||||
"o.created_at",
|
||||
@@ -475,8 +482,8 @@ async fn create_script(
|
||||
content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, \
|
||||
draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, \
|
||||
dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, \
|
||||
delete_after_use, timeout, concurrency_key, visible_to_runner_only) \
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28)",
|
||||
delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func) \
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29)",
|
||||
&w_id,
|
||||
&hash.0,
|
||||
ns.path,
|
||||
@@ -505,6 +512,7 @@ async fn create_script(
|
||||
ns.timeout,
|
||||
ns.concurrency_key,
|
||||
ns.visible_to_runner_only,
|
||||
ns.no_main_func
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
@@ -724,7 +732,7 @@ async fn get_script_by_path_w_draft(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let script_o = sqlx::query_as::<_, ScriptWDraft>(
|
||||
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only FROM script LEFT JOIN draft ON
|
||||
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func FROM script LEFT JOIN draft ON
|
||||
script.path = draft.path AND script.workspace_id = draft.workspace_id AND draft.typ = 'script'
|
||||
WHERE script.path = $1 AND script.workspace_id = $2 \
|
||||
AND script.created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND \
|
||||
|
||||
@@ -2183,6 +2183,8 @@ struct ScriptMetadata {
|
||||
pub restart_unless_cancelled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub visible_to_runner_only: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub no_main_func: Option<bool>,
|
||||
}
|
||||
|
||||
pub fn is_none_or_false(val: &Option<bool>) -> bool {
|
||||
@@ -2459,6 +2461,7 @@ async fn tarball_workspace(
|
||||
delete_after_use: script.delete_after_use,
|
||||
restart_unless_cancelled: script.restart_unless_cancelled,
|
||||
visible_to_runner_only: script.visible_to_runner_only,
|
||||
no_main_func: script.no_main_func,
|
||||
};
|
||||
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();
|
||||
archive
|
||||
|
||||
@@ -175,6 +175,8 @@ pub struct Script {
|
||||
pub concurrency_key: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub visible_to_runner_only: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub no_main_func: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, sqlx::FromRow)]
|
||||
@@ -194,6 +196,8 @@ pub struct ListableScript {
|
||||
pub draft_only: Option<bool>,
|
||||
pub has_deploy_errors: bool,
|
||||
pub ws_error_handler_muted: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub no_main_func: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -251,6 +255,7 @@ pub struct NewScript {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub concurrency_key: Option<String>,
|
||||
pub visible_to_runner_only: Option<bool>,
|
||||
pub no_main_func: Option<bool>,
|
||||
}
|
||||
|
||||
fn lock_deserialize<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
|
||||
@@ -320,6 +325,7 @@ pub struct ListScriptQuery {
|
||||
pub is_template: Option<bool>,
|
||||
pub kinds: Option<String>,
|
||||
pub starred_only: Option<bool>,
|
||||
pub hide_without_main: Option<bool>,
|
||||
}
|
||||
|
||||
pub fn to_i64(s: &str) -> crate::error::Result<i64> {
|
||||
|
||||
@@ -167,6 +167,7 @@ export async function handleFile(
|
||||
Boolean(remote.restart_unless_cancelled) &&
|
||||
Boolean(typed.visible_to_runner_only) ==
|
||||
Boolean(remote.visible_to_runner_only) &&
|
||||
Boolean(typed.no_main_func) == Boolean(remote.no_main_func) &&
|
||||
typed.priority == Boolean(remote.priority))
|
||||
) {
|
||||
log.info(colors.green(`Script ${remotePath} is up to date`));
|
||||
@@ -198,6 +199,7 @@ export async function handleFile(
|
||||
deployment_message: message,
|
||||
restart_unless_cancelled: typed?.restart_unless_cancelled,
|
||||
visible_to_runner_only: typed?.visible_to_runner_only,
|
||||
no_main_func: typed?.no_main_func,
|
||||
priority: typed?.priority,
|
||||
},
|
||||
});
|
||||
@@ -227,6 +229,7 @@ export async function handleFile(
|
||||
deployment_message: message,
|
||||
restart_unless_cancelled: typed?.restart_unless_cancelled,
|
||||
visible_to_runner_only: typed?.visible_to_runner_only,
|
||||
no_main_func: typed?.no_main_func,
|
||||
priority: typed?.priority,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -309,6 +309,7 @@
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
validCode = false
|
||||
schema = emptySchema()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import Popover from './Popover.svelte'
|
||||
import { Badge } from './common'
|
||||
</script>
|
||||
|
||||
<Popover notClickable>
|
||||
<svelte:fragment slot="text">The script has no main function exported</svelte:fragment>
|
||||
<Badge small color="yellow" baseClass="border border-indigo-200">No main</Badge>
|
||||
</Popover>
|
||||
@@ -215,7 +215,9 @@
|
||||
script.schema = script.schema ?? emptySchema()
|
||||
try {
|
||||
await inferArgs(script.language, script.content, script.schema as any)
|
||||
script.no_main_func = undefined
|
||||
} catch (error) {
|
||||
script.no_main_func = true
|
||||
sendUserToast(
|
||||
`The main signature was not parsable. This script is considered to be without main function`
|
||||
)
|
||||
@@ -245,7 +247,8 @@
|
||||
delete_after_use: script.delete_after_use,
|
||||
timeout: script.timeout,
|
||||
concurrency_key: emptyString(script.concurrency_key) ? undefined : script.concurrency_key,
|
||||
visible_to_runner_only: script.visible_to_runner_only
|
||||
visible_to_runner_only: script.visible_to_runner_only,
|
||||
no_main_func: script.no_main_func
|
||||
}
|
||||
})
|
||||
|
||||
@@ -333,7 +336,9 @@
|
||||
script.schema = script.schema ?? emptySchema()
|
||||
try {
|
||||
await inferArgs(script.language, script.content, script.schema as any)
|
||||
script.no_main_func = undefined
|
||||
} catch (error) {
|
||||
script.no_main_func = true
|
||||
sendUserToast(
|
||||
`The main signature was not parsable. This script is considered to be without main function`
|
||||
)
|
||||
@@ -372,7 +377,8 @@
|
||||
concurrency_key: emptyString(script.concurrency_key)
|
||||
? undefined
|
||||
: script.concurrency_key,
|
||||
visible_to_runner_only: script.visible_to_runner_only
|
||||
visible_to_runner_only: script.visible_to_runner_only,
|
||||
no_main_func: script.no_main_func
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
schema = nschema
|
||||
} catch (e) {
|
||||
validCode = false
|
||||
schema = emptySchema()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
} from 'lucide-svelte'
|
||||
import ScriptVersionHistory from '$lib/components/ScriptVersionHistory.svelte'
|
||||
import { Drawer, DrawerContent } from '..'
|
||||
import NoMainFuncBadge from '$lib/components/NoMainFuncBadge.svelte'
|
||||
|
||||
export let script: Script & { canWrite: boolean }
|
||||
export let marked: string | undefined
|
||||
@@ -108,6 +109,9 @@
|
||||
<Badge color="red" baseClass="border">archived</Badge>
|
||||
{/if}
|
||||
|
||||
{#if script.no_main_func}
|
||||
<NoMainFuncBadge />
|
||||
{/if}
|
||||
<SharedBadge canWrite={script.canWrite} extraPerms={script.extra_perms} />
|
||||
<DraftBadge has_draft={script.has_draft} draft_only={script.draft_only} />
|
||||
<div class="w-8 center-center">
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
ws_error_handler_muted?: boolean
|
||||
dedicated_worker?: boolean
|
||||
visible_to_runner_only?: boolean
|
||||
no_main_func?: boolean
|
||||
}
|
||||
| undefined = undefined
|
||||
|
||||
@@ -64,6 +65,7 @@
|
||||
ws_error_handler_muted?: boolean
|
||||
dedicated_worker?: boolean
|
||||
visible_to_runner_only?: boolean
|
||||
no_main_func?: boolean
|
||||
}
|
||||
| undefined = undefined
|
||||
|
||||
@@ -73,7 +75,9 @@
|
||||
script.schema = script.schema ?? emptySchema()
|
||||
try {
|
||||
await inferArgs(script.language, script.content, script.schema)
|
||||
script.no_main_func = undefined
|
||||
} catch (error) {
|
||||
script.no_main_func = true
|
||||
sendUserToast(
|
||||
`Impossible to infer the schema. Assuming this is a script without main function`,
|
||||
true
|
||||
|
||||
@@ -14,7 +14,14 @@
|
||||
} from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import type uFuzzy from '@leeoniya/ufuzzy'
|
||||
import { Code2, FoldVertical, LayoutDashboard, SearchCode, UnfoldVertical } from 'lucide-svelte'
|
||||
import {
|
||||
Code2,
|
||||
FoldVertical,
|
||||
LayoutDashboard,
|
||||
SearchCode,
|
||||
SlidersHorizontal,
|
||||
UnfoldVertical
|
||||
} from 'lucide-svelte'
|
||||
|
||||
export let filter = ''
|
||||
export let subtab: 'flow' | 'script' | 'app' = 'script'
|
||||
@@ -36,6 +43,7 @@
|
||||
import DrawerContent from '../common/drawer/DrawerContent.svelte'
|
||||
import Item from './Item.svelte'
|
||||
import TreeViewRoot from './TreeViewRoot.svelte'
|
||||
import { Popup } from '../common'
|
||||
|
||||
type TableItem<T, U extends 'script' | 'flow' | 'app' | 'raw_app'> = T & {
|
||||
canWrite: boolean
|
||||
@@ -64,10 +72,11 @@
|
||||
|
||||
let nbDisplayed = 15
|
||||
|
||||
async function loadScripts(): Promise<void> {
|
||||
async function loadScripts(hideWithoutMain: boolean): Promise<void> {
|
||||
const loadedScripts = await ScriptService.listScripts({
|
||||
workspace: $workspaceStore!,
|
||||
showArchived: archived ? true : undefined
|
||||
showArchived: archived ? true : undefined,
|
||||
hideWithoutMain: hideWithoutMain ? true : undefined
|
||||
})
|
||||
|
||||
scripts = loadedScripts.map((script: Script) => {
|
||||
@@ -192,7 +201,7 @@
|
||||
|
||||
$: {
|
||||
if ($userStore && $workspaceStore) {
|
||||
loadScripts()
|
||||
loadScripts(hideWithoutMain)
|
||||
loadFlows()
|
||||
if (!archived) {
|
||||
loadApps()
|
||||
@@ -265,11 +274,14 @@
|
||||
|
||||
const TREE_VIEW_SETTING_NAME = 'treeView'
|
||||
const FILTER_USER_FOLDER_SETTING_NAME = 'filterUserFolders'
|
||||
const HIDE_WITHOUT_MAIN_SETTING = 'hideWithoutMain'
|
||||
let treeView = getLocalSetting(TREE_VIEW_SETTING_NAME) == 'true'
|
||||
let filterUserFolders = getLocalSetting(FILTER_USER_FOLDER_SETTING_NAME) == 'true'
|
||||
let hideWithoutMain = getLocalSetting(HIDE_WITHOUT_MAIN_SETTING) == 'true'
|
||||
|
||||
$: storeLocalSetting(TREE_VIEW_SETTING_NAME, treeView ? 'true' : undefined)
|
||||
$: storeLocalSetting(FILTER_USER_FOLDER_SETTING_NAME, filterUserFolders ? 'true' : undefined)
|
||||
$: storeLocalSetting(HIDE_WITHOUT_MAIN_SETTING, hideWithoutMain ? 'true' : undefined)
|
||||
|
||||
let contentSearch: ContentSearch
|
||||
|
||||
@@ -404,7 +416,37 @@
|
||||
{/if}
|
||||
{#if !loading}
|
||||
<div class="flex w-full flex-row-reverse gap-2 mt-4 mb-1 items-center h-6">
|
||||
<Toggle size="xs" bind:checked={archived} options={{ right: 'Show archived' }} />
|
||||
<Popup
|
||||
floatingConfig={{ placement: 'bottom-end' }}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Button
|
||||
startIcon={{
|
||||
icon: SlidersHorizontal
|
||||
}}
|
||||
nonCaptureEvent
|
||||
iconOnly
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
spacingSize="xs2"
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<div>
|
||||
<span class="text-sm font-semibold">Filters</span>
|
||||
<div class="flex flex-col gap-2 mt-2">
|
||||
<Toggle size="xs" bind:checked={archived} options={{ right: 'Show archived' }} />
|
||||
{#if $userStore && !$userStore.operator}
|
||||
<Toggle
|
||||
size="xs"
|
||||
bind:checked={hideWithoutMain}
|
||||
options={{ right: 'Hide without main function' }}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
{#if $userStore?.is_super_admin && $userStore.username.includes('@')}
|
||||
<Toggle size="xs" bind:checked={filterUserFolders} options={{ right: 'Only f/*' }} />
|
||||
{:else if $userStore?.is_admin || $userStore?.is_super_admin}
|
||||
@@ -451,12 +493,12 @@
|
||||
{nbDisplayed}
|
||||
{collapseAll}
|
||||
isSearching={filter !== ''}
|
||||
on:scriptChanged={loadScripts}
|
||||
on:scriptChanged={() => loadScripts(hideWithoutMain)}
|
||||
on:flowChanged={loadFlows}
|
||||
on:appChanged={loadApps}
|
||||
on:rawAppChanged={loadRawApps}
|
||||
on:reload={() => {
|
||||
loadScripts()
|
||||
loadScripts(hideWithoutMain)
|
||||
loadFlows()
|
||||
loadApps()
|
||||
loadRawApps()
|
||||
@@ -468,12 +510,12 @@
|
||||
{#each (items ?? []).slice(0, nbDisplayed) as item (item.type + '/' + item.path)}
|
||||
<Item
|
||||
{item}
|
||||
on:scriptChanged={loadScripts}
|
||||
on:scriptChanged={() => loadScripts(hideWithoutMain)}
|
||||
on:flowChanged={loadFlows}
|
||||
on:appChanged={loadApps}
|
||||
on:rawAppChanged={loadRawApps}
|
||||
on:reload={() => {
|
||||
loadScripts()
|
||||
loadScripts(hideWithoutMain)
|
||||
loadFlows()
|
||||
loadApps()
|
||||
loadRawApps()
|
||||
|
||||
Reference in New Issue
Block a user