Skip to content

Commit b17f348

Browse files
committed
[web-console] Make pipelines table header sticky
Signed-off-by: Karakatiza666 <bulakh.96@gmail.com>
1 parent b5f753e commit b17f348

6 files changed

Lines changed: 297 additions & 258 deletions

File tree

js-packages/web-console/src/lib/components/layout/AppHeader.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
const healthStatus = useClusterHealth()
1616
</script>
1717

18-
<div class="flex w-full flex-row items-center justify-between gap-4 px-2 py-2 md:px-8">
18+
<div class="flex flex-row items-center justify-between gap-4 px-2 py-2 md:px-8">
1919
<a class="py-3 lg:pt-2 lg:pr-6 lg:pb-4" href={resolve('/')}>
2020
<span class="hidden lg:block">
2121
{#if darkMode.current === 'dark'}

js-packages/web-console/src/lib/components/pipelines/Table.svelte

Lines changed: 190 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,18 @@
2020
2121
let {
2222
pipelines,
23+
header,
2324
preHeaderEnd,
2425
selectedPipelines = $bindable()
25-
}: { pipelines: PipelineThumb[]; preHeaderEnd?: Snippet; selectedPipelines: string[] } = $props()
26+
}: {
27+
pipelines: PipelineThumb[]
28+
header?: Snippet
29+
preHeaderEnd?: Snippet
30+
selectedPipelines: string[]
31+
} = $props()
32+
33+
let controlsHeight = $state(0)
34+
2635
const pipelinesWithLastChange = $derived(
2736
pipelines.map((pipeline) => ({
2837
...pipeline,
@@ -78,165 +87,191 @@
7887
const td = 'py-1 text-base border-t-[0.5px]'
7988
</script>
8089

81-
<div class="relative mb-2 flex flex-col items-stretch gap-2 sm:flex-row sm:items-end sm:justify-end sm:gap-4 lg:-mt-7 lg:mb-0">
82-
<input
83-
data-testid="input-pipeline-search"
84-
class="input h-9 sm:w-60"
85-
type="search"
86-
placeholder="Search pipelines..."
87-
oninput={(e) => {
88-
nameSearch = e.currentTarget.value
89-
}}
90-
/>
91-
<select
92-
data-testid="select-pipeline-status"
93-
class="h_-9 select sm:w-40"
94-
onchange={(e) => {
95-
statusFilter.value = filterStatuses.find((v) => e.currentTarget.value === v[0])![0]
96-
statusFilter.set()
97-
}}
98-
>
99-
{#each filterStatuses as filter (filter[0])}
100-
<option value={filter[0]}>{filter[0]}</option>
101-
{/each}
102-
</select>
103-
{@render preHeaderEnd?.()}
104-
</div>
105-
<Datatable headless {table}>
106-
<table class="p-1">
107-
<thead>
108-
<tr>
109-
<th class="w-10 px-2 text-left"
110-
><input
111-
class="checkbox"
112-
type="checkbox"
113-
checked={table.isAllSelected}
114-
onclick={() => table.selectAll()}
115-
/></th
116-
>
117-
<ThSort class="px-1 py-1" {table} field="name"
118-
><span class="text-base font-normal text-surface-950-50">Pipeline name</span></ThSort
119-
>
120-
<th class="px-1 py-1 text-left"
121-
><span class="text-base font-normal text-surface-950-50">Storage</span></th
122-
>
123-
<ThSort {table} class="px-1 py-1" field="status"
124-
><span class="ml-8 text-base font-normal text-surface-950-50">Status</span></ThSort
125-
>
126-
<th class="px-1 py-1 text-left"
127-
><span class="text-base font-normal text-surface-950-50">Message</span></th
128-
>
129-
<ThSort
130-
{table}
131-
class="w-20 py-1 pr-4 text-right xl:w-32"
132-
field={(p) => p.connectors?.numErrors}
90+
<div class="pipeline-table-wrapper bg-white-dark">
91+
<div class="bg-white-dark sticky top-0 z-10 pb-2" bind:clientHeight={controlsHeight}>
92+
<div class="sticky left-0 max-w-[100cqi] px-2 md:px-8">
93+
{#if header}
94+
{@render header()}
95+
{/if}
96+
<div
97+
class="relative mt-2 flex items-stretch gap-2 flex-row sm:items-end sm:justify-end sm:gap-4"
98+
class:lg:-mt-7={!!header}
99+
class:lg:mb-0={!!header}
100+
>
101+
<input
102+
data-testid="input-pipeline-search"
103+
class="input h-9 sm:w-60"
104+
type="search"
105+
placeholder="Search pipelines..."
106+
oninput={(e) => {
107+
nameSearch = e.currentTarget.value
108+
}}
109+
/>
110+
<select
111+
data-testid="select-pipeline-status"
112+
class="h_-9 select sm:w-40"
113+
onchange={(e) => {
114+
statusFilter.value = filterStatuses.find((v) => e.currentTarget.value === v[0])![0]
115+
statusFilter.set()
116+
}}
133117
>
134-
<span class="text-base font-normal text-surface-950-50">
135-
<span class="inline xl:hidden">Errors</span>
136-
<span class="hidden xl:!inline">Runtime errors</span>
137-
</span>
138-
</ThSort>
139-
<ThSort {table} class="w-20 px-1 py-1 xl:w-32" field="platformVersion">
140-
<span class="text-base font-normal text-surface-950-50">
141-
Runtime <span class="hidden xl:!inline">version</span>
142-
</span>
143-
</ThSort>
144-
<ThSort {table} class="px-1 py-1" field="lastStatusSince"
145-
><span class="text-base font-normal text-surface-950-50">Status changed</span></ThSort
146-
>
147-
<ThSort {table} class="px-1 py-1" field="deploymentResourcesStatusSince"
148-
><span class="text-base font-normal text-surface-950-50">Deployed on</span></ThSort
149-
>
150-
</tr>
151-
</thead>
152-
<tbody>
153-
{#each table.rows as pipeline}
154-
<tr class="group" data-testid="box-row-{pipeline.name}"
155-
><td class="{td} border-surface-100-900 px-2 group-hover:bg-surface-50-950">
156-
<input
118+
{#each filterStatuses as filter (filter[0])}
119+
<option value={filter[0]}>{filter[0]}</option>
120+
{/each}
121+
</select>
122+
{@render preHeaderEnd?.()}
123+
</div>
124+
</div>
125+
</div>
126+
<Datatable headless {table}>
127+
<table class="md:px-6">
128+
<thead style="top: {controlsHeight}px; z-index: 1;">
129+
<tr>
130+
<th class="w-10 px-2 text-left"
131+
><input
157132
class="checkbox"
158133
type="checkbox"
159-
checked={table.selected.includes(pipeline.name)}
160-
onclick={() => table.select(pipeline.name)}
161-
/>
162-
</td>
163-
<td class="{td} relative w-3/12 border-surface-100-900 group-hover:bg-surface-50-950"
164-
><a
165-
class=" absolute top-2 w-full overflow-hidden overflow-ellipsis whitespace-nowrap"
166-
href="/pipelines/{pipeline.name}/">{pipeline.name}</a
167-
></td
134+
checked={table.isAllSelected}
135+
onclick={() => table.selectAll()}
136+
/></th
168137
>
169-
<td class="{td} relative w-12 border-surface-100-900 group-hover:bg-surface-50-950">
170-
<div
171-
class="fd {pipeline.storageStatus === 'Cleared'
172-
? 'fd-database-off text-surface-500'
173-
: 'fd-database'} text-center text-[20px]"
174-
></div>
175-
<Tooltip
176-
>{match(pipeline.storageStatus)
177-
.with('InUse', () => 'Storage in use')
178-
.with('Clearing', () => 'Clearing storage')
179-
.with('Cleared', () => 'Storage cleared')
180-
.exhaustive()}</Tooltip
181-
>
182-
</td>
183-
<td class="pr-2 {td} w-36 border-surface-100-900 group-hover:bg-surface-50-950"
184-
><PipelineStatus status={pipeline.status}></PipelineStatus></td
138+
<ThSort class="px-1 py-1" {table} field="name"
139+
><span class="text-base font-normal text-surface-950-50">Pipeline name</span></ThSort
185140
>
186-
<td
187-
class="{td} relative border-surface-100-900 whitespace-pre-wrap group-hover:bg-surface-50-950"
141+
<th class="px-1 py-1 text-left"
142+
><span class="text-base font-normal text-surface-950-50">Storage</span></th
188143
>
189-
<span
190-
class="absolute top-1.5 w-full overflow-hidden align-middle overflow-ellipsis whitespace-nowrap"
191-
>
192-
{#if pipeline.deploymentError}
193-
{@const message = pipeline.deploymentError.message}
194-
<span class="fd fd-circle-alert pr-2 text-[20px] text-error-500"></span>
195-
<Popover class="z-10" strategy="fixed">
196-
<div
197-
class="scrollbar flex max-h-[50vh] max-w-[80vw] overflow-auto whitespace-pre-wrap"
198-
>
199-
{message}
200-
</div>
201-
</Popover>
202-
{message.slice(0, ((idx) => (idx > 0 ? idx : undefined))(message.indexOf('\n')))}
203-
{/if}
144+
<ThSort {table} class="px-1 py-1" field="status"
145+
><span class="ml-8 text-base font-normal text-surface-950-50">Status</span></ThSort
146+
>
147+
<th class="px-1 py-1 text-left"
148+
><span class="text-base font-normal text-surface-950-50">Message</span></th
149+
>
150+
<ThSort
151+
{table}
152+
class="w-20 py-1 pr-4 text-right xl:w-32"
153+
field={(p) => p.connectors?.numErrors}
154+
>
155+
<span class="text-base font-normal text-surface-950-50">
156+
<span class="inline xl:hidden">Errors</span>
157+
<span class="hidden xl:!inline">Runtime errors</span>
204158
</span>
205-
</td>
206-
<td class="{td} border-surface-100-900 pr-4 group-hover:bg-surface-50-950">
207-
<div class="text-right text-nowrap">
208-
{pipeline.connectors?.numErrors ?? '-'}
209-
</div>
210-
</td>
211-
<td class="{td} relative border-surface-100-900 group-hover:bg-surface-50-950">
212-
<div class="flex w-full flex-nowrap items-center gap-2 text-nowrap">
213-
<PipelineVersion
214-
pipelineName={pipeline.name}
215-
runtimeVersion={pipeline.platformVersion}
216-
baseRuntimeVersion={page.data.feldera!.version}
217-
configuredRuntimeVersion={pipeline.programConfig.runtime_version}
218-
></PipelineVersion>
219-
</div>
220-
</td>
221-
<td class="{td} relative w-28 border-surface-100-900 group-hover:bg-surface-50-950">
222-
<div class="w-32 text-right text-nowrap">
223-
{formatElapsedTime(pipeline.lastStatusSince, 'dhm')} ago
224-
</div>
225-
</td>
226-
<td class="{td} relative w-40 border-surface-100-900 group-hover:bg-surface-50-950">
227-
<div class="pr-1 text-right text-nowrap">
228-
{pipeline.deploymentResourcesStatus === 'Provisioned'
229-
? formatDateTime(pipeline.deploymentResourcesStatusSince)
230-
: ''}
231-
</div>
232-
</td>
233-
</tr>
234-
{:else}
235-
<tr>
236-
<td class={td}></td>
237-
<td class={td} colspan={99}>No pipelines found</td>
159+
</ThSort>
160+
<ThSort {table} class="w-20 px-1 py-1 xl:w-32" field="platformVersion">
161+
<span class="text-base font-normal text-surface-950-50">
162+
Runtime <span class="hidden xl:!inline">version</span>
163+
</span>
164+
</ThSort>
165+
<ThSort {table} class="px-1 py-1" field="lastStatusSince"
166+
><span class="text-base font-normal text-surface-950-50">Status changed</span></ThSort
167+
>
168+
<ThSort {table} class="px-1 py-1" field="deploymentResourcesStatusSince"
169+
><span class="text-base font-normal text-surface-950-50">Deployed on</span></ThSort
170+
>
238171
</tr>
239-
{/each}
240-
</tbody>
241-
</table>
242-
</Datatable>
172+
</thead>
173+
<tbody>
174+
{#each table.rows as pipeline}
175+
<tr class="group" data-testid="box-row-{pipeline.name}"
176+
><td class="{td} border-surface-100-900 px-2 group-hover:bg-surface-50-950">
177+
<input
178+
class="checkbox"
179+
type="checkbox"
180+
checked={table.selected.includes(pipeline.name)}
181+
onclick={() => table.select(pipeline.name)}
182+
/>
183+
</td>
184+
<td class="{td} relative w-3/12 border-surface-100-900 group-hover:bg-surface-50-950"
185+
><a
186+
class=" absolute top-2 w-full overflow-hidden overflow-ellipsis whitespace-nowrap"
187+
href="/pipelines/{pipeline.name}/">{pipeline.name}</a
188+
></td
189+
>
190+
<td class="{td} relative w-12 border-surface-100-900 group-hover:bg-surface-50-950">
191+
<div
192+
class="fd {pipeline.storageStatus === 'Cleared'
193+
? 'fd-database-off text-surface-500'
194+
: 'fd-database'} text-center text-[20px]"
195+
></div>
196+
<Tooltip
197+
>{match(pipeline.storageStatus)
198+
.with('InUse', () => 'Storage in use')
199+
.with('Clearing', () => 'Clearing storage')
200+
.with('Cleared', () => 'Storage cleared')
201+
.exhaustive()}</Tooltip
202+
>
203+
</td>
204+
<td class="pr-2 {td} w-36 border-surface-100-900 group-hover:bg-surface-50-950"
205+
><PipelineStatus status={pipeline.status}></PipelineStatus></td
206+
>
207+
<td
208+
class="{td} relative border-surface-100-900 whitespace-pre-wrap group-hover:bg-surface-50-950"
209+
>
210+
<span
211+
class="absolute top-1.5 w-full overflow-hidden align-middle overflow-ellipsis whitespace-nowrap"
212+
>
213+
{#if pipeline.deploymentError}
214+
{@const message = pipeline.deploymentError.message}
215+
<span class="fd fd-circle-alert pr-2 text-[20px] text-error-500"></span>
216+
<Popover class="z-10" strategy="fixed">
217+
<div
218+
class="scrollbar flex max-h-[50vh] max-w-[80vw] overflow-auto whitespace-pre-wrap"
219+
>
220+
{message}
221+
</div>
222+
</Popover>
223+
{message.slice(0, ((idx) => (idx > 0 ? idx : undefined))(message.indexOf('\n')))}
224+
{/if}
225+
</span>
226+
</td>
227+
<td class="{td} border-surface-100-900 pr-4 group-hover:bg-surface-50-950">
228+
<div class="text-right text-nowrap">
229+
{pipeline.connectors?.numErrors ?? '-'}
230+
</div>
231+
</td>
232+
<td class="{td} relative border-surface-100-900 group-hover:bg-surface-50-950">
233+
<div class="flex w-full flex-nowrap items-center gap-2 text-nowrap">
234+
<PipelineVersion
235+
pipelineName={pipeline.name}
236+
runtimeVersion={pipeline.platformVersion}
237+
baseRuntimeVersion={page.data.feldera!.version}
238+
configuredRuntimeVersion={pipeline.programConfig.runtime_version}
239+
></PipelineVersion>
240+
</div>
241+
</td>
242+
<td class="{td} relative w-28 border-surface-100-900 group-hover:bg-surface-50-950">
243+
<div class="w-32 text-right text-nowrap">
244+
{formatElapsedTime(pipeline.lastStatusSince, 'dhm')} ago
245+
</div>
246+
</td>
247+
<td class="{td} relative w-40 border-surface-100-900 group-hover:bg-surface-50-950">
248+
<div class="pr-1 text-right text-nowrap">
249+
{pipeline.deploymentResourcesStatus === 'Provisioned'
250+
? formatDateTime(pipeline.deploymentResourcesStatusSince)
251+
: ''}
252+
</div>
253+
</td>
254+
</tr>
255+
{:else}
256+
<tr>
257+
<td class={td}></td>
258+
<td class={td} colspan={99}>No pipelines found</td>
259+
</tr>
260+
{/each}
261+
</tbody>
262+
</table>
263+
</Datatable>
264+
</div>
265+
266+
<style>
267+
.pipeline-table-wrapper {
268+
width: fit-content;
269+
min-width: 100%;
270+
}
271+
.pipeline-table-wrapper :global(article.thin-scrollbar) {
272+
overflow: visible !important;
273+
}
274+
.pipeline-table-wrapper :global(table) {
275+
background: inherit;
276+
}
277+
</style>

js-packages/web-console/src/lib/components/pipelines/list/PipelineStatus.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
const chipClass = $derived(pipelineStatusColor(status).chip)
88
</script>
99

10-
<div class={'chip w-28 uppercase ' + chipClass + ' ' + _class}>
10+
<div class={'chip w-28 uppercase transition-none ' + chipClass + ' ' + _class}>
1111
{getPipelineStatusLabel(status)}
1212
</div>

js-packages/web-console/src/routes/(system)/(authenticated)/+layout.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@
120120
ignoreAfterNavigate={() => false}
121121
></SvelteKitTopLoader>
122122
<div
123-
class="flex h-full w-full flex-col {api.isNetworkHealthy
123+
class="flex h-full flex-col {api.isNetworkHealthy
124124
? ''
125125
: 'disabled pointer-events-auto select-text [&_.monaco-editor-background]:pointer-events-none [&_[role="button"]]:pointer-events-none [&_[role="separator"]]:pointer-events-none [&_a]:pointer-events-none [&_button]:pointer-events-none'}"
126126
style={api.isNetworkHealthy ? '' : ''}

0 commit comments

Comments
 (0)