Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script lang="ts" generics="K extends string">
import { fly } from 'svelte/transition'
import type { Snippet } from '$lib/types/svelte'

/**
* A single-cell carousel: shows exactly one of `pages` at a time and slides the
* outgoing and incoming pages past each other when `current` changes. Used to
* give a small popup multiple "pages" (e.g. a menu and a detail view) that
* animate like a phone settings screen, without the caller tracking direction.
*/
let {
current,
pages,
width = 220,
duration = 200,
class: className = ''
}: {
/** Key of the page to show. Must match one of `pages[].key`. */
current: K
/** Pages in navigation order. Index 0 is the root. */
pages: { key: K; content: Snippet }[]
/** Horizontal slide distance, in pixels. */
width?: number
/** Transition duration, in milliseconds. */
duration?: number
class?: string
} = $props()
</script>

<!-- Every page lives in the same grid cell so the outgoing and incoming pages
overlap and slide past each other like carousel slides. The root page (index
0) is anchored to the left and every deeper page enters from the right, so
forward and back navigation animate as mirrored sweeps — no direction state
to track. -->
<div class="grid {className}">
{#each pages as page, i (page.key)}
{#if page.key === current}
{@const x = i === 0 ? -width : width}
<div
in:fly={{ x, duration }}
out:fly={{ x, duration }}
class="col-start-1 row-start-1 flex flex-col"
>
{@render page.content()}
</div>
{/if}
{/each}
</div>
42 changes: 32 additions & 10 deletions js-packages/web-console/src/lib/components/pipelines/Table.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import PipelineStatus from '$lib/components/pipelines/list/PipelineStatus.svelte'
import ThSort from '$lib/components/pipelines/table/ThSort.svelte'
import { useElapsedTime } from '$lib/compositions/common/useElapsedTime'
import { usePipelineManager } from '$lib/compositions/usePipelineManager.svelte'
import { dateMax } from '$lib/functions/common/date'
import { matchesSubstring } from '$lib/functions/common/string'
import { type NamesInUnion, unionName } from '$lib/functions/common/union'
Expand All @@ -16,6 +17,7 @@
} from '$lib/services/pipelineManager'
import type { Snippet } from '$lib/types/svelte'
import PipelineVersion from './table/PipelineVersion.svelte'
import Tags from './table/Tags.svelte'

let {
pipelines,
Expand Down Expand Up @@ -82,6 +84,20 @@
pipelinesWithLastChange.filter((p) => matchesSubstring(p.name, nameSearch))
)

// `knownTags` is the union of tags over every pipeline — the pool the per-row
// tag picker chooses from.
const knownTags = $derived.by(() => {
const tags = new Set<string>()
for (const pipeline of pipelines) {
for (const tag of pipeline.tags) {
tags.add(tag)
}
}
return tags
})

const api = usePipelineManager()

const { formatElapsedTime } = useElapsedTime()
const td = 'py-1 text-base border-t-[0.5px]'
</script>
Expand Down Expand Up @@ -146,6 +162,14 @@
<th class="px-1 py-1 text-left"
><span class="text-base font-normal text-surface-950-50">Message</span></th
>
<th class="px-1 py-1 text-left"
><span class="text-base font-normal text-surface-950-50">Tags</span></th
>
<ThSort {table} class="w-20 px-1 py-1 xl:w-32" field="platformVersion">
<span class="text-base font-normal text-surface-950-50">
Runtime <span class="hidden xl:!inline">version</span>
</span>
</ThSort>
<ThSort
{table}
class="w-20 py-1 pr-4 text-right xl:w-32"
Expand All @@ -156,11 +180,6 @@
<span class="hidden xl:!inline">Runtime errors</span>
</span>
</ThSort>
<ThSort {table} class="w-20 px-1 py-1 xl:w-32" field="platformVersion">
<span class="text-base font-normal text-surface-950-50">
Runtime <span class="hidden xl:!inline">version</span>
</span>
</ThSort>
<ThSort {table} class="px-1 py-1" field="lastStatusSince"
><span class="text-base font-normal text-surface-950-50">Status changed</span></ThSort
>
Expand Down Expand Up @@ -223,11 +242,9 @@
{/if}
</span>
</td>
<td class="{td} border-surface-100-900 pr-4 group-hover:bg-surface-50-950">
<div class="text-right text-nowrap">
{pipeline.connectors?.numErrors ?? '-'}
</div>
</td>
<td class="pr-2 {td} w-36 border-surface-100-900 group-hover:bg-surface-50-950"
><Tags pipelineName={pipeline.name} tags={pipeline.tags} {knownTags} {api}></Tags></td
>
<td class="{td} relative border-surface-100-900 group-hover:bg-surface-50-950">
<div class="flex w-full flex-nowrap items-center gap-2 text-nowrap">
<PipelineVersion
Expand All @@ -238,6 +255,11 @@
></PipelineVersion>
</div>
</td>
<td class="{td} border-surface-100-900 pr-4 group-hover:bg-surface-50-950">
<div class="text-right text-nowrap">
{pipeline.connectors?.numErrors ?? '-'}
</div>
</td>
<td class="{td} relative w-28 border-surface-100-900 group-hover:bg-surface-50-950">
<div class="w-32 text-right text-nowrap">
{formatElapsedTime(pipeline.lastStatusSince, 'dhm')} ago
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script lang="ts">
import { Tooltip } from 'common-ui'
import { fly, slide } from 'svelte/transition'
import { slide } from 'svelte/transition'
import Popup from '$lib/components/common/Popup.svelte'
import SlidingPanels from '$lib/components/common/SlidingPanels.svelte'
import DownloadProgressDisplay from '$lib/components/dialogs/DownloadProgressDisplay.svelte'
import GenericDialog from '$lib/components/dialogs/GenericDialog.svelte'
import { useGlobalDialog } from '$lib/compositions/layout/useGlobalDialog.svelte'
Expand Down Expand Up @@ -163,60 +164,54 @@
transition:slide={{ duration: 100 }}
class="bg-white-dark absolute top-10 right-0 z-30 flex min-w-[220px] flex-col overflow-hidden rounded shadow-md"
>
<!-- Grid with both pages in (1,1) so they overlap during the transition
and slide past each other like carousel slides. Each page has a fixed
direction (page 1 left, page 2 right) so forward/back animate as
mirrored sweeps without tracking direction state. -->
<div class="grid">
{#if pickedFile}
<!-- Confirmation view: opens the viewer tab on a direct click so the
browser preserves user activation through window.open. -->
<div
in:fly={{ x: 220, duration: 200 }}
out:fly={{ x: 220, duration: 200 }}
class="col-start-1 row-start-1 flex flex-col"
>
<div class="flex items-center gap-2 px-2 py-2">
<button class="btn-icon h-7 w-7" onclick={resetUpload} aria-label="Back" title="Back">
<span class="fd fd-chevron-left text-[20px]"></span>
</button>
<span class="min-w-0 flex-1 truncate text-sm" title={pickedFile.name}>
{pickedFile.name}
</span>
</div>
<div class="px-2 pb-2">
<button
class="btn h-8! w-full preset-filled-primary-500"
onclick={() => {
confirmOpenViewer()
close()
}}
data-testid="btn-confirm-view-profile"
>
<span class="fd fd-file-search text-[18px]"></span>
<span>View profile</span>
</button>
</div>
</div>
{:else}
<div
in:fly={{ x: -220, duration: 200 }}
out:fly={{ x: -220, duration: 200 }}
class="col-start-1 row-start-1 flex flex-col"
>
<SupportBundleMenu
bind:collectNewData={collectNewData.value}
onDownload={() => {
downloadData = { ...defaultData, collect: collectNewData.value }
globalDialog.dialog = supportBundleDialog
close()
}}
onFilePicked={handleFilePicked}
/>
</div>
{/if}
</div>
<SlidingPanels
current={pickedFile ? 'confirm' : 'menu'}
pages={[
{ key: 'menu', content: menuPage },
{ key: 'confirm', content: confirmPage }
]}
/>
</div>

{#snippet menuPage()}
<SupportBundleMenu
bind:collectNewData={collectNewData.value}
onDownload={() => {
downloadData = { ...defaultData, collect: collectNewData.value }
globalDialog.dialog = supportBundleDialog
close()
}}
onFilePicked={handleFilePicked}
/>
{/snippet}

<!-- Confirmation view: opens the viewer tab on a direct click so the browser
preserves user activation through window.open. -->
{#snippet confirmPage()}
{#if pickedFile}
<div class="flex items-center gap-2 px-2 py-2">
<button class="btn-icon h-7 w-7" onclick={resetUpload} aria-label="Back" title="Back">
<span class="fd fd-chevron-left text-[20px]"></span>
</button>
<span class="min-w-0 flex-1 truncate text-sm" title={pickedFile.name}>
{pickedFile.name}
</span>
</div>
<div class="px-2 pb-2">
<button
class="btn h-8! w-full preset-filled-primary-500"
onclick={() => {
confirmOpenViewer()
close()
}}
data-testid="btn-confirm-view-profile"
>
<span class="fd fd-file-search text-[18px]"></span>
<span>View profile</span>
</button>
</div>
{/if}
{/snippet}
{/snippet}
</Popup>
<Tooltip placement="top" class="w-[240px] text-wrap">
Expand Down
Loading
Loading