Feature/api (#82)

* my test changes for branch protection

* feat(api): introduced 'deploy as an API' button and updated workflows db to include status of deployment

* feat(api): added 'trigger' column for logs table to indicate source of workflow run, persist logs from API executions, removed session validation in favor of API key

* fix(bug): cleanup old reference to JSX element in favor of ReactElement

* feat(api): added persistent notification for one-click deployment with copy boxes for url, keys, & ex curl

* fix(ui/notifications): cleaned up deploy with one-click button ui
This commit is contained in:
waleedlatif1
2025-02-23 13:46:50 -08:00
committed by GitHub
parent 48b6095d53
commit f52de5d1d6
27 changed files with 2184 additions and 1480 deletions

View File

@@ -290,6 +290,7 @@ export async function GET(req: NextRequest) {
log.error || `Completed successfully`
}`,
duration: log.success ? `${log.durationMs}ms` : 'NA',
trigger: 'schedule',
createdAt: new Date(log.endedAt || log.startedAt),
})
}
@@ -309,6 +310,7 @@ export async function GET(req: NextRequest) {
? 'Scheduled workflow executed successfully'
: `Scheduled workflow execution failed: ${result.error}`,
duration: result.success ? `${totalDuration}ms` : 'NA',
trigger: 'schedule',
createdAt: new Date(),
})
@@ -353,6 +355,7 @@ export async function GET(req: NextRequest) {
level: 'error',
message: error.message || 'Unknown error during scheduled workflow execution',
createdAt: new Date(),
trigger: 'schedule',
})
// On error, increment next_run_at by a small delay to prevent immediate retries

View File

@@ -0,0 +1,40 @@
import { NextRequest } from 'next/server'
import { eq } from 'drizzle-orm'
import { v4 as uuidv4 } from 'uuid'
import { db } from '@/db'
import { workflow } from '@/db/schema'
import { validateWorkflowAccess } from '../../middleware'
import { createErrorResponse, createSuccessResponse } from '../../utils'
export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'
export async function POST(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)
}
// Generate a new API key
const apiKey = `wf_${uuidv4().replace(/-/g, '')}`
// Update the workflow with the API key and deployment status
await db
.update(workflow)
.set({
apiKey,
isDeployed: true,
deployedAt: new Date(),
})
.where(eq(workflow.id, id))
return createSuccessResponse({ apiKey })
} catch (error: any) {
console.error('Error deploying workflow:', error)
return createErrorResponse(error.message || 'Failed to deploy workflow', 500)
}
}

View File

@@ -0,0 +1,214 @@
import { NextRequest } from 'next/server'
import { eq } from 'drizzle-orm'
import { v4 as uuidv4 } from 'uuid'
import { z } from 'zod'
import { persistLog } from '@/lib/logging'
import { decryptSecret } from '@/lib/utils'
import { WorkflowState } from '@/stores/workflow/types'
import { mergeSubblockState } from '@/stores/workflow/utils'
import { db } from '@/db'
import { environment } from '@/db/schema'
import { Executor } from '@/executor'
import { Serializer } from '@/serializer'
import { validateWorkflowAccess } from '../../middleware'
import { createErrorResponse, createSuccessResponse } from '../../utils'
export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'
// Define the schema for environment variables
const EnvVarsSchema = z.record(z.string())
// Keep track of running executions to prevent overlap
const runningExecutions = new Set<string>()
async function executeWorkflow(workflow: any, input?: any) {
const workflowId = workflow.id
const executionId = uuidv4()
// Skip if this workflow is already running
if (runningExecutions.has(workflowId)) {
throw new Error('Workflow is already running')
}
try {
runningExecutions.add(workflowId)
// Get the workflow state
const state = workflow.state as WorkflowState
const { blocks, edges, loops } = state
// Use the same execution flow as in scheduled executions
const mergedStates = mergeSubblockState(blocks)
// Retrieve environment variables for this user
const [userEnv] = await db
.select()
.from(environment)
.where(eq(environment.userId, workflow.userId))
.limit(1)
if (!userEnv) {
throw new Error('No environment variables found for this user')
}
// Parse and validate environment variables
const variables = EnvVarsSchema.parse(userEnv.variables)
// Replace environment variables in the block states
const currentBlockStates = await Object.entries(mergedStates).reduce(
async (accPromise, [id, block]) => {
const acc = await accPromise
acc[id] = await Object.entries(block.subBlocks).reduce(
async (subAccPromise, [key, subBlock]) => {
const subAcc = await subAccPromise
let value = subBlock.value
// If the value is a string and contains environment variable syntax
if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) {
const matches = value.match(/{{([^}]+)}}/g)
if (matches) {
// Process all matches sequentially
for (const match of matches) {
const varName = match.slice(2, -2) // Remove {{ and }}
const encryptedValue = variables[varName]
if (!encryptedValue) {
throw new Error(`Environment variable "${varName}" was not found`)
}
try {
const { decrypted } = await decryptSecret(encryptedValue)
value = (value as string).replace(match, decrypted)
} catch (error: any) {
console.error('Error decrypting value:', error)
throw new Error(
`Failed to decrypt environment variable "${varName}": ${error.message}`
)
}
}
}
}
subAcc[key] = value
return subAcc
},
Promise.resolve({} as Record<string, any>)
)
return acc
},
Promise.resolve({} as Record<string, Record<string, any>>)
)
// Create a map of decrypted environment variables
const decryptedEnvVars: Record<string, string> = {}
for (const [key, encryptedValue] of Object.entries(variables)) {
try {
const { decrypted } = await decryptSecret(encryptedValue)
decryptedEnvVars[key] = decrypted
} catch (error: any) {
console.error(`Failed to decrypt ${key}:`, error)
throw new Error(`Failed to decrypt environment variable "${key}": ${error.message}`)
}
}
// Serialize and execute the workflow
const serializedWorkflow = new Serializer().serializeWorkflow(mergedStates, edges, loops)
const executor = new Executor(serializedWorkflow, currentBlockStates, decryptedEnvVars)
const result = await executor.execute(workflowId)
// Log each execution step
for (const log of result.logs || []) {
await persistLog({
id: uuidv4(),
workflowId,
executionId,
level: log.success ? 'info' : 'error',
message: `Block ${log.blockName || log.blockId} (${log.blockType}): ${
log.error || 'Completed successfully'
}`,
duration: log.success ? `${log.durationMs}ms` : 'NA',
trigger: 'api',
createdAt: new Date(log.endedAt || log.startedAt),
})
}
// Calculate total duration from successful block logs
const totalDuration = (result.logs || [])
.filter((log) => log.success)
.reduce((sum, log) => sum + log.durationMs, 0)
// Log the final execution result
await persistLog({
id: uuidv4(),
workflowId,
executionId,
level: result.success ? 'info' : 'error',
message: result.success
? 'API workflow executed successfully'
: `API workflow execution failed: ${result.error}`,
duration: result.success ? `${totalDuration}ms` : 'NA',
trigger: 'api',
createdAt: new Date(),
})
return result
} catch (error: any) {
// Log the error
await persistLog({
id: uuidv4(),
workflowId,
executionId,
level: 'error',
message: `API workflow execution failed: ${error.message}`,
duration: 'NA',
trigger: 'api',
createdAt: new Date(),
})
throw error
} finally {
runningExecutions.delete(workflowId)
}
}
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
try {
const validation = await validateWorkflowAccess(request, id)
if (validation.error) {
return createErrorResponse(validation.error.message, validation.error.status)
}
const result = await executeWorkflow(validation.workflow)
return createSuccessResponse(result)
} catch (error: any) {
console.error('Error executing workflow:', error)
return createErrorResponse(
error.message || 'Failed to execute workflow',
500,
'EXECUTION_ERROR'
)
}
}
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
try {
const validation = await validateWorkflowAccess(request, id)
if (validation.error) {
return createErrorResponse(validation.error.message, validation.error.status)
}
const body = await request.json().catch(() => ({}))
const result = await executeWorkflow(validation.workflow, body)
return createSuccessResponse(result)
} catch (error: any) {
console.error('Error executing workflow:', error)
return createErrorResponse(
error.message || 'Failed to execute workflow',
500,
'EXECUTION_ERROR'
)
}
}

View File

@@ -0,0 +1,54 @@
import { NextRequest } from 'next/server'
import { Executor } from '@/executor'
import { SerializedWorkflow } from '@/serializer/types'
import { validateWorkflowAccess } from '../middleware'
import { createErrorResponse, createSuccessResponse } from '../utils'
export const dynamic = 'force-dynamic'
async function executeWorkflow(workflow: any, input?: any) {
try {
const executor = new Executor(workflow.state as SerializedWorkflow, input)
const result = await executor.execute(workflow.id)
return result
} catch (error: any) {
console.error('Workflow execution failed:', { workflowId: workflow.id, error })
throw new Error(`Execution failed: ${error.message}`)
}
}
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params
const validation = await validateWorkflowAccess(request, id)
if (validation.error) {
return createErrorResponse(validation.error.message, validation.error.status)
}
const result = await executeWorkflow(validation.workflow)
return createSuccessResponse(result)
} catch (error: any) {
console.error('Error executing workflow:', error)
return createErrorResponse('Failed to execute workflow', 500, 'EXECUTION_ERROR')
}
}
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params
const validation = await validateWorkflowAccess(request, id)
if (validation.error) {
return createErrorResponse(validation.error.message, validation.error.status)
}
const body = await request.json().catch(() => ({}))
const result = await executeWorkflow(validation.workflow, body)
return createSuccessResponse(result)
} catch (error: any) {
console.error('Error executing workflow:', error)
return createErrorResponse('Failed to execute workflow', 500, 'EXECUTION_ERROR')
}
}

View File

@@ -0,0 +1,20 @@
import { NextRequest } from 'next/server'
import { validateWorkflowAccess } from '../../middleware'
import { createErrorResponse, createSuccessResponse } from '../../utils'
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params
const validation = await validateWorkflowAccess(request, id, false)
if (validation.error) {
return createErrorResponse(validation.error.message, validation.error.status)
}
return createSuccessResponse({
isDeployed: validation.workflow.isDeployed,
deployedAt: validation.workflow.deployedAt,
})
} catch (error) {
return createErrorResponse('Failed to get status', 500)
}
}

View File

@@ -0,0 +1,56 @@
import { NextRequest } from 'next/server'
import { getWorkflowById } from '@/lib/workflows'
export interface ValidationResult {
error?: { message: string; status: number }
workflow?: any
}
export async function validateWorkflowAccess(
request: NextRequest,
workflowId: string,
requireDeployment = true
): Promise<ValidationResult> {
try {
const workflow = await getWorkflowById(workflowId)
if (!workflow) {
return {
error: {
message: 'Workflow not found',
status: 404,
},
}
}
if (requireDeployment) {
if (!workflow.isDeployed) {
return {
error: {
message: 'Workflow is not deployed',
status: 403,
},
}
}
// API key authentication
const apiKey = request.headers.get('x-api-key')
if (!apiKey || !workflow.apiKey || apiKey !== workflow.apiKey) {
return {
error: {
message: 'Unauthorized',
status: 401,
},
}
}
}
return { workflow }
} catch (error) {
console.error('Validation error:', error)
return {
error: {
message: 'Internal server error',
status: 500,
},
}
}
}

15
app/api/workflow/utils.ts Normal file
View File

@@ -0,0 +1,15 @@
import { NextResponse } from 'next/server'
export function createErrorResponse(error: string, status: number, code?: string) {
return NextResponse.json(
{
error,
code: code || error.toUpperCase().replace(/\s+/g, '_'),
},
{ status }
)
}
export function createSuccessResponse(data: any) {
return NextResponse.json(data)
}

View File

@@ -1,3 +1,5 @@
import Landing from './(landing)/landing'
export default Landing
// adding test

View File

@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react'
import type { ReactElement } from 'react'
import { highlight, languages } from 'prismjs'
import 'prismjs/components/prism-javascript'
import 'prismjs/themes/prism.css'
@@ -148,7 +149,7 @@ export function Code({
// Render helpers
const renderLineNumbers = () => {
const numbers: JSX.Element[] = []
const numbers: ReactElement[] = []
let lineNumber = 1
visualLineHeights.forEach((height) => {

View File

@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react'
import type { ReactElement } from 'react'
import { ChevronDown, ChevronUp, Plus, Trash } from 'lucide-react'
import { highlight, languages } from 'prismjs'
import 'prismjs/components/prism-javascript'
@@ -220,7 +221,7 @@ export function ConditionInput({ blockId, subBlockId, isConnecting }: ConditionI
// Modify the line numbers rendering to be block-specific
const renderLineNumbers = (blockId: string) => {
const numbers: JSX.Element[] = []
const numbers: ReactElement[] = []
let lineNumber = 1
const blockHeights = visualLineHeights[blockId] || []

View File

@@ -1,27 +1,35 @@
import { useEffect, useState } from 'react'
import { formatDistanceToNow } from 'date-fns'
import { AlertCircle, Terminal } from 'lucide-react'
import { AlertCircle, Copy, Key, Terminal, X } from 'lucide-react'
import { ErrorIcon } from '@/components/icons'
import { Button } from '@/components/ui/button'
import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
import { cn } from '@/lib/utils'
import { useNotificationStore } from '@/stores/notifications/store'
import { NotificationStore, NotificationType } from '@/stores/notifications/types'
import {
NotificationOptions,
NotificationStore,
NotificationType,
} from '@/stores/notifications/types'
interface NotificationDropdownItemProps {
id: string
type: NotificationType
message: string
timestamp: number
options?: NotificationOptions
}
const NotificationIcon = {
error: ErrorIcon,
console: Terminal,
api: Key,
}
const NotificationColors = {
error: 'text-destructive',
console: 'text-foreground',
api: 'text-green-500',
}
export function NotificationDropdownItem({
@@ -29,10 +37,12 @@ export function NotificationDropdownItem({
type,
message,
timestamp,
options,
}: NotificationDropdownItemProps) {
const { showNotification } = useNotificationStore()
const { removeNotification } = useNotificationStore()
const Icon = NotificationIcon[type]
const [, forceUpdate] = useState({})
const [copied, setCopied] = useState(false)
// Update the time display every minute
useEffect(() => {
@@ -42,21 +52,58 @@ export function NotificationDropdownItem({
const timeAgo = formatDistanceToNow(timestamp, { addSuffix: true })
// Truncate message if it's too long
const truncatedMessage = message.length > 50 ? `${message.slice(0, 50)}...` : message
const handleCopy = async () => {
if (options?.copyableContent) {
await navigator.clipboard.writeText(options.copyableContent)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
}
return (
<DropdownMenuItem
className="flex items-start gap-2 p-3 cursor-pointer"
onClick={() => showNotification(id)}
className="flex items-start gap-2 p-3 cursor-default"
onSelect={(e) => e.preventDefault()}
>
<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 text-muted-foreground">{timeAgo}</span>
<Icon className={cn('h-4 w-4 mt-1', NotificationColors[type])} />
<div className="flex flex-col gap-1 flex-1">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span className="text-xs font-medium">
{type === 'error' ? 'Error' : type === 'api' ? 'API' : 'Console'}
</span>
<span className="text-xs text-muted-foreground">{timeAgo}</span>
</div>
{options?.isPersistent && (
<Button
variant="ghost"
size="icon"
className="h-4 w-4 hover:bg-transparent hover:text-destructive"
onClick={() => removeNotification(id)}
>
<X className="h-3 w-3" />
</Button>
)}
</div>
<p className="text-sm text-foreground">{truncatedMessage}</p>
<p className="text-sm text-foreground whitespace-pre-wrap">{message}</p>
{options?.copyableContent && (
<div className="mt-2 relative">
<pre className="bg-muted rounded-md p-2 text-xs font-mono">
{options.copyableContent}
</pre>
<Button
variant="ghost"
size="icon"
className="absolute top-2 right-2 h-6 w-6 hover:bg-muted-foreground/20"
onClick={handleCopy}
>
<Copy className="h-3 w-3" />
</Button>
{copied && (
<span className="absolute top-2 right-9 text-xs text-muted-foreground">Copied!</span>
)}
</div>
)}
</div>
</DropdownMenuItem>
)

View File

@@ -3,7 +3,7 @@
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { formatDistanceToNow } from 'date-fns'
import { Bell, History, MessageSquare, Play, Trash2 } from 'lucide-react'
import { Bell, History, Play, Rocket, Trash2 } from 'lucide-react'
import {
AlertDialog,
AlertDialogAction,
@@ -23,6 +23,7 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { useNotificationStore } from '@/stores/notifications/store'
import { useWorkflowRegistry } from '@/stores/workflow/registry/store'
import { useWorkflowStore } from '@/stores/workflow/store'
@@ -31,10 +32,12 @@ import { HistoryDropdownItem } from './components/history-dropdown-item'
import { NotificationDropdownItem } from './components/notification-dropdown-item'
export function ControlBar() {
const { notifications, getWorkflowNotifications } = useNotificationStore()
const { notifications, getWorkflowNotifications, addNotification } = useNotificationStore()
const { history, undo, redo, revertToHistoryState, lastSaved } = useWorkflowStore()
const [isEditing, setIsEditing] = useState(false)
const [editedName, setEditedName] = useState('')
const [historyOpen, setHistoryOpen] = useState(false)
const [notificationsOpen, setNotificationsOpen] = useState(false)
const { workflows, updateWorkflow, activeWorkflowId, removeWorkflow } = useWorkflowRegistry()
const [, forceUpdate] = useState({})
const { isExecuting, handleRunWorkflow } = useWorkflowExecution()
@@ -103,6 +106,66 @@ export function ControlBar() {
}
}
// Add the deployment state and handlers
const [isDeploying, setIsDeploying] = useState(false)
const [isDeployed, setIsDeployed] = useState(false)
// Check deployment status on mount
useEffect(() => {
async function checkStatus() {
if (!activeWorkflowId) return
try {
const response = await fetch(`/api/workflow/${activeWorkflowId}/status`)
if (response.ok) {
const data = await response.json()
setIsDeployed(data.isDeployed)
}
} catch (error) {
console.error('Failed to check deployment status:', error)
}
}
checkStatus()
}, [activeWorkflowId])
const handleDeploy = async () => {
if (!activeWorkflowId) return
try {
setIsDeploying(true)
const response = await fetch(`/api/workflow/${activeWorkflowId}/deploy`, {
method: 'POST',
})
if (!response.ok) throw new Error('Failed to deploy workflow')
const { apiKey } = await response.json()
const endpoint = `${process.env.NEXT_PUBLIC_APP_URL}/api/workflow/${activeWorkflowId}/execute`
addNotification('api', 'Workflow successfully deployed', 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}`,
},
],
})
setIsDeployed(true)
} catch (error) {
addNotification('error', 'Failed to deploy workflow. Please try again.', activeWorkflowId)
} finally {
setIsDeploying(false)
}
}
return (
<div className="flex h-16 w-full items-center justify-between bg-background px-6 border-b transition-all duration-300">
{/* Left Section - Workflow Info */}
@@ -177,13 +240,39 @@ export function ControlBar() {
</AlertDialogContent>
</AlertDialog>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<History />
<span className="sr-only">Version History</span>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={handleDeploy}
disabled={isDeploying}
className={cn(
'hover:text-foreground',
isDeployed && 'text-green-500 hover:text-green-500'
)}
>
<Rocket className={`h-5 w-5 ${isDeploying ? 'animate-pulse' : ''}`} />
<span className="sr-only">Deploy API</span>
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>
{isDeploying ? 'Deploying...' : isDeployed ? 'Deployed' : 'Deploy as API Endpoint'}
</TooltipContent>
</Tooltip>
<DropdownMenu open={historyOpen} onOpenChange={setHistoryOpen}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<History />
<span className="sr-only">Version History</span>
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
{!historyOpen && <TooltipContent>History</TooltipContent>}
</Tooltip>
{history.past.length === 0 && history.future.length === 0 ? (
<DropdownMenuContent align="end" className="w-40">
@@ -227,13 +316,18 @@ export function ControlBar() {
)}
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Bell />
<span className="sr-only">Notifications</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenu open={notificationsOpen} onOpenChange={setNotificationsOpen}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Bell />
<span className="sr-only">Notifications</span>
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
{!notificationsOpen && <TooltipContent>Notifications</TooltipContent>}
</Tooltip>
{workflowNotifications.length === 0 ? (
<DropdownMenuContent align="end" className="w-40">
@@ -246,7 +340,14 @@ export function ControlBar() {
{[...workflowNotifications]
.sort((a, b) => b.timestamp - a.timestamp)
.map((notification) => (
<NotificationDropdownItem key={notification.id} {...notification} />
<NotificationDropdownItem
key={notification.id}
id={notification.id}
type={notification.type}
message={notification.message}
timestamp={notification.timestamp}
options={notification.options}
/>
))}
</DropdownMenuContent>
)}

View File

@@ -1,7 +1,8 @@
import { useEffect, useState } from 'react'
import { AlertCircle, Terminal } from 'lucide-react'
import { AlertCircle, Copy, Key, Terminal, X } from 'lucide-react'
import { ErrorIcon } from '@/components/icons'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { useNotificationStore } from '@/stores/notifications/store'
@@ -11,6 +12,7 @@ const FADE_DURATION = 300
const NotificationIcon = {
error: ErrorIcon,
console: Terminal,
api: Key,
}
const NotificationColors = {
@@ -18,18 +20,19 @@ const NotificationColors = {
'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',
}
export function NotificationList() {
const { notifications, hideNotification } = useNotificationStore()
const [fadingNotifications, setFadingNotifications] = useState<Set<string>>(new Set())
const [copiedMap, setCopiedMap] = useState<Record<string, boolean>>({})
// Only show visible notifications in the display
const visibleNotifications = notifications.filter((n) => n.isVisible)
useEffect(() => {
notifications.forEach((notification) => {
if (!notification.isVisible) return
if (!notification.isVisible || notification.options?.isPersistent) return
// Start fade out
const fadeTimer = setTimeout(() => {
@@ -53,6 +56,14 @@ export function NotificationList() {
})
}, [notifications, hideNotification])
const handleCopy = async (id: string, sectionIndex: number, content: string) => {
await navigator.clipboard.writeText(content)
setCopiedMap((prev) => ({ ...prev, [`${id}-${sectionIndex}`]: true }))
setTimeout(() => {
setCopiedMap((prev) => ({ ...prev, [`${id}-${sectionIndex}`]: false }))
}, 2000)
}
if (visibleNotifications.length === 0) return null
return (
@@ -76,16 +87,55 @@ export function NotificationList() {
NotificationColors[notification.type]
)}
>
<Icon
className={cn('h-4 w-4', {
'!text-red-500': notification.type === 'error',
'text-foreground': notification.type === 'console',
})}
/>
<AlertTitle className="ml-2">
{notification.type === 'error' ? 'Error' : 'Console'}
</AlertTitle>
<AlertDescription className="ml-2">{notification.message}</AlertDescription>
<div className="flex items-start gap-2 w-full">
<Icon
className={cn('h-4 w-4 mt-1', {
'!text-red-500': notification.type === 'error',
'text-foreground': notification.type === 'console',
'!text-green-500': notification.type === 'api',
})}
/>
<div className="flex-1">
<AlertTitle className="flex items-center justify-between">
<span>
{notification.type === 'error'
? 'Error'
: notification.type === 'api'
? 'API'
: 'Console'}
</span>
{notification.options?.isPersistent && (
<Button
variant="ghost"
size="icon"
className="h-4 w-4 hover:bg-transparent hover:text-destructive -mt-1"
onClick={() => hideNotification(notification.id)}
>
<X className="h-3 w-3" />
</Button>
)}
</AlertTitle>
<AlertDescription>
<p className="mb-4">{notification.message}</p>
{notification.options?.sections?.map((section, index) => (
<div key={index} className="mt-4">
<div className="text-xs font-medium mb-2">{section.label}</div>
<div
className="relative group cursor-pointer"
onClick={() => handleCopy(notification.id, index, section.content)}
>
<pre className="bg-muted rounded-md p-2 pr-20 text-xs font-mono whitespace-pre-wrap transition-colors hover:bg-muted/80">
{section.content}
</pre>
<div className="absolute top-2 right-2 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>
)
})}

View File

@@ -0,0 +1,3 @@
ALTER TABLE "workflow" ADD COLUMN "is_deployed" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "workflow" ADD COLUMN "deployed_at" timestamp;--> statement-breakpoint
ALTER TABLE "workflow" ADD COLUMN "api_key" text;

View File

@@ -0,0 +1 @@
ALTER TABLE "workflow_logs" ADD COLUMN "trigger" text NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE "workflow_logs" ALTER COLUMN "trigger" DROP NOT NULL;

View File

@@ -1,6 +1,6 @@
{
"id": "b90e880d-8074-44c6-808a-fc57568d7bd3",
"prevId": "1e6e54f4-6927-4625-983d-b4a1b1fad36d",
"id": "7e51973f-f82e-4094-86b3-34866e50f43e",
"prevId": "b90e880d-8074-44c6-808a-fc57568d7bd3",
"version": "7",
"dialect": "postgresql",
"tables": {
@@ -503,6 +503,25 @@
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"is_deployed": {
"name": "is_deployed",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"deployed_at": {
"name": "deployed_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"api_key": {
"name": "api_key",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},

View File

@@ -0,0 +1,708 @@
{
"id": "141235ed-23e4-420b-8e3c-d502c64a4c28",
"prevId": "7e51973f-f82e-4094-86b3-34866e50f43e",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.environment": {
"name": "environment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"variables": {
"name": "variables",
"type": "json",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"environment_user_id_user_id_fk": {
"name": "environment_user_id_user_id_fk",
"tableFrom": "environment",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"environment_user_id_unique": {
"name": "environment_user_id_unique",
"nullsNotDistinct": false,
"columns": ["user_id"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": ["token"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.settings": {
"name": "settings",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"general": {
"name": "general",
"type": "json",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"settings_user_id_user_id_fk": {
"name": "settings_user_id_user_id_fk",
"tableFrom": "settings",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"settings_user_id_unique": {
"name": "settings_user_id_unique",
"nullsNotDistinct": false,
"columns": ["user_id"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": ["email"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verification": {
"name": "verification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.waitlist": {
"name": "waitlist",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'pending'"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"waitlist_email_unique": {
"name": "waitlist_email_unique",
"nullsNotDistinct": false,
"columns": ["email"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workflow": {
"name": "workflow",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "json",
"primaryKey": false,
"notNull": true
},
"last_synced": {
"name": "last_synced",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"is_deployed": {
"name": "is_deployed",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"deployed_at": {
"name": "deployed_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"api_key": {
"name": "api_key",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"workflow_user_id_user_id_fk": {
"name": "workflow_user_id_user_id_fk",
"tableFrom": "workflow",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workflow_logs": {
"name": "workflow_logs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"execution_id": {
"name": "execution_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"level": {
"name": "level",
"type": "text",
"primaryKey": false,
"notNull": true
},
"message": {
"name": "message",
"type": "text",
"primaryKey": false,
"notNull": true
},
"duration": {
"name": "duration",
"type": "text",
"primaryKey": false,
"notNull": false
},
"trigger": {
"name": "trigger",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_logs_workflow_id_workflow_id_fk": {
"name": "workflow_logs_workflow_id_workflow_id_fk",
"tableFrom": "workflow_logs",
"tableTo": "workflow",
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workflow_schedule": {
"name": "workflow_schedule",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"cron_expression": {
"name": "cron_expression",
"type": "text",
"primaryKey": false,
"notNull": false
},
"next_run_at": {
"name": "next_run_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"last_ran_at": {
"name": "last_ran_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"trigger_type": {
"name": "trigger_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_schedule_workflow_id_workflow_id_fk": {
"name": "workflow_schedule_workflow_id_workflow_id_fk",
"tableFrom": "workflow_schedule",
"tableTo": "workflow",
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"workflow_schedule_workflow_id_unique": {
"name": "workflow_schedule_workflow_id_unique",
"nullsNotDistinct": false,
"columns": ["workflow_id"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,708 @@
{
"id": "87a9f389-6dcc-441d-8000-a447c9c25522",
"prevId": "141235ed-23e4-420b-8e3c-d502c64a4c28",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.environment": {
"name": "environment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"variables": {
"name": "variables",
"type": "json",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"environment_user_id_user_id_fk": {
"name": "environment_user_id_user_id_fk",
"tableFrom": "environment",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"environment_user_id_unique": {
"name": "environment_user_id_unique",
"nullsNotDistinct": false,
"columns": ["user_id"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": ["token"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.settings": {
"name": "settings",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"general": {
"name": "general",
"type": "json",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"settings_user_id_user_id_fk": {
"name": "settings_user_id_user_id_fk",
"tableFrom": "settings",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"settings_user_id_unique": {
"name": "settings_user_id_unique",
"nullsNotDistinct": false,
"columns": ["user_id"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": ["email"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verification": {
"name": "verification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.waitlist": {
"name": "waitlist",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'pending'"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"waitlist_email_unique": {
"name": "waitlist_email_unique",
"nullsNotDistinct": false,
"columns": ["email"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workflow": {
"name": "workflow",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "json",
"primaryKey": false,
"notNull": true
},
"last_synced": {
"name": "last_synced",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"is_deployed": {
"name": "is_deployed",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"deployed_at": {
"name": "deployed_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"api_key": {
"name": "api_key",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"workflow_user_id_user_id_fk": {
"name": "workflow_user_id_user_id_fk",
"tableFrom": "workflow",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workflow_logs": {
"name": "workflow_logs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"execution_id": {
"name": "execution_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"level": {
"name": "level",
"type": "text",
"primaryKey": false,
"notNull": true
},
"message": {
"name": "message",
"type": "text",
"primaryKey": false,
"notNull": true
},
"duration": {
"name": "duration",
"type": "text",
"primaryKey": false,
"notNull": false
},
"trigger": {
"name": "trigger",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_logs_workflow_id_workflow_id_fk": {
"name": "workflow_logs_workflow_id_workflow_id_fk",
"tableFrom": "workflow_logs",
"tableTo": "workflow",
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workflow_schedule": {
"name": "workflow_schedule",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"cron_expression": {
"name": "cron_expression",
"type": "text",
"primaryKey": false,
"notNull": false
},
"next_run_at": {
"name": "next_run_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"last_ran_at": {
"name": "last_ran_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"trigger_type": {
"name": "trigger_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_schedule_workflow_id_workflow_id_fk": {
"name": "workflow_schedule_workflow_id_workflow_id_fk",
"tableFrom": "workflow_schedule",
"tableTo": "workflow",
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"workflow_schedule_workflow_id_unique": {
"name": "workflow_schedule_workflow_id_unique",
"nullsNotDistinct": false,
"columns": ["workflow_id"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -64,6 +64,27 @@
"when": 1740004147676,
"tag": "0008_quick_paladin",
"breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1740336159745,
"tag": "0009_cynical_bullseye",
"breakpoints": true
},
{
"idx": 10,
"version": "7",
"when": 1740340257207,
"tag": "0010_flashy_nebula",
"breakpoints": true
},
{
"idx": 11,
"version": "7",
"when": 1740340299261,
"tag": "0011_youthful_iron_lad",
"breakpoints": true
}
]
}

View File

@@ -61,6 +61,9 @@ export const workflow = pgTable('workflow', {
lastSynced: timestamp('last_synced').notNull(),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull(),
isDeployed: boolean('is_deployed').notNull().default(false),
deployedAt: timestamp('deployed_at'),
apiKey: text('api_key'),
})
export const waitlist = pgTable('waitlist', {
@@ -80,6 +83,7 @@ export const workflowLogs = pgTable('workflow_logs', {
level: text('level').notNull(), // e.g. "info", "error", etc.
message: text('message').notNull(),
duration: text('duration'), // Store as text to allow 'NA' for errors
trigger: text('trigger'), // e.g. "api", "schedule", "manual"
createdAt: timestamp('created_at').notNull().defaultNow(),
})

View File

@@ -9,6 +9,7 @@ export interface LogEntry {
message: string
createdAt: Date
duration?: string
trigger?: string
}
export async function persistLog(log: LogEntry) {

View File

@@ -1,5 +1,5 @@
import { type ClassValue, clsx } from 'clsx'
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
@@ -117,3 +117,17 @@ export function convertScheduleOptionsToCron(
throw new Error('Unsupported schedule type')
}
}
export async function generateApiKey(): Promise<string> {
const buffer = randomBytes(32)
const hash = createHash('sha256').update(buffer).digest('hex')
return `wf_${hash}`
}
export async function validateApiKey(
apiKey: string | null,
storedApiKey: string | null
): Promise<boolean> {
if (!apiKey || !storedApiKey) return false
return apiKey === storedApiKey
}

30
lib/workflows.ts Normal file
View File

@@ -0,0 +1,30 @@
import { eq } from 'drizzle-orm'
import { db } from '@/db'
import { workflow as workflowTable } from '@/db/schema'
export async function getWorkflowById(id: string) {
const workflows = await db.select().from(workflowTable).where(eq(workflowTable.id, id)).limit(1)
return workflows[0]
}
export async function updateWorkflowDeploymentStatus(
id: string,
isDeployed: boolean,
apiKey?: string
) {
return db
.update(workflowTable)
.set({
isDeployed,
deployedAt: isDeployed ? new Date() : null,
updatedAt: new Date(),
apiKey: apiKey || null,
})
.where(eq(workflowTable.id, id))
}
export function getWorkflowEndpoint(id: string) {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
return `${baseUrl}/api/workflow/${id}`
}

1428
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { Notification, NotificationStore, NotificationType } from './types'
import { Notification, NotificationOptions, NotificationStore, NotificationType } from './types'
const STORAGE_KEY = 'workflow-notifications'
// Maximum number of notifications to keep across all workflows
@@ -24,7 +24,7 @@ export const useNotificationStore = create<NotificationStore>()(
(set, get) => ({
notifications: loadPersistedNotifications(),
addNotification: (type, message, workflowId) => {
addNotification: (type, message, workflowId, options: NotificationOptions = {}) => {
// Only create notifications on the client side
if (typeof window === 'undefined') return
@@ -32,9 +32,10 @@ export const useNotificationStore = create<NotificationStore>()(
id: crypto.randomUUID(),
type,
message,
timestamp: Date.now(), // Simplified timestamp handling
timestamp: Date.now(),
isVisible: true,
workflowId,
options,
}
set((state) => {

View File

@@ -1,4 +1,4 @@
export type NotificationType = 'error' | 'console'
export type NotificationType = 'error' | 'console' | 'api'
export interface Notification {
id: string
@@ -7,11 +7,28 @@ export interface Notification {
timestamp: number
isVisible: boolean
workflowId: string | null
options?: NotificationOptions
}
export interface NotificationSection {
label: string
content: string
}
export interface NotificationOptions {
copyableContent?: string
isPersistent?: boolean
sections?: NotificationSection[]
}
export interface NotificationStore {
notifications: Notification[]
addNotification: (type: NotificationType, message: string, workflowId: string | null) => void
addNotification: (
type: NotificationType,
message: string,
workflowId: string | null,
options?: NotificationOptions
) => void
hideNotification: (id: string) => void
showNotification: (id: string) => void
removeNotification: (id: string) => void