Skip to content

Commit 27c98e6

Browse files
committed
Fix responsive text truncation and MinIO endpoint validation
Improves mobile layout handling and error messaging: - Badge component: Add shrink-0 and whitespace-nowrap to prevent text wrapping - QueueTable: Restructure with flexbox wrapping and truncation for responsive display - RunnerAvailabilityPanel: Add flex-col spacing, truncation, and shrink-0 badge - RunnerStatusBadge: Accept className prop for layout control - MinIO config: Warn when MINIO_ENDPOINT lacks explicit port (defaults to console, not API) - S3 bucket creation: Detect and clarify "InvalidArgument" as console-port misconfiguration
1 parent 4848348 commit 27c98e6

6 files changed

Lines changed: 75 additions & 29 deletions

File tree

backend/src/config.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,20 @@ fn validate_minio_endpoint(endpoint: &str) -> anyhow::Result<()> {
490490
if rest.is_empty() || rest.starts_with('/') {
491491
anyhow::bail!("MINIO_ENDPOINT is missing a host");
492492
}
493+
// A bare host with no explicit port defaults to 80/443 — almost always the
494+
// MinIO *console*, not the S3 API. Bucket/object operations against the
495+
// console fail with "S3 API Requests must be made to API port." Warn (never
496+
// fail: a fronting proxy on 443 may legitimately route to the API).
497+
let authority = rest.split('/').next().unwrap_or_default();
498+
let after_bracket = authority.rsplit(']').next().unwrap_or(authority);
499+
if !after_bracket.contains(':') {
500+
tracing::warn!(
501+
"MINIO_ENDPOINT has no explicit port — a bare host defaults to 80/443, which is \
502+
usually the MinIO console, not the S3 API. If bucket creation fails with \
503+
\"S3 API Requests must be made to API port.\", point MINIO_ENDPOINT at the API \
504+
endpoint (e.g. host:9000)"
505+
);
506+
}
493507
Ok(())
494508
}
495509

backend/src/services/object_store.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,27 @@ impl S3Store {
273273
{
274274
Ok(false)
275275
}
276-
Err(err) => Err(anyhow::Error::new(err)
277-
.context(format!("failed to create bucket on {} storage", self.backend))),
276+
Err(err) => {
277+
// "InvalidArgument" here is MinIO's "S3 API Requests must be made
278+
// to API port." — MINIO_ENDPOINT is pointing at the console, not
279+
// the S3 API. Surface the remediation so the boot warn is actionable.
280+
let hints_console_port = matches!(
281+
&err,
282+
aws_sdk_s3::error::SdkError::ServiceError(service_err)
283+
if service_err.err().meta().code() == Some("InvalidArgument")
284+
);
285+
let context = if hints_console_port {
286+
format!(
287+
"failed to create bucket on {} storage — MINIO_ENDPOINT appears to point \
288+
at the MinIO console, not the S3 API; set it to the API endpoint \
289+
(e.g. host:9000)",
290+
self.backend
291+
)
292+
} else {
293+
format!("failed to create bucket on {} storage", self.backend)
294+
};
295+
Err(anyhow::Error::new(err).context(context))
296+
}
278297
}
279298
}
280299
}

src/components/ui/Badge.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { ReactNode } from 'react';
33
import { cn } from '../../lib/cn';
44

55
const badgeVariants = cva(
6-
'inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium',
6+
'inline-flex shrink-0 items-center gap-1 whitespace-nowrap rounded px-1.5 py-0.5 text-xs font-medium',
77
{
88
variants: {
99
variant: {

src/features/jobs/components/QueueTable.tsx

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -60,24 +60,28 @@ export function QueueTable({ slug, jobs, onCancel }: QueueTableProps) {
6060
className="cursor-pointer transition-colors hover:bg-surface"
6161
>
6262
<Td className="font-mono text-xs text-steel">{index + 1}</Td>
63-
<Td>
64-
<div className="font-medium text-charcoal">{job.name ?? job.key}</div>
65-
<div className="text-xs text-steel">
66-
{job.workflowName}
67-
<span className="font-mono"> #{job.pipelineNumber}</span>
68-
<span className="inline-flex items-center gap-1 pl-2 font-mono">
69-
<GitBranch size={11} aria-hidden="true" />
70-
{branchOfRef(job.gitRef)}
71-
</span>
72-
{(job.actorLogin || job.actorAvatarUrl) && (
73-
<span className="inline-flex items-center gap-1 pl-2 align-middle">
74-
<Avatar size="xs" login={job.actorLogin} avatarUrl={job.actorAvatarUrl} />
75-
{job.actorLogin}
63+
<Td className="max-w-0">
64+
<div className="min-w-0">
65+
<div className="truncate font-medium text-charcoal">{job.name ?? job.key}</div>
66+
<div className="flex flex-wrap items-center gap-x-2 text-xs text-steel">
67+
<span className="min-w-0 truncate">
68+
{job.workflowName}
69+
<span className="font-mono"> #{job.pipelineNumber}</span>
7670
</span>
77-
)}
71+
<span className="inline-flex items-center gap-1 font-mono">
72+
<GitBranch size={11} aria-hidden="true" />
73+
{branchOfRef(job.gitRef)}
74+
</span>
75+
{(job.actorLogin || job.actorAvatarUrl) && (
76+
<span className="inline-flex items-center gap-1 align-middle">
77+
<Avatar size="xs" login={job.actorLogin} avatarUrl={job.actorAvatarUrl} />
78+
{job.actorLogin}
79+
</span>
80+
)}
81+
</div>
82+
{/* On phones the Repository column is hidden; fold it in here. */}
83+
<div className="truncate text-xs text-steel md:hidden">{job.repoFullName}</div>
7884
</div>
79-
{/* On phones the Repository column is hidden; fold it in here. */}
80-
<div className="text-xs text-steel md:hidden">{job.repoFullName}</div>
8185
</Td>
8286
<Td className="hidden sm:table-cell">
8387
<span className="inline-flex items-center gap-1.5 font-mono text-xs text-charcoal">

src/features/jobs/components/RunnerAvailabilityPanel.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function RunnerAvailabilityPanel({
4141

4242
return (
4343
<Card>
44-
<CardHeader>
44+
<CardHeader className="flex-col items-start gap-1">
4545
<h2 className="text-sm font-semibold text-charcoal">Runner availability</h2>
4646
<p className="text-xs text-steel">
4747
Jobs are matched to runners by label containment — every requested label must be
@@ -73,24 +73,24 @@ export function RunnerAvailabilityPanel({
7373
<div className="min-w-0">
7474
<Link
7575
to={workspacePath(slug, `runners/${runner.id}`)}
76-
className="text-sm font-medium text-charcoal hover:text-link hover:underline"
76+
className="block truncate text-sm font-medium text-charcoal hover:text-link hover:underline"
7777
>
7878
{runner.name}
7979
</Link>
80-
<div className="mt-1 flex flex-wrap gap-1">
80+
<div className="mt-1 flex max-w-[220px] flex-wrap gap-1">
8181
{runner.labels.map((label) => (
8282
<Badge key={label} variant="outline">
8383
{label}
8484
</Badge>
8585
))}
8686
</div>
8787
{runner.lastSeenAt && (
88-
<div className="mt-1 text-xs text-steel">
88+
<div className="mt-1 truncate text-xs text-steel">
8989
seen {formatDistanceToNow(new Date(runner.lastSeenAt), { addSuffix: true })}
9090
</div>
9191
)}
9292
</div>
93-
<RunnerStatusBadge status={runner.status} draining={runner.draining} />
93+
<RunnerStatusBadge status={runner.status} draining={runner.draining} className="shrink-0" />
9494
</li>
9595
))}
9696
</ul>

src/features/runners/components/RunnerStatusBadge.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,36 @@ interface RunnerStatusBadgeProps {
1313
status: RunnerStatus;
1414
/** True while the runner is finishing its current job and will not take another. */
1515
draining?: boolean;
16+
className?: string;
1617
}
1718

18-
export function RunnerStatusBadge({ status, draining }: RunnerStatusBadgeProps) {
19+
export function RunnerStatusBadge({ status, draining, className }: RunnerStatusBadgeProps) {
1920
if (status === 'busy' && draining) {
2021
return (
21-
<Badge variant="info">
22+
<Badge variant="info" className={className}>
2223
<Spinner className="h-3 w-3" />
2324
Draining
2425
</Badge>
2526
);
2627
}
2728
if (status === 'busy') {
2829
return (
29-
<Badge variant="primary">
30+
<Badge variant="primary" className={className}>
3031
<Spinner className="h-3 w-3" />
3132
{LABELS[status]}
3233
</Badge>
3334
);
3435
}
3536
if (status === 'idle') {
36-
return <Badge variant="success">{LABELS[status]}</Badge>;
37+
return (
38+
<Badge variant="success" className={className}>
39+
{LABELS[status]}
40+
</Badge>
41+
);
3742
}
38-
return <Badge variant="neutral">{LABELS[status]}</Badge>;
43+
return (
44+
<Badge variant="neutral" className={className}>
45+
{LABELS[status]}
46+
</Badge>
47+
);
3948
}

0 commit comments

Comments
 (0)