Skip to content

Commit 5e2ec69

Browse files
committed
more pipeline page polish
1 parent 093e985 commit 5e2ec69

2 files changed

Lines changed: 104 additions & 99 deletions

File tree

frontend/src/components/pages/rp-connect/pipeline/index.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ describe('PipelinePage', () => {
534534
// The page title (level-1 heading) is static; the displayName is shown
535535
// prominently as the summary card heading, with the ID as a labeled field below.
536536
expect(await screen.findByRole('heading', { level: 1, name: 'Pipeline view' })).toBeInTheDocument();
537-
expect(await screen.findByRole('heading', { level: 2, name: 'Test Pipeline' })).toBeInTheDocument();
537+
expect(await screen.findByRole('heading', { level: 3, name: 'Test Pipeline' })).toBeInTheDocument();
538538
});
539539

540540
it('hydrates the flow diagram with pipeline configYaml in view mode', async () => {
@@ -663,7 +663,7 @@ describe('PipelinePage', () => {
663663

664664
// In edit mode the name is pre-filled from the server and shown in the settings summary heading.
665665
await waitFor(() => {
666-
expect(screen.getByRole('heading', { level: 2, name: 'Test Pipeline' })).toBeInTheDocument();
666+
expect(screen.getByRole('heading', { level: 3, name: 'Test Pipeline' })).toBeInTheDocument();
667667
});
668668

669669
// The yaml editor textarea should be populated with the pipeline's configYaml

frontend/src/components/pages/rp-connect/pipeline/index.tsx

Lines changed: 102 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
1616
import { useBlocker, useNavigate, useRouter, useSearch } from '@tanstack/react-router';
1717
import { isSystemTag } from 'components/constants';
1818
import { Alert, AlertDescription, AlertTitle } from 'components/redpanda-ui/components/alert';
19+
import { Badge } from 'components/redpanda-ui/components/badge';
1920
import { Banner, BannerClose, BannerContent } from 'components/redpanda-ui/components/banner';
2021
import { Button } from 'components/redpanda-ui/components/button';
2122
import { CopyButton } from 'components/redpanda-ui/components/copy-button';
@@ -34,7 +35,7 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'components
3435
import { Separator } from 'components/redpanda-ui/components/separator';
3536
import { Skeleton } from 'components/redpanda-ui/components/skeleton';
3637
import { Spinner } from 'components/redpanda-ui/components/spinner';
37-
import { Heading, Text } from 'components/redpanda-ui/components/typography';
38+
import { Heading } from 'components/redpanda-ui/components/typography';
3839
import { cn } from 'components/redpanda-ui/lib/utils';
3940
import { LogExplorer } from 'components/ui/connect/log-explorer';
4041
import { DeleteResourceAlertDialog } from 'components/ui/delete-resource-alert-dialog';
@@ -61,7 +62,7 @@ import {
6162
PipelineUpdateSchema,
6263
UpdatePipelineRequestSchema as UpdatePipelineRequestSchemaDataPlane,
6364
} from 'protogen/redpanda/api/dataplane/v1/pipeline_pb';
64-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
65+
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
6566
import { type Resolver, type UseFormReturn, useForm, useWatch } from 'react-hook-form';
6667
import {
6768
useGetPipelineServiceConfigSchemaQuery,
@@ -476,81 +477,92 @@ function EditorSkeleton() {
476477
);
477478
}
478479

479-
const ConfigField = ({
480-
label,
481-
value,
482-
copyable = false,
483-
multiline = false,
484-
}: {
485-
label: string;
486-
value: string;
487-
copyable?: boolean;
488-
multiline?: boolean;
489-
}) => (
490-
<div className="group/field flex min-w-0 flex-col gap-1">
491-
<Text className="text-muted-foreground" variant="label">
492-
{label}
493-
</Text>
494-
<div className={cn('flex min-w-0 gap-1', multiline ? 'items-start' : 'items-center')}>
495-
<Text
496-
as={multiline ? 'p' : 'div'}
497-
className={cn(multiline ? 'whitespace-pre-wrap break-words' : 'truncate')}
498-
title={multiline ? undefined : value}
499-
>
500-
{value}
501-
</Text>
502-
{copyable && value ? (
503-
<CopyButton
504-
className="shrink-0 opacity-0 transition-opacity group-hover/field:opacity-100"
505-
content={value}
506-
size="sm"
507-
variant="ghost"
508-
/>
509-
) : null}
510-
</div>
511-
</div>
480+
// One row in the summary's definition list: an aligned label column and a value
481+
// column that fills the remaining width. Caller renders <InfoRow>s into a
482+
// `grid grid-cols-[max-content_minmax(0,1fr)]` <dl>.
483+
const InfoRow = ({ label, children }: { label: string; children: ReactNode }) => (
484+
<>
485+
<dt className="font-medium text-muted-foreground text-sm">{label}</dt>
486+
<dd className="min-w-0 text-foreground text-sm">{children}</dd>
487+
</>
488+
);
489+
490+
// Value that reveals a copy button on hover. Used for ID / URL / service account.
491+
const CopyableValue = ({ value, mono }: { value: string; mono?: boolean }) => (
492+
<span className="group/copy flex min-w-0 items-center gap-1">
493+
<span className={cn('min-w-0 truncate', mono && 'font-mono')} title={value}>
494+
{value}
495+
</span>
496+
<CopyButton
497+
className="shrink-0 opacity-0 transition-opacity group-hover/copy:opacity-100"
498+
content={value}
499+
size="sm"
500+
variant="ghost"
501+
/>
502+
</span>
512503
);
513504

505+
const TagBadges = ({ tags }: { tags: { key: string; value: string }[] }) =>
506+
tags.length > 0 ? (
507+
<div className="flex flex-wrap gap-1.5">
508+
{tags.map((t) => (
509+
<Badge key={t.key} variant="simple-outline">
510+
{t.value ? `${t.key}: ${t.value}` : t.key}
511+
</Badge>
512+
))}
513+
</div>
514+
) : (
515+
<span className="text-muted-foreground italic">None</span>
516+
);
517+
514518
// Pipeline identity + metadata shown as a summary card above the main panel in
515519
// view mode. The full set (including empty fields) lives in the details dialog.
516520
const PipelineSummary = ({ pipeline }: { pipeline: Pipeline }) => {
517521
const tasks = cpuToTasks(pipeline.resources?.cpuShares) ?? 0;
518522
const description = pipeline.description?.trim();
523+
const tags = Object.entries(pipeline.tags)
524+
.filter(([k]) => !isSystemTag(k))
525+
.map(([key, value]) => ({ key, value }));
519526
return (
520-
<div className="flex flex-col gap-5 rounded-lg border bg-muted/20 px-6 py-5">
521-
<div className="flex items-start justify-between gap-4">
522-
<div className="flex min-w-0 flex-col gap-1">
523-
<Text className="text-muted-foreground" variant="label">
524-
Name
525-
</Text>
526-
<Heading className="truncate" level={2} title={pipeline.displayName || pipeline.id}>
527-
{pipeline.displayName || pipeline.id}
528-
</Heading>
529-
</div>
530-
<PipelineStatusBadge state={pipeline.state} />
527+
<div className="flex flex-col gap-4 rounded-lg border px-5 py-4">
528+
<div className="flex items-center justify-between gap-4">
529+
<Heading className="min-w-0 truncate" level={3} title={pipeline.displayName || pipeline.id}>
530+
{pipeline.displayName || pipeline.id}
531+
</Heading>
532+
<PipelineStatusBadge size="sm" state={pipeline.state} />
531533
</div>
532534
<Separator variant="subtle" />
533-
<div className="grid grid-cols-1 gap-x-10 gap-y-4 sm:grid-cols-2 lg:grid-cols-3">
534-
<ConfigField copyable label="ID" value={pipeline.id} />
535-
<ConfigField label="Compute units" value={`${tasks}`} />
535+
<dl className="grid grid-cols-[max-content_minmax(0,1fr)] items-start gap-x-10 gap-y-3">
536+
<InfoRow label="ID">
537+
<CopyableValue mono value={pipeline.id} />
538+
</InfoRow>
539+
<InfoRow label="Compute units">{tasks}</InfoRow>
536540
{pipeline.serviceAccount ? (
537-
<ConfigField copyable label="Service account" value={pipeline.serviceAccount.clientId} />
541+
<InfoRow label="Service account">
542+
<CopyableValue mono value={pipeline.serviceAccount.clientId} />
543+
</InfoRow>
538544
) : null}
539-
{pipeline.url ? <ConfigField copyable label="URL" value={pipeline.url} /> : null}
540-
</div>
541-
{description ? (
542-
<div className="flex min-w-0 flex-col gap-1">
543-
<Text className="text-muted-foreground" variant="label">
544-
Description
545-
</Text>
546-
<Text className="line-clamp-3 whitespace-pre-wrap break-words text-sm" title={description}>
547-
{description}
548-
</Text>
549-
</div>
550-
) : null}
545+
{pipeline.url ? (
546+
<InfoRow label="URL">
547+
<CopyableValue value={pipeline.url} />
548+
</InfoRow>
549+
) : null}
550+
{tags.length > 0 ? (
551+
<InfoRow label="Tags">
552+
<TagBadges tags={tags} />
553+
</InfoRow>
554+
) : null}
555+
{description ? (
556+
<InfoRow label="Description">
557+
<p className="line-clamp-2 whitespace-pre-wrap break-words" title={description}>
558+
{description}
559+
</p>
560+
</InfoRow>
561+
) : null}
562+
</dl>
551563
<Separator variant="subtle" />
552-
{/* Run control lives at the card footer, away from the header's Edit button. */}
553-
<div className="flex items-center justify-end">
564+
{/* Run control at the footer-right (distinct intent from the edit-settings action). */}
565+
<div className="flex justify-end">
554566
<PipelineRunControl pipelineId={pipeline.id} pipelineState={pipeline.state} />
555567
</div>
556568
</div>
@@ -563,35 +575,31 @@ const EditSummary = ({ form, onEdit }: { form: UseFormReturn<PipelineFormValues>
563575
const name = useWatch({ control: form.control, name: 'name' });
564576
const description = useWatch({ control: form.control, name: 'description' })?.trim();
565577
const computeUnits = useWatch({ control: form.control, name: 'computeUnits' });
578+
const tags = (useWatch({ control: form.control, name: 'tags' }) ?? []).filter((t) => t.key);
566579
return (
567-
<div className="flex flex-col gap-5 rounded-lg border bg-muted/20 px-6 py-5">
568-
<div className="flex min-w-0 flex-col gap-1">
569-
<Text className="text-muted-foreground" variant="label">
570-
Name
571-
</Text>
572-
<Heading className="truncate" level={2} title={name}>
573-
{name || 'Untitled pipeline'}
574-
</Heading>
575-
</div>
580+
<div className="flex flex-col gap-4 rounded-lg border px-5 py-4">
581+
<Heading className="min-w-0 truncate" level={3} title={name}>
582+
{name || 'Untitled pipeline'}
583+
</Heading>
576584
<Separator variant="subtle" />
577-
<div className="grid grid-cols-1 gap-x-10 gap-y-4 sm:grid-cols-3">
578-
<ConfigField label="Compute units" value={`${computeUnits}`} />
579-
</div>
580-
<div className="flex min-w-0 flex-col gap-1">
581-
<Text className="text-muted-foreground" variant="label">
582-
Description
583-
</Text>
584-
{description ? (
585-
<Text className="line-clamp-3 whitespace-pre-wrap break-words text-sm" title={description}>
586-
{description}
587-
</Text>
588-
) : (
589-
<Text className="text-muted-foreground text-sm italic">No description</Text>
590-
)}
591-
</div>
585+
<dl className="grid grid-cols-[max-content_minmax(0,1fr)] items-start gap-x-10 gap-y-3">
586+
<InfoRow label="Compute units">{computeUnits}</InfoRow>
587+
<InfoRow label="Tags">
588+
<TagBadges tags={tags} />
589+
</InfoRow>
590+
<InfoRow label="Description">
591+
{description ? (
592+
<p className="line-clamp-2 whitespace-pre-wrap break-words" title={description}>
593+
{description}
594+
</p>
595+
) : (
596+
<span className="text-muted-foreground italic">None</span>
597+
)}
598+
</InfoRow>
599+
</dl>
592600
<Separator variant="subtle" />
593-
{/* Edit action at the card footer, away from the header's Save button. */}
594-
<div className="flex items-center justify-end">
601+
{/* Edit action at the footer-left, away from the header's Save button. */}
602+
<div className="flex justify-start">
595603
<Button icon={<Settings />} onClick={onEdit} size="sm" variant="outline">
596604
Edit settings
597605
</Button>
@@ -1006,12 +1014,7 @@ export default function PipelinePage() {
10061014
}, [mode, clearWizardStore, navigate, pipelineId, router]);
10071015

10081016
return (
1009-
<div
1010-
className={cn(
1011-
'flex max-w-[calc(100dvw-(--sidebar-width))] flex-col gap-4',
1012-
mode === 'view' ? 'h-full min-h-[calc(100dvh-10rem)]' : 'h-[calc(100dvh-10rem)]'
1013-
)}
1014-
>
1017+
<div className="flex min-h-[calc(100dvh-10rem)] max-w-[calc(100dvw-(--sidebar-width))] flex-col gap-4">
10151018
{/* Top framing border that lines up with the content edge, matching the
10161019
listings page header. Negative margin cancels the layout's pt-8. */}
10171020
<div className="-mt-8 border-divider-default border-b" />
@@ -1026,7 +1029,9 @@ export default function PipelinePage() {
10261029
/>
10271030
{mode === 'view' && pipeline ? <PipelineSummary pipeline={pipeline} /> : null}
10281031
{mode !== 'view' ? <EditSummary form={form} onEdit={() => setIsConfigDialogOpen(true)} /> : null}
1029-
<div className="flex min-h-0 flex-1 rounded-lg border border-border!">
1032+
{/* Grows to fill a tall viewport but keeps a usable minimum so the editor /
1033+
flow panels aren't squished when the summary card is tall. */}
1034+
<div className="flex min-h-[640px] flex-1 rounded-lg border border-border!">
10301035
<SidebarPanel
10311036
isPipelineDiagramsEnabled={isPipelineDiagramsEnabled}
10321037
mode={mode}

0 commit comments

Comments
 (0)