This repository has no description
5.5 kB
154 lines
1<script module lang="ts">
2 import { tv, type VariantProps } from "tailwind-variants";
3 import type { TagVariants } from "$lib/components/ui/Tag.svelte";
4 import type { PipelineStatus } from "$lib/components/repo/types";
5
6 export const pipelineCardContent = tv({
7 slots: {
8 // does not wrap: a long branch name wraps inside the trigger group instead, so the
9 // sha stays pinned top-right
10 header: "flex items-start justify-between gap-3 pb-2",
11 trigger: "flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-1",
12 triggerRow: "flex flex-wrap items-center gap-x-1.5 gap-y-1 text-foreground-default",
13 triggerIcon: "size-4 shrink-0 text-foreground-muted",
14 triggerVerb: "text-foreground-muted",
15 triggerRef: "font-semibold",
16 triggerArrow: "size-3 shrink-0 text-foreground-muted",
17 pullLink: "typography-paragraph-small text-foreground-muted no-underline hover:underline",
18 sha: "shrink-0 rounded-sm bg-background-inset px-2 py-0.5 typography-monospace-small text-foreground-default no-underline hover:underline",
19 meta: "flex flex-wrap items-center justify-between text-foreground-muted",
20 metaSub: "flex flex-wrap items-center gap-2",
21 workflows: "ml-1",
22 duration: "inline-flex items-center gap-1",
23 durationIcon: "size-3 shrink-0"
24 },
25 variants: {
26 // the trigger line is only a link once a workflow exists to open
27 linked: {
28 true: { triggerRow: "no-underline hover:underline" },
29 false: {}
30 }
31 },
32 defaultVariants: {
33 linked: false
34 }
35 });
36
37 export type PipelineCardContentVariants = VariantProps<typeof pipelineCardContent>;
38
39 /** which Tag colour each status uses */
40 export const STATUS_TAG_COLORS: Record<PipelineStatus, TagVariants["color"]> = {
41 pending: "gray",
42 running: "warning",
43 success: "success",
44 failed: "danger",
45 timeout: "warning",
46 cancelled: "gray"
47 };
48</script>
49
50<script lang="ts">
51 import { resolve } from "$app/paths";
52 import ArrowLeft from "$icon/arrow-left";
53 import CirclePlay from "$icon/circle-play";
54 import Clock from "$icon/clock";
55 import GitCommitHorizontal from "$icon/git-commit-horizontal";
56 import GitPullRequest from "$icon/git-pull-request";
57 import TimeAgo from "$lib/components/ui/TimeAgo.svelte";
58 import { formatDuration } from "$lib/format";
59 import Tag from "$lib/components/ui/Tag.svelte";
60 import type { PipelineSummary } from "$lib/components/repo/types";
61 import { aggregateStatus, longSummary, STATUS_LABELS, totalDuration } from "./pipeline";
62 import { STATUS_ICONS } from "./PipelineStatusIcon.svelte";
63 import PipelineWorkflows from "./PipelineWorkflows.svelte";
64 import Separator from "$lib/components/ui/Separator.svelte";
65
66 interface Props {
67 ownerHandle: string;
68 repoName: string;
69 pipeline: PipelineSummary;
70 }
71
72 let { ownerHandle, repoName, pipeline }: Props = $props();
73
74 const status = $derived(aggregateStatus(pipeline));
75 const duration = $derived(totalDuration(pipeline));
76
77 // links to the first workflow's logs, plain text until a spindle reports one
78 // TODO: the workflow log route does not exist in the svelte app yet
79 const href = $derived(
80 pipeline.workflows.length > 0
81 ? resolve(
82 `/${ownerHandle}/${repoName}/pipelines/${pipeline.id}/workflow/${pipeline.workflows[0].name}` as "/"
83 )
84 : undefined
85 );
86 const commitHref = $derived(resolve(`/${ownerHandle}/${repoName}/commit/${pipeline.sha}` as "/"));
87
88 const classes = $derived(pipelineCardContent({ linked: href !== undefined }));
89</script>
90
91{#snippet triggerLabel()}
92 {#if pipeline.trigger.kind === "push"}
93 <GitCommitHorizontal class={classes.triggerIcon()} aria-hidden="true" />
94 <span class={classes.triggerVerb()}>Push to</span>
95 <span class={classes.triggerRef()}>{pipeline.trigger.targetRef}</span>
96 {:else if pipeline.trigger.kind === "pull_request"}
97 <GitPullRequest class={classes.triggerIcon()} aria-hidden="true" />
98 <span class={classes.triggerVerb()}>Pull request</span>
99 <span class={classes.triggerRef()}>{pipeline.trigger.targetRef}</span>
100 <ArrowLeft class={classes.triggerArrow()} aria-hidden="true" />
101 <span class={classes.triggerRef()}>{pipeline.trigger.sourceLabel}</span>
102 {:else}
103 <CirclePlay class={classes.triggerIcon()} aria-hidden="true" />
104 <span class={classes.triggerVerb()}>Manual dispatch</span>
105 {/if}
106{/snippet}
107
108<div class={classes.header()}>
109 <div class={classes.trigger()}>
110 {#if href}
111 <a {href} class={classes.triggerRow()}>
112 {@render triggerLabel()}
113 </a>
114 {:else}
115 <span class={classes.triggerRow()}>
116 {@render triggerLabel()}
117 </span>
118 {/if}
119
120 {#if pipeline.trigger.kind === "pull_request" && pipeline.trigger.pullPath}
121 <a href={resolve(pipeline.trigger.pullPath as "/")} class={classes.pullLink()}> (view PR) </a>
122 {/if}
123 </div>
124
125 {#if pipeline.sha}
126 <a href={commitHref} class={classes.sha()}>
127 {pipeline.sha.slice(0, 8)}
128 </a>
129 {/if}
130</div>
131
132<div class={classes.meta()}>
133 <span class={classes.metaSub()}>
134 <Tag class="mt-px" size="lg" color={STATUS_TAG_COLORS[status]} icon={STATUS_ICONS[status]}
135 >{STATUS_LABELS[status]}</Tag
136 >
137
138 <span class={classes.workflows()} title={longSummary(pipeline) || undefined}>
139 <PipelineWorkflows {ownerHandle} {repoName} {pipeline} />
140 </span>
141 </span>
142
143 <span class={classes.metaSub()}>
144 <TimeAgo value={pipeline.createdAt} />
145
146 {#if duration > 0}
147 <Separator variant="dot" />
148 <span class={classes.duration()}>
149 <Clock class={classes.durationIcon()} aria-hidden="true" />
150 {formatDuration(duration)}
151 </span>
152 {/if}
153 </span>
154</div>