Skip to content
Open
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
1 change: 1 addition & 0 deletions frontend/providers/devbox/api/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type BaseTemplateRepository = {
name: string;
description: string | null;
iconId: string | null;
icon?: string | null;
templateRepositoryTags: TemplateRepositoryTagItem[];
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,24 @@ export const Name = memo<NameProps>(
<Tooltip>
<TooltipTrigger asChild>
<div className="flex h-8 min-w-8 items-center justify-center rounded-lg border-[0.5px] border-zinc-200 bg-zinc-50">
<RuntimeIcon iconId={item.template.templateRepository.iconId} alt={item.id} />
<RuntimeIcon
iconId={item.template.templateRepository.iconId}
icon={item.template.templateRepository.icon}
alt={item.id}
/>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" align="start" sideOffset={1}>
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg border-[0.5px] border-zinc-200 bg-zinc-50">
<RuntimeIcon iconId={item.template.templateRepository.iconId} alt={item.id} />
<RuntimeIcon
iconId={item.template.templateRepository.iconId}
icon={item.template.templateRepository.icon}
alt={item.id}
/>
</div>
<div className="flex flex-col">
<p className="text-sm/5 font-medium">
{item.template.templateRepository.iconId}
</p>
<p className="text-sm/5 font-medium">{item.template.templateRepository.iconId}</p>
<p className="text-xs/5 text-zinc-500">{item.template.name}</p>
</div>
</div>
Expand All @@ -57,9 +63,7 @@ export const Name = memo<NameProps>(
</div>
{item.remark && (
<div className="group flex w-[80%] items-center gap-1">
<span className="truncate text-xs font-normal text-zinc-500">
{item.remark}
</span>
<span className="truncate text-xs font-normal text-zinc-500">{item.remark}</span>
<PencilLine
className="h-4 min-h-4 w-4 min-w-4 cursor-pointer text-neutral-500 opacity-0 transition-opacity group-hover:opacity-100"
onClick={() => onEditRemark(item)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export default function Runtime({ isEdit = false }: RuntimeProps) {

const displayTemplate = {
iconId: isEdit ? devboxDetail?.iconId || 'custom' : startedTemplate?.iconId || 'custom',
icon: isEdit ? devboxDetail?.icon || null : startedTemplate?.icon || null,
name: isEdit ? devboxDetail?.templateRepositoryName || '' : startedTemplate?.name || '',
description: isEdit
? devboxDetail?.templateRepositoryDescription || ''
Expand All @@ -207,6 +208,7 @@ export default function Runtime({ isEdit = false }: RuntimeProps) {
<div className="flex h-8 w-8 items-center justify-center rounded-lg border-[0.5px] border-zinc-200 bg-zinc-50">
<RuntimeIcon
iconId={displayTemplate.iconId}
icon={displayTemplate.icon}
alt={displayTemplate.name}
width={40}
height={40}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export default function PrivateTemplate({ search }: { search: string }) {
isPublic={tr.isPublic}
isDisabled={tr.templates.length === 0}
iconId={tr.iconId || ''}
icon={tr.icon || null}
templateRepositoryName={tr.name}
templateRepositoryDescription={tr.description}
templateRepositoryUid={tr.uid}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ const PublicTemplate = ({
uid: firstTemplate.uid,
name: firstTemplate.name,
iconId: firstTemplate.iconId || '',
icon: firstTemplate.icon || null,
templateUid: firstTemplate.templates?.[0]?.uid || '',
description: firstTemplate.description
});
Expand Down Expand Up @@ -638,6 +639,7 @@ const PublicTemplate = ({
<TemplateCard
key={tr.uid}
iconId={tr.iconId || ''}
icon={tr.icon || null}
templateRepositoryName={tr.name}
templateRepositoryDescription={tr.description}
templateRepositoryUid={tr.uid}
Expand Down Expand Up @@ -689,6 +691,7 @@ const PublicTemplate = ({
<TemplateCard
key={tr.uid}
iconId={tr.iconId || ''}
icon={tr.icon || null}
templateRepositoryName={tr.name}
templateRepositoryDescription={tr.description}
templateRepositoryUid={tr.uid}
Expand Down Expand Up @@ -785,6 +788,7 @@ const TemplateSection = ({
<TemplateCard
key={tr.uid}
iconId={tr.iconId || ''}
icon={tr.icon || null}
templateRepositoryName={tr.name}
templateRepositoryDescription={tr.description}
templateRepositoryUid={tr.uid}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { destroyDriver } from '@/hooks/driver';
interface TemplateCardProps {
isPublic?: boolean;
iconId: string;
icon?: string | null;
isDisabled?: boolean;
inPublicStore?: boolean;
templateRepositoryName: string;
Expand All @@ -53,6 +54,7 @@ interface TemplateCardProps {
const TemplateCard = ({
isPublic,
iconId,
icon,
templateRepositoryName,
templateRepositoryDescription,
templateRepositoryUid,
Expand Down Expand Up @@ -111,18 +113,32 @@ const TemplateCard = ({
};

const loadImages = async () => {
await preloadImage(`/images/runtime/${iconId}.svg`);
const fallbackSrc = iconId ? `/images/runtime/${iconId}.svg` : '/images/runtime/custom.svg';
const src = (() => {
if (icon) {
const trimmed = icon.trim();
if (/^<svg[\s>]/i.test(trimmed)) {
return `data:image/svg+xml;utf8,${encodeURIComponent(trimmed)}`;

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same data URI encoding issue exists here as in RuntimeIcon. The format data:image/svg+xml;utf8, should be data:image/svg+xml;charset=utf-8, or data:image/svg+xml, to be standards-compliant.

Since this logic is duplicated from RuntimeIcon (see related maintainability comment), fixing it in one place would require fixing it in both places, which reinforces the need to extract this into a shared utility function.

Copilot uses AI. Check for mistakes.
}
if (/^https:\/\//i.test(trimmed)) {
return trimmed;
}
}
return fallbackSrc;
})();
Comment on lines +117 to +128

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The icon resolution and SVG detection logic is duplicated between this component and RuntimeIcon. The TemplateCard component has inline logic in lines 117-128 that replicates the functionality of RuntimeIcon's resolveRuntimeIconSrc function. This duplication increases maintenance burden and risk of inconsistencies.

Consider extracting the icon resolution logic into a shared utility function that both components can use, or refactor to use RuntimeIcon's resolveRuntimeIconSrc function directly.

Copilot uses AI. Check for mistakes.
await preloadImage(src);
setImageLoaded(true);
};

loadImages();
}, [iconId]);
}, [iconId, icon]);

const handleSelectTemplate = () => {
setStartedTemplate({
uid: templateRepositoryUid,
name: templateRepositoryName,
iconId,
icon,
templateUid: selectedVersion,
description: templateRepositoryDescription
});
Expand Down Expand Up @@ -152,6 +168,7 @@ const TemplateCard = ({
{!imageLoaded && <Skeleton className="absolute inset-0 h-8 w-8 rounded-lg" />}
<RuntimeIcon
iconId={iconId}
icon={icon}
alt={templateRepositoryName}
width={32}
height={32}
Expand Down
1 change: 1 addition & 0 deletions frontend/providers/devbox/app/api/deployDevbox/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export async function POST(req: NextRequest) {
select: {
uid: true,
iconId: true,
icon: true,
name: true,
kind: true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export async function GET(req: NextRequest) {
select: {
uid: true,
iconId: true,
icon: true,
name: true,
kind: true,
description: true
Expand Down
3 changes: 3 additions & 0 deletions frontend/providers/devbox/app/api/getDevboxByName/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export const TemplateRepositorySchema = z.object({
iconId: z.string().openapi({
description: 'Runtime Icon'
}),
icon: z.string().nullable().openapi({
description: 'Runtime icon (svg content or https url)'
}),
name: z.string().openapi({
description: 'Runtime Name'
}),
Expand Down
3 changes: 2 additions & 1 deletion frontend/providers/devbox/app/api/getDevboxList/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export async function GET(req: NextRequest) {
uid: true,
templateRepository: {
select: {
iconId: true
iconId: true,
icon: true
}
},
name: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export async function GET(req: NextRequest) {
uid: true,
isPublic: true,
iconId: true,
icon: true,
// organizationUid: true,
organization: {
select: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export async function GET(req: NextRequest) {
uid: true,
description: true,
iconId: true,
icon: true,
createdAt: true,
usageCount: true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const GET = async function GET(req: NextRequest) {
select: {
kind: true,
iconId: true,
icon: true,
name: true,
uid: true,
description: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const TemplateRepositorySchema = z.object({
iconId: z.string().openapi({
description: 'Template repository icon Id'
}),
icon: z.string().nullable().openapi({
description: 'Template repository icon (svg content or https url)'
}),
templateRepositoryTags: z
.array(
z.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export async function GET() {
uid: true,
description: true,
iconId: true,
icon: true,
createdAt: true,
usageCount: true
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ export async function GET(req: NextRequest) {
uid: true,
isPublic: true,
description: true,
iconId: true
iconId: true,
icon: true
},
skip: (page - 1) * pageSize,
take: pageSize,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { authSessionWithJWT } from '@/services/backend/auth';
import { jsonRes } from '@/services/backend/response';
import { devboxDB } from '@/services/db/init';
import { getRegionUid } from '@/utils/env';
import { updateTemplateRepositorySchema } from '@/utils/validate';
import {
normalizeTemplateIcon,
updateTemplateRepositorySchema,
validateTemplateIcon
} from '@/utils/validate';
import { NextRequest } from 'next/server';
import { z } from 'zod';
export async function POST(req: NextRequest) {
Expand All @@ -18,6 +22,15 @@ export async function POST(req: NextRequest) {
});
}
const query = updateTemplateRepositorySchema.parse(queryRaw);
const hasIconInput = query.icon !== undefined;
const iconCheck = validateTemplateIcon(query.icon);
if (!iconCheck.ok) {
return jsonRes({
code: 400,
error: iconCheck.error
});
}
const normalizedIcon = normalizeTemplateIcon(query.icon);
const { payload } = await authSessionWithJWT(headerList);
const templateRepository = await devboxDB.templateRepository.findUnique({
where: {
Expand Down Expand Up @@ -89,7 +102,8 @@ export async function POST(req: NextRequest) {
data: {
description: query.description,
name: query.templateRepositoryName,
isPublic: query.isPublic
isPublic: query.isPublic,

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditional update logic using hasIconInput may lead to unexpected behavior. When hasIconInput is true but the icon value is null or empty string, normalizedIcon will be null (from normalizeTemplateIcon). This means setting icon to explicitly null will update the database field to null, but if icon is undefined in the request, the field won't be updated at all.

This is correct behavior for an optional field update, but it's worth noting that there's an asymmetry: you can explicitly set icon to null to clear it, but you cannot distinguish between "field not provided" and "field provided as null" at the schema level since z.string().nullable().optional() accepts both. The hasIconInput check addresses this, which is good. However, consider documenting this behavior or adding a comment explaining the intended semantics.

Suggested change
isPublic: query.isPublic,
isPublic: query.isPublic,
// NOTE: `hasIconInput` is used to distinguish "no icon field in request"
// from "icon explicitly provided (possibly as null)".
// - If hasIconInput is false, we omit `icon` so the DB value is untouched.
// - If hasIconInput is true, we always include `icon: normalizedIcon`;
// `normalizedIcon` may be null to explicitly clear the stored icon.
// This preserves partial-update semantics despite using
// `z.string().nullable().optional()` in the schema.

Copilot uses AI. Check for mistakes.
...(hasIconInput ? { icon: normalizedIcon } : {})
}
});
await tx.templateRepositoryTag.deleteMany({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import { ERROR_ENUM } from '@/services/error';
import { retagSvcClient } from '@/services/retag';
import { KBDevboxReleaseType, KBDevboxTypeV2 } from '@/types/k8s';
import { getRegionUid } from '@/utils/env';
import { createTemplateRepositorySchema } from '@/utils/validate';
import {
createTemplateRepositorySchema,
normalizeTemplateIcon,
validateTemplateIcon
} from '@/utils/validate';
import { NextRequest } from 'next/server';
import { z } from 'zod';

Expand All @@ -23,6 +27,15 @@ export async function POST(req: NextRequest) {
});
}
const query = createTemplateRepositorySchema.parse(queryRaw);
const hasIconInput = query.icon !== undefined;
const iconCheck = validateTemplateIcon(query.icon);
if (!iconCheck.ok) {
return jsonRes({
code: 400,
error: iconCheck.error
});
}
const normalizedIcon = normalizeTemplateIcon(query.icon);
const { kubeConfig, payload, token } = await authSessionWithJWT(headerList);
const { namespace, k8sCustomObjects } = await getK8s({
kubeconfig: kubeConfig
Expand Down Expand Up @@ -114,7 +127,8 @@ export async function POST(req: NextRequest) {
select: {
templateRepository: {
select: {
iconId: true
iconId: true,
icon: true
}
}
}
Expand Down Expand Up @@ -149,6 +163,7 @@ export async function POST(req: NextRequest) {
regionUid: getRegionUid(),
organizationUid: payload.organizationUid,
iconId: origionalTemplate?.templateRepository.iconId,
icon: hasIconInput ? normalizedIcon : origionalTemplate?.templateRepository.icon || null,
kind: TemplateRepositoryKind.CUSTOM,
name: query.templateRepositoryName,
isPublic: query.isPublic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export async function POST(
select: {
uid: true,
iconId: true,
icon: true,
name: true,
kind: true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,7 @@ export async function GET(req: NextRequest, { params }: { params: { name: string
select: {
uid: true,
iconId: true,
icon: true,
name: true,
kind: true,
description: true
Expand Down
4 changes: 3 additions & 1 deletion frontend/providers/devbox/app/api/v1/devbox/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ export async function POST(req: NextRequest) {
select: {
uid: true,
iconId: true,
icon: true,
name: true,
kind: true,
description: true
Expand Down Expand Up @@ -669,7 +670,8 @@ export async function GET(req: NextRequest) {
uid: true,
templateRepository: {
select: {
iconId: true
iconId: true,
icon: true
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export async function GET(req: NextRequest) {
select: {
uid: true,
iconId: true,
icon: true,
name: true,
kind: true,
description: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const RuntimeSchema = z.object({
iconId: z.string().nullable().openapi({
description: 'Runtime icon ID (runtime name)'
}),
icon: z.string().nullable().openapi({
description: 'Runtime icon (svg content or https url)'
}),
name: z.string().openapi({
description: 'Template repository name'
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export async function POST(
select: {
uid: true,
iconId: true,
icon: true,
name: true,
kind: true
}
Expand Down
Loading
Loading