mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-07 08:02:20 +00:00
feat: hub actions and better search (#7056)
* feat: hub actions and better search * nit
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user