feat: chart interactivity: click points on the graph to select the corresponding jobs (#3851)
* Make click on RunChart higlight and select job * Select job when clicking on graph point * Fix type of chart data object * Persist selection on refresh and other changes
This commit is contained in:
@@ -22,11 +22,18 @@
|
||||
export let maxIsNow: boolean = false
|
||||
export let minTimeSet: string | undefined = undefined
|
||||
export let maxTimeSet: string | undefined = undefined
|
||||
export let selectedIds: string[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const SUCCESS_COLOR = '#4ade80'
|
||||
// const SUCCESS_COLOR_TRANSPARENT = '#c9b638'
|
||||
const SUCCESS_COLOR_TRANSPARENT = mergeColors(SUCCESS_COLOR, getBackgorundColor(), 0.8)
|
||||
const FAIL_COLOR = '#f87171'
|
||||
const FAIL_COLOR_TRANSPARENT = mergeColors(FAIL_COLOR, getBackgorundColor(), 0.8)
|
||||
|
||||
$: success = jobs?.filter((x) => x.success)
|
||||
$: failed = jobs?.filter((x) => !x.success)
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
@@ -41,11 +48,11 @@
|
||||
)
|
||||
|
||||
$: data = {
|
||||
labels: ['Duration'],
|
||||
datasets: [
|
||||
{
|
||||
// borderColor: 'rgba(99,0,125, .2)',
|
||||
backgroundColor: '#f87171',
|
||||
borderColor: 'rgba(99,0,125, 0)',
|
||||
backgroundColor: FAIL_COLOR as string | string[],
|
||||
radius: 3,
|
||||
label: 'Failed',
|
||||
data:
|
||||
failed?.map((job) => ({
|
||||
@@ -56,8 +63,9 @@
|
||||
})) ?? []
|
||||
},
|
||||
{
|
||||
// borderColor: 'rgba(99,0,125, .2)',
|
||||
backgroundColor: '#4ade80',
|
||||
borderColor: 'rgba(99,0,125, 0)',
|
||||
backgroundColor: SUCCESS_COLOR as string | string[],
|
||||
radius: 3,
|
||||
label: 'Successful',
|
||||
data:
|
||||
success?.map((job) => ({
|
||||
@@ -95,6 +103,58 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getBackgorundColor(): string {
|
||||
const isDark = document.documentElement.classList.contains('dark')
|
||||
|
||||
return isDark ? '#2e3440' : '#ffffff'
|
||||
}
|
||||
function hexToRgb(hexColor: string): number[] {
|
||||
hexColor = hexColor.replace(/^#/, '')
|
||||
|
||||
const r = parseInt(hexColor.substring(0, 2), 16)
|
||||
const g = parseInt(hexColor.substring(2, 4), 16)
|
||||
const b = parseInt(hexColor.substring(4, 6), 16)
|
||||
|
||||
return [r, g, b]
|
||||
}
|
||||
|
||||
function rgbToHex(rgbColor: number[]): string {
|
||||
// Convert RGB components to hexadecimal string
|
||||
return (
|
||||
'#' +
|
||||
rgbColor
|
||||
.map((c) => {
|
||||
const hex = c.toString(16)
|
||||
return hex.length === 1 ? '0' + hex : hex
|
||||
})
|
||||
.join('')
|
||||
)
|
||||
}
|
||||
|
||||
function mergeColors(color1: string, color2: string, slider: number): string {
|
||||
const rgb1 = hexToRgb(color1)
|
||||
const rgb2 = hexToRgb(color2)
|
||||
|
||||
// Blend colors based on percentage
|
||||
const blendedRgb = rgb1.map((c1, i) => Math.round(c1 * (1 - slider) + rgb2[i] * slider))
|
||||
|
||||
return rgbToHex(blendedRgb)
|
||||
}
|
||||
|
||||
function highlightSelectedPoints(ids: string[]) {
|
||||
if (ids.length === 0) {
|
||||
data.datasets[0].backgroundColor = FAIL_COLOR
|
||||
data.datasets[1].backgroundColor = SUCCESS_COLOR
|
||||
} else {
|
||||
data.datasets[0].backgroundColor = data.datasets[0].data.map((p) =>
|
||||
ids.includes(p.id) ? FAIL_COLOR : FAIL_COLOR_TRANSPARENT
|
||||
)
|
||||
data.datasets[1].backgroundColor = data.datasets[1].data.map((p) =>
|
||||
ids.includes(p.id) ? SUCCESS_COLOR : SUCCESS_COLOR_TRANSPARENT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function getPath(x: any): string {
|
||||
return x.path
|
||||
}
|
||||
@@ -176,6 +236,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick: (e, u) => {
|
||||
const ids = u.map((j) => data.datasets[j.datasetIndex].data[j.index].id)
|
||||
selectedIds = ids
|
||||
},
|
||||
|
||||
scales: {
|
||||
x: {
|
||||
@@ -199,6 +263,9 @@
|
||||
},
|
||||
animation: false
|
||||
} as any
|
||||
|
||||
$: data && scatterOptions && highlightSelectedPoints(selectedIds)
|
||||
|
||||
</script>
|
||||
|
||||
<!-- {JSON.stringify(minTime)}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let job: Job
|
||||
export let selectedId: string | undefined = undefined
|
||||
export let selected: boolean = false;
|
||||
export let containerWidth: number = 0
|
||||
export let containsLabel: boolean = false
|
||||
export let activeLabel: string | null
|
||||
@@ -46,7 +46,7 @@
|
||||
<div
|
||||
class={twMerge(
|
||||
'hover:bg-surface-hover cursor-pointer',
|
||||
selectedId === job.id && !isExternal ? 'bg-blue-50 dark:bg-blue-900/50' : '',
|
||||
selected ? 'bg-blue-50 dark:bg-blue-900/50' : '',
|
||||
'flex flex-row items-center h-full'
|
||||
)}
|
||||
style="width: {containerWidth}px"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
export let externalJobs: Job[] = []
|
||||
export let omittedObscuredJobs: boolean
|
||||
export let showExternalJobs: boolean = false
|
||||
export let selectedId: string | undefined = undefined
|
||||
export let selectedIds: string[] = []
|
||||
export let selectedWorkspace: string | undefined = undefined
|
||||
export let activeLabel: string | null = null
|
||||
// const loadMoreQuantity: number = 100
|
||||
@@ -138,7 +138,7 @@
|
||||
|
||||
function jobCountString(jobCount: number) {
|
||||
const jc = jobCount
|
||||
const isTruncated = jc == 1000
|
||||
const isTruncated = jc >= 1000
|
||||
|
||||
return `${jc}${isTruncated ? '+' : ''} job${jc != 1 ? 's' : ''}`
|
||||
}
|
||||
@@ -213,10 +213,10 @@
|
||||
<RunRow
|
||||
{containsLabel}
|
||||
job={jobOrDate.job}
|
||||
{selectedId}
|
||||
selected={jobOrDate.job.id !== '-' && selectedIds.includes(jobOrDate.job.id)}
|
||||
on:select={() => {
|
||||
selectedWorkspace = jobOrDate.job.workspace_id
|
||||
selectedId = jobOrDate.job.id
|
||||
selectedIds = [jobOrDate.job.id]
|
||||
dispatch('select')
|
||||
}}
|
||||
{activeLabel}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
|
||||
let jobs: Job[] | undefined
|
||||
let selectedId: string | undefined = undefined
|
||||
let selectedIds: string[] = []
|
||||
let selectedWorkspace: string | undefined = undefined
|
||||
|
||||
// All Filters
|
||||
@@ -247,7 +247,7 @@
|
||||
jobs = undefined
|
||||
completedJobs = undefined
|
||||
selectedManualDate = 0
|
||||
selectedId = undefined
|
||||
selectedIds = []
|
||||
selectedWorkspace = undefined
|
||||
jobLoader?.loadJobs(minTs, maxTs, true)
|
||||
}
|
||||
@@ -384,11 +384,11 @@
|
||||
|
||||
<Drawer bind:this={runDrawer}>
|
||||
<DrawerContent title="Run details" on:close={runDrawer.closeDrawer}>
|
||||
{#if selectedId}
|
||||
{#if selectedId === '-'}
|
||||
{#if selectedIds.length === 1}
|
||||
{#if selectedIds[0] === '-'}
|
||||
<div class="p-4">There is no information available for this job</div>
|
||||
{:else}
|
||||
<JobPreview blankLink id={selectedId} workspace={selectedWorkspace} />
|
||||
<JobPreview blankLink id={selectedIds[0]} workspace={selectedWorkspace} />
|
||||
{/if}
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
@@ -464,6 +464,7 @@
|
||||
</div>
|
||||
{#if graph === 'RunChart'}
|
||||
<RunChart
|
||||
bind:selectedIds
|
||||
minTimeSet={minTs}
|
||||
maxTimeSet={maxTs}
|
||||
maxIsNow={maxTs == undefined}
|
||||
@@ -602,7 +603,7 @@
|
||||
omittedObscuredJobs={extendedJobs?.omitted_obscured_jobs ?? false}
|
||||
showExternalJobs={!graphIsRunsChart}
|
||||
activeLabel={label}
|
||||
bind:selectedId
|
||||
bind:selectedIds
|
||||
bind:selectedWorkspace
|
||||
on:filterByPath={filterByPath}
|
||||
on:filterByUser={filterByUser}
|
||||
@@ -619,16 +620,20 @@
|
||||
{/if}
|
||||
</Pane>
|
||||
<Pane size={40} minSize={15} class="border-t">
|
||||
{#if selectedId}
|
||||
{#if selectedId === '-'}
|
||||
{#if selectedIds.length === 1}
|
||||
{#if selectedIds[0] === '-'}
|
||||
<div class="p-4">There is no information available for this job</div>
|
||||
{:else}
|
||||
<JobPreview
|
||||
on:filterByConcurrencyKey={filterByConcurrencyKey}
|
||||
id={selectedId}
|
||||
id={selectedIds[0]}
|
||||
workspace={selectedWorkspace}
|
||||
/>
|
||||
{/if}
|
||||
{:else if selectedIds.length > 1}
|
||||
<div class="text-xs m-4"
|
||||
>There are {selectedIds.length} jobs selected. Choose 1 to see detailed information</div
|
||||
>
|
||||
{:else}
|
||||
<div class="text-xs m-4">No job selected</div>
|
||||
{/if}
|
||||
@@ -689,6 +694,7 @@
|
||||
</div>
|
||||
{#if graph === 'RunChart'}
|
||||
<RunChart
|
||||
bind:selectedIds
|
||||
minTimeSet={minTs}
|
||||
maxTimeSet={maxTs}
|
||||
maxIsNow={maxTs == undefined}
|
||||
@@ -828,7 +834,7 @@
|
||||
externalJobs={externalJobs ?? []}
|
||||
omittedObscuredJobs={extendedJobs?.omitted_obscured_jobs ?? false}
|
||||
showExternalJobs={!graphIsRunsChart}
|
||||
bind:selectedId
|
||||
bind:selectedIds
|
||||
bind:selectedWorkspace
|
||||
on:select={() => {
|
||||
runDrawer.openDrawer()
|
||||
|
||||
Reference in New Issue
Block a user