fix: render integration sources in Workshop details

This commit is contained in:
ajnart committed 2026-09-12 22:45:52 +02:00
1 parent d0529079ec
commit 879a4ae23c
1 file changed
+24 -15
@@ -32,6 +32,7 @@ import type { WorkshopSubmission, WorkshopVote } from "@site/src/lib/pocketbase"
import { getWorkshopBackend } from "@site/src/lib/pocketbase";
import { getRuntimeWorkshopApiUrl } from "@site/src/lib/runtime-config";
import type { SubmissionType } from "@site/src/lib/workshop-schema";
import { integrationDefs } from "@homarr/definitions";
import type { HomarrCustomWidgetV2 } from "@homarr/custom-widgets/core";
import {
githubAvatarUrl,
@@ -96,7 +97,11 @@ const isNotFound = (caught: unknown) =>
const avatarFallback = (name: string) => name.trim().slice(0, 1).toUpperCase() || "?";
const sourceHost = (baseUrl: string) => {
type WidgetSource = HomarrCustomWidgetV2["sources"][string];
const sourceHost = (source: WidgetSource) => {
if (source.type === "integration") return `${integrationDefs[source.integrationKind].name} integration`;
const { baseUrl } = source;
try {
return new URL(baseUrl).host;
} catch {
@@ -104,26 +109,30 @@ const sourceHost = (baseUrl: string) => {
}
};
const sourceAuthLabel = (auth: HomarrCustomWidgetV2["sources"][string]["auth"]) => {
if (typeof auth === "string")
return auth === "none" ? "No credentials" : auth === "basic" ? "Basic auth" : "Bearer token";
return auth.type === "apiKeyHeader" ? `API key header · ${auth.name}` : `API key query · ${auth.name}`;
const sourceAuthLabel = (source: WidgetSource) => {
if (source.type === "integration") return "Uses stored integration credentials";
const { auth } = source;
if (auth === "none") return "No credentials";
if (auth === "basic") return "Basic auth";
if (auth === "bearer") return "Bearer token";
if (auth.type === "apiKeyHeader") return `API key header · ${auth.name}`;
return `API key query · ${auth.name}`;
};
const sourceNeedsSecret = (auth: HomarrCustomWidgetV2["sources"][string]["auth"]) => auth !== "none";
const sourceNeedsSecret = (source: WidgetSource) => source.type === "integration" || source.auth !== "none";
const WidgetSafetySummary = ({ widget }: { widget: HomarrCustomWidgetV2 }) => {
const sources = Object.entries(widget.sources);
const requests = Object.values(widget.requests);
const protectedSources = sources.filter(([, source]) => sourceNeedsSecret(source.auth));
const protectedSources = sources.filter(([, source]) => sourceNeedsSecret(source));
const queryCount = requests.filter((request) => request.kind === "query").length;
const actionCount = requests.filter((request) => request.kind === "action").length;
const credentialLabel =
protectedSources.length === 0
? "None required"
: protectedSources.length === 1
? sourceAuthLabel(protectedSources[0][1].auth)
: `${protectedSources.length} credentials required`;
? sourceAuthLabel(protectedSources[0][1])
: `${protectedSources.length} authenticated sources`;
return (
<section
@@ -152,9 +161,9 @@ const WidgetSafetySummary = ({ widget }: { widget: HomarrCustomWidgetV2 }) => {
<dd className="mt-1.5 space-y-1.5">
{sources.slice(0, 2).map(([id, source]) => (
<span key={id} className="flex min-w-0 items-center justify-between gap-2">
<span className="truncate font-mono text-xs font-medium">{sourceHost(source.baseUrl)}</span>
<span className="truncate font-mono text-xs font-medium">{sourceHost(source)}</span>
<Badge variant="secondary" className="shrink-0 font-normal">
{source.networkScope}
{source.networkScope ?? "integration"}
</Badge>
</span>
))}
@@ -536,7 +545,7 @@ const MarketplaceDetail = ({ workshopUrl }: { workshopUrl: string }) => {
const sources = widgetDefinition ? Object.entries(widgetDefinition.sources) : [];
const requests = widgetDefinition ? Object.entries(widgetDefinition.requests) : [];
const options = widgetDefinition ? Object.entries(widgetDefinition.options) : [];
const protectedSources = sources.filter(([, source]) => sourceNeedsSecret(source.auth));
const protectedSources = sources.filter(([, source]) => sourceNeedsSecret(source));
const displayedContent = formatWorkshopContent(submission.type, submission.content);
const socialTitle = `${submission.title} · Homarr Workshop`;
const socialDescription = `${typeSocialLabels[submission.type]} for Homarr. ${submission.description}`.trim();
@@ -779,11 +788,11 @@ const MarketplaceDetail = ({ workshopUrl }: { workshopUrl: string }) => {
<div className="flex items-center justify-between gap-2 text-sm">
<span className="truncate font-medium">{source.name || id}</span>
<Badge variant="secondary" className="shrink-0">
{source.networkScope}
{source.networkScope ?? "integration"}
</Badge>
</div>
<p className="truncate text-xs text-muted-foreground">{sourceHost(source.baseUrl)}</p>
<p className="mt-0.5 text-xs text-muted-foreground">{sourceAuthLabel(source.auth)}</p>
<p className="truncate text-xs text-muted-foreground">{sourceHost(source)}</p>
<p className="mt-0.5 text-xs text-muted-foreground">{sourceAuthLabel(source)}</p>
</div>
))}
</div>