feat: hub actions and better search (#7056)

* feat: hub actions and better search

* nit
This commit is contained in:
hugocasa
2025-11-05 11:10:48 +00:00
committed by GitHub
parent 186eb3a96b
commit ac84bae8f1
13 changed files with 179 additions and 25 deletions
+21
View File
@@ -4872,6 +4872,27 @@ paths:
- content
- language
/scripts/hub/pick/{path}:
get:
summary: record hub script pick
operationId: pickHubScriptByPath
tags:
- script
parameters:
- $ref: "#/components/parameters/ScriptPath"
responses:
"200":
description: script pick recorded
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
required:
- success
/scripts/hub/top:
get:
summary: get top hub scripts
+12 -2
View File
@@ -441,7 +441,7 @@ impl EmbeddingsDb {
&query_embedding,
limit.unwrap_or(10) as usize,
Some(&filter),
Some(0.75),
Some(0.8),
);
let results: Result<Vec<_>> = results
@@ -480,7 +480,17 @@ impl EmbeddingsDb {
})
.collect();
results
let mut results = results?;
if results.len() > 1 {
let top_score = results[0].score;
results = results
.into_iter()
.take_while(|r| (top_score - r.score) / top_score <= 0.05)
.collect();
}
Ok(results)
}
pub async fn query_resource_types(
+35
View File
@@ -131,6 +131,7 @@ pub fn global_service() -> Router {
.route("/hub/top", get(get_top_hub_scripts))
.route("/hub/get/*path", get(get_hub_script_by_path))
.route("/hub/get_full/*path", get(get_full_hub_script_by_path))
.route("/hub/pick/*path", get(pick_hub_script_by_path))
}
pub fn global_unauthed_service() -> Router {
@@ -1112,6 +1113,40 @@ pub async fn get_full_hub_script_by_path(
))
}
pub async fn pick_hub_script_by_path(
Path(path): Path<StripPath>,
Extension(db): Extension<DB>,
) -> impl IntoResponse {
let path_str = path.to_path();
// Extract version_id from path (format: {version_id}/{app}/{summary})
let version_id = path_str.split('/').nth(1).unwrap_or("");
let hub_base_url = HUB_BASE_URL.read().await.clone();
// Determine which hub to use based on version_id
// If version_id < PRIVATE_HUB_MIN_VERSION, use default hub
let target_hub_url = if version_id
.parse::<i32>()
.is_ok_and(|v| v < windmill_common::PRIVATE_HUB_MIN_VERSION)
{
windmill_common::DEFAULT_HUB_BASE_URL
} else {
&hub_base_url
};
// Call the hub's pick endpoint: /scripts/{version_id}/pick
let (status_code, headers, response) = query_elems_from_hub(
&HTTP_CLIENT,
&format!("{}/scripts/{}/pick", target_hub_url, version_id),
None,
&db,
)
.await?;
Ok::<_, Error>((status_code, headers, response))
}
async fn get_script_by_path(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
+1
View File
@@ -97,6 +97,7 @@ pub const DEFAULT_MAX_CONNECTIONS_WORKER: u32 = 5;
pub const DEFAULT_MAX_CONNECTIONS_INDEXER: u32 = 5;
pub const DEFAULT_HUB_BASE_URL: &str = "https://hub.windmill.dev";
pub const PRIVATE_HUB_MIN_VERSION: i32 = 10_000_000;
pub const SERVICE_LOG_RETENTION_SECS: i64 = 60 * 60 * 24 * 14; // 2 weeks retention period for logs
#[macro_export]
+5 -6
View File
@@ -17,7 +17,7 @@ use crate::{
assets::AssetWithAltAccessType,
error::{to_anyhow, Error},
utils::http_get_from_hub,
DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL,
DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL, PRIVATE_HUB_MIN_VERSION,
};
use crate::worker::HUB_CACHE_DIR;
@@ -585,7 +585,7 @@ pub async fn get_hub_script_by_path(
&& path
.split("/")
.next()
.is_some_and(|x| x.parse::<i32>().is_ok_and(|x| x < 10_000_000))
.is_some_and(|x| x.parse::<i32>().is_ok_and(|x| x < PRIVATE_HUB_MIN_VERSION))
{
tracing::info!(
"Not found on private hub, fallback to default hub for {}",
@@ -670,10 +670,9 @@ async fn get_full_hub_script_by_path_inner(
Ok(response) => Ok(response),
Err(e) => {
if hub_base_url != DEFAULT_HUB_BASE_URL
&& path
.split("/")
.next()
.is_some_and(|x| x.parse::<i32>().is_ok_and(|x| x < 10_000_000))
&& path.split("/").next().is_some_and(|x| {
x.parse::<i32>().is_ok_and(|x| x < PRIVATE_HUB_MIN_VERSION)
})
{
// TODO: should only fallback to default hub if status is 404 (hub returns 500 currently)
tracing::info!(
@@ -14,11 +14,12 @@
import MetadataGen from '$lib/components/copilot/MetadataGen.svelte'
import IconedPath from '$lib/components/IconedPath.svelte'
import { ScriptService, type FlowModuleValue, type PathScript } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { Lock, RefreshCw, Unlock } from 'lucide-svelte'
import { hubBaseUrlStore, workspaceStore } from '$lib/stores'
import { Flag, Lock, RefreshCw, Unlock } from 'lucide-svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { twMerge } from 'tailwind-merge'
import { validateToolName } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
import { DEFAULT_HUB_BASE_URL, PRIVATE_HUB_MIN_VERSION } from '$lib/hub'
interface Props {
flowModuleValue?: FlowModuleValue | undefined
@@ -39,6 +40,14 @@
}: Props = $props()
let latestHash: string | undefined = $state(undefined)
// Extract version_id from hub path (format: hub/{version_id}/{app}/{summary})
let hubVersionId = $derived(
flowModuleValue?.type === 'script' && flowModuleValue.path?.startsWith('hub/')
? flowModuleValue.path.split('/')[1]
: undefined
)
function getCachedKey(path: string) {
return `${$workspaceStore}-${path}`
}
@@ -97,6 +106,26 @@
{:else if flowModuleValue.type === 'script' && 'path' in flowModuleValue && flowModuleValue.path}
<IconedPath path={flowModuleValue.path} hash={flowModuleValue.hash} class="grow" />
{#if hubVersionId}
<Button
title="Report an issue with this hub script"
unifiedSize="sm"
variant="subtle"
on:click={() => {
const targetHubBaseUrl =
Number(hubVersionId) < PRIVATE_HUB_MIN_VERSION
? DEFAULT_HUB_BASE_URL
: $hubBaseUrlStore
window.open(
`${targetHubBaseUrl}/from_version/${hubVersionId}?report-issue=${hubVersionId}`,
'_blank'
)
}}
>
<Flag size={12} />Report issue
</Button>
{/if}
{#if flowModuleValue.hash}
{#if latestHash != flowModuleValue.hash}
<Button
@@ -8,13 +8,19 @@
import { sendUserToast } from '$lib/toast'
import FlowScriptPickerQuick from '../pickers/FlowScriptPickerQuick.svelte'
import { defaultScriptLanguages, processLangs } from '$lib/scripts'
import { defaultScripts, enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import {
defaultScripts,
enterpriseLicense,
hubBaseUrlStore,
userStore,
workspaceStore
} from '$lib/stores'
import type { SupportedLanguage } from '$lib/common'
import { createEventDispatcher, getContext, onDestroy, onMount, untrack } from 'svelte'
import type { FlowBuilderWhitelabelCustomUi } from '$lib/components/custom_ui'
import { type Script, type ScriptLang, type HubScriptKind } from '$lib/gen'
import ListFiltersQuick from '$lib/components/home/ListFiltersQuick.svelte'
import { Folder, User, X } from 'lucide-svelte'
import { ExternalLink, Folder, User, X } from 'lucide-svelte'
import type { FlowEditorContext } from '../../flows/types'
import { fade } from 'svelte/transition'
import { flip } from 'svelte/animate'
@@ -296,6 +302,16 @@
bind:selectedFilter={selected}
resourceType
/>
{#if !selected}
<div class="pl-2 py-1">
<a
href={`${$hubBaseUrlStore}?suggest_integration=true`}
target="_blank"
class="text-2xs flex flex-row items-center gap-1"
>Suggest integration <ExternalLink class="size-3" />
</a>
</div>
{/if}
{/if}
{:else if selectedKind === 'flow'}
{#if owners.length > 0}
@@ -78,13 +78,13 @@
filter.length > 0
? await ScriptService.queryHubScripts({
text: `${filter}`,
limit: 40,
limit: 20,
kind: filterKind,
app: appFilter
})
: ((
await ScriptService.getTopHubScripts({
limit: 40,
limit: 20,
app: appFilter,
kind: filterKind
})
@@ -114,6 +114,20 @@
}
}
async function handlePick(item: (typeof items)[number]) {
if (item.path.startsWith('hub/')) {
try {
await ScriptService.pickHubScriptByPath({ path: item.path })
} catch (error) {
console.error('Failed to track hub script pick:', error)
// Don't block the flow if tracking fails
}
}
// Dispatch the event to continue with the selection
dispatch('pick', item)
}
$effect(() => {
;[filter, kind, appFilter]
untrack(() => applyFilter(filter, kind, appFilter))
@@ -153,7 +167,7 @@
<li class="flex flex-row w-full">
<button
class="p-4 gap-4 flex flex-row grow hover:bg-surface-hover transition-all items-center"
onclick={() => dispatch('pick', item)}
onclick={() => handlePick(item)}
>
<div class="flex items-center gap-4">
<div class="flex justify-center items-center">
@@ -180,7 +194,7 @@
{/each}
</ul>
{/if}
{#if items.length == 40}
{#if items.length == 20}
<div class="text-primary text-xs font-normal py-4">
There are more items than being displayed. Refine your search.
</div>
@@ -41,10 +41,10 @@
import { classNames, createCache } from '$lib/utils'
import { APP_TO_ICON_COMPONENT } from '$lib/components/icons'
import { IntegrationService, ScriptService, type HubScriptKind } from '$lib/gen'
import { Circle } from 'lucide-svelte'
import { Circle, ExternalLink } from 'lucide-svelte'
import Popover from '$lib/components/Popover.svelte'
import { usePromise } from '$lib/svelte5Utils.svelte'
import { userStore } from '$lib/stores'
import { hubBaseUrlStore, userStore } from '$lib/stores'
import { get } from 'svelte/store'
import Button from '$lib/components/common/button/Button.svelte'
@@ -139,6 +139,20 @@
})
})
async function handlePickScript(item: (typeof items)[number]) {
if (item.path.startsWith('hub/')) {
try {
await ScriptService.pickHubScriptByPath({ path: item.path })
} catch (error) {
console.error('Failed to track hub script pick:', error)
// Don't block the flow if tracking fails
}
}
// Dispatch the event to continue with the selection
dispatch('pickScript', item)
}
function onKeyDown(e: KeyboardEvent) {
if (
selected != undefined &&
@@ -149,7 +163,7 @@
) {
e.preventDefault()
let item = items![selected]
dispatch('pickScript', item)
handlePickScript(item)
}
}
$effect(() => {
@@ -189,7 +203,7 @@
variant="subtle"
unifiedSize="sm"
btnClasses="justify-start"
onClick={() => dispatch('pickScript', item)}
onClick={() => handlePickScript(item)}
>
<div class={classNames('flex justify-center items-center')}>
{#if item['app'] in APP_TO_ICON_COMPONENT}
@@ -220,9 +234,18 @@
</li>
{/each}
</ul>
{#if items.length == 40}
{#if items.length == 20}
<div class="text-2xs text-tercary font-extralight text-center py-2 px-3 items-center">
There are more items than being displayed. Refine your search.
</div>
{:else}
<div class="px-2 py-1">
<a
href={`${$hubBaseUrlStore}?suggest_script=true`}
target="_blank"
class="text-xs flex flex-row items-center gap-1 text-blue-500 hover:text-blue-600"
>Suggest script <ExternalLink class="size-3" />
</a>
</div>
{/if}
{/if}
@@ -21,6 +21,7 @@
import hubPaths from '$lib/hubPaths.json'
import type { GitSyncRepository } from './GitSyncContext.svelte'
import GitSyncModeDisplay from './GitSyncModeDisplay.svelte'
import { DEFAULT_HUB_BASE_URL } from '$lib/hub'
let {
idx = null,
@@ -324,12 +325,12 @@
<Alert type="warning" title="Script version mismatch">
The git sync version for this repository is not latest. Current: <a
target="_blank"
href="https://hub.windmill.dev/scripts/windmill/6943/sync-script-to-git-repo-windmill/9014/versions"
href="{DEFAULT_HUB_BASE_URL}/scripts/windmill/6943/sync-script-to-git-repo-windmill/9014/versions"
>{repo.script_path}</a
>, latest:
<a
target="_blank"
href="https://hub.windmill.dev/scripts/windmill/6943/sync-script-to-git-repo-windmill/9014/versions"
href="{DEFAULT_HUB_BASE_URL}/scripts/windmill/6943/sync-script-to-git-repo-windmill/9014/versions"
>{hubPaths.gitSync}</a
>
<div class="flex mt-2">
+3
View File
@@ -8,6 +8,9 @@ import {
SIGNATURE_TEMPLATE_SCRIPT_HUB_PATH
} from './components/triggers/http/utils'
export const DEFAULT_HUB_BASE_URL = 'https://hub.windmill.dev'
export const PRIVATE_HUB_MIN_VERSION = 10_000_000
export function scriptToHubUrl(
content: string,
summary: string,
+2 -1
View File
@@ -11,6 +11,7 @@ import {
} from './gen'
import { getLocalSetting, type StateStore } from './utils'
import { createState } from './svelte5Utils.svelte'
import { DEFAULT_HUB_BASE_URL } from './hub'
export interface UserExt {
email: string
@@ -78,7 +79,7 @@ export const usersWorkspaceStore = writable<UserWorkspaceList | undefined>(undef
export const superadmin = writable<string | false | undefined>(undefined)
export const devopsRole = writable<string | false | undefined>(undefined)
export const lspTokenStore = writable<string | undefined>(undefined)
export const hubBaseUrlStore = writable<string>('https://hub.windmill.dev')
export const hubBaseUrlStore = writable<string>(DEFAULT_HUB_BASE_URL)
export const userWorkspaces: Readable<Array<UserWorkspace>> = derived(
[usersWorkspaceStore, superadmin],
([store, superadmin]) => {
@@ -53,6 +53,7 @@
import { Menubar } from '$lib/components/meltComponents'
import { aiChatManager } from '$lib/components/copilot/chat/AIChatManager.svelte'
import AiChatLayout from '$lib/components/copilot/chat/AiChatLayout.svelte'
import { DEFAULT_HUB_BASE_URL } from '$lib/hub'
interface Props {
children?: import('svelte').Snippet
}
@@ -147,7 +148,7 @@
$hubBaseUrlStore =
((await SettingService.getGlobal({ key: 'hub_accessible_url' })) as string) ||
((await SettingService.getGlobal({ key: 'hub_base_url' })) as string) ||
'https://hub.windmill.dev'
DEFAULT_HUB_BASE_URL
}
async function loadFavorites() {