mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
777 lines
20 KiB
Svelte
777 lines
20 KiB
Svelte
<script lang="ts">
|
|
import { onDestroy, onMount, tick, untrack } from 'svelte'
|
|
import {
|
|
AppService,
|
|
FlowService,
|
|
ScriptService,
|
|
type Flow,
|
|
type ListableApp,
|
|
type ListableRawApp,
|
|
type Script,
|
|
type SearchJobsIndexResponse
|
|
} from '$lib/gen'
|
|
import { clickOutside, isMac, scroll_into_view_if_needed_polyfill } from '$lib/utils'
|
|
import {
|
|
AlertTriangle,
|
|
BoxesIcon,
|
|
CalendarIcon,
|
|
Code2Icon,
|
|
Database,
|
|
DollarSignIcon,
|
|
HomeIcon,
|
|
LayoutDashboardIcon,
|
|
MailIcon,
|
|
PlayIcon,
|
|
Route,
|
|
Search,
|
|
SearchCode,
|
|
Unplug,
|
|
WandSparkles
|
|
} from 'lucide-svelte'
|
|
import Portal from '$lib/components/Portal.svelte'
|
|
|
|
import { twMerge } from 'tailwind-merge'
|
|
import ContentSearchInner from '../ContentSearchInner.svelte'
|
|
import { goto } from '$app/navigation'
|
|
import QuickMenuItem from '../search/QuickMenuItem.svelte'
|
|
import { devopsRole, enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
|
import uFuzzy from '@leeoniya/ufuzzy'
|
|
import BarsStaggered from '../icons/BarsStaggered.svelte'
|
|
import { Alert } from '../common'
|
|
import Popover from '../Popover.svelte'
|
|
import Logs from 'lucide-svelte/icons/logs'
|
|
import { AwsIcon, AzureIcon, GoogleCloudIcon, KafkaIcon, MqttIcon, NatsIcon } from '../icons'
|
|
import RunsSearch from './RunsSearch.svelte'
|
|
import AskAiButton from '../copilot/AskAiButton.svelte'
|
|
|
|
let open: boolean = $state(false)
|
|
|
|
let searchTerm: string = $state('')
|
|
let textInput: HTMLInputElement | undefined = $state()
|
|
let selectedWorkspace: string | undefined = $state(undefined)
|
|
let contentSearch: ContentSearchInner | undefined = $state(undefined)
|
|
|
|
const RUNS_PREFIX = '>'
|
|
const LOGS_PREFIX = '!'
|
|
const CONTENT_SEARCH_PREFIX = '#'
|
|
const SWITCH_MODE_PREFIX = '?'
|
|
|
|
type SearchMode = 'default' | 'switch-mode' | 'runs' | 'content' | 'logs'
|
|
|
|
let tab: SearchMode = $state('default')
|
|
|
|
type quickMenuItem = {
|
|
search_id: string
|
|
label: string
|
|
action: () => void
|
|
icon?: any
|
|
shortcutKey?: string
|
|
disabled?: boolean
|
|
}
|
|
let switchModeItems: quickMenuItem[] = [
|
|
{
|
|
search_id: 'switchto:run-search',
|
|
label: 'Search across completed runs' + (!$enterpriseLicense ? '' : ' (EE)'),
|
|
action: () => switchMode('runs'),
|
|
shortcutKey: RUNS_PREFIX,
|
|
icon: Search,
|
|
disabled: false
|
|
},
|
|
{
|
|
search_id: 'switchto:content-search',
|
|
label: 'Search scripts/flows/apps based on content',
|
|
action: () => switchMode('content'),
|
|
shortcutKey: CONTENT_SEARCH_PREFIX,
|
|
icon: SearchCode,
|
|
disabled: false
|
|
}
|
|
]
|
|
|
|
// These items are searchable but do not appear initially on the menu.
|
|
let hiddenMenuItems = [
|
|
{
|
|
search_id: 'nav:http_routes',
|
|
label: 'Go to HTTP routes',
|
|
action: (newtab: boolean = false) => gotoPage('/routes', newtab),
|
|
icon: Route,
|
|
disabled: $userStore?.operator
|
|
},
|
|
{
|
|
search_id: 'nav:web_sockets',
|
|
label: 'Go to WebSockets',
|
|
action: (newtab: boolean = false) => gotoPage('/websocket_triggers', newtab),
|
|
icon: Unplug,
|
|
disabled: $userStore?.operator
|
|
},
|
|
{
|
|
search_id: 'nav:postgres_triggers',
|
|
label: 'Go to Postgres triggers',
|
|
action: (newtab: boolean = false) => gotoPage('/postgres_triggers', newtab),
|
|
icon: Database,
|
|
disabled: $userStore?.operator
|
|
},
|
|
{
|
|
search_id: 'nav:kafka_triggers',
|
|
label: 'Go to Kafka triggers' + (!$enterpriseLicense ? '' : ' (EE)'),
|
|
action: (newtab: boolean = false) => gotoPage('/kafka_triggers', newtab),
|
|
icon: KafkaIcon,
|
|
disabled: $userStore?.operator
|
|
},
|
|
{
|
|
search_id: 'nav:nats_triggers',
|
|
label: 'Go to NATS triggers' + (!$enterpriseLicense ? '' : ' (EE)'),
|
|
action: (newtab: boolean = false) => gotoPage('/nats_triggers', newtab),
|
|
icon: NatsIcon,
|
|
disabled: $userStore?.operator
|
|
},
|
|
{
|
|
search_id: 'nav:sqs_triggers',
|
|
label: 'Go to SQS triggers' + (!$enterpriseLicense ? '' : ' (EE)'),
|
|
action: (newtab: boolean = false) => gotoPage('/sqs_triggers', newtab),
|
|
icon: AwsIcon,
|
|
disabled: $userStore?.operator
|
|
},
|
|
{
|
|
search_id: 'nav:gcp_pub_sub',
|
|
label: 'Go to GCP Pub/Sub' + (!$enterpriseLicense ? '' : ' (EE)'),
|
|
action: (newtab: boolean = false) => gotoPage('/gcp_triggers', newtab),
|
|
icon: GoogleCloudIcon,
|
|
disabled: $userStore?.operator
|
|
},
|
|
{
|
|
search_id: 'nav:azure_event_grid',
|
|
label: 'Go to Azure Event Grid' + (!$enterpriseLicense ? '' : ' (EE)'),
|
|
action: (newtab: boolean = false) => gotoPage('/azure_triggers', newtab),
|
|
icon: AzureIcon,
|
|
disabled: $userStore?.operator
|
|
},
|
|
{
|
|
search_id: 'nav:mqtt_triggers',
|
|
label: 'Go to MQTT triggers',
|
|
action: (newtab: boolean = false) => gotoPage('/mqtt_triggers', newtab),
|
|
icon: MqttIcon,
|
|
disabled: $userStore?.operator
|
|
},
|
|
{
|
|
search_id: 'nav:email_triggers',
|
|
label: 'Go to Email triggers',
|
|
action: (newtab: boolean = false) => gotoPage('/email_triggers', newtab),
|
|
icon: MailIcon,
|
|
disabled: $userStore?.operator
|
|
}
|
|
]
|
|
|
|
let defaultMenuItems: quickMenuItem[] = [
|
|
{
|
|
search_id: 'nav:home',
|
|
label: 'Go to Home',
|
|
action: (newtab: boolean = false) => gotoPage('/', newtab),
|
|
icon: HomeIcon,
|
|
disabled: false
|
|
},
|
|
{
|
|
search_id: 'nav:runs',
|
|
label: 'Go to Runs',
|
|
action: (newtab: boolean = false) => gotoPage('/runs', newtab),
|
|
icon: PlayIcon,
|
|
disabled: false
|
|
},
|
|
{
|
|
search_id: 'nav:variables',
|
|
label: 'Go to Variables',
|
|
action: (newtab: boolean = false) => gotoPage('/variables', newtab),
|
|
icon: DollarSignIcon,
|
|
disabled: false
|
|
},
|
|
{
|
|
search_id: 'nav:resources',
|
|
label: 'Go to Resources',
|
|
action: (newtab: boolean = false) => gotoPage('/resources', newtab),
|
|
icon: BoxesIcon,
|
|
disabled: false
|
|
},
|
|
{
|
|
search_id: 'nav:schedules_triggers',
|
|
label: 'Go to Schedules',
|
|
action: (newtab: boolean = false) => gotoPage('/schedules', newtab),
|
|
icon: CalendarIcon,
|
|
disabled: false
|
|
},
|
|
...switchModeItems,
|
|
{
|
|
search_id: 'nav:service_logs',
|
|
label: 'Explore windmill service logs',
|
|
action: (newtab: boolean = false) => gotoPage('/service_logs', newtab),
|
|
shortcutKey: LOGS_PREFIX,
|
|
icon: Logs,
|
|
disabled: !$devopsRole
|
|
}
|
|
]
|
|
|
|
let defaultMenuItemsWithHidden = [...defaultMenuItems, ...hiddenMenuItems]
|
|
|
|
let itemMap = $state({
|
|
default: defaultMenuItems as any[],
|
|
'switch-mode': switchModeItems,
|
|
runs: [] as any[],
|
|
content: [] as any[],
|
|
logs: [] as any[]
|
|
})
|
|
|
|
$effect(() => {
|
|
tab === 'content' && contentSearch?.open()
|
|
})
|
|
|
|
async function switchPrompt(tab: string) {
|
|
if (tab === 'default') {
|
|
searchTerm = ''
|
|
}
|
|
if (tab === 'runs') {
|
|
searchTerm = RUNS_PREFIX
|
|
}
|
|
if (tab === 'content') {
|
|
searchTerm = CONTENT_SEARCH_PREFIX
|
|
}
|
|
if (tab === 'switch-mode') {
|
|
searchTerm = SWITCH_MODE_PREFIX
|
|
}
|
|
if (tab === 'logs') {
|
|
searchTerm = LOGS_PREFIX
|
|
}
|
|
selectedItem = selectItem(0)
|
|
textInput?.focus()
|
|
}
|
|
|
|
function removePrefix(str: string, prefix: string): string {
|
|
if (str.startsWith(prefix)) {
|
|
return str.substring(prefix.length)
|
|
}
|
|
return str
|
|
}
|
|
|
|
let opts: uFuzzy.Options = {}
|
|
|
|
let uf = new uFuzzy(opts)
|
|
// let defaultMenuItemLabels = defaultMenuItems.map((item) => item.label)
|
|
let defaultMenuItemAndHiddenLabels = defaultMenuItemsWithHidden.map((item) => item.label)
|
|
let switchModeItemLabels = switchModeItems.map((item) => item.label)
|
|
let askAiButton: AskAiButton | undefined = $state()
|
|
|
|
function fuzzyFilter(filter: string, items: any[], itemsPlainText: string[]) {
|
|
if (filter === '') {
|
|
return items
|
|
}
|
|
let idxs = uf.filter(itemsPlainText, filter) ?? []
|
|
|
|
let info: uFuzzy.Info
|
|
// parts is undefined error happens when filter is similar
|
|
// to `.>!` (string with no letters but some symbols)
|
|
try {
|
|
info = uf.info(idxs, itemsPlainText, filter)
|
|
} catch (e) {
|
|
return items
|
|
}
|
|
let order = uf.sort(info, itemsPlainText, filter)
|
|
|
|
let r: any[] = []
|
|
for (let o of order) {
|
|
r.push(items[info.idx[o]])
|
|
}
|
|
return r
|
|
}
|
|
|
|
let queryParseErrors: string[] = $state([])
|
|
|
|
async function handleSearch() {
|
|
queryParseErrors = []
|
|
|
|
if (
|
|
tab !== 'default' &&
|
|
(searchTerm === '' ||
|
|
![RUNS_PREFIX, LOGS_PREFIX, CONTENT_SEARCH_PREFIX, SWITCH_MODE_PREFIX].includes(
|
|
searchTerm[0]
|
|
))
|
|
) {
|
|
_switchMode('default')
|
|
}
|
|
if (tab != 'switch-mode' && searchTerm.length > 0 && searchTerm[0] === SWITCH_MODE_PREFIX) {
|
|
_switchMode('switch-mode')
|
|
}
|
|
if (tab != 'logs' && searchTerm.length > 0 && searchTerm[0] === LOGS_PREFIX) {
|
|
_switchMode('logs')
|
|
}
|
|
if (tab != 'runs' && searchTerm.length > 0 && searchTerm[0] === RUNS_PREFIX) {
|
|
_switchMode('runs')
|
|
}
|
|
if (tab != 'content' && searchTerm.length > 0 && searchTerm[0] === CONTENT_SEARCH_PREFIX) {
|
|
_switchMode('content')
|
|
}
|
|
|
|
if (tab === 'default') {
|
|
if (searchTerm === '') itemMap['default'] = defaultMenuItems
|
|
else
|
|
itemMap['default'] = fuzzyFilter(
|
|
searchTerm,
|
|
defaultMenuItemsWithHidden,
|
|
defaultMenuItemAndHiddenLabels
|
|
)
|
|
if (combinedItems) {
|
|
itemMap['default'] = itemMap['default'].concat(
|
|
fuzzyFilter(
|
|
searchTerm,
|
|
combinedItems,
|
|
combinedItems.map((i) => `${i.path} ${i.summary}`)
|
|
)
|
|
)
|
|
}
|
|
itemMap['default'] = itemMap['default'].filter((e) => !e.disabled)
|
|
}
|
|
if (tab === 'switch-mode') {
|
|
itemMap['switch-mode'] = fuzzyFilter(
|
|
removePrefix(searchTerm, SWITCH_MODE_PREFIX),
|
|
switchModeItems,
|
|
switchModeItemLabels
|
|
)
|
|
}
|
|
if (tab === 'runs') {
|
|
await tick()
|
|
runsSearch?.handleRunSearch(removePrefix(searchTerm, RUNS_PREFIX))
|
|
}
|
|
selectedItem = selectItem(0)
|
|
}
|
|
|
|
function selectItem(index: number) {
|
|
if (!itemMap[tab] || itemMap[tab].length <= index) {
|
|
return undefined
|
|
}
|
|
onHover(itemMap[tab][index])
|
|
return itemMap[tab][index]
|
|
}
|
|
|
|
let selectedItem: any = $state()
|
|
|
|
async function handleKeydown(event: KeyboardEvent) {
|
|
if ((!isMac() ? event.ctrlKey : event.metaKey) && event.key === 'k') {
|
|
event.preventDefault()
|
|
await openModal()
|
|
}
|
|
if (open) {
|
|
if (event.key === 'Escape') {
|
|
event.preventDefault()
|
|
if (searchTerm.length != 0 || tab != 'default') {
|
|
switchMode('default')
|
|
textInput?.focus()
|
|
} else {
|
|
open = false
|
|
}
|
|
}
|
|
if (tab != 'logs') {
|
|
if (event.key === 'ArrowDown') {
|
|
event.preventDefault()
|
|
let idx = itemMap[tab].indexOf(selectedItem)
|
|
if (idx != -1) {
|
|
idx = (idx + 1) % itemMap[tab].length
|
|
selectedItem = selectItem(idx)
|
|
let el = document.getElementById(selectedItem.search_id)
|
|
if (el) scroll_into_view_if_needed_polyfill(el, false)
|
|
}
|
|
} else if (event.key === 'ArrowUp') {
|
|
event.preventDefault()
|
|
let idx = itemMap[tab].indexOf(selectedItem)
|
|
if (idx != -1) {
|
|
idx = (idx - 1 + itemMap[tab].length) % itemMap[tab].length
|
|
selectedItem = selectItem(idx)
|
|
let el = document.getElementById(selectedItem.search_id)
|
|
if (el) scroll_into_view_if_needed_polyfill(el, false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//internal, should not be called outside of the handleSearch function
|
|
function _switchMode(mode: SearchMode) {
|
|
selectedItem = undefined
|
|
tab = mode
|
|
}
|
|
// Used by callbacks, call this to change the mode
|
|
function switchMode(mode: SearchMode) {
|
|
switchPrompt(mode)
|
|
textInput?.focus()
|
|
}
|
|
|
|
function gotoWindmillItemPage(e: TableAny, newtab: boolean = false) {
|
|
let path: string
|
|
switch (e.type) {
|
|
case 'flow':
|
|
path = `/flows/get/${e.path}`
|
|
break
|
|
case 'script':
|
|
path = `/scripts/get/${e.path}`
|
|
break
|
|
case 'app':
|
|
path = `/apps/get/${e.path}`
|
|
break
|
|
case 'raw_app':
|
|
path = `/raw_apps/get/${e.path}`
|
|
break
|
|
default:
|
|
path = '/'
|
|
}
|
|
gotoPage(path, newtab)
|
|
}
|
|
|
|
function gotoPage(path: string, newtab: boolean = false) {
|
|
searchTerm = ''
|
|
if (!newtab) {
|
|
open = false
|
|
goto(path)
|
|
} else {
|
|
window.open(path, '_blank')
|
|
}
|
|
}
|
|
|
|
let mouseMoved: boolean = $state(false)
|
|
function handleMouseMove() {
|
|
mouseMoved = true
|
|
}
|
|
|
|
onMount(() => {
|
|
window.addEventListener('keydown', handleKeydown)
|
|
window.addEventListener('mousemove', handleMouseMove)
|
|
})
|
|
|
|
onDestroy(() => {
|
|
window.removeEventListener('keydown', handleKeydown)
|
|
window.removeEventListener('mousemove', handleMouseMove)
|
|
})
|
|
|
|
$effect(() => {
|
|
searchTerm
|
|
untrack(() => handleSearch())
|
|
})
|
|
|
|
function placeholderFromPrefix(text: string): string {
|
|
switch (text) {
|
|
case '':
|
|
return ' Search or type `?` for search options'
|
|
case RUNS_PREFIX:
|
|
return ' Search across completed runs'
|
|
case LOGS_PREFIX:
|
|
return ' Search across completed runs'
|
|
case CONTENT_SEARCH_PREFIX:
|
|
return ' Search flows/scripts/apps by content'
|
|
default:
|
|
return ''
|
|
}
|
|
}
|
|
|
|
type TableItem<T, U extends 'script' | 'flow' | 'app' | 'raw_app'> = T & {
|
|
search_id: string
|
|
marked?: string
|
|
type?: U
|
|
time?: number
|
|
starred?: boolean
|
|
}
|
|
|
|
// interface SelectableSearchMenuItem {
|
|
// search_id: string
|
|
// }
|
|
|
|
type TableScript = TableItem<Script, 'script'>
|
|
type TableFlow = TableItem<Flow, 'flow'>
|
|
type TableApp = TableItem<ListableApp, 'app'>
|
|
type TableRawApp = TableItem<ListableRawApp, 'raw_app'>
|
|
|
|
type TableAny = TableScript | TableFlow | TableApp | TableRawApp
|
|
|
|
let combinedItems: TableAny[] | undefined = $state(undefined)
|
|
|
|
async function fetchCombinedItems() {
|
|
const scripts = await ScriptService.listScripts({
|
|
workspace: $workspaceStore!,
|
|
withoutDescription: true
|
|
})
|
|
const flows = await FlowService.listFlows({
|
|
workspace: $workspaceStore!,
|
|
withoutDescription: true
|
|
})
|
|
const apps = await AppService.listApps({ workspace: $workspaceStore! })
|
|
|
|
let combinedItems: (TableScript | TableFlow | TableApp | TableRawApp)[] | undefined = [
|
|
...flows.map((x) => ({
|
|
...x,
|
|
type: 'flow' as 'flow',
|
|
time: new Date(x.edited_at).getTime(),
|
|
search_id: x.path
|
|
})),
|
|
...scripts.map((x) => ({
|
|
...x,
|
|
type: 'script' as 'script',
|
|
time: new Date(x.created_at).getTime(),
|
|
search_id: x.path
|
|
})),
|
|
...apps.map((x) => ({
|
|
...x,
|
|
type: 'app' as 'app',
|
|
time: new Date(x.edited_at).getTime(),
|
|
search_id: x.path
|
|
}))
|
|
].sort((a, b) => (a.starred != b.starred ? (a.starred ? -1 : 1) : a.time - b.time > 0 ? -1 : 1))
|
|
|
|
return combinedItems
|
|
}
|
|
|
|
function iconForWindmillItem(type: string) {
|
|
switch (type) {
|
|
case 'flow':
|
|
return BarsStaggered
|
|
case 'script':
|
|
return Code2Icon
|
|
case 'app':
|
|
return LayoutDashboardIcon
|
|
case 'raw_app':
|
|
return LayoutDashboardIcon
|
|
}
|
|
}
|
|
|
|
function onHover(selectedItem: any) {
|
|
if (tab === 'runs') {
|
|
selectedWorkspace = selectedItem?.document?.workspace_id[0]
|
|
}
|
|
}
|
|
|
|
export async function openSearchWithPrefilledText(text?: string) {
|
|
await openModal()
|
|
searchTerm = text ?? searchTerm
|
|
await handleSearch()
|
|
}
|
|
|
|
async function focusTextInput() {
|
|
await tick()
|
|
|
|
textInput?.focus()
|
|
textInput?.select()
|
|
|
|
if (open) {
|
|
if (combinedItems == undefined) {
|
|
combinedItems = await fetchCombinedItems()
|
|
handleSearch()
|
|
}
|
|
selectedItem = selectItem(0)
|
|
}
|
|
}
|
|
|
|
async function openModal() {
|
|
open = !open
|
|
focusTextInput()
|
|
}
|
|
|
|
function closeModal() {
|
|
open = false
|
|
}
|
|
|
|
function maxModalWidth(tab: SearchMode) {
|
|
if (tab === 'runs') {
|
|
return 'max-w-7xl'
|
|
} else {
|
|
return 'max-w-4xl'
|
|
}
|
|
}
|
|
|
|
function maxModalHeight(tab: SearchMode) {
|
|
if (tab === 'runs') {
|
|
return ''
|
|
} else if (tab === 'content') {
|
|
return 'max-h-[70vh]'
|
|
} else {
|
|
return 'max-h-[60vh]'
|
|
}
|
|
}
|
|
|
|
let runsSearch: RunsSearch | undefined = $state()
|
|
let runSearchRemainingCount: number | undefined = $state(undefined)
|
|
let runSearchTotalCount: number | undefined = $state(undefined)
|
|
let indexMetadata: SearchJobsIndexResponse['index_metadata'] = $state(undefined)
|
|
</script>
|
|
|
|
{#if open}
|
|
<Portal name="global-search">
|
|
<div
|
|
class={twMerge(
|
|
`fixed top-0 bottom-0 left-0 right-0 transition-all duration-50 flex items-start justify-center`,
|
|
' bg-black bg-opacity-40',
|
|
'z-[1100]'
|
|
)}
|
|
>
|
|
<div
|
|
class="{maxModalWidth(tab)} w-full mt-36 bg-surface rounded-lg relative"
|
|
use:clickOutside={{
|
|
capture: false,
|
|
onClickOutside: () => {
|
|
open = false
|
|
}
|
|
}}
|
|
>
|
|
<div class="px-4 py-2 flex flex-row gap-1 items-center border-b">
|
|
<Search size="16" />
|
|
<div class="relative inline-block flex-1">
|
|
<input
|
|
id="quickSearchInput"
|
|
bind:this={textInput}
|
|
type="text"
|
|
class="no-default-style !bg-transparent !border-none w-full !ring-0 !text-sm"
|
|
bind:value={searchTerm}
|
|
autocomplete="off"
|
|
/>
|
|
<label
|
|
for="quickSearchInput"
|
|
class="absolute top-1/2 left-2 transform -translate-y-1/2 pointer-events-none text-gray-400 transition-all duration-200 whitespace-pre text-xs"
|
|
>
|
|
{placeholderFromPrefix(searchTerm)}
|
|
</label>
|
|
</div>
|
|
{#if (itemMap[tab] ?? []).length === 0 && searchTerm.length > 0}
|
|
<AskAiButton
|
|
bind:this={askAiButton}
|
|
label="Ask AI"
|
|
initialInput={searchTerm}
|
|
onClick={() => {
|
|
closeModal()
|
|
}}
|
|
/>
|
|
{/if}
|
|
{#if queryParseErrors.length > 0}
|
|
<Popover notClickable placement="bottom-start">
|
|
<AlertTriangle size={16} class="text-yellow-500" />
|
|
{#snippet text()}
|
|
Some of your search terms have been ignored because one or more parse errors:<br
|
|
/><br />
|
|
<ul>
|
|
{#each queryParseErrors as msg}
|
|
<li>- {msg}</li>
|
|
{/each}
|
|
</ul>
|
|
{/snippet}
|
|
</Popover>
|
|
{/if}
|
|
</div>
|
|
<div class="overflow-y-auto relative {maxModalHeight(tab)}">
|
|
{#if tab === 'default' || tab === 'switch-mode'}
|
|
{@const items = (itemMap[tab] ?? []).filter((e) =>
|
|
defaultMenuItemsWithHidden.some((x) => e.search_id === x.search_id)
|
|
)}
|
|
{#if items.length > 0}
|
|
<div
|
|
class={tab === 'switch-mode' || itemMap[tab].length === items.length
|
|
? 'p-2'
|
|
: 'p-2 border-b'}
|
|
>
|
|
{#each items as el}
|
|
<QuickMenuItem
|
|
onselect={(shift) => el?.action(shift)}
|
|
onhover={() => (selectedItem = el)}
|
|
id={el?.search_id}
|
|
hovered={el?.search_id === selectedItem?.search_id}
|
|
label={el?.label}
|
|
icon={el?.icon}
|
|
shortcutKey={el?.shortcutKey}
|
|
bind:mouseMoved
|
|
/>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
|
|
{#if tab === 'default'}
|
|
{#if (itemMap[tab] ?? []).filter((e) => (combinedItems ?? []).includes(e)).length > 0}
|
|
<div class="p-2">
|
|
<div class="py-2 px-1 text-xs font-semibold text-primary"> Flows/Scripts/Apps </div>
|
|
{#each (itemMap[tab] ?? []).filter((e) => (combinedItems ?? []).includes(e)) as el}
|
|
<QuickMenuItem
|
|
onselect={(shift) => {
|
|
gotoWindmillItemPage(el, shift)
|
|
}}
|
|
onhover={() => (selectedItem = el)}
|
|
id={el?.search_id}
|
|
hovered={el?.path === selectedItem?.path}
|
|
label={(el.summary ? `${el.summary} - ` : '') +
|
|
el.path +
|
|
(el.starred ? ' ★' : '')}
|
|
icon={iconForWindmillItem(el.type)}
|
|
bind:mouseMoved
|
|
/>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
|
|
{#if (itemMap[tab] ?? []).length === 0}
|
|
<div class="p-2">
|
|
<QuickMenuItem
|
|
onselect={() => {
|
|
askAiButton?.onClick()
|
|
}}
|
|
id={'ai:no-results-ask-ai'}
|
|
hovered={true}
|
|
label={`Try asking \`${searchTerm}\` to AI`}
|
|
icon={WandSparkles}
|
|
bind:mouseMoved
|
|
/>
|
|
<div class="flex w-full justify-center items-center">
|
|
<div class="text-primary text-center">
|
|
<div class="pt-1 text-sm">Tip: press `esc` to quickly clear the search bar</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{:else if tab === 'content'}
|
|
<ContentSearchInner
|
|
search={removePrefix(searchTerm, '#')}
|
|
bind:this={contentSearch}
|
|
on:close={() => {
|
|
closeModal()
|
|
}}
|
|
/>
|
|
{:else if tab === 'logs'}
|
|
<div class="p-2">
|
|
{#if !$devopsRole}
|
|
<Alert title="Service logs are only available to superadmins" type="warning">
|
|
Service logs are only available to superadmins
|
|
</Alert>
|
|
{:else}
|
|
<QuickMenuItem
|
|
onselect={() =>
|
|
gotoPage(
|
|
`/service_logs?query=${encodeURIComponent(removePrefix(searchTerm, '!'))}`
|
|
)}
|
|
id="goto_service_logs_search"
|
|
hovered={true}
|
|
label={searchTerm === '!'
|
|
? 'Explore Windmill service logs'
|
|
: `Search '${removePrefix(searchTerm, '!')}' in Windmill's service logs`}
|
|
icon={searchTerm === '!' ? Logs : Search}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
{:else if tab === 'runs'}
|
|
<RunsSearch
|
|
bind:queryParseErrors
|
|
bind:this={runsSearch}
|
|
bind:selectedItem
|
|
bind:selectedWorkspace
|
|
bind:mouseMoved
|
|
bind:loadedRuns={itemMap['runs']}
|
|
bind:open
|
|
{selectItem}
|
|
searchTerm={removePrefix(searchTerm, RUNS_PREFIX)}
|
|
bind:runSearchRemainingCount
|
|
bind:runSearchTotalCount
|
|
bind:indexMetadata
|
|
/>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Portal>
|
|
{/if}
|