mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(deploy): ui/ux improvement, added deletion to API, added handling when inactive
This commit is contained in:
@@ -39,3 +39,33 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
return createErrorResponse(error.message || 'Failed to deploy workflow', 500)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const validation = await validateWorkflowAccess(request, id, false)
|
||||
|
||||
if (validation.error) {
|
||||
return createErrorResponse(validation.error.message, validation.error.status)
|
||||
}
|
||||
|
||||
// Update the workflow to remove deployment
|
||||
await db
|
||||
.update(workflow)
|
||||
.set({
|
||||
apiKey: null,
|
||||
isDeployed: false,
|
||||
deployedAt: null,
|
||||
})
|
||||
.where(eq(workflow.id, id))
|
||||
|
||||
return createSuccessResponse({ isDeployed: false, deployedAt: null, apiKey: null })
|
||||
} catch (error: any) {
|
||||
console.error('Error undeploying workflow:', error)
|
||||
return createErrorResponse(error.message || 'Failed to undeploy workflow', 500)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { AlertCircle, Copy, Key, Terminal, X } from 'lucide-react'
|
||||
import { AlertCircle, Copy, Rocket, Terminal, X } from 'lucide-react'
|
||||
import { ErrorIcon } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
|
||||
@@ -23,13 +23,13 @@ interface NotificationDropdownItemProps {
|
||||
const NotificationIcon = {
|
||||
error: ErrorIcon,
|
||||
console: Terminal,
|
||||
api: Key,
|
||||
api: Rocket,
|
||||
}
|
||||
|
||||
const NotificationColors = {
|
||||
error: 'text-destructive',
|
||||
console: 'text-foreground',
|
||||
api: 'text-green-500',
|
||||
api: 'text-[#7F2FFF]',
|
||||
}
|
||||
|
||||
export function NotificationDropdownItem({
|
||||
@@ -37,6 +37,7 @@ export function NotificationDropdownItem({
|
||||
type,
|
||||
message,
|
||||
timestamp,
|
||||
options,
|
||||
}: NotificationDropdownItemProps) {
|
||||
const { showNotification } = useNotificationStore()
|
||||
const Icon = NotificationIcon[type]
|
||||
@@ -58,7 +59,9 @@ export function NotificationDropdownItem({
|
||||
<Icon className={cn('h-4 w-4', NotificationColors[type])} />
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium">{type === 'error' ? 'Error' : 'Console'}</span>
|
||||
<span className="text-xs font-medium">
|
||||
{type === 'error' ? 'Error' : type === 'api' ? 'API' : 'Console'}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{timeAgo}</span>
|
||||
</div>
|
||||
<p className="text-sm text-foreground">{message}</p>
|
||||
|
||||
@@ -40,7 +40,8 @@ export function ControlBar() {
|
||||
const router = useRouter()
|
||||
|
||||
// Store hooks
|
||||
const { notifications, getWorkflowNotifications, addNotification } = useNotificationStore()
|
||||
const { notifications, getWorkflowNotifications, addNotification, showNotification } =
|
||||
useNotificationStore()
|
||||
const { history, revertToHistoryState, lastSaved, isDeployed, setDeploymentStatus } =
|
||||
useWorkflowStore()
|
||||
const { workflows, updateWorkflow, activeWorkflowId, removeWorkflow } = useWorkflowRegistry()
|
||||
@@ -152,6 +153,57 @@ export function ControlBar() {
|
||||
*/
|
||||
const handleDeploy = async () => {
|
||||
if (!activeWorkflowId) return
|
||||
|
||||
// If already deployed, show the existing deployment info instead of redeploying
|
||||
if (isDeployed) {
|
||||
// Find existing API notification for this workflow
|
||||
const apiNotification = workflowNotifications.find(
|
||||
(n) => n.type === 'api' && n.workflowId === activeWorkflowId
|
||||
)
|
||||
|
||||
if (apiNotification) {
|
||||
// Show the existing notification
|
||||
showNotification(apiNotification.id)
|
||||
return
|
||||
}
|
||||
|
||||
// If notification not found but workflow is deployed, fetch deployment info
|
||||
try {
|
||||
setIsDeploying(true)
|
||||
|
||||
const response = await fetch(`/api/workflow/${activeWorkflowId}/deploy/info`)
|
||||
if (!response.ok) throw new Error('Failed to fetch deployment info')
|
||||
|
||||
const { apiKey } = await response.json()
|
||||
const endpoint = `${process.env.NEXT_PUBLIC_APP_URL}/api/workflow/${activeWorkflowId}/execute`
|
||||
|
||||
// Create a new notification with the deployment info
|
||||
addNotification('api', 'Workflow deployment information', activeWorkflowId, {
|
||||
isPersistent: true,
|
||||
sections: [
|
||||
{
|
||||
label: 'API Endpoint',
|
||||
content: endpoint,
|
||||
},
|
||||
{
|
||||
label: 'API Key',
|
||||
content: apiKey,
|
||||
},
|
||||
{
|
||||
label: 'Example curl command',
|
||||
content: `curl -X POST -H "X-API-Key: ${apiKey}" -H "Content-Type: application/json" ${endpoint}`,
|
||||
},
|
||||
],
|
||||
})
|
||||
} catch (error) {
|
||||
addNotification('error', 'Failed to fetch deployment information', activeWorkflowId)
|
||||
} finally {
|
||||
setIsDeploying(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If not deployed, proceed with deployment
|
||||
try {
|
||||
setIsDeploying(true)
|
||||
|
||||
@@ -277,17 +329,14 @@ export function ControlBar() {
|
||||
size="icon"
|
||||
onClick={handleDeploy}
|
||||
disabled={isDeploying}
|
||||
className={cn(
|
||||
'hover:text-foreground',
|
||||
isDeployed && 'text-green-500 hover:text-green-500'
|
||||
)}
|
||||
className={cn('hover:text-[#7F2FFF]', (isDeployed || isDeploying) && 'text-[#7F2FFF]')}
|
||||
>
|
||||
<Rocket className={`h-5 w-5 ${isDeploying ? 'animate-pulse' : ''}`} />
|
||||
<Rocket className={cn('h-5 w-5', isDeploying && 'animate-rocket-pulse text-[#7F2FFF]')} />
|
||||
<span className="sr-only">Deploy API</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isDeploying ? 'Deploying...' : isDeployed ? 'Deployed' : 'Deploy as API Endpoint'}
|
||||
{isDeploying ? 'Deploying...' : isDeployed ? 'Deployed' : 'Deploy as API'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
@@ -419,9 +468,9 @@ export function ControlBar() {
|
||||
{/* Right Section - Actions */}
|
||||
<div className="flex items-center gap-3">
|
||||
{renderDeleteButton()}
|
||||
{renderDeployButton()}
|
||||
{renderHistoryDropdown()}
|
||||
{renderNotificationsDropdown()}
|
||||
{renderDeployButton()}
|
||||
{renderRunButton()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,45 +1,113 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AlertCircle, Copy, Key, Terminal, X } from 'lucide-react'
|
||||
import { Copy, Rocket, Terminal, X } from 'lucide-react'
|
||||
import { ErrorIcon } from '@/components/icons'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useNotificationStore } from '@/stores/notifications/store'
|
||||
import { Notification, NotificationType } from '@/stores/notifications/types'
|
||||
import { useWorkflowStore } from '@/stores/workflow/store'
|
||||
|
||||
// Constants
|
||||
const NOTIFICATION_TIMEOUT = 4000
|
||||
const FADE_DURATION = 300
|
||||
|
||||
// Icon mapping for notification types
|
||||
const NotificationIcon = {
|
||||
error: ErrorIcon,
|
||||
console: Terminal,
|
||||
api: Key,
|
||||
api: Rocket,
|
||||
}
|
||||
|
||||
// Color schemes for different notification types
|
||||
const NotificationColors = {
|
||||
error:
|
||||
'border-red-500 bg-red-50 text-destructive dark:border-border dark:text-foreground dark:bg-background',
|
||||
console:
|
||||
'border-border bg-background text-foreground dark:border-border dark:text-foreground dark:bg-background',
|
||||
api: 'border-green-500 bg-green-50 text-green-700 dark:border-border dark:text-green-500 dark:bg-background',
|
||||
api: 'border-border bg-background text-foreground dark:border-border dark:text-foreground dark:bg-background',
|
||||
}
|
||||
|
||||
// API deployment status styling
|
||||
const ApiStatusStyles = {
|
||||
active: 'text-green-600 bg-green-50 dark:bg-green-900/20 dark:text-green-400',
|
||||
inactive: 'text-red-600 bg-red-50 dark:bg-red-900/20 dark:text-red-400',
|
||||
}
|
||||
|
||||
/**
|
||||
* AlertDialog component for API deletion confirmation
|
||||
*/
|
||||
function DeleteApiConfirmation({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
workflowId,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onConfirm: () => void
|
||||
workflowId: string | null
|
||||
}) {
|
||||
return (
|
||||
<AlertDialog open={isOpen} onOpenChange={onClose}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete API Deployment</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete this API deployment? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onConfirm} className="bg-red-600 hover:bg-red-700">
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* NotificationList component displays all active notifications as alerts
|
||||
* with support for auto-dismissal, copying content, and different styling per type
|
||||
*/
|
||||
export function NotificationList() {
|
||||
// Store access
|
||||
const { notifications, hideNotification } = useNotificationStore()
|
||||
|
||||
// Local state
|
||||
const [fadingNotifications, setFadingNotifications] = useState<Set<string>>(new Set())
|
||||
const [copiedMap, setCopiedMap] = useState<Record<string, boolean>>({})
|
||||
|
||||
// Filter to only show visible notifications
|
||||
const visibleNotifications = notifications.filter((n) => n.isVisible)
|
||||
|
||||
// Handle auto-dismissal of non-persistent notifications
|
||||
useEffect(() => {
|
||||
// Setup timers for each notification
|
||||
const timers: ReturnType<typeof setTimeout>[] = []
|
||||
|
||||
notifications.forEach((notification) => {
|
||||
// Skip if already hidden or marked as persistent
|
||||
if (!notification.isVisible || notification.options?.isPersistent) return
|
||||
|
||||
// Start fade out
|
||||
// Start fade out animation
|
||||
const fadeTimer = setTimeout(() => {
|
||||
setFadingNotifications((prev) => new Set([...prev, notification.id]))
|
||||
}, NOTIFICATION_TIMEOUT)
|
||||
|
||||
// Hide notification after fade
|
||||
// Hide notification after fade completes
|
||||
const hideTimer = setTimeout(() => {
|
||||
hideNotification(notification.id)
|
||||
setFadingNotifications((prev) => {
|
||||
@@ -49,21 +117,32 @@ export function NotificationList() {
|
||||
})
|
||||
}, NOTIFICATION_TIMEOUT + FADE_DURATION)
|
||||
|
||||
return () => {
|
||||
clearTimeout(fadeTimer)
|
||||
clearTimeout(hideTimer)
|
||||
}
|
||||
timers.push(fadeTimer, hideTimer)
|
||||
})
|
||||
|
||||
// Cleanup timers on unmount or when notifications change
|
||||
return () => timers.forEach(clearTimeout)
|
||||
}, [notifications, hideNotification])
|
||||
|
||||
/**
|
||||
* Handles copying section content to clipboard
|
||||
* @param id - Notification ID
|
||||
* @param sectionIndex - Index of the section being copied
|
||||
* @param content - Text content to copy
|
||||
*/
|
||||
const handleCopy = async (id: string, sectionIndex: number, content: string) => {
|
||||
await navigator.clipboard.writeText(content)
|
||||
|
||||
// Show "Copied!" indicator
|
||||
setCopiedMap((prev) => ({ ...prev, [`${id}-${sectionIndex}`]: true }))
|
||||
|
||||
// Reset after 2 seconds
|
||||
setTimeout(() => {
|
||||
setCopiedMap((prev) => ({ ...prev, [`${id}-${sectionIndex}`]: false }))
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// Early return if no notifications to show
|
||||
if (visibleNotifications.length === 0) return null
|
||||
|
||||
return (
|
||||
@@ -74,73 +153,246 @@ export function NotificationList() {
|
||||
transform: 'translateX(-50%)',
|
||||
}}
|
||||
>
|
||||
{visibleNotifications.map((notification) => {
|
||||
const Icon = NotificationIcon[notification.type]
|
||||
const isFading = fadingNotifications.has(notification.id)
|
||||
|
||||
return (
|
||||
<Alert
|
||||
key={notification.id}
|
||||
className={cn(
|
||||
'transition-all duration-300 ease-in-out opacity-0 translate-y-[-100%]',
|
||||
isFading ? 'animate-notification-fade-out' : 'animate-notification-slide',
|
||||
NotificationColors[notification.type]
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-4 py-1">
|
||||
<Icon
|
||||
className={cn('h-4 w-4', {
|
||||
'!text-red-500': notification.type === 'error',
|
||||
'text-foreground': notification.type === 'console',
|
||||
'!text-green-500': notification.type === 'api',
|
||||
})}
|
||||
/>
|
||||
<div className="flex-1 space-y-2">
|
||||
<AlertTitle className="flex items-center justify-between -mt-0.5">
|
||||
<span>
|
||||
{notification.type === 'error'
|
||||
? 'Error'
|
||||
: notification.type === 'api'
|
||||
? 'API'
|
||||
: 'Console'}
|
||||
</span>
|
||||
{notification.options?.isPersistent && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 hover:bg-transparent hover:text-destructive"
|
||||
onClick={() => hideNotification(notification.id)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="space-y-4">
|
||||
<p>{notification.message}</p>
|
||||
{notification.options?.sections?.map((section, index) => (
|
||||
<div key={index} className="space-y-1.5">
|
||||
<div className="text-xs font-medium text-muted-foreground">
|
||||
{section.label}
|
||||
</div>
|
||||
<div
|
||||
className="relative group cursor-pointer"
|
||||
onClick={() => handleCopy(notification.id, index, section.content)}
|
||||
>
|
||||
<pre className="bg-muted rounded-md p-3 text-xs font-mono whitespace-pre-wrap transition-colors hover:bg-muted/80">
|
||||
{section.content}
|
||||
</pre>
|
||||
<div className="absolute top-3 right-3 text-xs text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{copiedMap[`${notification.id}-${index}`] ? 'Copied!' : 'Click to copy'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</AlertDescription>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
)
|
||||
})}
|
||||
{visibleNotifications.map((notification) => (
|
||||
<NotificationAlert
|
||||
key={notification.id}
|
||||
notification={notification}
|
||||
isFading={fadingNotifications.has(notification.id)}
|
||||
copiedMap={copiedMap}
|
||||
onHide={hideNotification}
|
||||
onCopy={handleCopy}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Individual notification alert component
|
||||
*/
|
||||
interface NotificationAlertProps {
|
||||
notification: Notification
|
||||
isFading: boolean
|
||||
copiedMap: Record<string, boolean>
|
||||
onHide: (id: string) => void
|
||||
onCopy: (id: string, sectionIndex: number, content: string) => void
|
||||
}
|
||||
|
||||
function NotificationAlert({
|
||||
notification,
|
||||
isFading,
|
||||
copiedMap,
|
||||
onHide,
|
||||
onCopy,
|
||||
}: NotificationAlertProps) {
|
||||
const { id, type, message, options, workflowId } = notification
|
||||
const Icon = NotificationIcon[type]
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
|
||||
const { setDeploymentStatus } = useWorkflowStore()
|
||||
const { isDeployed } = useWorkflowStore((state) => ({
|
||||
isDeployed: state.isDeployed,
|
||||
}))
|
||||
|
||||
const handleDeleteApi = async () => {
|
||||
if (!workflowId) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/workflow/${workflowId}/deploy`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (!response.ok) throw new Error('Failed to delete API deployment')
|
||||
|
||||
// Update deployment status in the store
|
||||
setDeploymentStatus(false)
|
||||
|
||||
// Close the notification
|
||||
onHide(id)
|
||||
|
||||
// Close the dialog
|
||||
setIsDeleteDialogOpen(false)
|
||||
} catch (error) {
|
||||
console.error('Error deleting API deployment:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Alert
|
||||
className={cn(
|
||||
'transition-all duration-300 ease-in-out opacity-0 translate-y-[-100%]',
|
||||
isFading ? 'animate-notification-fade-out' : 'animate-notification-slide',
|
||||
NotificationColors[type]
|
||||
)}
|
||||
>
|
||||
{type === 'api' ? (
|
||||
// Special layout for API notifications with equal spacing
|
||||
<div className="flex items-start py-1">
|
||||
{/* Left icon */}
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
<Icon className="!text-[#7F2FFF] h-4 w-4" />
|
||||
</div>
|
||||
|
||||
{/* Content area with equal margins */}
|
||||
<div className="flex-1 space-y-2 pt-[3.5px] mx-4">
|
||||
<AlertTitle className="-mt-0.5">
|
||||
<span>API</span>
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription className="space-y-4">
|
||||
<p>{!isDeployed ? 'Workflow currently not deployed' : message}</p>
|
||||
|
||||
{/* Optional sections with copyable content */}
|
||||
{options?.sections?.map((section, index) => (
|
||||
<div key={index} className="space-y-1.5">
|
||||
<div className="text-xs font-medium text-muted-foreground">{section.label}</div>
|
||||
|
||||
{/* Copyable code block */}
|
||||
<div
|
||||
className="relative group rounded-md border bg-muted/50 hover:bg-muted/80 transition-colors cursor-pointer"
|
||||
onClick={() => onCopy(id, index, section.content)}
|
||||
>
|
||||
<pre className="p-3 text-xs font-mono whitespace-pre-wrap overflow-x-auto">
|
||||
{section.content}
|
||||
</pre>
|
||||
|
||||
{/* Copy indicator */}
|
||||
<div className="absolute top-2 right-2 flex items-center gap-1.5">
|
||||
<div className="text-xs text-muted-foreground bg-background/100 px-2 py-1 rounded-md opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{copiedMap[`${id}-${index}`] ? 'Copied!' : 'Click to copy'}
|
||||
</div>
|
||||
<Copy className="h-3.5 w-3.5 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity bg-muted/100" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Status and Delete button row - with pulsing green indicator */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">Status:</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="relative flex items-center justify-center">
|
||||
{isDeployed ? (
|
||||
<>
|
||||
<div className="absolute h-3 w-3 rounded-full bg-green-500/20 animate-ping"></div>
|
||||
<div className="relative h-2 w-2 rounded-full bg-green-500"></div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="absolute h-3 w-3 rounded-full bg-red-500/20 animate-ping"></div>
|
||||
<div className="relative h-2 w-2 rounded-full bg-red-500"></div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs font-medium',
|
||||
isDeployed ? ApiStatusStyles.active : ApiStatusStyles.inactive
|
||||
)}
|
||||
>
|
||||
{isDeployed ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2.5 text-xs font-medium text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={() => setIsDeleteDialogOpen(true)}
|
||||
disabled={!isDeployed}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</div>
|
||||
|
||||
{/* Close button on the right, aligned with header */}
|
||||
<div className="flex-shrink-0 mt-[-3.5px]">
|
||||
{options?.isPersistent && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 hover:bg-transparent hover:text-destructive"
|
||||
onClick={() => onHide(id)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Original layout for error and console notifications
|
||||
<div className="flex items-start gap-4 py-1">
|
||||
{/* Icon with proper vertical alignment */}
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
<Icon
|
||||
className={cn('h-4 w-4', {
|
||||
'!text-red-500 mt-[-3px]': type === 'error',
|
||||
'text-foreground mt-[-3px]': type === 'console',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content area with right margin for balance */}
|
||||
<div className="flex-1 space-y-2 mr-4">
|
||||
<AlertTitle className="flex items-center justify-between -mt-0.5">
|
||||
<span>{type === 'error' ? 'Error' : 'Console'}</span>
|
||||
|
||||
{/* Close button for persistent notifications */}
|
||||
{options?.isPersistent && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 hover:bg-transparent hover:text-destructive"
|
||||
onClick={() => onHide(id)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription className="space-y-4">
|
||||
<p>{message}</p>
|
||||
|
||||
{/* Optional sections with copyable content */}
|
||||
{options?.sections?.map((section, index) => (
|
||||
<div key={index} className="space-y-1.5">
|
||||
<div className="text-xs font-medium text-muted-foreground">{section.label}</div>
|
||||
|
||||
{/* Copyable code block */}
|
||||
<div
|
||||
className="relative group rounded-md border bg-muted/50 hover:bg-muted/80 transition-colors cursor-pointer"
|
||||
onClick={() => onCopy(id, index, section.content)}
|
||||
>
|
||||
<pre className="p-3 text-xs font-mono whitespace-pre-wrap overflow-x-auto">
|
||||
{section.content}
|
||||
</pre>
|
||||
|
||||
{/* Copy indicator */}
|
||||
<div className="absolute top-2 right-2 flex items-center gap-1.5">
|
||||
<div className="text-xs text-muted-foreground bg-background/80 px-2 py-1 rounded-md opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{copiedMap[`${id}-${index}`] ? 'Copied!' : 'Click to copy'}
|
||||
</div>
|
||||
<Copy className="h-3.5 w-3.5 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</AlertDescription>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Alert>
|
||||
|
||||
{/* Delete API confirmation dialog */}
|
||||
<DeleteApiConfirmation
|
||||
isOpen={isDeleteDialogOpen}
|
||||
onClose={() => setIsDeleteDialogOpen(false)}
|
||||
onConfirm={handleDeleteApi}
|
||||
workflowId={workflowId}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+11
-2
@@ -73,11 +73,11 @@ export default {
|
||||
},
|
||||
'notification-fade-out': {
|
||||
'0%': {
|
||||
opacity: '1',
|
||||
opacity: '0',
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
'100%': {
|
||||
opacity: '0',
|
||||
opacity: '1',
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
},
|
||||
@@ -91,12 +91,21 @@ export default {
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
},
|
||||
'rocket-pulse': {
|
||||
'0%, 100%': {
|
||||
opacity: '1',
|
||||
},
|
||||
'50%': {
|
||||
opacity: '0.7',
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'slide-down': 'slide-down 0.3s ease-out',
|
||||
'notification-slide': 'notification-slide 0.3s ease-out forwards',
|
||||
'notification-fade-out': 'notification-fade-out 0.2s ease-out forwards',
|
||||
'fade-up': 'fade-up 0.5s ease-out forwards',
|
||||
'rocket-pulse': 'rocket-pulse 1.5s ease-in-out infinite',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user