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:
wendrul
2024-08-06 10:31:08 +02:00
committed by GitHub
parent 063af02166
commit ee8a1f6cb0
6 changed files with 229 additions and 10 deletions
@@ -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>