mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 00:00:46 +00:00
feat: Tag filter on Runs page (#4193)
* Add warning of job search parse error + front fixes * Add filter tag and add filters to small screen * Fill missing property
This commit is contained in:
@@ -186,6 +186,7 @@
|
||||
label={null}
|
||||
folder={null}
|
||||
concurrencyKey={null}
|
||||
tag={null}
|
||||
success="running"
|
||||
argFilter={undefined}
|
||||
bind:loading
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
export let completedJobs: CompletedJob[] | undefined = undefined
|
||||
export let externalJobs: Job[] | undefined = undefined
|
||||
export let concurrencyKey: string | null
|
||||
export let tag: string | null
|
||||
export let extendedJobs: ExtendedJobs | undefined = undefined
|
||||
export let argError = ''
|
||||
export let resultError = ''
|
||||
@@ -58,6 +59,7 @@
|
||||
isSkipped != undefined &&
|
||||
jobKinds &&
|
||||
concurrencyKey &&
|
||||
tag &&
|
||||
lookback &&
|
||||
user &&
|
||||
folder &&
|
||||
@@ -143,6 +145,7 @@
|
||||
? true
|
||||
: undefined,
|
||||
label: label === null || label === '' ? undefined : label,
|
||||
tag: tag === null || tag === '' ? undefined : tag,
|
||||
isNotSchedule: showSchedules == false ? true : undefined,
|
||||
scheduledForBeforeNow: showFutureJobs == false ? true : undefined,
|
||||
args:
|
||||
@@ -190,6 +193,7 @@
|
||||
isSkipped: isSkipped ? undefined : false,
|
||||
isFlowStep: jobKindsCat != 'all' ? false : undefined,
|
||||
label: label === null || label === '' ? undefined : label,
|
||||
tag: tag === null || tag === '' ? undefined : tag,
|
||||
isNotSchedule: showSchedules == false ? true : undefined,
|
||||
scheduledForBeforeNow: showFutureJobs == false ? true : undefined,
|
||||
args:
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
export let path: string | null = null
|
||||
export let label: string | null = null
|
||||
export let concurrencyKey: string | null = null
|
||||
export let tag: string | null = null
|
||||
export let success: 'running' | 'success' | 'failure' | undefined = undefined
|
||||
export let isSkipped: boolean | undefined = undefined
|
||||
export let argFilter: string
|
||||
@@ -37,11 +38,12 @@
|
||||
|
||||
$: displayedLabel = label
|
||||
$: displayedConcurrencyKey = concurrencyKey
|
||||
$: displayedTag = tag
|
||||
|
||||
let copyArgFilter = argFilter
|
||||
let copyResultFilter = resultFilter
|
||||
|
||||
export let filterBy: 'path' | 'user' | 'folder' | 'label' | 'concurrencyKey' = 'path'
|
||||
export let filterBy: 'path' | 'user' | 'folder' | 'label' | 'concurrencyKey' | 'tag' = 'path'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -63,11 +65,15 @@
|
||||
} else if (concurrencyKey !== null && concurrencyKey !== '' && filterBy !== 'concurrencyKey') {
|
||||
manuallySet = true
|
||||
filterBy = 'concurrencyKey'
|
||||
} else if (tag !== null && tag !== '' && filterBy !== 'tag') {
|
||||
manuallySet = true
|
||||
filterBy = 'tag'
|
||||
}
|
||||
}
|
||||
|
||||
let labelTimeout: NodeJS.Timeout | undefined = undefined
|
||||
let concurrencyKeyTimeout: NodeJS.Timeout | undefined = undefined
|
||||
let tagTimeout: NodeJS.Timeout | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="flex gap-4">
|
||||
@@ -94,6 +100,7 @@
|
||||
folder = null
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
} else {
|
||||
manuallySet = false
|
||||
}
|
||||
@@ -105,7 +112,8 @@
|
||||
<ToggleButtonMore
|
||||
togglableItems={[
|
||||
{ label: 'Concurrency key', value: 'concurrencyKey' },
|
||||
{ label: 'Label', value: 'label' }
|
||||
{ label: 'Label', value: 'label' },
|
||||
{ label: 'Tag', value: 'tag' }
|
||||
]}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
@@ -295,6 +303,39 @@
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{:else if filterBy === 'tag'}
|
||||
{#key tag}
|
||||
<div class="relative">
|
||||
{#if tag}
|
||||
<button
|
||||
class="absolute top-2 right-2 z-50"
|
||||
on:click={() => {
|
||||
tag = null
|
||||
dispatch('reset')
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
<span class="text-xs absolute -top-4"> Tag </span>
|
||||
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
class="!h-[32px] py-1 !text-xs !w-64"
|
||||
bind:value={displayedTag}
|
||||
on:keydown={(e) => {
|
||||
if (tagTimeout) {
|
||||
clearTimeout(tagTimeout)
|
||||
}
|
||||
|
||||
tagTimeout = setTimeout(() => {
|
||||
tag = displayedTag
|
||||
}, 1000)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="relative">
|
||||
@@ -383,6 +424,8 @@
|
||||
user = null
|
||||
folder = null
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
} else {
|
||||
manuallySet = false
|
||||
}
|
||||
@@ -391,6 +434,9 @@
|
||||
<ToggleButton value="path" label="Path" />
|
||||
<ToggleButton value="user" label="User" />
|
||||
<ToggleButton value="folder" label="Folder" />
|
||||
<ToggleButton value="concurrencyKey" label="Concurrency" />
|
||||
<ToggleButton value="tag" label="Tag" />
|
||||
<ToggleButton value="label" label="Label" />
|
||||
</ToggleButtonGroup>
|
||||
</Label>
|
||||
|
||||
@@ -415,10 +461,10 @@
|
||||
items={usernames}
|
||||
value={user}
|
||||
bind:selectedItem={user}
|
||||
inputClassName="!h-[32px] py-1 !text-xs !w-64"
|
||||
inputClassName="!h-[32px] py-1 !text-xs !w-80"
|
||||
hideArrow
|
||||
className={user ? '!font-bold' : ''}
|
||||
dropdownClassName="!font-normal !w-64 !max-w-64"
|
||||
dropdownClassName="!font-normal !w-80 !max-w-80"
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
@@ -445,10 +491,10 @@
|
||||
items={folders}
|
||||
value={folder}
|
||||
bind:selectedItem={folder}
|
||||
inputClassName="!h-[32px] py-1 !text-xs !w-64"
|
||||
inputClassName="!h-[32px] py-1 !text-xs !w-80"
|
||||
hideArrow
|
||||
className={folder ? '!font-bold' : ''}
|
||||
dropdownClassName="!font-normal !w-64 !max-w-64"
|
||||
dropdownClassName="!font-normal !w-80 !max-w-80"
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
@@ -483,6 +529,107 @@
|
||||
</div>
|
||||
</Label>
|
||||
{/key}
|
||||
{:else if filterBy === 'tag'}
|
||||
{#key tag}
|
||||
<Label label="Tag">
|
||||
<div class="relative w-full">
|
||||
{#if tag}
|
||||
<button
|
||||
class="absolute top-2 right-2 z-50"
|
||||
on:click={() => {
|
||||
tag = null
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
class="!h-[32px] py-1 !text-xs !w-80"
|
||||
bind:value={displayedTag}
|
||||
on:keydown={(e) => {
|
||||
if (tagTimeout) {
|
||||
clearTimeout(tagTimeout)
|
||||
}
|
||||
|
||||
tagTimeout = setTimeout(() => {
|
||||
tag = displayedTag
|
||||
console.log(tag)
|
||||
}, 1000)
|
||||
}}
|
||||
/>
|
||||
</div></Label
|
||||
>
|
||||
{/key}
|
||||
{:else if filterBy === 'label'}
|
||||
{#key label}
|
||||
<Label label="Label">
|
||||
<div class="relative w-full">
|
||||
{#if label}
|
||||
<button
|
||||
class="absolute top-2 right-2 z-50"
|
||||
on:click={() => {
|
||||
label = null
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
class="!h-[32px] py-1 !text-xs !w-80"
|
||||
bind:value={displayedLabel}
|
||||
on:keydown={(e) => {
|
||||
if (labelTimeout) {
|
||||
clearTimeout(labelTimeout)
|
||||
}
|
||||
|
||||
labelTimeout = setTimeout(() => {
|
||||
label = displayedLabel
|
||||
}, 1000)
|
||||
}}
|
||||
/>
|
||||
</div></Label
|
||||
>
|
||||
{/key}
|
||||
{:else if filterBy === 'concurrencyKey'}
|
||||
{#key concurrencyKey}
|
||||
<Label label="Concurrency Key">
|
||||
<div class="relative w-full">
|
||||
{#if concurrencyKey}
|
||||
<button
|
||||
class="absolute top-2 right-2 z-50"
|
||||
on:click={() => {
|
||||
concurrencyKey = null
|
||||
// dispatch('reset')
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
class="!h-[32px] py-1 !text-xs !w-80"
|
||||
bind:value={displayedConcurrencyKey}
|
||||
on:keydown={(e) => {
|
||||
if (concurrencyKeyTimeout) {
|
||||
clearTimeout(concurrencyKeyTimeout)
|
||||
}
|
||||
|
||||
concurrencyKeyTimeout = setTimeout(() => {
|
||||
concurrencyKey = displayedConcurrencyKey
|
||||
}, 1000)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
<Label label="Kind">
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { clickOutside, displayDateOnly, isMac, sendUserToast } from '$lib/utils'
|
||||
import TimeAgo from '../TimeAgo.svelte'
|
||||
import {
|
||||
AlertTriangle,
|
||||
BoxesIcon,
|
||||
CalendarIcon,
|
||||
Code2Icon,
|
||||
@@ -36,6 +37,7 @@
|
||||
import BarsStaggered from '../icons/BarsStaggered.svelte'
|
||||
import { scroll_into_view_if_needed_polyfill } from '../multiselect/utils'
|
||||
import { Alert } from '../common'
|
||||
import Popover from '../Popover.svelte'
|
||||
|
||||
let open: boolean = false
|
||||
|
||||
@@ -176,7 +178,11 @@
|
||||
let debounceTimeout: any = undefined
|
||||
const debouncePeriod: number = 1000
|
||||
let loadingCompletedRuns: boolean = false
|
||||
let queryParseErrors: string[] = []
|
||||
|
||||
async function handleSearch() {
|
||||
queryParseErrors = []
|
||||
|
||||
if (
|
||||
tab !== 'default' &&
|
||||
(searchTerm === '' ||
|
||||
@@ -231,6 +237,7 @@
|
||||
workspace: $workspaceStore!
|
||||
})
|
||||
itemMap['runs'] = searchResults.hits
|
||||
queryParseErrors = searchResults.query_parse_errors
|
||||
} catch (e) {
|
||||
sendUserToast(e, true)
|
||||
}
|
||||
@@ -325,12 +332,20 @@
|
||||
goto(path)
|
||||
}
|
||||
|
||||
let mouseMoved: boolean = false
|
||||
function handleMouseMove () {
|
||||
mouseMoved = true
|
||||
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
})
|
||||
|
||||
$: searchTerm, handleSearch()
|
||||
@@ -512,12 +527,25 @@
|
||||
>{placeholderFromPrefix(searchTerm)}</label
|
||||
>
|
||||
</div>
|
||||
{#if queryParseErrors.length > 0}
|
||||
<Popover notClickable placement="bottom-start">
|
||||
<AlertTriangle size={16} class="text-yellow-500" />
|
||||
<svelte:fragment slot="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>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="overflow-y-auto relative {maxModalHeight(tab)}">
|
||||
{#if tab === 'default' || tab === 'switch-mode'}
|
||||
{@const items = (itemMap[tab] ?? []).filter((e) => defaultMenuItems.includes(e))}
|
||||
{#if items.length > 0}
|
||||
<div class="p-2 border-b">
|
||||
<div class={tab === 'switch-mode' ? "p-2" : "p-2 border-b"}>
|
||||
{#each items as el}
|
||||
<QuickMenuItem
|
||||
on:select={el?.action}
|
||||
@@ -527,6 +555,7 @@
|
||||
label={el?.label}
|
||||
icon={el?.icon}
|
||||
shortcutKey={el?.shortcutKey}
|
||||
bind:mouseMoved
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -549,6 +578,7 @@
|
||||
el.path +
|
||||
(el.starred ? ' ★' : '')}
|
||||
icon={iconForWindmillItem(el.type)}
|
||||
bind:mouseMoved
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -600,6 +630,7 @@
|
||||
hovered={selectedItem && r?.document.id[0] === selectedItem?.document.id[0]}
|
||||
icon={r?.icon}
|
||||
containerClass="rounded-md px-2 py-1 my-2"
|
||||
bind:mouseMoved
|
||||
>
|
||||
<svelte:fragment slot="itemReplacement">
|
||||
<div
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
export let icon: any = undefined
|
||||
export let shortcutKey: string | undefined = undefined
|
||||
export let containerClass: string | undefined = undefined
|
||||
export let mouseMoved = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -49,7 +50,12 @@
|
||||
<div
|
||||
{id}
|
||||
on:click|stopPropagation={runAction}
|
||||
on:mouseenter={() => dispatch('hover')}
|
||||
on:mouseenter={() => {
|
||||
if (mouseMoved) {
|
||||
dispatch('hover')
|
||||
}
|
||||
mouseMoved=false
|
||||
}}
|
||||
class={twMerge(
|
||||
`rounded-md w-full transition-all cursor-pointer ${
|
||||
hovered ? 'bg-surface-hover' : ''
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
let folder: string | null = $page.url.searchParams.get('folder')
|
||||
let label: string | null = $page.url.searchParams.get('label')
|
||||
let concurrencyKey: string | null = $page.url.searchParams.get('concurrency_key')
|
||||
let tag: string | null = $page.url.searchParams.get('tag')
|
||||
// Rest of filters handled by RunsFilter
|
||||
let success: 'running' | 'success' | 'failure' | undefined = ($page.url.searchParams.get(
|
||||
'success'
|
||||
@@ -125,6 +126,7 @@
|
||||
schedulePath ||
|
||||
jobKindsCat ||
|
||||
concurrencyKey ||
|
||||
tag ||
|
||||
graph ||
|
||||
minTs ||
|
||||
maxTs ||
|
||||
@@ -218,6 +220,12 @@
|
||||
searchParams.delete('concurrency_key')
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
searchParams.set('tag', tag)
|
||||
} else {
|
||||
searchParams.delete('tag')
|
||||
}
|
||||
|
||||
if (label) {
|
||||
searchParams.set('label', label)
|
||||
} else {
|
||||
@@ -287,6 +295,7 @@
|
||||
folder = null
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
}
|
||||
|
||||
function filterByUser(e: CustomEvent<string>) {
|
||||
@@ -295,6 +304,7 @@
|
||||
user = e.detail
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
}
|
||||
|
||||
function filterByFolder(e: CustomEvent<string>) {
|
||||
@@ -303,6 +313,7 @@
|
||||
folder = e.detail
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
}
|
||||
|
||||
function filterByLabel(e: CustomEvent<string>) {
|
||||
@@ -311,6 +322,7 @@
|
||||
folder = null
|
||||
label = e.detail
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
}
|
||||
|
||||
function filterByConcurrencyKey(e: CustomEvent<string>) {
|
||||
@@ -319,6 +331,16 @@
|
||||
folder = null
|
||||
label = null
|
||||
concurrencyKey = e.detail
|
||||
tag = null
|
||||
}
|
||||
|
||||
function filterByTag(e: CustomEvent<string>) {
|
||||
path = null
|
||||
user = null
|
||||
folder = null
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = e.detail
|
||||
}
|
||||
|
||||
let calendarChangeTimeout: NodeJS.Timeout | undefined = undefined
|
||||
@@ -369,7 +391,8 @@
|
||||
? resultFilter
|
||||
: undefined,
|
||||
allWorkspaces: allWorkspaces ? true : undefined,
|
||||
concurrencyKey: concurrencyKey ?? undefined
|
||||
concurrencyKey: concurrencyKey ?? undefined,
|
||||
tag: tag ?? undefined
|
||||
}
|
||||
|
||||
selectedFiltersString = JSON.stringify(selectedFilters, null, 4)
|
||||
@@ -395,7 +418,7 @@
|
||||
}
|
||||
|
||||
const warnJobLimitMsg =
|
||||
'The exact number of concurrent job at the beginning of the time range may be incorrect as only the last 1000 jobs are taken into account: a job that was started earlier than this limit will not be taken into account'
|
||||
'The exact number of concurrent jobs at the beginning of the time range may be incorrect as only the last 1000 jobs are taken into account: a job that was started earlier than this limit will not be taken into account'
|
||||
|
||||
$: warnJobLimit =
|
||||
graph === 'ConcurrencyChart' &&
|
||||
@@ -430,6 +453,7 @@
|
||||
{concurrencyKey}
|
||||
{argError}
|
||||
{resultError}
|
||||
{tag}
|
||||
bind:loading
|
||||
bind:this={jobLoader}
|
||||
lookback={graphIsRunsChart ? 0 : lookback}
|
||||
@@ -521,6 +545,7 @@
|
||||
bind:folder
|
||||
bind:label
|
||||
bind:concurrencyKey
|
||||
bind:tag
|
||||
bind:path
|
||||
bind:success
|
||||
bind:argFilter
|
||||
@@ -815,6 +840,7 @@
|
||||
on:filterByFolder={filterByFolder}
|
||||
on:filterByLabel={filterByLabel}
|
||||
on:filterByConcurrencyKey={filterByConcurrencyKey}
|
||||
on:filterByTag={filterByTag}
|
||||
/>
|
||||
{:else}
|
||||
<div class="gap-1 flex flex-col">
|
||||
@@ -872,6 +898,9 @@
|
||||
bind:folder
|
||||
bind:path
|
||||
bind:user
|
||||
bind:label
|
||||
bind:concurrencyKey
|
||||
bind:tag
|
||||
bind:success
|
||||
bind:argFilter
|
||||
bind:resultFilter
|
||||
@@ -1158,6 +1187,7 @@
|
||||
on:filterByFolder={filterByFolder}
|
||||
on:filterByLabel={filterByLabel}
|
||||
on:filterByConcurrencyKey={filterByConcurrencyKey}
|
||||
on:filterByTag={filterByTag}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user