mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 16:00:38 +00:00
feat: Full-text search on runs using tantivy and command palette for quick actions (#4046)
* Add indexer crate and files * POC searcher incomplete schema only indexes at startup * POC search component frontend * Demo of the frontend element * add Results and Args as text * minimal functionality * Make jump to scripts by name also flows and apps * Add button on sidebar to open search * Update lock on indexer after merge * Make arrow key navigation compatible with scrol * Show empty result screen and log as a coming feat * Add summary to script searchable items * Catch `parts is undefined` error (uFuzzy) * Index refreshing using tokio interval * Fix JobLoader workspace being wrongly defined * Fix click outside * Add debouncing for completed run search * Binary mode working + job index tracker * Warning for no license + fix height scrollbars on content search * Make it compile without EE files * remove panic to use errors * Move global search * Cleanup UI, no more tab switcher but clear placeholders and actions * Add tantivy feature flag for windmill-api * Rework indexer mode * Mac compatibility for shortcut * Update test for new run_server * Prepare sqlx * Mac compatibility * Fix openapi yaml * Fix frontend * Frontend api fix * Update docker-compose.yml and caddyfile With the (by default deactivated) container and reverse proxy to use the windmill indexer * fix feature flag for tests * fix feature falg for running tests * fix feature flag for running tests * Make content search use search modal instead * Add tantivy feature to ee build steps * Remove old Content search * change volume location for indexer * Update dependencies * Prepare sqlx * Uncomment line on docker compose * Add line between input and results * Update ee repo ref
This commit is contained in:
@@ -135,7 +135,7 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
build-args: |
|
||||
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc
|
||||
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
|
||||
${{ steps.meta-ee-public.outputs.tags }}
|
||||
@@ -198,7 +198,7 @@ jobs:
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
build-args: |
|
||||
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc
|
||||
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy
|
||||
PYTHON_IMAGE=python:3.12.2-slim-bookworm
|
||||
tags: |
|
||||
${{ steps.meta-ee-public-py312.outputs.tags }}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
bind {$ADDRESS}
|
||||
reverse_proxy /ws/* http://lsp:3001
|
||||
# reverse_proxy /ws_mp/* http://multiplayer:3002
|
||||
# reverse_proxy /api/srch/* http://windmill_indexer:8001
|
||||
reverse_proxy /* http://windmill_server:8000
|
||||
# tls /certs/cert.pem /certs/key.pem
|
||||
}
|
||||
|
||||
+2
-1
@@ -4,4 +4,5 @@ oauth.json
|
||||
oauth2.json
|
||||
windmill-api/openapi-deref.yaml
|
||||
tracing.folded
|
||||
heaptrack*
|
||||
heaptrack*
|
||||
index/
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id FROM queue WHERE created_at <= $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "8455e77a6a87bd0b8dbe55c854abfb3938cb9a440e9d53323a93ec9473417890"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT MAX(created_at) FROM completed_job",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "max",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "919c9432f36a6212ba79b50fbf887589bf7c37d3557cd831787b9b16b3665e47"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT CONCAT(coalesce(completed_job.logs, ''), coalesce(job_logs.logs, '')) as logs FROM completed_job\n LEFT JOIN job_logs ON job_logs.job_id = completed_job.id\n WHERE completed_job.id = $1 AND completed_job.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "logs",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "f036e930beff03e8a3f1f91c32267910c7d050f514dfba76991b644b86b914b9"
|
||||
}
|
||||
Generated
+332
-1
@@ -175,6 +175,12 @@ version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||
|
||||
[[package]]
|
||||
name = "archiver-rs"
|
||||
version = "0.5.1"
|
||||
@@ -1304,6 +1310,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitpacking"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c1d3e2bfd8d06048a179f7b17afc3188effa10385e7b00dc65af6aae732ea92"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
version = "1.0.1"
|
||||
@@ -1619,6 +1634,12 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "census"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0"
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
@@ -2988,6 +3009,12 @@ version = "0.15.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "downcast-rs"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||
|
||||
[[package]]
|
||||
name = "dprint-swc-ext"
|
||||
version = "0.16.0"
|
||||
@@ -3209,6 +3236,12 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
|
||||
|
||||
[[package]]
|
||||
name = "fastdivide"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59668941c55e5c186b8b58c391629af56774ec768f73c08bbcd56f09348eb00b"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.9.0"
|
||||
@@ -3389,6 +3422,16 @@ dependencies = [
|
||||
"syn 2.0.70",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs4"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8"
|
||||
dependencies = [
|
||||
"rustix 0.38.34",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fslock"
|
||||
version = "0.2.1"
|
||||
@@ -3676,6 +3719,20 @@ dependencies = [
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "186014d53bc231d0090ef8d6f03e0920c54d85a5ed22f4f2f74315ec56cf83fb"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -4010,6 +4067,12 @@ dependencies = [
|
||||
"triomphe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "htmlescape"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.12"
|
||||
@@ -4244,7 +4307,7 @@ dependencies = [
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
"windows-core 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4531,6 +4594,12 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "levenshtein_automata"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25"
|
||||
|
||||
[[package]]
|
||||
name = "lexical-core"
|
||||
version = "0.8.5"
|
||||
@@ -4720,6 +4789,20 @@ dependencies = [
|
||||
"prost-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"generator",
|
||||
"pin-utils",
|
||||
"scoped-tls",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.3"
|
||||
@@ -4929,6 +5012,16 @@ dependencies = [
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "measure_time"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbefd235b0aadd181626f281e1d684e116972988c14c264e42069d5e8a5775cc"
|
||||
dependencies = [
|
||||
"instant",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
@@ -5035,6 +5128,12 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "murmurhash32"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b"
|
||||
|
||||
[[package]]
|
||||
name = "mysql-common-derive"
|
||||
version = "0.31.1"
|
||||
@@ -5361,6 +5460,15 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "oneshot"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071d1cf3298ad8e543dca18217d198cb6a3884443d204757b9624b935ef09fa0"
|
||||
dependencies = [
|
||||
"loom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "onig"
|
||||
version = "6.4.0"
|
||||
@@ -5509,6 +5617,15 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "ownedbytes"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3a059efb063b8f425b948e042e6b9bd85edfe60e913630ed727b23e2dfcc558"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.13.2"
|
||||
@@ -6881,6 +6998,16 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-stemmers"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_decimal"
|
||||
version = "1.35.0"
|
||||
@@ -7665,6 +7792,15 @@ version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||
|
||||
[[package]]
|
||||
name = "sketches-ddsketch"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.9"
|
||||
@@ -8644,6 +8780,147 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tantivy"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8d0582f186c0a6d55655d24543f15e43607299425c5ad8352c242b914b31856"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"arc-swap",
|
||||
"base64 0.22.1",
|
||||
"bitpacking",
|
||||
"byteorder",
|
||||
"census",
|
||||
"crc32fast",
|
||||
"crossbeam-channel",
|
||||
"downcast-rs",
|
||||
"fastdivide",
|
||||
"fnv",
|
||||
"fs4",
|
||||
"htmlescape",
|
||||
"itertools 0.12.1",
|
||||
"levenshtein_automata",
|
||||
"log",
|
||||
"lru",
|
||||
"lz4_flex",
|
||||
"measure_time",
|
||||
"memmap2",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"oneshot",
|
||||
"rayon",
|
||||
"regex",
|
||||
"rust-stemmers",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sketches-ddsketch",
|
||||
"smallvec",
|
||||
"tantivy-bitpacker",
|
||||
"tantivy-columnar",
|
||||
"tantivy-common",
|
||||
"tantivy-fst",
|
||||
"tantivy-query-grammar",
|
||||
"tantivy-stacker",
|
||||
"tantivy-tokenizer-api",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"time",
|
||||
"uuid 1.10.0",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-bitpacker"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "284899c2325d6832203ac6ff5891b297fc5239c3dc754c5bc1977855b23c10df"
|
||||
dependencies = [
|
||||
"bitpacking",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-columnar"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12722224ffbe346c7fec3275c699e508fd0d4710e629e933d5736ec524a1f44e"
|
||||
dependencies = [
|
||||
"downcast-rs",
|
||||
"fastdivide",
|
||||
"itertools 0.12.1",
|
||||
"serde",
|
||||
"tantivy-bitpacker",
|
||||
"tantivy-common",
|
||||
"tantivy-sstable",
|
||||
"tantivy-stacker",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-common"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8019e3cabcfd20a1380b491e13ff42f57bb38bf97c3d5fa5c07e50816e0621f4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"byteorder",
|
||||
"ownedbytes",
|
||||
"serde",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-fst"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"regex-syntax 0.8.4",
|
||||
"utf8-ranges",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-query-grammar"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "847434d4af57b32e309f4ab1b4f1707a6c566656264caa427ff4285c4d9d0b82"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-sstable"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c69578242e8e9fc989119f522ba5b49a38ac20f576fc778035b96cc94f41f98e"
|
||||
dependencies = [
|
||||
"tantivy-bitpacker",
|
||||
"tantivy-common",
|
||||
"tantivy-fst",
|
||||
"zstd 0.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-stacker"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c56d6ff5591fc332739b3ce7035b57995a3ce29a93ffd6012660e0949c956ea8"
|
||||
dependencies = [
|
||||
"murmurhash32",
|
||||
"rand_distr",
|
||||
"tantivy-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-tokenizer-api"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a0dcade25819a89cfe6f17d932c9cedff11989936bf6dd4f336d50392053b04"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tap"
|
||||
version = "1.0.1"
|
||||
@@ -9696,6 +9973,12 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8-ranges"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
@@ -10077,6 +10360,7 @@ dependencies = [
|
||||
"windmill-api-client",
|
||||
"windmill-common",
|
||||
"windmill-git-sync",
|
||||
"windmill-indexer",
|
||||
"windmill-queue",
|
||||
"windmill-worker",
|
||||
]
|
||||
@@ -10153,6 +10437,7 @@ dependencies = [
|
||||
"windmill-audit",
|
||||
"windmill-common",
|
||||
"windmill-git-sync",
|
||||
"windmill-indexer",
|
||||
"windmill-parser",
|
||||
"windmill-parser-py-imports",
|
||||
"windmill-parser-ts",
|
||||
@@ -10244,6 +10529,23 @@ dependencies = [
|
||||
"windmill-queue",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.360.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"futures",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tantivy",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uuid 1.10.0",
|
||||
"windmill-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.361.1"
|
||||
@@ -10494,6 +10796,16 @@ dependencies = [
|
||||
"zstd 0.12.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.54.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
|
||||
dependencies = [
|
||||
"windows-core 0.54.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
@@ -10503,6 +10815,25 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.54.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
|
||||
+8
-1
@@ -13,6 +13,7 @@ members = [
|
||||
"./windmill-common",
|
||||
"./windmill-audit",
|
||||
"./windmill-git-sync",
|
||||
"./windmill-indexer",
|
||||
"./parsers/windmill-parser",
|
||||
"./parsers/windmill-parser-ts",
|
||||
"./parsers/windmill-parser-wasm",
|
||||
@@ -52,6 +53,7 @@ flow_testing = ["windmill-worker/flow_testing"]
|
||||
openidconnect = ["windmill-api/openidconnect"]
|
||||
cloud = ["windmill-queue/cloud", "windmill-worker/cloud"]
|
||||
jemalloc = ["windmill-common/jemalloc", "dep:tikv-jemallocator", "dep:tikv-jemalloc-sys", "dep:tikv-jemalloc-ctl"]
|
||||
tantivy = ["windmill-indexer/tantivy"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
@@ -62,6 +64,7 @@ windmill-common = { workspace = true, default-features = false }
|
||||
windmill-git-sync.workspace = true
|
||||
windmill-api = { workspace = true, default-features = false }
|
||||
windmill-worker.workspace = true
|
||||
windmill-indexer.workspace = true
|
||||
futures.workspace = true
|
||||
tracing.workspace = true
|
||||
sqlx.workspace = true
|
||||
@@ -80,7 +83,8 @@ gethostname.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde.workspace = true
|
||||
deno_core.workspace = true
|
||||
pg-embed = {git = "https://github.com/faokunega/pg-embed", optional = true, default-features = false, features = ['rt_tokio']}
|
||||
pg-embed = {git = "https://github.com/faokunega/pg-embed", optional = true, default-features = false, features = ['rt_tokio']}
|
||||
|
||||
|
||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||
tikv-jemallocator = { optional = true, workspace = true }
|
||||
@@ -103,6 +107,7 @@ windmill-worker = { path = "./windmill-worker" }
|
||||
windmill-common = { path = "./windmill-common", default-features = false }
|
||||
windmill-audit = { path = "./windmill-audit" }
|
||||
windmill-git-sync = { path = "./windmill-git-sync" }
|
||||
windmill-indexer = {path = "./windmill-indexer"}
|
||||
windmill-parser = { path = "./parsers/windmill-parser" }
|
||||
windmill-parser-ts = { path = "./parsers/windmill-parser-ts" }
|
||||
windmill-parser-py = { path = "./parsers/windmill-parser-py" }
|
||||
@@ -254,3 +259,5 @@ tikv-jemalloc-ctl = { version = "^0.5" }
|
||||
|
||||
# 0.1.12 broken (nested dependency of swc_common)
|
||||
triomphe = "<0.1.12"
|
||||
|
||||
tantivy = "0.22.0"
|
||||
|
||||
@@ -1 +1 @@
|
||||
d414242f92cfaf9f364e9aeb321a511c3ad46fa7
|
||||
49714fc57e3fc8e716f080ecf99fc71bc8e8083e
|
||||
|
||||
+44
-3
@@ -174,6 +174,15 @@ async fn windmill_main() -> anyhow::Result<()> {
|
||||
}
|
||||
#[cfg(feature = "enterprise")]
|
||||
Mode::Agent
|
||||
} else if &x == "indexer" {
|
||||
tracing::info!("Binary is in 'indexer' mode");
|
||||
#[cfg(not(feature = "tantivy"))]
|
||||
{
|
||||
panic!("Indexer mode requires the tantivy feature flag");
|
||||
}
|
||||
|
||||
#[cfg(feature = "tantivy")]
|
||||
Mode::Indexer
|
||||
} else {
|
||||
if &x != "standalone" {
|
||||
tracing::error!("mode not recognized, defaulting to standalone: {x}");
|
||||
@@ -188,7 +197,7 @@ async fn windmill_main() -> anyhow::Result<()> {
|
||||
Mode::Standalone
|
||||
});
|
||||
|
||||
let num_workers = if mode == Mode::Server {
|
||||
let num_workers = if mode == Mode::Server || mode == Mode::Indexer {
|
||||
0
|
||||
} else {
|
||||
std::env::var("NUM_WORKERS")
|
||||
@@ -207,7 +216,7 @@ async fn windmill_main() -> anyhow::Result<()> {
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<bool>().ok())
|
||||
.unwrap_or(false)
|
||||
&& (mode == Mode::Server || mode == Mode::Standalone);
|
||||
&& (mode == Mode::Server || mode == Mode::Standalone || mode == Mode::Indexer);
|
||||
|
||||
let server_bind_address: IpAddr = if server_mode {
|
||||
std::env::var("SERVER_BIND_ADDR")
|
||||
@@ -346,11 +355,36 @@ Windmill Community Edition {GIT_VERSION}
|
||||
.await
|
||||
.expect("could not create initial server dir");
|
||||
|
||||
#[cfg(feature = "tantivy")]
|
||||
let should_index_jobs = mode == Mode::Indexer || mode == Mode::Standalone;
|
||||
|
||||
#[cfg(not(feature = "tantivy"))]
|
||||
let should_index_jobs = false;
|
||||
|
||||
let (index_reader, index_writer) = if should_index_jobs {
|
||||
let (r, w) = windmill_indexer::indexer_ee::init_index()?;
|
||||
(Some(r), Some(w))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let indexer_rx = killpill_rx.resubscribe();
|
||||
let index_writer2 = index_writer.clone();
|
||||
let indexer_f = async {
|
||||
if let Some(index_writer) = index_writer2 {
|
||||
windmill_indexer::indexer_ee::run_indexer(db.clone(), index_writer, indexer_rx)
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let server_f = async {
|
||||
if !is_agent {
|
||||
windmill_api::run_server(
|
||||
db.clone(),
|
||||
rsmq2,
|
||||
index_reader,
|
||||
index_writer,
|
||||
addr,
|
||||
server_killpill_rx,
|
||||
base_internal_tx,
|
||||
@@ -597,7 +631,14 @@ Windmill Community Edition {GIT_VERSION}
|
||||
schedule_key_renewal(&HTTP_CLIENT, &db).await;
|
||||
}
|
||||
|
||||
futures::try_join!(shutdown_signal, workers_f, monitor_f, server_f, metrics_f)?;
|
||||
futures::try_join!(
|
||||
shutdown_signal,
|
||||
workers_f,
|
||||
monitor_f,
|
||||
server_f,
|
||||
metrics_f,
|
||||
indexer_f
|
||||
)?;
|
||||
} else {
|
||||
tracing::info!("Nothing to do, exiting.");
|
||||
}
|
||||
|
||||
@@ -122,6 +122,8 @@ impl ApiServer {
|
||||
let task = tokio::task::spawn(windmill_api::run_server(
|
||||
db.clone(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
addr,
|
||||
rx,
|
||||
port_tx,
|
||||
|
||||
@@ -27,6 +27,7 @@ windmill-parser.workspace = true
|
||||
windmill-parser-py-imports.workspace = true
|
||||
windmill-parser-ts.workspace = true
|
||||
windmill-git-sync.workspace = true
|
||||
windmill-indexer.workspace = true
|
||||
tokio.workspace = true
|
||||
anyhow.workspace = true
|
||||
argon2.workspace = true
|
||||
@@ -92,4 +93,4 @@ pin-project.workspace = true
|
||||
crc.workspace = true
|
||||
http.workspace = true
|
||||
async-stream.workspace = true
|
||||
ulid.workspace = true
|
||||
ulid.workspace = true
|
||||
|
||||
@@ -8552,6 +8552,42 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ExtendedJobs"
|
||||
|
||||
/srch/w/{workspace}/index/search/job:
|
||||
get:
|
||||
summary: Search through jobs with a string query
|
||||
operationId: searchJobsIndex
|
||||
tags:
|
||||
- indexSearch
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: search_query
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: search results
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
query_parse_errors:
|
||||
description: a list of the terms that couldn't be parsed (and thus ignored)
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
dancer:
|
||||
type: string
|
||||
hits:
|
||||
description: the jobs that matched the query
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/JobSearchHit"
|
||||
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
@@ -11085,3 +11121,9 @@ components:
|
||||
required:
|
||||
- jobs
|
||||
- obscured_jobs
|
||||
|
||||
JobSearchHit:
|
||||
type: object
|
||||
properties:
|
||||
dancer:
|
||||
type: string
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
use axum::Router;
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new()
|
||||
}
|
||||
@@ -57,6 +57,7 @@ mod flows;
|
||||
mod folders;
|
||||
mod granular_acls;
|
||||
mod groups;
|
||||
mod indexer_ee;
|
||||
mod inputs;
|
||||
mod integration;
|
||||
#[cfg(feature = "parquet")]
|
||||
@@ -143,6 +144,8 @@ pub async fn add_webhook_allowed_origin(
|
||||
pub async fn run_server(
|
||||
db: DB,
|
||||
rsmq: Option<rsmq_async::MultiplexedRsmq>,
|
||||
index_reader: Option<windmill_indexer::indexer_ee::IndexReader>,
|
||||
index_writer: Option<windmill_indexer::indexer_ee::IndexWriter>,
|
||||
addr: SocketAddr,
|
||||
mut rx: tokio::sync::broadcast::Receiver<()>,
|
||||
port_tx: tokio::sync::oneshot::Sender<String>,
|
||||
@@ -177,6 +180,8 @@ pub async fn run_server(
|
||||
.layer(Extension(rsmq))
|
||||
.layer(Extension(user_db))
|
||||
.layer(Extension(auth_cache.clone()))
|
||||
.layer(Extension(index_reader))
|
||||
.layer(Extension(index_writer))
|
||||
.layer(CookieManagerLayer::new())
|
||||
.layer(Extension(WebhookShared::new(rx.resubscribe(), db.clone())))
|
||||
.layer(DefaultBodyLimit::max(
|
||||
@@ -266,6 +271,10 @@ pub async fn run_server(
|
||||
.route_layer(from_extractor::<ApiAuthed>())
|
||||
.route_layer(from_extractor::<users::Tokened>())
|
||||
.nest("/jobs", jobs::global_root_service())
|
||||
.nest(
|
||||
"/srch/w/:workspace_id/index",
|
||||
indexer_ee::workspaced_service(),
|
||||
)
|
||||
.nest("/oidc", oidc_ee::global_service())
|
||||
.nest(
|
||||
"/saml",
|
||||
|
||||
@@ -183,6 +183,7 @@ pub enum Mode {
|
||||
Agent,
|
||||
Server,
|
||||
Standalone,
|
||||
Indexer,
|
||||
}
|
||||
|
||||
pub async fn send_email(
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "windmill-indexer"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "windmill_indexer"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
tantivy = ["dep:tantivy"]
|
||||
|
||||
[dependencies]
|
||||
windmill-common.workspace = true
|
||||
tantivy = {workspace = true, optional = true}
|
||||
tokio.workspace = true
|
||||
sqlx.workspace = true
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tracing.workspace = true
|
||||
chrono.workspace = true
|
||||
uuid.workspace = true
|
||||
futures.workspace = true
|
||||
@@ -0,0 +1,21 @@
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::error::Error;
|
||||
use anyhow::anyhow;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexReader;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexWriter;
|
||||
|
||||
pub fn init_index() -> Result<(IndexReader, IndexWriter), Error> {
|
||||
Err(anyhow!("Cannot initialize index: not in EE").into())
|
||||
}
|
||||
|
||||
pub async fn run_indexer(
|
||||
_db: Pool<Postgres>,
|
||||
mut _index_writer: IndexWriter,
|
||||
mut _killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) {
|
||||
tracing::error!("Cannot run indexer: not in EE");
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod indexer_ee;
|
||||
@@ -109,6 +109,27 @@ services:
|
||||
# - /var/run/docker.sock:/var/run/docker.sock
|
||||
# - worker_dependency_cache:/tmp/windmill/cache
|
||||
|
||||
# The indexer powers full-text job and log search, an EE feature.
|
||||
windmill_indexer:
|
||||
image: ${WM_IMAGE}
|
||||
pull_policy: always
|
||||
deploy:
|
||||
replicas: 0 # set to 1 to enable full-text job and log search
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 8001
|
||||
environment:
|
||||
- PORT=8001
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- MODE=indexer
|
||||
#how often will new jobs be added to the index to be searched
|
||||
- TANTIVY_REFRESH_INDEX_PERIOD__S=300
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- windmill_index:/tmp/windmill/search
|
||||
|
||||
lsp:
|
||||
image: ghcr.io/windmill-labs/windmill-lsp:latest
|
||||
pull_policy: always
|
||||
@@ -147,4 +168,5 @@ volumes:
|
||||
db_data: null
|
||||
worker_dependency_cache: null
|
||||
worker_logs: null
|
||||
windmill_index: null
|
||||
lsp_cache: null
|
||||
|
||||
@@ -1,440 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { AppService, FlowService, ResourceService, ScriptService } from '$lib/gen'
|
||||
import { enterpriseLicense, workspaceStore } from '$lib/stores'
|
||||
import { clickOutside } from '$lib/utils'
|
||||
import { Boxes, Code2, Edit, LayoutDashboard, Loader2, X } from 'lucide-svelte'
|
||||
import Portal from 'svelte-portal'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import SearchItems from './SearchItems.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import FlowIcon from './home/FlowIcon.svelte'
|
||||
import { Alert, Button } from './common'
|
||||
import YAML from 'yaml'
|
||||
|
||||
let search: string = ''
|
||||
|
||||
export async function open(nsearch?: string) {
|
||||
isOpen = true
|
||||
await Promise.all([loadScripts(), loadResources(), loadApps(), loadFlows()])
|
||||
if (nsearch) {
|
||||
search = nsearch
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadScripts() {
|
||||
scripts = await ScriptService.listSearchScript({ workspace: $workspaceStore ?? '' })
|
||||
}
|
||||
|
||||
export async function loadResources() {
|
||||
resources = await ResourceService.listSearchResource({ workspace: $workspaceStore ?? '' })
|
||||
}
|
||||
|
||||
export async function loadApps() {
|
||||
apps = await AppService.listSearchApp({ workspace: $workspaceStore ?? '' })
|
||||
}
|
||||
|
||||
export async function loadFlows() {
|
||||
flows = await FlowService.listSearchFlow({ workspace: $workspaceStore ?? '' })
|
||||
}
|
||||
|
||||
let searchKind: 'all' | 'scripts' | 'flows' | 'apps' | 'resources' = 'all'
|
||||
|
||||
let isOpen = false
|
||||
let inputElement
|
||||
|
||||
// Reactive statement to focus the input when the modal opens
|
||||
$: if (isOpen && inputElement) {
|
||||
// Use a timeout to ensure focus after any animations or rendering
|
||||
setTimeout(() => inputElement.focus(), 0)
|
||||
}
|
||||
|
||||
let scripts: undefined | { path: string; content: string }[] = undefined
|
||||
let filteredScriptItems: { path: string; content: string; marked: any }[] = []
|
||||
|
||||
let resources: undefined | { path: string; value: any }[] = undefined
|
||||
let filteredResourceItems: { path: string; value: any; marked: any }[] = []
|
||||
|
||||
let flows: undefined | { path: string; value: any }[] = undefined
|
||||
let filteredFlowItems: { path: string; value: any; marked: any }[] = []
|
||||
|
||||
let apps: undefined | { path: string; value: any }[] = undefined
|
||||
let filteredAppItems: { path: string; value: any; marked: any }[] = []
|
||||
|
||||
function getCounts(n: number) {
|
||||
return ` (${n})`
|
||||
}
|
||||
|
||||
function escape(htmlStr) {
|
||||
return htmlStr
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
$: counts =
|
||||
search == '' ||
|
||||
!scripts ||
|
||||
!resources ||
|
||||
!flows ||
|
||||
!apps ||
|
||||
!filteredAppItems ||
|
||||
!filteredFlowItems ||
|
||||
!filteredResourceItems ||
|
||||
!filteredScriptItems
|
||||
? {
|
||||
all: '',
|
||||
apps: '',
|
||||
flows: '',
|
||||
resources: '',
|
||||
scripts: ''
|
||||
}
|
||||
: {
|
||||
all: getCounts(
|
||||
filteredAppItems.length +
|
||||
filteredFlowItems.length +
|
||||
filteredResourceItems.length +
|
||||
filteredScriptItems.length
|
||||
),
|
||||
apps: getCounts(filteredAppItems.length),
|
||||
resources: getCounts(filteredResourceItems.length),
|
||||
flows: getCounts(filteredFlowItems.length),
|
||||
scripts: getCounts(filteredScriptItems.length)
|
||||
}
|
||||
|
||||
let showNbScripts = 10
|
||||
let showNbApps = 10
|
||||
let showNbResources = 10
|
||||
let showNbFlows = 10
|
||||
|
||||
$: search && resetShows()
|
||||
|
||||
function resetShows() {
|
||||
showNbScripts = 10
|
||||
showNbApps = 10
|
||||
showNbResources = 10
|
||||
showNbFlows = 10
|
||||
}
|
||||
</script>
|
||||
|
||||
<SearchItems
|
||||
filter={search}
|
||||
items={scripts}
|
||||
f={(s) => {
|
||||
return escape(s.content)
|
||||
}}
|
||||
bind:filteredItems={filteredScriptItems}
|
||||
/>
|
||||
|
||||
<SearchItems
|
||||
filter={search}
|
||||
items={resources}
|
||||
f={(s) => {
|
||||
return escape(YAML.stringify(s.value))
|
||||
}}
|
||||
bind:filteredItems={filteredResourceItems}
|
||||
/>
|
||||
|
||||
<SearchItems
|
||||
filter={search}
|
||||
items={flows}
|
||||
f={(s) => {
|
||||
return escape(YAML.stringify(s.value, null, 4))
|
||||
}}
|
||||
bind:filteredItems={filteredFlowItems}
|
||||
/>
|
||||
|
||||
<SearchItems
|
||||
filter={search}
|
||||
items={apps}
|
||||
f={(s) => {
|
||||
return escape(YAML.stringify(s.value, null, 4))
|
||||
}}
|
||||
bind:filteredItems={filteredAppItems}
|
||||
/>
|
||||
|
||||
{#if isOpen}
|
||||
<Portal>
|
||||
<div
|
||||
class={twMerge(
|
||||
`fixed top-0 bottom-0 left-0 right-0 transition-all duration-50`,
|
||||
' bg-black bg-opacity-60',
|
||||
'z-[1100]'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
class={'max-w-4xl lg:mx-auto mx-10 mt-8 bg-surface rounded-lg relative'}
|
||||
use:clickOutside={false}
|
||||
on:click_outside={() => {
|
||||
isOpen = false
|
||||
}}
|
||||
>
|
||||
<div class="px-4 py-2 border-b flex justify-between items-center">
|
||||
<div>Search by content</div>
|
||||
<div class="w-8">
|
||||
<button
|
||||
on:click|stopPropagation={() => {
|
||||
isOpen = false
|
||||
}}
|
||||
class="hover:bg-surface-hover bg-surface-secondary rounded-full w-8 h-8 flex items-center justify-center transition-all"
|
||||
>
|
||||
<X class="text-tertiary" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-2 py-2 overflow-auto">
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<div class="flex justify-start">
|
||||
<ToggleButtonGroup bind:selected={searchKind} class="h-10">
|
||||
<ToggleButton small light value="all" label={'All' + counts.all} />
|
||||
<ToggleButton
|
||||
small
|
||||
light
|
||||
value="scripts"
|
||||
icon={Code2}
|
||||
label={'Scripts' + counts.scripts}
|
||||
/>
|
||||
<ToggleButton
|
||||
small
|
||||
light
|
||||
value="resources"
|
||||
icon={Boxes}
|
||||
label={'Resources' + counts.resources}
|
||||
/>
|
||||
<ToggleButton
|
||||
small
|
||||
light
|
||||
value="flows"
|
||||
label={'Flows' + counts.flows}
|
||||
icon={FlowIcon}
|
||||
selectedColor="#14b8a6"
|
||||
/>
|
||||
<ToggleButton
|
||||
small
|
||||
light
|
||||
value="apps"
|
||||
label={'Apps' + counts.apps}
|
||||
icon={LayoutDashboard}
|
||||
selectedColor="#fb923c"
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="relative text-tertiary grow min-w-[100px]">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
bind:this={inputElement}
|
||||
placeholder="Search in the content of resources, scripts, flows and apps"
|
||||
bind:value={search}
|
||||
class="bg-surface !h-10 !px-4 !pr-10 !rounded-lg text-sm focus:outline-none"
|
||||
/>
|
||||
<button type="submit" class="absolute right-0 top-0 mt-3 mr-4">
|
||||
<svg
|
||||
class="h-4 w-4 fill-current"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 56.966 56.966"
|
||||
style="enable-background:new 0 0 56.966 56.966;"
|
||||
xml:space="preserve"
|
||||
width="512px"
|
||||
height="512px"
|
||||
>
|
||||
<path
|
||||
d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23 s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92 c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17 s-17-7.626-17-17S14.61,6,23.984,6z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<div class="text-xs text-secondary"
|
||||
>Searching among <div class="inline-flex"
|
||||
>{#if scripts}{scripts?.length}{:else}
|
||||
<Loader2 size={10} class="animate-spin " />
|
||||
{/if}</div
|
||||
>
|
||||
scripts,
|
||||
<div class="inline-flex"
|
||||
>{#if resources}{resources?.length}{:else}
|
||||
<Loader2 size={10} class="animate-spin " />
|
||||
{/if}</div
|
||||
>
|
||||
resources,
|
||||
<div class="inline-flex"
|
||||
>{#if flows}{flows?.length}{:else}
|
||||
<Loader2 size={10} class="animate-spin " />
|
||||
{/if}</div
|
||||
>
|
||||
flows,
|
||||
<div class="inline-flex"
|
||||
>{#if apps}{apps?.length}{:else}
|
||||
<Loader2 size={10} class="animate-spin " />
|
||||
{/if}</div
|
||||
>
|
||||
apps</div
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 overflow-auto max-h-[80vh]">
|
||||
{#if !$enterpriseLicense}
|
||||
<div class="py-1" />
|
||||
|
||||
<Alert title="Content Search is an EE feature" type="warning">
|
||||
Without EE, content search will only search among 10 scripts, 3 flows, 3 apps and 3
|
||||
resources.
|
||||
</Alert>
|
||||
<div class="py-1" />
|
||||
{/if}
|
||||
|
||||
{#if search.trim().length > 0}
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if (searchKind == 'all' || searchKind == 'scripts') && filteredScriptItems?.length > 0}
|
||||
{#each filteredScriptItems.slice(0, showNbScripts) ?? [] as item}
|
||||
<div>
|
||||
<div class="text-sm font-semibold"
|
||||
><a href="/scripts/get/{item.path}">Script: {item.path}</a></div
|
||||
>
|
||||
<div class="flex gap-2 justify-between">
|
||||
<pre class="text-xs border p-2 overflow-auto max-h-40 w-full max-w-2xl"
|
||||
><code>{@html item.marked}</code></pre
|
||||
>
|
||||
<div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
on:click|once={() => {
|
||||
window
|
||||
.open(`/scripts/edit/${item.path}?no_draft=true`, '_blank')
|
||||
?.focus()
|
||||
}}
|
||||
color="light"
|
||||
size="sm"
|
||||
startIcon={{ icon: Edit }}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#if filteredScriptItems.length > showNbScripts}
|
||||
<a
|
||||
href="#"
|
||||
class="text-center font-semibold cursor-pointer pb-40"
|
||||
on:click={() => {
|
||||
showNbScripts += 30
|
||||
}}
|
||||
>
|
||||
({showNbScripts} of {filteredScriptItems.length}) Show more scripts
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if (searchKind == 'all' || searchKind == 'resources') && filteredResourceItems?.length > 0}
|
||||
{#each filteredResourceItems.slice(0, showNbResources) ?? [] as item}
|
||||
<div>
|
||||
<div class="text-sm font-semibold">Resource: {item.path}</div>
|
||||
<div class="flex gap-2 justify-between">
|
||||
<pre class="text-xs border p-2 overflow-auto max-h-40 w-full max-w-2xl"
|
||||
><code>{@html item.marked}</code></pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#if filteredResourceItems.length > showNbResources}
|
||||
<a
|
||||
href="#"
|
||||
class="text-center font-semibold cursor-pointer pb-40"
|
||||
on:click={() => {
|
||||
showNbResources += 30
|
||||
}}
|
||||
>
|
||||
({showNbResources} of {filteredResourceItems.length}) Show more resources
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if (searchKind == 'all' || searchKind == 'flows') && filteredFlowItems?.length > 0}
|
||||
{#each filteredFlowItems.slice(0, showNbFlows) ?? [] as item}
|
||||
<div>
|
||||
<div class="text-sm font-semibold"
|
||||
><a href="/flows/get/{item.path}">Flow: {item.path}</a></div
|
||||
>
|
||||
<div class="flex gap-2 justify-between">
|
||||
<pre class="text-xs border p-2 overflow-auto max-h-40 w-full max-w-2xl"
|
||||
><code>{@html item.marked}</code></pre
|
||||
>
|
||||
<div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
on:click|once={() => {
|
||||
window
|
||||
.open(`/flows/edit/${item.path}?no_draft=true`, '_blank')
|
||||
?.focus()
|
||||
}}
|
||||
color="light"
|
||||
size="sm"
|
||||
startIcon={{ icon: Edit }}>Edit</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#if filteredFlowItems.length > showNbFlows}
|
||||
<a
|
||||
href="#"
|
||||
class="text-center font-semibold cursor-pointer pb-40"
|
||||
on:click={() => {
|
||||
showNbScripts += 30
|
||||
}}
|
||||
>
|
||||
({showNbFlows} of {filteredFlowItems.length}) Show more flows
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if (searchKind == 'all' || searchKind == 'apps') && filteredAppItems?.length > 0}
|
||||
{#each filteredAppItems.slice(0, showNbApps) ?? [] as item}
|
||||
<div>
|
||||
<div class="text-sm font-semibold"
|
||||
><a href="/apps/get/{item.path}">App: {item.path}</a></div
|
||||
>
|
||||
<div class="flex gap-2 justify-between">
|
||||
<pre class="text-xs border p-2 overflow-auto max-h-40 w-full max-w-2xl"
|
||||
><code>{@html item.marked}</code></pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#if filteredAppItems.length > showNbApps}
|
||||
<a
|
||||
href="#"
|
||||
class="text-center font-semibold cursor-pointer pb-40"
|
||||
on:click={() => {
|
||||
showNbApps += 30
|
||||
}}
|
||||
>
|
||||
({showNbApps} of {filteredAppItems.length}) Show more apps
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex justify-center items-center h-48">
|
||||
<div class="text-tertiary text-center">
|
||||
<div class="text-2xl font-bold">Empty Search Filter</div>
|
||||
<div class="text-sm"
|
||||
>Start writing, search everywhere a path is referenced for instance</div
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div></div
|
||||
></div
|
||||
></Portal
|
||||
>
|
||||
{/if}
|
||||
@@ -0,0 +1,379 @@
|
||||
<script lang="ts">
|
||||
import { AppService, FlowService, ResourceService, ScriptService } from '$lib/gen'
|
||||
import { enterpriseLicense, workspaceStore } from '$lib/stores'
|
||||
import { Boxes, Code2, Edit, LayoutDashboard, Loader2 } from 'lucide-svelte'
|
||||
import SearchItems from './SearchItems.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import FlowIcon from './home/FlowIcon.svelte'
|
||||
import { Alert, Button } from './common'
|
||||
import YAML from 'yaml'
|
||||
|
||||
export let search: string = ''
|
||||
export let classNameInner = 'max-h-[80vh]'
|
||||
|
||||
export async function open(nsearch?: string) {
|
||||
await Promise.all([loadScripts(), loadResources(), loadApps(), loadFlows()])
|
||||
if (nsearch) {
|
||||
search = nsearch
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadScripts() {
|
||||
scripts = await ScriptService.listSearchScript({ workspace: $workspaceStore ?? '' })
|
||||
}
|
||||
|
||||
export async function loadResources() {
|
||||
resources = await ResourceService.listSearchResource({ workspace: $workspaceStore ?? '' })
|
||||
}
|
||||
|
||||
export async function loadApps() {
|
||||
apps = await AppService.listSearchApp({ workspace: $workspaceStore ?? '' })
|
||||
}
|
||||
|
||||
export async function loadFlows() {
|
||||
flows = await FlowService.listSearchFlow({ workspace: $workspaceStore ?? '' })
|
||||
}
|
||||
|
||||
let searchKind: 'all' | 'scripts' | 'flows' | 'apps' | 'resources' = 'all'
|
||||
|
||||
let scripts: undefined | { path: string; content: string }[] = undefined
|
||||
let filteredScriptItems: { path: string; content: string; marked: any }[] = []
|
||||
|
||||
let resources: undefined | { path: string; value: any }[] = undefined
|
||||
let filteredResourceItems: { path: string; value: any; marked: any }[] = []
|
||||
|
||||
let flows: undefined | { path: string; value: any }[] = undefined
|
||||
let filteredFlowItems: { path: string; value: any; marked: any }[] = []
|
||||
|
||||
let apps: undefined | { path: string; value: any }[] = undefined
|
||||
let filteredAppItems: { path: string; value: any; marked: any }[] = []
|
||||
|
||||
function getCounts(n: number) {
|
||||
return ` (${n})`
|
||||
}
|
||||
|
||||
function escape(htmlStr) {
|
||||
return htmlStr
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
$: counts =
|
||||
search == '' ||
|
||||
!scripts ||
|
||||
!resources ||
|
||||
!flows ||
|
||||
!apps ||
|
||||
!filteredAppItems ||
|
||||
!filteredFlowItems ||
|
||||
!filteredResourceItems ||
|
||||
!filteredScriptItems
|
||||
? {
|
||||
all: '',
|
||||
apps: '',
|
||||
flows: '',
|
||||
resources: '',
|
||||
scripts: ''
|
||||
}
|
||||
: {
|
||||
all: getCounts(
|
||||
filteredAppItems.length +
|
||||
filteredFlowItems.length +
|
||||
filteredResourceItems.length +
|
||||
filteredScriptItems.length
|
||||
),
|
||||
apps: getCounts(filteredAppItems.length),
|
||||
resources: getCounts(filteredResourceItems.length),
|
||||
flows: getCounts(filteredFlowItems.length),
|
||||
scripts: getCounts(filteredScriptItems.length)
|
||||
}
|
||||
|
||||
let showNbScripts = 10
|
||||
let showNbApps = 10
|
||||
let showNbResources = 10
|
||||
let showNbFlows = 10
|
||||
|
||||
$: search && resetShows()
|
||||
|
||||
function resetShows() {
|
||||
showNbScripts = 10
|
||||
showNbApps = 10
|
||||
showNbResources = 10
|
||||
showNbFlows = 10
|
||||
}
|
||||
</script>
|
||||
|
||||
<SearchItems
|
||||
filter={search}
|
||||
items={scripts}
|
||||
f={(s) => {
|
||||
return escape(s.content)
|
||||
}}
|
||||
bind:filteredItems={filteredScriptItems}
|
||||
/>
|
||||
|
||||
<SearchItems
|
||||
filter={search}
|
||||
items={resources}
|
||||
f={(s) => {
|
||||
return escape(YAML.stringify(s.value))
|
||||
}}
|
||||
bind:filteredItems={filteredResourceItems}
|
||||
/>
|
||||
|
||||
<SearchItems
|
||||
filter={search}
|
||||
items={flows}
|
||||
f={(s) => {
|
||||
return escape(YAML.stringify(s.value, null, 4))
|
||||
}}
|
||||
bind:filteredItems={filteredFlowItems}
|
||||
/>
|
||||
|
||||
<SearchItems
|
||||
filter={search}
|
||||
items={apps}
|
||||
f={(s) => {
|
||||
return escape(YAML.stringify(s.value, null, 4))
|
||||
}}
|
||||
bind:filteredItems={filteredAppItems}
|
||||
/>
|
||||
|
||||
<div class="px-2 py-2 overflow-auto">
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<div class="flex justify-start">
|
||||
<ToggleButtonGroup bind:selected={searchKind} class="h-10">
|
||||
<ToggleButton small light value="all" label={'All' + counts.all} />
|
||||
<ToggleButton small light value="scripts" icon={Code2} label={'Scripts' + counts.scripts} />
|
||||
<ToggleButton
|
||||
small
|
||||
light
|
||||
value="resources"
|
||||
icon={Boxes}
|
||||
label={'Resources' + counts.resources}
|
||||
/>
|
||||
<ToggleButton
|
||||
small
|
||||
light
|
||||
value="flows"
|
||||
label={'Flows' + counts.flows}
|
||||
icon={FlowIcon}
|
||||
selectedColor="#14b8a6"
|
||||
/>
|
||||
<ToggleButton
|
||||
small
|
||||
light
|
||||
value="apps"
|
||||
label={'Apps' + counts.apps}
|
||||
icon={LayoutDashboard}
|
||||
selectedColor="#fb923c"
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="relative text-tertiary grow min-w-[100px]">
|
||||
<slot name="input-slot" />
|
||||
<button type="submit" class="absolute right-0 top-0 mt-3 mr-4">
|
||||
<svg
|
||||
class="h-4 w-4 fill-current"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 56.966 56.966"
|
||||
style="enable-background:new 0 0 56.966 56.966;"
|
||||
xml:space="preserve"
|
||||
width="512px"
|
||||
height="512px"
|
||||
>
|
||||
<path
|
||||
d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23 s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92 c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17 s-17-7.626-17-17S14.61,6,23.984,6z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<div class="text-xs text-secondary"
|
||||
>Searching among <div class="inline-flex"
|
||||
>{#if scripts}{scripts?.length}{:else}
|
||||
<Loader2 size={10} class="animate-spin " />
|
||||
{/if}</div
|
||||
>
|
||||
scripts,
|
||||
<div class="inline-flex"
|
||||
>{#if resources}{resources?.length}{:else}
|
||||
<Loader2 size={10} class="animate-spin " />
|
||||
{/if}</div
|
||||
>
|
||||
resources,
|
||||
<div class="inline-flex"
|
||||
>{#if flows}{flows?.length}{:else}
|
||||
<Loader2 size={10} class="animate-spin " />
|
||||
{/if}</div
|
||||
>
|
||||
flows,
|
||||
<div class="inline-flex"
|
||||
>{#if apps}{apps?.length}{:else}
|
||||
<Loader2 size={10} class="animate-spin " />
|
||||
{/if}</div
|
||||
>
|
||||
apps</div
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 overflow-auto {classNameInner}">
|
||||
{#if !$enterpriseLicense}
|
||||
<div class="py-1" />
|
||||
|
||||
<Alert title="Content Search is an EE feature" type="warning">
|
||||
Without EE, content search will only search among 10 scripts, 3 flows, 3 apps and 3
|
||||
resources.
|
||||
</Alert>
|
||||
<div class="py-1" />
|
||||
{/if}
|
||||
|
||||
{#if search.trim().length > 0}
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if (searchKind == 'all' || searchKind == 'scripts') && filteredScriptItems?.length > 0}
|
||||
{#each filteredScriptItems.slice(0, showNbScripts) ?? [] as item}
|
||||
<div>
|
||||
<div class="text-sm font-semibold"
|
||||
><a href="/scripts/get/{item.path}">Script: {item.path}</a></div
|
||||
>
|
||||
<div class="flex gap-2 justify-between">
|
||||
<pre class="text-xs border p-2 overflow-auto max-h-40 w-full max-w-2xl"
|
||||
><code>{@html item.marked}</code></pre
|
||||
>
|
||||
<div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
on:click|once={() => {
|
||||
window.open(`/scripts/edit/${item.path}?no_draft=true`, '_blank')?.focus()
|
||||
}}
|
||||
color="light"
|
||||
size="sm"
|
||||
startIcon={{ icon: Edit }}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#if filteredScriptItems.length > showNbScripts}
|
||||
<a
|
||||
href="#"
|
||||
class="text-center font-semibold cursor-pointer pb-40"
|
||||
on:click={() => {
|
||||
showNbScripts += 30
|
||||
}}
|
||||
>
|
||||
({showNbScripts} of {filteredScriptItems.length}) Show more scripts
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if (searchKind == 'all' || searchKind == 'resources') && filteredResourceItems?.length > 0}
|
||||
{#each filteredResourceItems.slice(0, showNbResources) ?? [] as item}
|
||||
<div>
|
||||
<div class="text-sm font-semibold">Resource: {item.path}</div>
|
||||
<div class="flex gap-2 justify-between">
|
||||
<pre class="text-xs border p-2 overflow-auto max-h-40 w-full max-w-2xl"
|
||||
><code>{@html item.marked}</code></pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#if filteredResourceItems.length > showNbResources}
|
||||
<a
|
||||
href="#"
|
||||
class="text-center font-semibold cursor-pointer pb-40"
|
||||
on:click={() => {
|
||||
showNbResources += 30
|
||||
}}
|
||||
>
|
||||
({showNbResources} of {filteredResourceItems.length}) Show more resources
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if (searchKind == 'all' || searchKind == 'flows') && filteredFlowItems?.length > 0}
|
||||
{#each filteredFlowItems.slice(0, showNbFlows) ?? [] as item}
|
||||
<div>
|
||||
<div class="text-sm font-semibold"
|
||||
><a href="/flows/get/{item.path}">Flow: {item.path}</a></div
|
||||
>
|
||||
<div class="flex gap-2 justify-between">
|
||||
<pre class="text-xs border p-2 overflow-auto max-h-40 w-full max-w-2xl"
|
||||
><code>{@html item.marked}</code></pre
|
||||
>
|
||||
<div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
on:click|once={() => {
|
||||
window.open(`/flows/edit/${item.path}?no_draft=true`, '_blank')?.focus()
|
||||
}}
|
||||
color="light"
|
||||
size="sm"
|
||||
startIcon={{ icon: Edit }}>Edit</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#if filteredFlowItems.length > showNbFlows}
|
||||
<a
|
||||
href="#"
|
||||
class="text-center font-semibold cursor-pointer pb-40"
|
||||
on:click={() => {
|
||||
showNbScripts += 30
|
||||
}}
|
||||
>
|
||||
({showNbFlows} of {filteredFlowItems.length}) Show more flows
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if (searchKind == 'all' || searchKind == 'apps') && filteredAppItems?.length > 0}
|
||||
{#each filteredAppItems.slice(0, showNbApps) ?? [] as item}
|
||||
<div>
|
||||
<div class="text-sm font-semibold"
|
||||
><a href="/apps/get/{item.path}">App: {item.path}</a></div
|
||||
>
|
||||
<div class="flex gap-2 justify-between">
|
||||
<pre class="text-xs border p-2 overflow-auto max-h-40 w-full max-w-2xl"
|
||||
><code>{@html item.marked}</code></pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#if filteredAppItems.length > showNbApps}
|
||||
<a
|
||||
href="#"
|
||||
class="text-center font-semibold cursor-pointer pb-40"
|
||||
on:click={() => {
|
||||
showNbApps += 30
|
||||
}}
|
||||
>
|
||||
({showNbApps} of {filteredAppItems.length}) Show more apps
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex justify-center items-center h-48">
|
||||
<div class="text-tertiary text-center">
|
||||
<div class="text-2xl font-bold">Empty Search Filter</div>
|
||||
<div class="text-sm"
|
||||
>Start writing, search everywhere a path is referenced for instance</div
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -15,7 +15,7 @@
|
||||
VariableService
|
||||
} from '$lib/gen'
|
||||
import { superadmin, userStore, workspaceStore } from '$lib/stores'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import { Alert, Button, Drawer, DrawerContent } from './common'
|
||||
import Badge from './common/badge/Badge.svelte'
|
||||
@@ -26,7 +26,6 @@
|
||||
import Required from './Required.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { Eye, Folder, Plus, SearchCode, User } from 'lucide-svelte'
|
||||
import ContentSearch from './ContentSearch.svelte'
|
||||
|
||||
type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule' | 'app' | 'raw_app'
|
||||
let meta: Meta | undefined = undefined
|
||||
@@ -263,13 +262,9 @@
|
||||
!dirty && (dirty = true)
|
||||
}
|
||||
|
||||
let contentSearch: ContentSearch
|
||||
const openSearchWithPrefilledText: (t?: string) => void = getContext("openSearchWithPrefilledText")
|
||||
</script>
|
||||
|
||||
{#if kind != 'app' && kind != 'schedule' && initialPath != '' && initialPath != undefined}
|
||||
<ContentSearch bind:this={contentSearch} />
|
||||
{/if}
|
||||
|
||||
<Drawer bind:this={newFolder}>
|
||||
<DrawerContent
|
||||
title="New Folder"
|
||||
@@ -462,7 +457,7 @@
|
||||
variant="border"
|
||||
color="dark"
|
||||
on:click={() => {
|
||||
contentSearch?.open(initialPath)
|
||||
openSearchWithPrefilledText("#")
|
||||
}}
|
||||
startIcon={{ icon: SearchCode }}
|
||||
>
|
||||
|
||||
@@ -37,13 +37,13 @@
|
||||
import { canWrite, getLocalSetting, storeLocalSetting } from '$lib/utils'
|
||||
import { page } from '$app/stores'
|
||||
import { setQuery } from '$lib/navigation'
|
||||
import ContentSearch from '../ContentSearch.svelte'
|
||||
import Drawer from '../common/drawer/Drawer.svelte'
|
||||
import HighlightCode from '../HighlightCode.svelte'
|
||||
import DrawerContent from '../common/drawer/DrawerContent.svelte'
|
||||
import Item from './Item.svelte'
|
||||
import TreeViewRoot from './TreeViewRoot.svelte'
|
||||
import { Popup } from '../common'
|
||||
import { getContext } from 'svelte'
|
||||
|
||||
type TableItem<T, U extends 'script' | 'flow' | 'app' | 'raw_app'> = T & {
|
||||
canWrite: boolean
|
||||
@@ -289,7 +289,7 @@
|
||||
$: storeLocalSetting(FILTER_USER_FOLDER_SETTING_NAME, filterUserFolders ? 'true' : undefined)
|
||||
$: storeLocalSetting(INCLUDE_WITHOUT_MAIN_SETTING_NAME, includeWithoutMain ? 'true' : undefined)
|
||||
|
||||
let contentSearch: ContentSearch
|
||||
const openSearchWithPrefilledText: (t?: string) => void = getContext("openSearchWithPrefilledText")
|
||||
|
||||
let viewCodeDrawer: Drawer
|
||||
let viewCodeTitle: string | undefined
|
||||
@@ -332,7 +332,6 @@
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<ContentSearch bind:this={contentSearch} />
|
||||
<CenteredPage>
|
||||
<div class="flex flex-wrap gap-2 items-center justify-between w-full mt-2">
|
||||
<div class="flex justify-start">
|
||||
@@ -397,7 +396,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<Button
|
||||
on:click={() => contentSearch?.open()}
|
||||
on:click={() => openSearchWithPrefilledText("#")}
|
||||
variant="border"
|
||||
size="sm"
|
||||
spacingSize="lg"
|
||||
|
||||
@@ -0,0 +1,651 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount, tick } from 'svelte'
|
||||
import {
|
||||
AppService,
|
||||
FlowService,
|
||||
IndexSearchService,
|
||||
RawAppService,
|
||||
ScriptService,
|
||||
type Flow,
|
||||
type ListableApp,
|
||||
type ListableRawApp,
|
||||
type Script
|
||||
} from '$lib/gen'
|
||||
import { clickOutside, displayDateOnly, isMac, sendUserToast } from '$lib/utils'
|
||||
import TimeAgo from '../TimeAgo.svelte'
|
||||
import {
|
||||
BoxesIcon,
|
||||
CalendarIcon,
|
||||
Code2Icon,
|
||||
DollarSignIcon,
|
||||
HomeIcon,
|
||||
LayoutDashboardIcon,
|
||||
Loader2,
|
||||
PlayIcon,
|
||||
Search,
|
||||
SearchCode
|
||||
} from 'lucide-svelte'
|
||||
import JobPreview from '../runs/JobPreview.svelte'
|
||||
import Portal from 'svelte-portal'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import ContentSearchInner from '../ContentSearchInner.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import QuickMenuItem from '../search/QuickMenuItem.svelte'
|
||||
import { enterpriseLicense, workspaceStore } from '$lib/stores'
|
||||
import uFuzzy from '@leeoniya/ufuzzy'
|
||||
import BarsStaggered from '../icons/BarsStaggered.svelte'
|
||||
import { scroll_into_view_if_needed_polyfill } from '../multiselect/utils'
|
||||
import { Alert } from '../common'
|
||||
|
||||
let open: boolean = false
|
||||
|
||||
let searchTerm: string = ''
|
||||
let textInput: HTMLInputElement
|
||||
let selectedWorkspace: string | undefined = undefined
|
||||
let contentSearch: ContentSearchInner | undefined = 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 = 'default'
|
||||
|
||||
type quickMenuItem = {
|
||||
search_id: string
|
||||
label: string
|
||||
action: () => void
|
||||
icon?: any
|
||||
shortcutKey?: string
|
||||
}
|
||||
let switchModeItems: quickMenuItem[] = [
|
||||
{
|
||||
search_id: 'switchto:run-search',
|
||||
label: 'Search across completed runs',
|
||||
action: () => switchMode('runs'),
|
||||
shortcutKey: RUNS_PREFIX,
|
||||
icon: Search
|
||||
},
|
||||
{
|
||||
search_id: 'switchto:content-search',
|
||||
label: 'Search scripts/flows/apps based on content',
|
||||
action: () => switchMode('content'),
|
||||
shortcutKey: CONTENT_SEARCH_PREFIX,
|
||||
icon: SearchCode
|
||||
},
|
||||
{
|
||||
search_id: 'switchto:log-search',
|
||||
label: 'Search windmill logs',
|
||||
action: () => switchMode('logs'),
|
||||
shortcutKey: LOGS_PREFIX,
|
||||
icon: Search
|
||||
}
|
||||
]
|
||||
let defaultMenuItems: quickMenuItem[] = [
|
||||
{ search_id: 'nav:home', label: 'Go to Home', action: () => gotoPage('/'), icon: HomeIcon },
|
||||
{ search_id: 'nav:runs', label: 'Go to Runs', action: () => gotoPage('/runs'), icon: PlayIcon },
|
||||
{
|
||||
search_id: 'nav:variables',
|
||||
label: 'Go to Variables',
|
||||
action: () => gotoPage('/variables'),
|
||||
icon: DollarSignIcon
|
||||
},
|
||||
{
|
||||
search_id: 'nav:resources',
|
||||
label: 'Go to Resources',
|
||||
action: () => gotoPage('/resources'),
|
||||
icon: BoxesIcon
|
||||
},
|
||||
{
|
||||
search_id: 'nav:schedules',
|
||||
label: 'Go to Schedules',
|
||||
action: () => gotoPage('/schedules'),
|
||||
icon: CalendarIcon
|
||||
},
|
||||
...switchModeItems
|
||||
]
|
||||
|
||||
let itemMap = {
|
||||
default: defaultMenuItems as any[],
|
||||
'switch-mode': switchModeItems,
|
||||
runs: [] as any[],
|
||||
content: [] as any[],
|
||||
logs: [] as any[]
|
||||
}
|
||||
|
||||
$: 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 switchModeItemLabels = switchModeItems.map((item) => item.label)
|
||||
|
||||
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 debounceTimeout: any = undefined
|
||||
const debouncePeriod: number = 1000
|
||||
let loadingCompletedRuns: boolean = false
|
||||
async function handleSearch() {
|
||||
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') {
|
||||
itemMap['default'] = fuzzyFilter(searchTerm, defaultMenuItems, defaultMenuItemLabels)
|
||||
if (combinedItems) {
|
||||
itemMap['default'] = itemMap['default'].concat(
|
||||
fuzzyFilter(
|
||||
searchTerm,
|
||||
combinedItems,
|
||||
combinedItems.map((i) => `${i.path} ${i.summary}`)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (tab === 'switch-mode') {
|
||||
itemMap['switch-mode'] = fuzzyFilter(
|
||||
removePrefix(searchTerm, SWITCH_MODE_PREFIX),
|
||||
switchModeItems,
|
||||
switchModeItemLabels
|
||||
)
|
||||
}
|
||||
if (tab === 'runs') {
|
||||
const s = removePrefix(searchTerm, RUNS_PREFIX)
|
||||
clearTimeout(debounceTimeout)
|
||||
loadingCompletedRuns = true
|
||||
debounceTimeout = setTimeout(async () => {
|
||||
clearTimeout(debounceTimeout)
|
||||
let searchResults
|
||||
try {
|
||||
searchResults = await IndexSearchService.searchJobsIndex({
|
||||
searchQuery: s,
|
||||
workspace: $workspaceStore!
|
||||
})
|
||||
itemMap['runs'] = searchResults.hits
|
||||
} catch (e) {
|
||||
sendUserToast(e, true)
|
||||
}
|
||||
loadingCompletedRuns = false
|
||||
selectedItem = selectItem(0)
|
||||
}, debouncePeriod)
|
||||
}
|
||||
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
|
||||
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 (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) {
|
||||
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)
|
||||
}
|
||||
|
||||
function gotoPage(path: string) {
|
||||
open = false
|
||||
searchTerm = ''
|
||||
goto(path)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
$: searchTerm, 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
|
||||
has_draft?: 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 = undefined
|
||||
|
||||
async function fetchCombinedItems() {
|
||||
const scripts = await ScriptService.listScripts({
|
||||
workspace: $workspaceStore!
|
||||
})
|
||||
const flows = await FlowService.listFlows({
|
||||
workspace: $workspaceStore!
|
||||
})
|
||||
const apps = await AppService.listApps({ workspace: $workspaceStore! })
|
||||
const raw_apps = await RawAppService.listRawApps({ 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
|
||||
})),
|
||||
...raw_apps.map((x) => ({
|
||||
...x,
|
||||
type: 'raw_app' as 'raw_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 openModal() {
|
||||
open = !open
|
||||
await tick()
|
||||
if (open) {
|
||||
if (combinedItems == undefined) {
|
||||
combinedItems = await fetchCombinedItems()
|
||||
handleSearch()
|
||||
}
|
||||
selectedItem = selectItem(0)
|
||||
textInput.focus()
|
||||
textInput.select()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<Portal>
|
||||
<div
|
||||
class={twMerge(
|
||||
`fixed top-0 bottom-0 left-0 right-0 transition-all duration-50`,
|
||||
' bg-black bg-opacity-40',
|
||||
'z-[1100]'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
class={'max-w-4xl lg:mx-auto mx-10 mt-40 bg-surface rounded-lg relative'}
|
||||
use:clickOutside={false}
|
||||
on:click_outside={() => {
|
||||
open = false
|
||||
}}
|
||||
>
|
||||
<div class="py-2 items-center">
|
||||
<div class="px-4 flex flex-row gap-1 items-center pb-1 mb-2 border-b">
|
||||
<Search />
|
||||
<div class="relative inline-block w-full">
|
||||
<input
|
||||
id="quickSearchInput"
|
||||
bind:this={textInput}
|
||||
type="text"
|
||||
class="quick-search-input"
|
||||
bind:value={searchTerm}
|
||||
/>
|
||||
<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"
|
||||
>{placeholderFromPrefix(searchTerm)}</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 overflow-scroll max-h-[30rem]">
|
||||
{#if tab === 'default' || tab === 'switch-mode'}
|
||||
{#each (itemMap[tab] ?? []).filter((e) => defaultMenuItems.includes(e)) as el}
|
||||
<QuickMenuItem
|
||||
on:select={el?.action}
|
||||
on:hover={() => (selectedItem = el)}
|
||||
id={el?.search_id}
|
||||
hovered={el?.search_id === selectedItem?.search_id}
|
||||
label={el?.label}
|
||||
icon={el?.icon}
|
||||
shortcutKey={el?.shortcutKey}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if tab === 'default'}
|
||||
{#if (itemMap[tab] ?? []).filter((e) => (combinedItems ?? []).includes(e)).length > 0}
|
||||
<div class="mt-2 pt-2 pb-1 px-1 text-xs text-sm font-bold border-t text-tertiary"
|
||||
>Flows/Scripts/Apps</div
|
||||
>
|
||||
{/if}
|
||||
{#each (itemMap[tab] ?? []).filter((e) => (combinedItems ?? []).includes(e)) as el}
|
||||
<QuickMenuItem
|
||||
on:select={() => gotoWindmillItemPage(el)}
|
||||
on:hover={() => (selectedItem = el)}
|
||||
id={el?.search_id}
|
||||
hovered={el?.path === selectedItem?.path}
|
||||
label={(el.summary ? `${el.summary} - ` : '') +
|
||||
el.path +
|
||||
(el.starred ? ' ★' : '')}
|
||||
icon={iconForWindmillItem(el.type)}
|
||||
/>
|
||||
{/each}
|
||||
{#if (itemMap[tab] ?? []).length === 0}
|
||||
<div class="flex w-full justify-center items-center h-48">
|
||||
<div class="text-tertiary text-center">
|
||||
<div class="text-2xl font-bold">Nothing found</div>
|
||||
<div class="text-sm">Tip: press `esc` to quickly clear the search bar</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if tab === 'content'}
|
||||
<ContentSearchInner
|
||||
classNameInner="max-h-[20rem]"
|
||||
search={removePrefix(searchTerm, '#')}
|
||||
bind:this={contentSearch}
|
||||
/>
|
||||
{:else if tab === 'logs'}
|
||||
<div class="p-2">
|
||||
<Alert title="Service log search is coming soon" type="info">
|
||||
Full text search on windmill's service logs is coming soon
|
||||
</Alert>
|
||||
</div>
|
||||
{:else if tab === 'runs'}
|
||||
<div class="flex h-96">
|
||||
{#if loadingCompletedRuns}
|
||||
<div class="flex w-full justify-center items-center h-48">
|
||||
<div class="text-tertiary text-center">
|
||||
<Loader2 size={34} class="animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
{:else if itemMap['runs'] && itemMap['runs'].length > 0}
|
||||
<div class="w-5/12 overflow-scroll">
|
||||
{#each itemMap['runs'] ?? [] as r}
|
||||
<QuickMenuItem
|
||||
on:hover={() => {
|
||||
selectedItem = r
|
||||
selectedWorkspace = r?.document.workspace_id[0]
|
||||
}}
|
||||
on:select={() => {
|
||||
open = false
|
||||
goto(`/run/${r?.document.id[0]}`)
|
||||
}}
|
||||
id={r?.document.id[0]}
|
||||
hovered={selectedItem && r?.document.id[0] === selectedItem?.document.id[0]}
|
||||
icon={r?.icon}
|
||||
>
|
||||
<svelte:fragment slot="itemReplacement">
|
||||
<button
|
||||
class={twMerge(
|
||||
`w-full flex items-center justify-between gap-1 py-2 px-2 text-left border rounded-sm transition-a`,
|
||||
r?.document.id === selectedItem?.document?.id
|
||||
? 'bg-surface-hover'
|
||||
: ''
|
||||
)}
|
||||
on:click={() => {}}
|
||||
>
|
||||
<div
|
||||
class="w-full h-full items-center text-xs font-normal grid grid-cols-10 gap-0 min-w-0"
|
||||
>
|
||||
<div class="col-span-1">
|
||||
<div
|
||||
class="rounded-full w-2 h-2 {r?.document.success[0]
|
||||
? 'bg-green-400'
|
||||
: 'bg-red-400'}"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-5">
|
||||
{r?.document.script_path}
|
||||
</div>
|
||||
<div
|
||||
class="whitespace-nowrap col-span-2 !text-tertiary !text-2xs overflow-hidden text-ellipsis flex-shrink text-center"
|
||||
>
|
||||
{displayDateOnly(new Date(r?.document.created_at[0]))}
|
||||
</div>
|
||||
<div
|
||||
class="whitespace-nowrap col-span-2 !text-tertiary !text-2xs overflow-hidden text-ellipsis flex-shrink text-center"
|
||||
>
|
||||
<TimeAgo date={r?.document.created_at[0] ?? ''} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</QuickMenuItem>
|
||||
{/each}
|
||||
</div>
|
||||
{#if selectedItem === undefined}
|
||||
select a result to preview
|
||||
{:else}
|
||||
<div class="w-7/12 overflow-y-scroll">
|
||||
<JobPreview
|
||||
id={selectedItem?.document?.id[0]}
|
||||
workspace={selectedWorkspace}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex w-full justify-center items-center h-96">
|
||||
<div class="text-tertiary text-center">
|
||||
<div class="text-2xl font-bold">No runs found</div>
|
||||
<div class="text-sm">There were no completed runs that match your query</div>
|
||||
<div class="text-sm"
|
||||
>Note that new runs might take a while to become searchable (by default
|
||||
~5min)</div
|
||||
>
|
||||
{#if !$enterpriseLicense}
|
||||
<div class="py-6" />
|
||||
|
||||
<Alert title="This is an EE feature" type="warning">
|
||||
Full-text search on jobs is only available on EE.
|
||||
</Alert>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.quick-search-input {
|
||||
outline: none;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.quick-search-input:focus-visible {
|
||||
outline: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,87 @@
|
||||
<script lang="ts">
|
||||
import { isMac } from '$lib/utils'
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let hovered: boolean = false
|
||||
export let id: string
|
||||
export let label: string = ''
|
||||
export let icon: any = undefined
|
||||
export let shortcutKey: string | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
async function handleKeydown(event: KeyboardEvent) {
|
||||
if (hovered && event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
runAction()
|
||||
}
|
||||
}
|
||||
|
||||
function runAction() {
|
||||
dispatch('select')
|
||||
}
|
||||
export let kbdClass = ''
|
||||
export let small = true
|
||||
if (small) {
|
||||
kbdClass = twMerge(
|
||||
kbdClass,
|
||||
'!text-[10px] px-1',
|
||||
false && isMac() ? '!text-lg ' : 'text-xs',
|
||||
'leading-none'
|
||||
)
|
||||
} else {
|
||||
kbdClass += ' !text-xs px-1.5'
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
{id}
|
||||
on:click|stopPropagation={runAction}
|
||||
on:mouseenter={() => dispatch('hover')}
|
||||
class={`rounded-md w-full ${hovered ? 'bg-surface-hover' : ''}`}
|
||||
>
|
||||
{#if $$slots.itemReplacement}
|
||||
<slot name="itemReplacement" />
|
||||
{:else}
|
||||
<div class="flex flex-row gap-2 items-center px-1 py-0.5 rounded-md pr-6 font-light">
|
||||
<div class="w-4">
|
||||
{#if icon}
|
||||
<svelte:component this={icon} size={14} />
|
||||
{:else if shortcutKey != undefined}
|
||||
<div class="font-bold flex items-center justify-center w-full">
|
||||
<span
|
||||
class="h-4 center-center ml-0.5 rounded border bg-surface-secondary text-primary shadow-sm font-light transition-all group-hover:border-primary-500 group-hover:text-primary-inverse"
|
||||
>
|
||||
<kbd class={kbdClass}>
|
||||
{shortcutKey}
|
||||
</kbd>
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{label}
|
||||
{#if shortcutKey != undefined}
|
||||
<div class="ml-auto">
|
||||
<div class="font-bold flex items-center justify-center w-full">
|
||||
<span
|
||||
class="flex h-4 center-center ml-0.5 rounded border bg-surface-secondary text-primary shadow-sm font-light transition-all group-hover:border-primary-500 group-hover:text-primary-inverse"
|
||||
>
|
||||
<kbd class={kbdClass}>
|
||||
{shortcutKey}
|
||||
</kbd>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,17 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import Popover from '../Popover.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let label: string | undefined = undefined
|
||||
export let icon: any | undefined = undefined
|
||||
export let isCollapsed: boolean
|
||||
export let disabled: boolean = false
|
||||
export let lightMode: boolean = false
|
||||
export let stopPropagationOnClick: boolean = false
|
||||
export let shortcut: string = ""
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
{#if !disabled}
|
||||
<Popover appearTimeout={0} disappearTimeout={0} class="w-full" disablePopup={!isCollapsed}>
|
||||
<button
|
||||
on:click={(e) => {
|
||||
if (stopPropagationOnClick) e.preventDefault()
|
||||
dispatch('click')
|
||||
}}
|
||||
class={twMerge(
|
||||
'group flex items-center px-2 py-2 font-light rounded-md h-8 gap-3 w-full',
|
||||
lightMode
|
||||
@@ -46,6 +55,9 @@
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
<span class="pl-2 text-xs dark:text-secondary light:text-secondary-inverse font-semibold">
|
||||
{shortcut}
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
UserService,
|
||||
WorkspaceService
|
||||
} from '$lib/gen'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { classNames, getModifierKey } from '$lib/utils'
|
||||
import WorkspaceMenu from '$lib/components/sidebar/WorkspaceMenu.svelte'
|
||||
import SidebarContent from '$lib/components/sidebar/SidebarContent.svelte'
|
||||
import {
|
||||
@@ -37,14 +37,18 @@
|
||||
import { SUPERADMIN_SETTINGS_HASH, USER_SETTINGS_HASH } from '$lib/components/sidebar/settings'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { syncTutorialsTodos } from '$lib/tutorialUtils'
|
||||
import { ArrowLeft } from 'lucide-svelte'
|
||||
import { ArrowLeft, Search } from 'lucide-svelte'
|
||||
import { getUserExt } from '$lib/user'
|
||||
import { workspacedOpenai } from '$lib/components/copilot/lib'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import OperatorMenu from '$lib/components/sidebar/OperatorMenu.svelte'
|
||||
import GlobalSearchModal from '$lib/components/search/GlobalSearchModal.svelte'
|
||||
import MenuButton from '$lib/components/sidebar/MenuButton.svelte'
|
||||
import { setContext } from 'svelte'
|
||||
|
||||
OpenAPI.WITH_CREDENTIALS = true
|
||||
let menuOpen = false
|
||||
let globalSearchModal: GlobalSearchModal | undefined = undefined
|
||||
let isCollapsed = false
|
||||
let userSettings: UserSettings
|
||||
let superadminSettings: SuperadminSettings
|
||||
@@ -244,6 +248,12 @@
|
||||
$: if (isCollapsed && $userStore?.operator) {
|
||||
isCollapsed = false
|
||||
}
|
||||
|
||||
function openSearchModal(text?: string): void {
|
||||
globalSearchModal?.openSearchWithPrefilledText(text)
|
||||
}
|
||||
|
||||
setContext("openSearchWithPrefilledText", openSearchModal)
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth />
|
||||
@@ -258,6 +268,7 @@
|
||||
</div>
|
||||
</CenteredModal>
|
||||
{:else if $userStore}
|
||||
<GlobalSearchModal bind:this={globalSearchModal} />
|
||||
{#if $superadmin}
|
||||
<SuperadminSettings bind:this={superadminSettings} />
|
||||
{/if}
|
||||
@@ -328,6 +339,15 @@
|
||||
<div class="px-2 py-4 space-y-2 border-y border-gray-500">
|
||||
<WorkspaceMenu />
|
||||
<FavoriteMenu {favoriteLinks} />
|
||||
<MenuButton
|
||||
stopPropagationOnClick={true}
|
||||
on:click={() => openSearchModal()}
|
||||
isCollapsed={false}
|
||||
icon={Search}
|
||||
label="Search"
|
||||
class="!text-xs"
|
||||
shortcut={`${getModifierKey()}k`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SidebarContent isCollapsed={false} />
|
||||
@@ -367,6 +387,15 @@
|
||||
<div class="px-2 py-4 space-y-2 border-y border-gray-700">
|
||||
<WorkspaceMenu {isCollapsed} />
|
||||
<FavoriteMenu {favoriteLinks} {isCollapsed} />
|
||||
<MenuButton
|
||||
stopPropagationOnClick={true}
|
||||
on:click={() => openSearchModal()}
|
||||
{isCollapsed}
|
||||
icon={Search}
|
||||
label="Search"
|
||||
class="!text-xs"
|
||||
shortcut={`${getModifierKey()}k`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SidebarContent {isCollapsed} />
|
||||
@@ -446,6 +475,15 @@
|
||||
<div class="px-2 py-4 space-y-2 border-y border-gray-500">
|
||||
<WorkspaceMenu />
|
||||
<FavoriteMenu {favoriteLinks} />
|
||||
<MenuButton
|
||||
stopPropagationOnClick={true}
|
||||
on:click={() => openSearchModal()}
|
||||
{isCollapsed}
|
||||
icon={Search}
|
||||
label="Search"
|
||||
class="!text-xs"
|
||||
shortcut={`${getModifierKey()}k`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SidebarContent {isCollapsed} />
|
||||
|
||||
Reference in New Issue
Block a user