mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-01 14:59:19 +08:00
feat(versions): added the ability to rename deployment versions (#1610)
This commit is contained in:
@@ -19,7 +19,6 @@ export async function GET(
|
||||
const { id, version } = await params
|
||||
|
||||
try {
|
||||
// Validate permissions and get workflow data
|
||||
const { error } = await validateWorkflowPermissions(id, requestId, 'read')
|
||||
if (error) {
|
||||
return createErrorResponse(error.message, error.status)
|
||||
@@ -54,3 +53,66 @@ export async function GET(
|
||||
return createErrorResponse(error.message || 'Failed to fetch deployment version', 500)
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string; version: string }> }
|
||||
) {
|
||||
const requestId = generateRequestId()
|
||||
const { id, version } = await params
|
||||
|
||||
try {
|
||||
const { error } = await validateWorkflowPermissions(id, requestId, 'write')
|
||||
if (error) {
|
||||
return createErrorResponse(error.message, error.status)
|
||||
}
|
||||
|
||||
const versionNum = Number(version)
|
||||
if (!Number.isFinite(versionNum)) {
|
||||
return createErrorResponse('Invalid version', 400)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { name } = body
|
||||
|
||||
if (typeof name !== 'string') {
|
||||
return createErrorResponse('Name must be a string', 400)
|
||||
}
|
||||
|
||||
const trimmedName = name.trim()
|
||||
if (trimmedName.length === 0) {
|
||||
return createErrorResponse('Name cannot be empty', 400)
|
||||
}
|
||||
|
||||
if (trimmedName.length > 100) {
|
||||
return createErrorResponse('Name must be 100 characters or less', 400)
|
||||
}
|
||||
|
||||
const [updated] = await db
|
||||
.update(workflowDeploymentVersion)
|
||||
.set({ name: trimmedName })
|
||||
.where(
|
||||
and(
|
||||
eq(workflowDeploymentVersion.workflowId, id),
|
||||
eq(workflowDeploymentVersion.version, versionNum)
|
||||
)
|
||||
)
|
||||
.returning({ id: workflowDeploymentVersion.id, name: workflowDeploymentVersion.name })
|
||||
|
||||
if (!updated) {
|
||||
return createErrorResponse('Deployment version not found', 404)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Renamed deployment version ${version} for workflow ${id} to "${trimmedName}"`
|
||||
)
|
||||
|
||||
return createSuccessResponse({ name: updated.name })
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`[${requestId}] Error renaming deployment version ${version} for workflow ${id}`,
|
||||
error
|
||||
)
|
||||
return createErrorResponse(error.message || 'Failed to rename deployment version', 500)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
.select({
|
||||
id: workflowDeploymentVersion.id,
|
||||
version: workflowDeploymentVersion.version,
|
||||
name: workflowDeploymentVersion.name,
|
||||
isActive: workflowDeploymentVersion.isActive,
|
||||
createdAt: workflowDeploymentVersion.createdAt,
|
||||
createdBy: workflowDeploymentVersion.createdBy,
|
||||
|
||||
+117
-13
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Loader2, MoreVertical, X } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
@@ -102,6 +102,18 @@ export function DeployModal({
|
||||
const [previewDeployedState, setPreviewDeployedState] = useState<WorkflowState | null>(null)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const itemsPerPage = 5
|
||||
const [editingVersion, setEditingVersion] = useState<number | null>(null)
|
||||
const [editValue, setEditValue] = useState('')
|
||||
const [isRenaming, setIsRenaming] = useState(false)
|
||||
const [openDropdown, setOpenDropdown] = useState<number | null>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (editingVersion !== null && inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
inputRef.current.select()
|
||||
}
|
||||
}, [editingVersion])
|
||||
|
||||
const getInputFormatExample = (includeStreaming = false) => {
|
||||
let inputFormatExample = ''
|
||||
@@ -419,6 +431,52 @@ export function DeployModal({
|
||||
}
|
||||
}
|
||||
|
||||
const handleStartRename = (version: number, currentName: string | null | undefined) => {
|
||||
setOpenDropdown(null) // Close dropdown first
|
||||
setEditingVersion(version)
|
||||
setEditValue(currentName || `v${version}`)
|
||||
}
|
||||
|
||||
const handleSaveRename = async (version: number) => {
|
||||
if (!workflowId || !editValue.trim()) {
|
||||
setEditingVersion(null)
|
||||
return
|
||||
}
|
||||
|
||||
const currentVersion = versions.find((v) => v.version === version)
|
||||
const currentName = currentVersion?.name || `v${version}`
|
||||
|
||||
if (editValue.trim() === currentName) {
|
||||
setEditingVersion(null)
|
||||
return
|
||||
}
|
||||
|
||||
setIsRenaming(true)
|
||||
try {
|
||||
const res = await fetch(`/api/workflows/${workflowId}/deployments/${version}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: editValue.trim() }),
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
await fetchVersions()
|
||||
setEditingVersion(null)
|
||||
} else {
|
||||
logger.error('Failed to rename version')
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error renaming version:', error)
|
||||
} finally {
|
||||
setIsRenaming(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancelRename = () => {
|
||||
setEditingVersion(null)
|
||||
setEditValue('')
|
||||
}
|
||||
|
||||
const handleUndeploy = async () => {
|
||||
try {
|
||||
setIsUndeploying(true)
|
||||
@@ -539,7 +597,7 @@ export function DeployModal({
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleCloseModal}>
|
||||
<DialogContent
|
||||
className='flex max-h-[90vh] flex-col gap-0 overflow-hidden p-0 sm:max-w-[600px]'
|
||||
className='flex max-h-[90vh] flex-col gap-0 overflow-hidden p-0 sm:max-w-[700px]'
|
||||
hideCloseButton
|
||||
>
|
||||
<DialogHeader className='flex-shrink-0 border-b px-6 py-4'>
|
||||
@@ -650,13 +708,13 @@ export function DeployModal({
|
||||
<thead className='border-b bg-muted/50'>
|
||||
<tr>
|
||||
<th className='w-10' />
|
||||
<th className='px-4 py-2 text-left font-medium text-muted-foreground text-xs'>
|
||||
<th className='w-[200px] whitespace-nowrap px-4 py-2 text-left font-medium text-muted-foreground text-xs'>
|
||||
Version
|
||||
</th>
|
||||
<th className='px-4 py-2 text-left font-medium text-muted-foreground text-xs'>
|
||||
<th className='whitespace-nowrap px-4 py-2 text-left font-medium text-muted-foreground text-xs'>
|
||||
Deployed By
|
||||
</th>
|
||||
<th className='px-4 py-2 text-left font-medium text-muted-foreground text-xs'>
|
||||
<th className='whitespace-nowrap px-4 py-2 text-left font-medium text-muted-foreground text-xs'>
|
||||
Created
|
||||
</th>
|
||||
<th className='w-10' />
|
||||
@@ -669,7 +727,11 @@ export function DeployModal({
|
||||
<tr
|
||||
key={v.id}
|
||||
className='cursor-pointer transition-colors hover:bg-muted/30'
|
||||
onClick={() => openVersionPreview(v.version)}
|
||||
onClick={() => {
|
||||
if (editingVersion !== v.version) {
|
||||
openVersionPreview(v.version)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<td className='px-4 py-2.5'>
|
||||
<div
|
||||
@@ -679,22 +741,54 @@ export function DeployModal({
|
||||
title={v.isActive ? 'Active' : 'Inactive'}
|
||||
/>
|
||||
</td>
|
||||
<td className='px-4 py-2.5'>
|
||||
<span className='font-medium text-sm'>v{v.version}</span>
|
||||
<td className='w-[220px] max-w-[220px] px-4 py-2.5'>
|
||||
{editingVersion === v.version ? (
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleSaveRename(v.version)
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
handleCancelRename()
|
||||
}
|
||||
}}
|
||||
onBlur={() => handleSaveRename(v.version)}
|
||||
className='w-full border-0 bg-transparent p-0 font-medium text-sm leading-5 outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
maxLength={100}
|
||||
disabled={isRenaming}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
spellCheck='false'
|
||||
/>
|
||||
) : (
|
||||
<span className='block whitespace-pre-wrap break-words break-all font-medium text-sm leading-5'>
|
||||
{v.name || `v${v.version}`}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className='px-4 py-2.5'>
|
||||
<td className='whitespace-nowrap px-4 py-2.5'>
|
||||
<span className='text-muted-foreground text-sm'>
|
||||
{v.deployedBy || 'Unknown'}
|
||||
</span>
|
||||
</td>
|
||||
<td className='px-4 py-2.5'>
|
||||
<td className='whitespace-nowrap px-4 py-2.5'>
|
||||
<span className='text-muted-foreground text-sm'>
|
||||
{new Date(v.createdAt).toLocaleDateString()}{' '}
|
||||
{new Date(v.createdAt).toLocaleTimeString()}
|
||||
</span>
|
||||
</td>
|
||||
<td className='px-4 py-2.5' onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu
|
||||
open={openDropdown === v.version}
|
||||
onOpenChange={(open) =>
|
||||
setOpenDropdown(open ? v.version : null)
|
||||
}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
@@ -705,7 +799,10 @@ export function DeployModal({
|
||||
<MoreVertical className='h-4 w-4' />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuContent
|
||||
align='end'
|
||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => activateVersion(v.version)}
|
||||
disabled={v.isActive || activatingVersion === v.version}
|
||||
@@ -721,6 +818,11 @@ export function DeployModal({
|
||||
>
|
||||
Inspect
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleStartRename(v.version, v.name)}
|
||||
>
|
||||
Rename
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</td>
|
||||
@@ -889,7 +991,9 @@ export function DeployModal({
|
||||
selectedVersion={previewVersion}
|
||||
onActivateVersion={() => activateVersion(previewVersion)}
|
||||
isActivating={activatingVersion === previewVersion}
|
||||
selectedVersionLabel={`v${previewVersion}`}
|
||||
selectedVersionLabel={
|
||||
versions.find((v) => v.version === previewVersion)?.name || `v${previewVersion}`
|
||||
}
|
||||
workflowId={workflowId}
|
||||
isSelectedVersionActive={versions.find((v) => v.version === previewVersion)?.isActive}
|
||||
/>
|
||||
|
||||
@@ -22,6 +22,7 @@ export type WorkflowDeploymentVersion = InferSelectModel<typeof workflowDeployme
|
||||
export interface WorkflowDeploymentVersionResponse {
|
||||
id: string
|
||||
version: number
|
||||
name?: string | null
|
||||
isActive: boolean
|
||||
createdAt: string
|
||||
createdBy?: string | null
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "workflow_deployment_version" ADD COLUMN "name" text;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -687,6 +687,13 @@
|
||||
"when": 1760206888564,
|
||||
"tag": "0098_thick_prima",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 99,
|
||||
"version": "7",
|
||||
"when": 1760240967304,
|
||||
"tag": "0099_deep_sir_ram",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1317,6 +1317,7 @@ export const workflowDeploymentVersion = pgTable(
|
||||
.notNull()
|
||||
.references(() => workflow.id, { onDelete: 'cascade' }),
|
||||
version: integer('version').notNull(),
|
||||
name: text('name'),
|
||||
state: json('state').notNull(),
|
||||
isActive: boolean('is_active').notNull().default(false),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
|
||||
Reference in New Issue
Block a user