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:
wendrul
2024-05-30 19:26:11 +02:00
committed by GitHub
parent 3c9e8bc4d0
commit 78d704b2d6
4 changed files with 94 additions and 21 deletions
+72 -5
View File
@@ -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()