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:
HugoCasa
2024-04-22 15:59:13 +02:00
committed by GitHub
parent bccb87b8f4
commit 9e36ae066c
16 changed files with 121 additions and 17 deletions
@@ -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;
+1
View File
@@ -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();
+12
View File
@@ -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
+12 -4
View File
@@ -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 \
+3
View File
@@ -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
+6
View File
@@ -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> {
+3
View File
@@ -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,
},
});
+1
View File
@@ -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()