mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-08 08:02:27 +00:00
feat(frontend): Filter runs by user (#2187)
* feat(frontend): Filter runs by user * feat(frontend): Improve UI * feat(frontend): Add filter by folder * feat(frontend): Clean up filters * feat(frontend): Clean up filters * feat(frontend): add shortcut to filter by folder * fix(frontend): Fix filters * fix(frontend): remove logs * fix(frontend): Fix filter by syncing * fix(frontend): refactor all filters * fix(frontend): refactor all filters * fix(frontend): refactor all filters * fix(frontend): refactor all filters * fix(frontend): add missing tooltips * fix(frontend): correcly set args and result filters + handle status filter
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
|
||||
import { Info } from 'lucide-svelte'
|
||||
|
||||
export let value: any
|
||||
export let label: string | undefined = undefined
|
||||
export let iconOnly: boolean = false
|
||||
@@ -14,6 +16,7 @@
|
||||
export let selectedColor: string = '#3b82f6'
|
||||
export let small: boolean = false
|
||||
export let iconProps: Record<string, any> = {}
|
||||
export let showTooltipIcon: boolean = false
|
||||
|
||||
const { select, selected } = getContext<ToggleButtonContext>('ToggleButtonGroup')
|
||||
</script>
|
||||
@@ -45,6 +48,9 @@
|
||||
{#if label && !iconOnly}
|
||||
{label}
|
||||
{/if}
|
||||
{#if showTooltipIcon}
|
||||
<Info size={14} class="text-gray-400" />
|
||||
{/if}
|
||||
</Tab>
|
||||
<svelte:fragment slot="text">
|
||||
{tooltip}
|
||||
|
||||
@@ -8,7 +8,16 @@
|
||||
import ScheduleEditor from '../ScheduleEditor.svelte'
|
||||
import Row from '../table/Row.svelte'
|
||||
import Cell from '../table/Cell.svelte'
|
||||
import { Calendar, Check, FastForward, Hourglass, ListFilter, Play, X } from 'lucide-svelte'
|
||||
import {
|
||||
Calendar,
|
||||
Check,
|
||||
FastForward,
|
||||
Folder,
|
||||
Hourglass,
|
||||
ListFilter,
|
||||
Play,
|
||||
X
|
||||
} from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import TimeAgo from '../TimeAgo.svelte'
|
||||
import { forLater } from '$lib/forLater'
|
||||
@@ -101,7 +110,7 @@
|
||||
{#if job === undefined}
|
||||
No job found
|
||||
{:else}
|
||||
<div class="flex flex-row space-x-2">
|
||||
<div class="flex flex-row gap-1">
|
||||
<div class="whitespace-nowrap text-xs font-semibold">
|
||||
{#if job.script_path}
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
@@ -115,6 +124,20 @@
|
||||
>
|
||||
<ListFilter size={10} />
|
||||
</Button>
|
||||
{#if job.script_path?.startsWith('f/')}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
// split script_path by / and get the second element
|
||||
const folder = job.script_path?.split('/')[1]
|
||||
|
||||
dispatch('filterByFolder', folder)
|
||||
}}
|
||||
>
|
||||
<Folder size={10} />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if 'job_kind' in job && job.job_kind == 'preview'}
|
||||
<a href="/run/{job.id}?workspace={job.workspace_id}">Preview without path </a>
|
||||
@@ -158,7 +181,18 @@
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
{job.created_by}
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
{job.created_by}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
dispatch('filterByUser', job.created_by)
|
||||
}}
|
||||
>
|
||||
<ListFilter size={10} />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</Cell>
|
||||
</Row>
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Button, Popup } from '../common'
|
||||
import { page } from '$app/stores'
|
||||
import { setQuery } from '$lib/navigation'
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import Tooltip from '../Tooltip.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import AutoComplete from 'simple-svelte-autocomplete'
|
||||
import { ChevronDown, Filter, X } from 'lucide-svelte'
|
||||
import JsonEditor from '../apps/editor/settingsPanel/inputEditor/JsonEditor.svelte'
|
||||
import Toggle from '../Toggle.svelte'
|
||||
|
||||
export let paths: string[] = []
|
||||
export let selectedPath: string | undefined = undefined
|
||||
// Filters
|
||||
export let path: string | null = null
|
||||
export let success: boolean | undefined = undefined
|
||||
export let isSkipped: boolean | undefined = undefined
|
||||
export let argFilter: string
|
||||
@@ -19,87 +17,187 @@
|
||||
export let resultFilter: string
|
||||
export let resultError: string
|
||||
export let jobKindsCat: string
|
||||
export let user: string | null = null
|
||||
export let folder: string | null = null
|
||||
|
||||
// Autocomplete data
|
||||
export let paths: string[] = []
|
||||
export let usernames: string[] = []
|
||||
export let folders: string[] = []
|
||||
|
||||
let copyArgFilter = argFilter
|
||||
let copyResultFilter = resultFilter
|
||||
|
||||
export let filterBy: 'path' | 'user' | 'folder' = 'path'
|
||||
|
||||
let manuallySet = false
|
||||
|
||||
$: {
|
||||
if (path !== null && path !== '' && filterBy !== 'path') {
|
||||
manuallySet = true
|
||||
filterBy = 'path'
|
||||
} else if (user !== null && user !== '' && filterBy !== 'user') {
|
||||
manuallySet = true
|
||||
filterBy = 'user'
|
||||
} else if (folder !== null && folder !== '' && filterBy !== 'folder') {
|
||||
manuallySet = true
|
||||
filterBy = 'folder'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-start gap-6 xl:gap-2 xl:flex-row mt-4 xl:mt-0">
|
||||
{#key selectedPath}
|
||||
<div class="flex flex-col xl:flex-row gap-6 xl:gap-2 w-full">
|
||||
<div class="relative">
|
||||
{#if selectedPath}
|
||||
<button
|
||||
class="absolute top-2 right-2 z-50"
|
||||
on:click={() => {
|
||||
selectedPath = undefined
|
||||
goto('/runs')
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{:else}
|
||||
<ChevronDown class="absolute top-2 right-2" size={14} />
|
||||
{/if}
|
||||
|
||||
<span class="text-xs absolute -top-4">Path</span>
|
||||
|
||||
<AutoComplete
|
||||
items={paths}
|
||||
value={selectedPath}
|
||||
bind:selectedItem={selectedPath}
|
||||
inputClassName="!h-[30px] py-1 !text-xs !w-64"
|
||||
hideArrow
|
||||
className={selectedPath ? '!font-bold' : ''}
|
||||
dropdownClassName="!font-normal !w-64 !max-w-64"
|
||||
/>
|
||||
<span class="text-xs absolute -top-4">Filter by</span>
|
||||
<ToggleButtonGroup
|
||||
bind:selected={filterBy}
|
||||
on:selected={() => {
|
||||
if (!manuallySet) {
|
||||
path = null
|
||||
user = null
|
||||
folder = null
|
||||
} else {
|
||||
manuallySet = false
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="path" label="Path" />
|
||||
<ToggleButton value="user" label="User" />
|
||||
<ToggleButton value="folder" label="Folder" />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
{#if filterBy == 'user'}
|
||||
{#key user}
|
||||
<div class="relative">
|
||||
{#if user}
|
||||
<button
|
||||
class="absolute top-2 right-2 z-50"
|
||||
on:click={() => {
|
||||
user = null
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{:else}
|
||||
<ChevronDown class="absolute top-2 right-2" size={14} />
|
||||
{/if}
|
||||
|
||||
<span class="text-xs absolute -top-4">User</span>
|
||||
<AutoComplete
|
||||
items={usernames}
|
||||
value={user}
|
||||
bind:selectedItem={user}
|
||||
inputClassName="!h-[30px] py-1 !text-xs !w-64"
|
||||
hideArrow
|
||||
className={user ? '!font-bold' : ''}
|
||||
dropdownClassName="!font-normal !w-64 !max-w-64"
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
{#if filterBy == 'folder'}
|
||||
{#key folder}
|
||||
<div class="relative">
|
||||
{#if folder}
|
||||
<button
|
||||
class="absolute top-2 right-2 z-50"
|
||||
on:click={() => {
|
||||
folder = null
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{:else}
|
||||
<ChevronDown class="absolute top-2 right-2" size={14} />
|
||||
{/if}
|
||||
|
||||
<span class="text-xs absolute -top-4">Folder</span>
|
||||
|
||||
<AutoComplete
|
||||
items={folders}
|
||||
value={folder}
|
||||
bind:selectedItem={folder}
|
||||
inputClassName="!h-[30px] py-1 !text-xs !w-64"
|
||||
hideArrow
|
||||
className={folder ? '!font-bold' : ''}
|
||||
dropdownClassName="!font-normal !w-64 !max-w-64"
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
{#if filterBy === 'path'}
|
||||
{#key path}
|
||||
<div class="relative">
|
||||
{#if path}
|
||||
<button
|
||||
class="absolute top-2 right-2 z-50"
|
||||
on:click={() => {
|
||||
path = null
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{:else}
|
||||
<ChevronDown class="absolute top-2 right-2" size={14} />
|
||||
{/if}
|
||||
|
||||
<span class="text-xs absolute -top-4">Path</span>
|
||||
|
||||
<AutoComplete
|
||||
items={paths}
|
||||
value={path}
|
||||
bind:selectedItem={path}
|
||||
inputClassName="!h-[30px] py-1 !text-xs !w-64"
|
||||
hideArrow
|
||||
className={path ? '!font-bold' : ''}
|
||||
dropdownClassName="!font-normal !w-64 !max-w-64"
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
<div class="flex flex-col xl:flex-row gap-6 xl:gap-2 w-full">
|
||||
<div class="relative">
|
||||
<span class="text-xs absolute -top-4">Kind</span>
|
||||
<ToggleButtonGroup
|
||||
bind:selected={jobKindsCat}
|
||||
on:selected={(e) => {
|
||||
const url = new URL($page.url)
|
||||
|
||||
url.searchParams.set('job_kinds', e.detail)
|
||||
goto(url)
|
||||
}}
|
||||
>
|
||||
<ToggleButtonGroup bind:selected={jobKindsCat}>
|
||||
<ToggleButton value="all" label="All" />
|
||||
<ToggleButton value="runs" label="Runs" />
|
||||
<ToggleButton value="previews" label="Previews" />
|
||||
<ToggleButton value="dependencies" label="Dependencies" />
|
||||
<ToggleButton
|
||||
value="runs"
|
||||
label="Runs"
|
||||
showTooltipIcon
|
||||
tooltip="Runs are jobs that have no parent jobs (flows are jobs that are parent of the jobs they start), they have been triggered through the UI, a schedule or webhook"
|
||||
/>
|
||||
<ToggleButton
|
||||
value="previews"
|
||||
label="Previews"
|
||||
showTooltipIcon
|
||||
tooltip="Previews are jobs that have been started in the editor as 'Tests'"
|
||||
/>
|
||||
<ToggleButton
|
||||
value="dependencies"
|
||||
label="Deps"
|
||||
showTooltipIcon
|
||||
tooltip="Deploying a script, flow or an app launch a dependency job that create and then attach the lockfile to the deployed item. This mechanism ensure that logic is always executed with the exact same direct and indirect dependencies."
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<span class="text-xs absolute -top-4">Status</span>
|
||||
<ToggleButtonGroup
|
||||
bind:selected={success}
|
||||
on:selected={async () =>
|
||||
await setQuery($page.url, 'success', success === undefined ? success : String(success))}
|
||||
>
|
||||
<ToggleButtonGroup bind:selected={success}>
|
||||
<ToggleButton value={undefined} label="All" />
|
||||
<ToggleButton value={true} label="Success" class="whitespace-nowrap" />
|
||||
<ToggleButton value={false} label="Failure" class="whitespace-nowrap" />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<span class="text-xs absolute -top-4">
|
||||
Flow
|
||||
<Tooltip light>Skipped flows are flows that did an early break</Tooltip></span
|
||||
>
|
||||
<div class="relative w-32">
|
||||
<span class="text-xs absolute -top-4"> Show Skipped Flows </span>
|
||||
|
||||
<ToggleButtonGroup
|
||||
bind:selected={isSkipped}
|
||||
on:selected={async () =>
|
||||
await setQuery(
|
||||
$page.url,
|
||||
'is_skipped',
|
||||
isSkipped === undefined ? isSkipped : String(isSkipped)
|
||||
)}
|
||||
>
|
||||
<ToggleButton value={undefined} label="All" class="whitespace-nowrap" />
|
||||
<ToggleButton value={false} label="Not skipped" class="whitespace-nowrap" />
|
||||
<ToggleButton value={true} label="Skipped" class="whitespace-nowrap" />
|
||||
</ToggleButtonGroup>
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
<Toggle size="xs" bind:checked={isSkipped} />
|
||||
<Tooltip light>Skipped flows are flows that did an early break</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -117,14 +215,26 @@
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<div class="flex flex-col w-72 p-2 gap-2">
|
||||
<span class="text-sm eading-6 font-semibold">Filters</span>
|
||||
<div class="flex flex-row w-full items-center justify-between">
|
||||
<span class="text-sm eading-6 font-semibold">Filters</span>
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
close(null)
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<span class="text-xs leading-6">
|
||||
{`Filter by a json being a subset of the args/result. Try '\{"foo": "bar"\}'`}
|
||||
</span>
|
||||
<span class="text-xs eading-6 font-semibold">Filter by args</span>
|
||||
<JsonEditor on:change bind:error={argError} bind:code={argFilter} />
|
||||
<JsonEditor on:change bind:error={argError} bind:code={copyArgFilter} />
|
||||
<span class="text-xs eading-6 font-semibold">Filter by result</span>
|
||||
<JsonEditor on:change bind:error={resultError} bind:code={resultFilter} />
|
||||
<JsonEditor on:change bind:error={resultError} bind:code={copyResultFilter} />
|
||||
|
||||
<div class="flex flex-row gap-2 justify-between">
|
||||
<Button
|
||||
@@ -133,18 +243,22 @@
|
||||
on:click={() => {
|
||||
argFilter = ''
|
||||
resultFilter = ''
|
||||
close(null)
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
on:click={() => {
|
||||
argFilter = copyArgFilter
|
||||
resultFilter = copyResultFilter
|
||||
close(null)
|
||||
}}
|
||||
>
|
||||
Close
|
||||
Set
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
</Cell>
|
||||
</tr>
|
||||
{#each jobsByDay as job (job.id)}
|
||||
<RunRow {job} bind:selectedId on:select on:filterByPath />
|
||||
<RunRow {job} bind:selectedId on:select on:filterByPath on:filterByUser on:filterByFolder />
|
||||
{/each}
|
||||
{/each}
|
||||
</tbody>
|
||||
|
||||
@@ -360,21 +360,21 @@ export async function copyToClipboard(value?: string, sendToast = true): Promise
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
} else {
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = value;
|
||||
textArea.style.position = "fixed";
|
||||
textArea.style.left = "-999999px";
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = value
|
||||
textArea.style.position = 'fixed'
|
||||
textArea.style.left = '-999999px'
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.body.appendChild(textArea)
|
||||
textArea.select()
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
success = true;
|
||||
document.execCommand('copy')
|
||||
success = true
|
||||
} catch (error) {
|
||||
// ignore (success = false)
|
||||
} finally {
|
||||
textArea.remove();
|
||||
textArea.remove()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { JobService, Job, CompletedJob, ScriptService, FlowService } from '$lib/gen'
|
||||
import { setQueryWithoutLoad } from '$lib/utils'
|
||||
import {
|
||||
JobService,
|
||||
Job,
|
||||
CompletedJob,
|
||||
ScriptService,
|
||||
FlowService,
|
||||
UserService,
|
||||
FolderService
|
||||
} from '$lib/gen'
|
||||
|
||||
import { page } from '$app/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { superadmin, userStore, workspaceStore } from '$lib/stores'
|
||||
import { Button, Drawer, DrawerContent, Skeleton } from '$lib/components/common'
|
||||
import { goto } from '$app/navigation'
|
||||
import RunChart from '$lib/components/RunChart.svelte'
|
||||
|
||||
import JobPreview from '$lib/components/runs/JobPreview.svelte'
|
||||
@@ -23,11 +29,18 @@
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
|
||||
import { tweened, type Tweened } from 'svelte/motion'
|
||||
import { goto } from '$app/navigation'
|
||||
|
||||
let jobs: Job[] | undefined
|
||||
let intervalId: NodeJS.Timer | undefined
|
||||
let selectedId: string | undefined = undefined
|
||||
|
||||
// All Filters
|
||||
// Filter by
|
||||
let path: string | null = $page.params.path
|
||||
let user: string | null = $page.url.searchParams.get('user')
|
||||
let folder: string | null = $page.url.searchParams.get('folder')
|
||||
// Rest of filters handled by RunsFilter
|
||||
let success: boolean | undefined =
|
||||
$page.url.searchParams.get('success') != undefined
|
||||
? $page.url.searchParams.get('success') == 'true'
|
||||
@@ -37,18 +50,54 @@
|
||||
? $page.url.searchParams.get('is_skipped') == 'true'
|
||||
: false
|
||||
|
||||
let argFilter: any = $page.url.searchParams.get('arg') ?? undefined
|
||||
let resultFilter: any = $page.url.searchParams.get('result') ?? undefined
|
||||
let argFilter: any = $page.url.searchParams.get('arg')
|
||||
? JSON.parse(decodeURIComponent($page.url.searchParams.get('arg') ?? '{}'))
|
||||
: undefined
|
||||
let resultFilter: any = $page.url.searchParams.get('result')
|
||||
? JSON.parse(decodeURIComponent($page.url.searchParams.get('result') ?? '{}'))
|
||||
: undefined
|
||||
|
||||
let schedulePath = $page.url.searchParams.get('schedule_path') ?? undefined
|
||||
let jobKindsCat = $page.url.searchParams.get('job_kinds') ?? 'runs'
|
||||
|
||||
// Handled on the main page
|
||||
let minTs = $page.url.searchParams.get('min_ts') ?? undefined
|
||||
let maxTs = $page.url.searchParams.get('max_ts') ?? undefined
|
||||
let schedulePath = $page.url.searchParams.get('schedule_path') ?? undefined
|
||||
|
||||
// This reactive statement is used to sync the url with the current state of the filters
|
||||
$: {
|
||||
let searchParams = new URLSearchParams()
|
||||
|
||||
user && searchParams.set('user', user)
|
||||
folder && searchParams.set('folder', folder)
|
||||
|
||||
if (success !== undefined) {
|
||||
searchParams.set('success', success.toString())
|
||||
}
|
||||
|
||||
if (isSkipped) {
|
||||
searchParams.set('is_skipped', isSkipped.toString())
|
||||
}
|
||||
|
||||
// ArgFilter is an object. Encode it to a string
|
||||
argFilter && searchParams.set('arg', encodeURIComponent(JSON.stringify(argFilter)))
|
||||
resultFilter && searchParams.set('result', encodeURIComponent(JSON.stringify(resultFilter)))
|
||||
schedulePath && searchParams.set('schedule_path', schedulePath)
|
||||
|
||||
jobKindsCat != 'runs' && searchParams.set('job_kinds', jobKindsCat)
|
||||
|
||||
minTs && searchParams.set('min_ts', minTs)
|
||||
maxTs && searchParams.set('max_ts', maxTs)
|
||||
|
||||
let newPath = path ? `/${path}` : '/'
|
||||
let newUrl = `/runs${newPath}?${searchParams.toString()}`
|
||||
|
||||
goto(newUrl, { replaceState: true })
|
||||
}
|
||||
|
||||
let nbOfJobs = 30
|
||||
|
||||
let queue_count: Tweened<number> | undefined = undefined
|
||||
|
||||
$: path = $page.params.path
|
||||
$: jobKindsCat = $page.url.searchParams.get('job_kinds') ?? 'runs'
|
||||
$: jobKinds = computeJobKinds(jobKindsCat)
|
||||
|
||||
function computeJobKinds(jobKindsCat: string | undefined): string {
|
||||
@@ -63,7 +112,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: ($workspaceStore && loadJobs()) || (path && success && isSkipped && jobKinds)
|
||||
$: ($workspaceStore && loadJobs()) ||
|
||||
(path && success && isSkipped && jobKinds && user && folder && minTs && maxTs)
|
||||
|
||||
async function fetchJobs(
|
||||
startedBefore: string | undefined,
|
||||
@@ -74,7 +124,9 @@
|
||||
createdOrStartedBefore: startedBefore,
|
||||
createdOrStartedAfter: startedAfter,
|
||||
schedulePath,
|
||||
scriptPathExact: path === '' ? undefined : path,
|
||||
scriptPathExact: path === null || path === '' ? undefined : path,
|
||||
createdBy: user === null || user === '' ? undefined : user,
|
||||
scriptPathStart: folder === null || folder === '' ? undefined : `f/${folder}/`,
|
||||
jobKinds,
|
||||
success,
|
||||
isSkipped,
|
||||
@@ -110,6 +162,7 @@
|
||||
queue_count = tweened(qc, { duration: 1000 })
|
||||
}
|
||||
}
|
||||
|
||||
async function syncer() {
|
||||
getCount()
|
||||
if (sync && jobs && maxTs == undefined) {
|
||||
@@ -150,6 +203,8 @@
|
||||
onMount(() => {
|
||||
mounted = true
|
||||
loadPaths()
|
||||
loadUsernames()
|
||||
loadFolders()
|
||||
intervalId = setInterval(syncer, 5000)
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
@@ -177,6 +232,18 @@
|
||||
}
|
||||
|
||||
let paths: string[] = []
|
||||
let usernames: string[] = []
|
||||
let folders: string[] = []
|
||||
|
||||
async function loadUsernames(): Promise<void> {
|
||||
usernames = await UserService.listUsernames({ workspace: $workspaceStore! })
|
||||
}
|
||||
|
||||
async function loadFolders(): Promise<void> {
|
||||
folders = await FolderService.listFolders({
|
||||
workspace: $workspaceStore!
|
||||
}).then((x) => x.map((y) => y.name))
|
||||
}
|
||||
|
||||
async function loadPaths() {
|
||||
const npaths_scripts = await ScriptService.listScriptPaths({ workspace: $workspaceStore ?? '' })
|
||||
@@ -184,15 +251,6 @@
|
||||
paths = npaths_scripts.concat(npaths_flows).sort()
|
||||
}
|
||||
|
||||
async function syncTsWithURL(minTs?: string, maxTs?: string) {
|
||||
setQueryWithoutLoad($page.url, [
|
||||
{ key: 'min_ts', value: minTs },
|
||||
{ key: 'max_ts', value: maxTs }
|
||||
])
|
||||
}
|
||||
|
||||
$: syncTsWithURL(minTs, maxTs)
|
||||
|
||||
let completedJobs: CompletedJob[] | undefined = undefined
|
||||
|
||||
function computeCompletedJobs() {
|
||||
@@ -200,15 +258,6 @@
|
||||
jobs?.filter((x) => x.type == 'CompletedJob').map((x) => x as CompletedJob) ?? []
|
||||
}
|
||||
|
||||
let searchPath = ''
|
||||
$: searchPath = path
|
||||
|
||||
$: searchPath && onSearchPathChange()
|
||||
|
||||
function onSearchPathChange() {
|
||||
goto(`/runs/${searchPath}?${$page.url.searchParams.toString()}`)
|
||||
}
|
||||
|
||||
let argError = ''
|
||||
let resultError = ''
|
||||
let filterTimeout: NodeJS.Timeout | undefined = undefined
|
||||
@@ -320,20 +369,19 @@
|
||||
<div class="hidden xl:block">
|
||||
<RunsFilter
|
||||
bind:isSkipped
|
||||
{paths}
|
||||
{jobKindsCat}
|
||||
bind:selectedPath={searchPath}
|
||||
bind:user
|
||||
bind:folder
|
||||
bind:path
|
||||
bind:success
|
||||
bind:argFilter
|
||||
bind:resultFilter
|
||||
bind:argError
|
||||
bind:resultError
|
||||
bind:jobKindsCat
|
||||
on:change={reloadLogsWithoutFilterError}
|
||||
on:clearFilters={() => {
|
||||
minTs = undefined
|
||||
maxTs = undefined
|
||||
autoRefresh = true
|
||||
}}
|
||||
{usernames}
|
||||
{folders}
|
||||
{paths}
|
||||
/>
|
||||
</div>
|
||||
<div class="xl:hidden">
|
||||
@@ -343,19 +391,18 @@
|
||||
<RunsFilter
|
||||
bind:isSkipped
|
||||
{paths}
|
||||
{jobKindsCat}
|
||||
bind:selectedPath={searchPath}
|
||||
{usernames}
|
||||
{folders}
|
||||
bind:jobKindsCat
|
||||
bind:folder
|
||||
bind:path
|
||||
bind:user
|
||||
bind:success
|
||||
bind:argFilter
|
||||
bind:resultFilter
|
||||
bind:argError
|
||||
bind:resultError
|
||||
on:change={reloadLogsWithoutFilterError}
|
||||
on:clearFilters={() => {
|
||||
minTs = undefined
|
||||
maxTs = undefined
|
||||
autoRefresh = true
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</MobileFilters>
|
||||
@@ -370,7 +417,6 @@
|
||||
on:zoom={async (e) => {
|
||||
minTs = e.detail.min.toISOString()
|
||||
maxTs = e.detail.max.toISOString()
|
||||
loadJobs()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -403,7 +449,6 @@
|
||||
label="Min datetimes"
|
||||
on:change={async ({ detail }) => {
|
||||
minTs = new Date(detail).toISOString()
|
||||
loadJobs()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -417,7 +462,6 @@
|
||||
label="Max datetimes"
|
||||
on:change={async ({ detail }) => {
|
||||
maxTs = new Date(detail).toISOString()
|
||||
loadJobs()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -431,6 +475,7 @@
|
||||
on:click={() => {
|
||||
minTs = undefined
|
||||
maxTs = undefined
|
||||
|
||||
autoRefresh = true
|
||||
jobs = undefined
|
||||
completedJobs = undefined
|
||||
@@ -484,7 +529,19 @@
|
||||
bind:selectedId
|
||||
bind:nbOfJobs
|
||||
on:filterByPath={(e) => {
|
||||
searchPath = e.detail
|
||||
user = null
|
||||
folder = null
|
||||
path = e.detail
|
||||
}}
|
||||
on:filterByUser={(e) => {
|
||||
path = null
|
||||
folder = null
|
||||
user = e.detail
|
||||
}}
|
||||
on:filterByFolder={(e) => {
|
||||
path = null
|
||||
user = null
|
||||
folder = e.detail
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
@@ -516,6 +573,21 @@
|
||||
on:select={() => {
|
||||
runDrawer.openDrawer()
|
||||
}}
|
||||
on:filterByPath={(e) => {
|
||||
user = null
|
||||
folder = null
|
||||
path = e.detail
|
||||
}}
|
||||
on:filterByUser={(e) => {
|
||||
path = null
|
||||
folder = null
|
||||
user = e.detail
|
||||
}}
|
||||
on:filterByFolder={(e) => {
|
||||
path = null
|
||||
user = null
|
||||
folder = e.detail
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user