mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b9019d9a2 | ||
|
|
6d00d6bf2c | ||
|
|
3267d8cc24 | ||
|
|
2e69f85364 | ||
|
|
57e5bac121 | ||
|
|
8ce0299400 | ||
|
|
a0796f088b | ||
|
|
98fe4cd40b | ||
|
|
34d210c66c | ||
|
|
2334f2dca4 | ||
|
|
65fc138bfc | ||
|
|
e8f7fe0989 | ||
|
|
ace87791d8 | ||
|
|
74af452175 | ||
|
|
ec51f73596 | ||
|
|
6866da590c | ||
|
|
b0c0ee29a8 | ||
|
|
f0d1950477 | ||
|
|
0fdd8ffb55 | ||
|
|
d581009099 | ||
|
|
7d0fdefb22 | ||
|
|
73e00f53e1 | ||
|
|
1d7ae906bc | ||
|
|
560fa75155 | ||
|
|
14089f7dbb | ||
|
|
e615816dce | ||
|
|
ca87d7ce29 | ||
|
|
6bebbc5e29 | ||
|
|
7b572f1f61 | ||
|
|
ed9a71f0af | ||
|
|
c78c870fda | ||
|
|
19442f19e2 | ||
|
|
1731a4d7f0 | ||
|
|
9fcd02fd3b | ||
|
|
ff7b5b528c | ||
|
|
30f2d1a0fc | ||
|
|
4bd0731871 | ||
|
|
4f3bc37fe4 | ||
|
|
84d6fdc423 | ||
|
|
4c12914d35 | ||
|
|
e9bdc57616 | ||
|
|
36612ae42a | ||
|
|
1c2c2c65d4 | ||
|
|
ecd3536a72 | ||
|
|
8c0a2e04b1 | ||
|
|
6586c5ce40 | ||
|
|
3ce947566d | ||
|
|
70c36cb7aa | ||
|
|
f1ec5fe824 | ||
|
|
e07e3c34cc | ||
|
|
0d2e6ff31d | ||
|
|
4fd0989264 | ||
|
|
67f8a687f6 | ||
|
|
af592349d3 | ||
|
|
0d86ea01f0 | ||
|
|
115f04e989 | ||
|
|
34d92fae89 | ||
|
|
67aa4bb332 | ||
|
|
15ace5e63f | ||
|
|
fdca73679d | ||
|
|
da46a387c9 | ||
|
|
b7e377ec4b |
150
apps/docs/content/docs/en/blocks/credential.mdx
Normal file
150
apps/docs/content/docs/en/blocks/credential.mdx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
---
|
||||||
|
title: Credential
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Callout } from 'fumadocs-ui/components/callout'
|
||||||
|
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||||
|
import { Image } from '@/components/ui/image'
|
||||||
|
import { FAQ } from '@/components/ui/faq'
|
||||||
|
|
||||||
|
The Credential block has two operations: **Select Credential** picks a single OAuth credential and outputs its ID reference for downstream blocks; **List Credentials** returns all OAuth credentials in the workspace (optionally filtered by provider) as an array for iteration.
|
||||||
|
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Image
|
||||||
|
src="/static/blocks/credential.png"
|
||||||
|
alt="Credential Block"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="my-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Callout>
|
||||||
|
The Credential block outputs credential **ID references**, not secrets. Downstream blocks receive the ID and resolve the actual OAuth token securely during their own execution.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
### Operation
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|---|---|
|
||||||
|
| **Select Credential** | Pick one OAuth credential and output its reference — use this to wire a single credential into downstream blocks |
|
||||||
|
| **List Credentials** | Return all OAuth credentials in the workspace as an array — use this with a ForEach loop |
|
||||||
|
|
||||||
|
### Credential (Select operation)
|
||||||
|
|
||||||
|
Select an OAuth credential from your workspace. The dropdown shows all connected OAuth accounts (Google, GitHub, Slack, etc.).
|
||||||
|
|
||||||
|
In advanced mode, paste a credential ID directly. You can copy a credential ID from your workspace's Credentials settings page.
|
||||||
|
|
||||||
|
### Provider (List operation)
|
||||||
|
|
||||||
|
Filter the returned OAuth credentials by provider. Select one or more providers from the dropdown — only providers you have credentials for will appear. Leave empty to return all OAuth credentials.
|
||||||
|
|
||||||
|
| Example | Returns |
|
||||||
|
|---|---|
|
||||||
|
| Gmail | Gmail credentials only |
|
||||||
|
| Slack | Slack credentials only |
|
||||||
|
| Gmail + Slack | Gmail and Slack credentials |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
<Tabs items={['Select Credential', 'List Credentials']}>
|
||||||
|
<Tab>
|
||||||
|
| Output | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `credentialId` | `string` | The credential ID — pipe this into other blocks' credential fields |
|
||||||
|
| `displayName` | `string` | Human-readable name (e.g. "waleed@company.com") |
|
||||||
|
| `providerId` | `string` | OAuth provider ID (e.g. `google-email`, `slack`) |
|
||||||
|
</Tab>
|
||||||
|
<Tab>
|
||||||
|
| Output | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `credentials` | `json` | Array of OAuth credential objects (see shape below) |
|
||||||
|
| `count` | `number` | Number of credentials returned |
|
||||||
|
|
||||||
|
Each object in the `credentials` array:
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `credentialId` | `string` | The credential ID |
|
||||||
|
| `displayName` | `string` | Human-readable name |
|
||||||
|
| `providerId` | `string` | OAuth provider ID |
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## Example Use Cases
|
||||||
|
|
||||||
|
**Shared credential across multiple blocks** — Define once, use everywhere
|
||||||
|
```
|
||||||
|
Credential (Select, Google) → Gmail (Send) & Google Drive (Upload) & Google Calendar (Create)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Multi-account workflows** — Route to different credentials based on logic
|
||||||
|
```
|
||||||
|
Agent (Determine account) → Condition → Credential A or Credential B → Slack (Post)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Iterate over all Gmail accounts**
|
||||||
|
```
|
||||||
|
Credential (List, Provider: Gmail) → ForEach Loop → Gmail (Send) using <loop.currentItem.credentialId>
|
||||||
|
```
|
||||||
|
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Image
|
||||||
|
src="/static/blocks/credential-loop.png"
|
||||||
|
alt="Credential List wired into a ForEach Loop"
|
||||||
|
width={900}
|
||||||
|
height={400}
|
||||||
|
className="my-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## How to wire a Credential block
|
||||||
|
|
||||||
|
### Select Credential
|
||||||
|
|
||||||
|
1. Drop a **Credential** block and select your OAuth credential from the picker
|
||||||
|
2. In the downstream block, switch to **advanced mode** on its credential field
|
||||||
|
3. Enter `<credentialBlockName.credentialId>` as the value
|
||||||
|
|
||||||
|
<Tabs items={['Gmail', 'Slack']}>
|
||||||
|
<Tab>
|
||||||
|
In the Gmail block's credential field (advanced mode):
|
||||||
|
```
|
||||||
|
<myCredential.credentialId>
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab>
|
||||||
|
In the Slack block's credential field (advanced mode):
|
||||||
|
```
|
||||||
|
<myCredential.credentialId>
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
### List Credentials
|
||||||
|
|
||||||
|
1. Drop a **Credential** block, set Operation to **List Credentials**
|
||||||
|
2. Optionally select one or more **Providers** to narrow results (only your connected providers appear)
|
||||||
|
3. Wire `<credentialBlockName.credentials>` into a **ForEach Loop** as the items source
|
||||||
|
4. Inside the loop, reference `<loop.currentItem.credentialId>` in downstream blocks' credential fields
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- **Define once, reference many times**: When five blocks use the same Google account, use one Credential block and wire all five to `<credential.credentialId>` instead of selecting the account five times
|
||||||
|
- **Outputs are safe to log**: The `credentialId` output is a UUID reference, not a secret. It is safe to inspect in execution logs
|
||||||
|
- **Use for environment switching**: Pair with a Condition block to route to a production or staging OAuth credential based on a workflow variable
|
||||||
|
- **Advanced mode is required**: Downstream blocks must be in advanced mode on their credential field to accept a dynamic reference
|
||||||
|
- **Use List + ForEach for fan-out**: When you need to run the same action across all accounts of a provider, List Credentials feeds naturally into a ForEach loop
|
||||||
|
- **Narrow by provider**: Use the Provider multiselect to filter to specific services — only providers you have credentials for are shown
|
||||||
|
|
||||||
|
<FAQ items={[
|
||||||
|
{ question: "Does the Credential block expose my secret or token?", answer: "No. The block outputs a credential ID (a UUID), not the actual OAuth token. Downstream blocks receive the ID and resolve the token securely in their own execution context. Secrets never appear in workflow state, logs, or the canvas." },
|
||||||
|
{ question: "What credential types does it support?", answer: "OAuth connected accounts only (Google, GitHub, Slack, etc.). Environment variables and service accounts cannot be resolved by ID in downstream blocks, so they are not supported." },
|
||||||
|
{ question: "How is Select different from just copying a credential ID into advanced mode?", answer: "Functionally identical — both pass the same credential ID to the downstream block. The Credential block adds value when you need to use one credential in many blocks (change it once), or when you want to select between credentials dynamically using a Condition block." },
|
||||||
|
{ question: "Can I list all OAuth credentials in my workspace?", answer: "Yes. Set the Operation to 'List Credentials'. Optionally filter by provider using the Provider multiselect. Wire the credentials output into a ForEach loop to process each credential individually." },
|
||||||
|
{ question: "Can I use a Credential block output in a Function block?", answer: "Yes. Reference <credential.credentialId> in your Function block's code. Note that the function will receive the raw UUID string — if you need the resolved token, the downstream block must handle the resolution (as integration blocks do). The Function block does not automatically resolve credential IDs." },
|
||||||
|
{ question: "What happens if the credential is deleted?", answer: "The Select operation will throw an error at execution time: 'Credential not found'. The List operation will simply omit the deleted credential from the results. Update the Credential block to select a valid credential before re-running." },
|
||||||
|
]} />
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
"agent",
|
"agent",
|
||||||
"api",
|
"api",
|
||||||
"condition",
|
"condition",
|
||||||
|
"credential",
|
||||||
"evaluator",
|
"evaluator",
|
||||||
"function",
|
"function",
|
||||||
"guardrails",
|
"guardrails",
|
||||||
|
|||||||
BIN
apps/docs/public/static/blocks/credential-loop.png
Normal file
BIN
apps/docs/public/static/blocks/credential-loop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
BIN
apps/docs/public/static/blocks/credential.png
Normal file
BIN
apps/docs/public/static/blocks/credential.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -1,16 +1,18 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Suspense, useMemo, useRef, useState } from 'react'
|
import { Suspense, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'
|
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { Eye, EyeOff, Loader2 } from 'lucide-react'
|
import { Eye, EyeOff, Loader2 } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
|
import { usePostHog } from 'posthog-js/react'
|
||||||
import { Input, Label } from '@/components/emcn'
|
import { Input, Label } from '@/components/emcn'
|
||||||
import { client, useSession } from '@/lib/auth/auth-client'
|
import { client, useSession } from '@/lib/auth/auth-client'
|
||||||
import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env'
|
import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||||
|
import { captureEvent } from '@/lib/posthog/client'
|
||||||
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
|
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
|
||||||
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
|
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
|
||||||
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
|
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
|
||||||
@@ -81,7 +83,12 @@ function SignupFormContent({
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const { refetch: refetchSession } = useSession()
|
const { refetch: refetchSession } = useSession()
|
||||||
|
const posthog = usePostHog()
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
captureEvent(posthog, 'signup_page_viewed', {})
|
||||||
|
}, [posthog])
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
||||||
|
|||||||
15
apps/sim/app/(home)/landing-analytics.tsx
Normal file
15
apps/sim/app/(home)/landing-analytics.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { usePostHog } from 'posthog-js/react'
|
||||||
|
import { captureEvent } from '@/lib/posthog/client'
|
||||||
|
|
||||||
|
export function LandingAnalytics() {
|
||||||
|
const posthog = usePostHog()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
captureEvent(posthog, 'landing_page_viewed', {})
|
||||||
|
}, [posthog])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Templates,
|
Templates,
|
||||||
Testimonials,
|
Testimonials,
|
||||||
} from '@/app/(home)/components'
|
} from '@/app/(home)/components'
|
||||||
|
import { LandingAnalytics } from '@/app/(home)/landing-analytics'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Landing page root component.
|
* Landing page root component.
|
||||||
@@ -45,6 +46,7 @@ export default async function Landing() {
|
|||||||
>
|
>
|
||||||
Skip to main content
|
Skip to main content
|
||||||
</a>
|
</a>
|
||||||
|
<LandingAnalytics />
|
||||||
<StructuredData />
|
<StructuredData />
|
||||||
<header>
|
<header>
|
||||||
<Navbar blogPosts={blogPosts} />
|
<Navbar blogPosts={blogPosts} />
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* @see stores/constants.ts for the source of truth
|
* @see stores/constants.ts for the source of truth
|
||||||
*/
|
*/
|
||||||
:root {
|
:root {
|
||||||
--sidebar-width: 248px; /* SIDEBAR_WIDTH.DEFAULT */
|
--sidebar-width: 0px; /* 0 outside workspace; blocking script always sets actual value on workspace pages */
|
||||||
--panel-width: 320px; /* PANEL_WIDTH.DEFAULT */
|
--panel-width: 320px; /* PANEL_WIDTH.DEFAULT */
|
||||||
--toolbar-triggers-height: 300px; /* TOOLBAR_TRIGGERS_HEIGHT.DEFAULT */
|
--toolbar-triggers-height: 300px; /* TOOLBAR_TRIGGERS_HEIGHT.DEFAULT */
|
||||||
--editor-connections-height: 172px; /* EDITOR_CONNECTIONS_HEIGHT.DEFAULT */
|
--editor-connections-height: 172px; /* EDITOR_CONNECTIONS_HEIGHT.DEFAULT */
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { generateAgentCard, generateSkillsFromWorkflow } from '@/lib/a2a/agent-c
|
|||||||
import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types'
|
import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { getRedisClient } from '@/lib/core/config/redis'
|
import { getRedisClient } from '@/lib/core/config/redis'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||||
|
|
||||||
@@ -180,6 +181,17 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
|
|||||||
|
|
||||||
logger.info(`Deleted A2A agent: ${agentId}`)
|
logger.info(`Deleted A2A agent: ${agentId}`)
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
auth.userId,
|
||||||
|
'a2a_agent_deleted',
|
||||||
|
{
|
||||||
|
agent_id: agentId,
|
||||||
|
workflow_id: existingAgent.workflowId,
|
||||||
|
workspace_id: existingAgent.workspaceId,
|
||||||
|
},
|
||||||
|
{ groups: { workspace: existingAgent.workspaceId } }
|
||||||
|
)
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error deleting agent:', error)
|
logger.error('Error deleting agent:', error)
|
||||||
@@ -251,6 +263,16 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Published A2A agent: ${agentId}`)
|
logger.info(`Published A2A agent: ${agentId}`)
|
||||||
|
captureServerEvent(
|
||||||
|
auth.userId,
|
||||||
|
'a2a_agent_published',
|
||||||
|
{
|
||||||
|
agent_id: agentId,
|
||||||
|
workflow_id: existingAgent.workflowId,
|
||||||
|
workspace_id: existingAgent.workspaceId,
|
||||||
|
},
|
||||||
|
{ groups: { workspace: existingAgent.workspaceId } }
|
||||||
|
)
|
||||||
return NextResponse.json({ success: true, isPublished: true })
|
return NextResponse.json({ success: true, isPublished: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,6 +295,16 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Unpublished A2A agent: ${agentId}`)
|
logger.info(`Unpublished A2A agent: ${agentId}`)
|
||||||
|
captureServerEvent(
|
||||||
|
auth.userId,
|
||||||
|
'a2a_agent_unpublished',
|
||||||
|
{
|
||||||
|
agent_id: agentId,
|
||||||
|
workflow_id: existingAgent.workflowId,
|
||||||
|
workspace_id: existingAgent.workspaceId,
|
||||||
|
},
|
||||||
|
{ groups: { workspace: existingAgent.workspaceId } }
|
||||||
|
)
|
||||||
return NextResponse.json({ success: true, isPublished: false })
|
return NextResponse.json({ success: true, isPublished: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
|
|||||||
import { A2A_DEFAULT_CAPABILITIES } from '@/lib/a2a/constants'
|
import { A2A_DEFAULT_CAPABILITIES } from '@/lib/a2a/constants'
|
||||||
import { sanitizeAgentName } from '@/lib/a2a/utils'
|
import { sanitizeAgentName } from '@/lib/a2a/utils'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||||
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
|
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
|
||||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||||
@@ -201,6 +202,16 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
logger.info(`Created A2A agent ${agentId} for workflow ${workflowId}`)
|
logger.info(`Created A2A agent ${agentId} for workflow ${workflowId}`)
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
auth.userId,
|
||||||
|
'a2a_agent_created',
|
||||||
|
{ agent_id: agentId, workflow_id: workflowId, workspace_id: workspaceId },
|
||||||
|
{
|
||||||
|
groups: { workspace: workspaceId },
|
||||||
|
setOnce: { first_a2a_agent_created_at: new Date().toISOString() },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return NextResponse.json({ success: true, agent }, { status: 201 })
|
return NextResponse.json({ success: true, agent }, { status: 201 })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error creating agent:', error)
|
logger.error('Error creating agent:', error)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
hasUsableSubscriptionStatus,
|
hasUsableSubscriptionStatus,
|
||||||
} from '@/lib/billing/subscriptions/utils'
|
} from '@/lib/billing/subscriptions/utils'
|
||||||
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
|
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
|
|
||||||
const logger = createLogger('SwitchPlan')
|
const logger = createLogger('SwitchPlan')
|
||||||
|
|
||||||
@@ -173,6 +174,13 @@ export async function POST(request: NextRequest) {
|
|||||||
interval: targetInterval,
|
interval: targetInterval,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'subscription_changed',
|
||||||
|
{ from_plan: sub.plan ?? 'unknown', to_plan: targetPlanName, interval: targetInterval },
|
||||||
|
{ set: { plan: targetPlanName } }
|
||||||
|
)
|
||||||
|
|
||||||
return NextResponse.json({ success: true, plan: targetPlanName, interval: targetInterval })
|
return NextResponse.json({ success: true, plan: targetPlanName, interval: targetInterval })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to switch subscription', {
|
logger.error('Failed to switch subscription', {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
createRequestTracker,
|
createRequestTracker,
|
||||||
createUnauthorizedResponse,
|
createUnauthorizedResponse,
|
||||||
} from '@/lib/copilot/request-helpers'
|
} from '@/lib/copilot/request-helpers'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import {
|
import {
|
||||||
authorizeWorkflowByWorkspacePermission,
|
authorizeWorkflowByWorkspacePermission,
|
||||||
resolveWorkflowIdForUser,
|
resolveWorkflowIdForUser,
|
||||||
@@ -188,6 +189,22 @@ export async function POST(req: NextRequest) {
|
|||||||
.warn('Failed to resolve workspaceId from workflow')
|
.warn('Failed to resolve workspaceId from workflow')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
authenticatedUserId,
|
||||||
|
'copilot_chat_sent',
|
||||||
|
{
|
||||||
|
workflow_id: workflowId,
|
||||||
|
workspace_id: resolvedWorkspaceId ?? '',
|
||||||
|
has_file_attachments: Array.isArray(fileAttachments) && fileAttachments.length > 0,
|
||||||
|
has_contexts: Array.isArray(contexts) && contexts.length > 0,
|
||||||
|
mode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groups: resolvedWorkspaceId ? { workspace: resolvedWorkspaceId } : undefined,
|
||||||
|
setOnce: { first_copilot_use_at: new Date().toISOString() },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const userMessageIdToUse = userMessageId || crypto.randomUUID()
|
const userMessageIdToUse = userMessageId || crypto.randomUUID()
|
||||||
const reqLogger = logger.withMetadata({
|
const reqLogger = logger.withMetadata({
|
||||||
requestId: tracker.requestId,
|
requestId: tracker.requestId,
|
||||||
|
|||||||
@@ -304,7 +304,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
|
|||||||
loops: {},
|
loops: {},
|
||||||
parallels: {},
|
parallels: {},
|
||||||
isDeployed: true,
|
isDeployed: true,
|
||||||
deploymentStatuses: { production: 'deployed' },
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,7 +348,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
|
|||||||
loops: {},
|
loops: {},
|
||||||
parallels: {},
|
parallels: {},
|
||||||
isDeployed: true,
|
isDeployed: true,
|
||||||
deploymentStatuses: { production: 'deployed' },
|
|
||||||
lastSaved: 1640995200000,
|
lastSaved: 1640995200000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -370,7 +368,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
|
|||||||
loops: {},
|
loops: {},
|
||||||
parallels: {},
|
parallels: {},
|
||||||
isDeployed: true,
|
isDeployed: true,
|
||||||
deploymentStatuses: { production: 'deployed' },
|
|
||||||
lastSaved: 1640995200000,
|
lastSaved: 1640995200000,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@@ -473,7 +470,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
|
|||||||
edges: undefined,
|
edges: undefined,
|
||||||
loops: null,
|
loops: null,
|
||||||
parallels: undefined,
|
parallels: undefined,
|
||||||
deploymentStatuses: null,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,7 +504,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
|
|||||||
loops: {},
|
loops: {},
|
||||||
parallels: {},
|
parallels: {},
|
||||||
isDeployed: false,
|
isDeployed: false,
|
||||||
deploymentStatuses: {},
|
|
||||||
lastSaved: 1640995200000,
|
lastSaved: 1640995200000,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -768,10 +763,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
|
|||||||
parallel1: { branches: ['branch1', 'branch2'] },
|
parallel1: { branches: ['branch1', 'branch2'] },
|
||||||
},
|
},
|
||||||
isDeployed: true,
|
isDeployed: true,
|
||||||
deploymentStatuses: {
|
|
||||||
production: 'deployed',
|
|
||||||
staging: 'pending',
|
|
||||||
},
|
|
||||||
deployedAt: '2024-01-01T10:00:00.000Z',
|
deployedAt: '2024-01-01T10:00:00.000Z',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -816,10 +807,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
|
|||||||
parallel1: { branches: ['branch1', 'branch2'] },
|
parallel1: { branches: ['branch1', 'branch2'] },
|
||||||
},
|
},
|
||||||
isDeployed: true,
|
isDeployed: true,
|
||||||
deploymentStatuses: {
|
|
||||||
production: 'deployed',
|
|
||||||
staging: 'pending',
|
|
||||||
},
|
|
||||||
deployedAt: '2024-01-01T10:00:00.000Z',
|
deployedAt: '2024-01-01T10:00:00.000Z',
|
||||||
lastSaved: 1640995200000,
|
lastSaved: 1640995200000,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ export async function POST(request: NextRequest) {
|
|||||||
loops: checkpointState?.loops || {},
|
loops: checkpointState?.loops || {},
|
||||||
parallels: checkpointState?.parallels || {},
|
parallels: checkpointState?.parallels || {},
|
||||||
isDeployed: checkpointState?.isDeployed || false,
|
isDeployed: checkpointState?.isDeployed || false,
|
||||||
deploymentStatuses: checkpointState?.deploymentStatuses || {},
|
|
||||||
lastSaved: Date.now(),
|
lastSaved: Date.now(),
|
||||||
...(checkpointState?.deployedAt &&
|
...(checkpointState?.deployedAt &&
|
||||||
checkpointState.deployedAt !== null &&
|
checkpointState.deployedAt !== null &&
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
createRequestTracker,
|
createRequestTracker,
|
||||||
createUnauthorizedResponse,
|
createUnauthorizedResponse,
|
||||||
} from '@/lib/copilot/request-helpers'
|
} from '@/lib/copilot/request-helpers'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
|
|
||||||
const logger = createLogger('CopilotFeedbackAPI')
|
const logger = createLogger('CopilotFeedbackAPI')
|
||||||
|
|
||||||
@@ -76,6 +77,12 @@ export async function POST(req: NextRequest) {
|
|||||||
duration: tracker.getDuration(),
|
duration: tracker.getDuration(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
captureServerEvent(authenticatedUserId, 'copilot_feedback_submitted', {
|
||||||
|
is_positive: isPositiveFeedback,
|
||||||
|
has_text_feedback: !!feedback,
|
||||||
|
has_workflow_yaml: !!workflowYaml,
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
feedbackId: feedbackRecord.feedbackId,
|
feedbackId: feedbackRecord.feedbackId,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
syncPersonalEnvCredentialsForUser,
|
syncPersonalEnvCredentialsForUser,
|
||||||
syncWorkspaceEnvCredentials,
|
syncWorkspaceEnvCredentials,
|
||||||
} from '@/lib/credentials/environment'
|
} from '@/lib/credentials/environment'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
|
|
||||||
const logger = createLogger('CredentialByIdAPI')
|
const logger = createLogger('CredentialByIdAPI')
|
||||||
|
|
||||||
@@ -236,6 +237,17 @@ export async function DELETE(
|
|||||||
envKeys: Object.keys(current),
|
envKeys: Object.keys(current),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
session.user.id,
|
||||||
|
'credential_deleted',
|
||||||
|
{
|
||||||
|
credential_type: 'env_personal',
|
||||||
|
provider_id: access.credential.envKey,
|
||||||
|
workspace_id: access.credential.workspaceId,
|
||||||
|
},
|
||||||
|
{ groups: { workspace: access.credential.workspaceId } }
|
||||||
|
)
|
||||||
|
|
||||||
return NextResponse.json({ success: true }, { status: 200 })
|
return NextResponse.json({ success: true }, { status: 200 })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,10 +290,33 @@ export async function DELETE(
|
|||||||
actingUserId: session.user.id,
|
actingUserId: session.user.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
session.user.id,
|
||||||
|
'credential_deleted',
|
||||||
|
{
|
||||||
|
credential_type: 'env_workspace',
|
||||||
|
provider_id: access.credential.envKey,
|
||||||
|
workspace_id: access.credential.workspaceId,
|
||||||
|
},
|
||||||
|
{ groups: { workspace: access.credential.workspaceId } }
|
||||||
|
)
|
||||||
|
|
||||||
return NextResponse.json({ success: true }, { status: 200 })
|
return NextResponse.json({ success: true }, { status: 200 })
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.delete(credential).where(eq(credential.id, id))
|
await db.delete(credential).where(eq(credential.id, id))
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
session.user.id,
|
||||||
|
'credential_deleted',
|
||||||
|
{
|
||||||
|
credential_type: access.credential.type as 'oauth' | 'service_account',
|
||||||
|
provider_id: access.credential.providerId ?? id,
|
||||||
|
workspace_id: access.credential.workspaceId,
|
||||||
|
},
|
||||||
|
{ groups: { workspace: access.credential.workspaceId } }
|
||||||
|
)
|
||||||
|
|
||||||
return NextResponse.json({ success: true }, { status: 200 })
|
return NextResponse.json({ success: true }, { status: 200 })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to delete credential', error)
|
logger.error('Failed to delete credential', error)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
|||||||
import { getWorkspaceMemberUserIds } from '@/lib/credentials/environment'
|
import { getWorkspaceMemberUserIds } from '@/lib/credentials/environment'
|
||||||
import { syncWorkspaceOAuthCredentialsForUser } from '@/lib/credentials/oauth'
|
import { syncWorkspaceOAuthCredentialsForUser } from '@/lib/credentials/oauth'
|
||||||
import { getServiceConfigByProviderId } from '@/lib/oauth'
|
import { getServiceConfigByProviderId } from '@/lib/oauth'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||||
import { isValidEnvVarName } from '@/executor/constants'
|
import { isValidEnvVarName } from '@/executor/constants'
|
||||||
|
|
||||||
@@ -600,6 +601,16 @@ export async function POST(request: NextRequest) {
|
|||||||
.where(eq(credential.id, credentialId))
|
.where(eq(credential.id, credentialId))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
session.user.id,
|
||||||
|
'credential_connected',
|
||||||
|
{ credential_type: type, provider_id: resolvedProviderId ?? type, workspace_id: workspaceId },
|
||||||
|
{
|
||||||
|
groups: { workspace: workspaceId },
|
||||||
|
setOnce: { first_credential_connected_at: new Date().toISOString() },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return NextResponse.json({ credential: created }, { status: 201 })
|
return NextResponse.json({ credential: created }, { status: 201 })
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error?.code === '23505') {
|
if (error?.code === '23505') {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { deleteDocumentStorageFiles } from '@/lib/knowledge/documents/service'
|
import { deleteDocumentStorageFiles } from '@/lib/knowledge/documents/service'
|
||||||
import { cleanupUnusedTagDefinitions } from '@/lib/knowledge/tags/service'
|
import { cleanupUnusedTagDefinitions } from '@/lib/knowledge/tags/service'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||||
import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
|
import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
|
||||||
import { CONNECTOR_REGISTRY } from '@/connectors/registry'
|
import { CONNECTOR_REGISTRY } from '@/connectors/registry'
|
||||||
@@ -351,6 +352,19 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
|||||||
`[${requestId}] Deleted connector ${connectorId}${deleteDocuments ? ` and ${docCount} documents` : `, kept ${docCount} documents`}`
|
`[${requestId}] Deleted connector ${connectorId}${deleteDocuments ? ` and ${docCount} documents` : `, kept ${docCount} documents`}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const kbWorkspaceId = writeCheck.knowledgeBase.workspaceId ?? ''
|
||||||
|
captureServerEvent(
|
||||||
|
auth.userId,
|
||||||
|
'knowledge_base_connector_removed',
|
||||||
|
{
|
||||||
|
knowledge_base_id: knowledgeBaseId,
|
||||||
|
workspace_id: kbWorkspaceId,
|
||||||
|
connector_type: existingConnector[0].connectorType,
|
||||||
|
documents_deleted: deleteDocuments ? docCount : 0,
|
||||||
|
},
|
||||||
|
kbWorkspaceId ? { groups: { workspace: kbWorkspaceId } } : undefined
|
||||||
|
)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
workspaceId: writeCheck.knowledgeBase.workspaceId,
|
workspaceId: writeCheck.knowledgeBase.workspaceId,
|
||||||
actorId: auth.userId,
|
actorId: auth.userId,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
|||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { dispatchSync } from '@/lib/knowledge/connectors/sync-engine'
|
import { dispatchSync } from '@/lib/knowledge/connectors/sync-engine'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
|
import { checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
|
||||||
|
|
||||||
const logger = createLogger('ConnectorManualSyncAPI')
|
const logger = createLogger('ConnectorManualSyncAPI')
|
||||||
@@ -55,6 +56,18 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Manual sync triggered for connector ${connectorId}`)
|
logger.info(`[${requestId}] Manual sync triggered for connector ${connectorId}`)
|
||||||
|
|
||||||
|
const kbWorkspaceId = writeCheck.knowledgeBase.workspaceId ?? ''
|
||||||
|
captureServerEvent(
|
||||||
|
auth.userId,
|
||||||
|
'knowledge_base_connector_synced',
|
||||||
|
{
|
||||||
|
knowledge_base_id: knowledgeBaseId,
|
||||||
|
workspace_id: kbWorkspaceId,
|
||||||
|
connector_type: connectorRows[0].connectorType,
|
||||||
|
},
|
||||||
|
kbWorkspaceId ? { groups: { workspace: kbWorkspaceId } } : undefined
|
||||||
|
)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
workspaceId: writeCheck.knowledgeBase.workspaceId,
|
workspaceId: writeCheck.knowledgeBase.workspaceId,
|
||||||
actorId: auth.userId,
|
actorId: auth.userId,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
|||||||
import { dispatchSync } from '@/lib/knowledge/connectors/sync-engine'
|
import { dispatchSync } from '@/lib/knowledge/connectors/sync-engine'
|
||||||
import { allocateTagSlots } from '@/lib/knowledge/constants'
|
import { allocateTagSlots } from '@/lib/knowledge/constants'
|
||||||
import { createTagDefinition } from '@/lib/knowledge/tags/service'
|
import { createTagDefinition } from '@/lib/knowledge/tags/service'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { getCredential } from '@/app/api/auth/oauth/utils'
|
import { getCredential } from '@/app/api/auth/oauth/utils'
|
||||||
import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
|
import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
|
||||||
import { CONNECTOR_REGISTRY } from '@/connectors/registry'
|
import { CONNECTOR_REGISTRY } from '@/connectors/registry'
|
||||||
@@ -227,6 +228,22 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Created connector ${connectorId} for KB ${knowledgeBaseId}`)
|
logger.info(`[${requestId}] Created connector ${connectorId} for KB ${knowledgeBaseId}`)
|
||||||
|
|
||||||
|
const kbWorkspaceId = writeCheck.knowledgeBase.workspaceId ?? ''
|
||||||
|
captureServerEvent(
|
||||||
|
auth.userId,
|
||||||
|
'knowledge_base_connector_added',
|
||||||
|
{
|
||||||
|
knowledge_base_id: knowledgeBaseId,
|
||||||
|
workspace_id: kbWorkspaceId,
|
||||||
|
connector_type: connectorType,
|
||||||
|
sync_interval_minutes: syncIntervalMinutes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groups: kbWorkspaceId ? { workspace: kbWorkspaceId } : undefined,
|
||||||
|
setOnce: { first_connector_added_at: new Date().toISOString() },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
workspaceId: writeCheck.knowledgeBase.workspaceId,
|
workspaceId: writeCheck.knowledgeBase.workspaceId,
|
||||||
actorId: auth.userId,
|
actorId: auth.userId,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
type TagFilterCondition,
|
type TagFilterCondition,
|
||||||
} from '@/lib/knowledge/documents/service'
|
} from '@/lib/knowledge/documents/service'
|
||||||
import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types'
|
import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
||||||
import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
|
import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
|
||||||
|
|
||||||
@@ -214,6 +215,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const kbWorkspaceId = accessCheck.knowledgeBase?.workspaceId
|
||||||
|
|
||||||
if (body.bulk === true) {
|
if (body.bulk === true) {
|
||||||
try {
|
try {
|
||||||
const validatedData = BulkCreateDocumentsSchema.parse(body)
|
const validatedData = BulkCreateDocumentsSchema.parse(body)
|
||||||
@@ -240,6 +243,21 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
// Silently fail
|
// Silently fail
|
||||||
}
|
}
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'knowledge_base_document_uploaded',
|
||||||
|
{
|
||||||
|
knowledge_base_id: knowledgeBaseId,
|
||||||
|
workspace_id: kbWorkspaceId ?? '',
|
||||||
|
document_count: createdDocuments.length,
|
||||||
|
upload_type: 'bulk',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...(kbWorkspaceId ? { groups: { workspace: kbWorkspaceId } } : {}),
|
||||||
|
setOnce: { first_document_uploaded_at: new Date().toISOString() },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
processDocumentsWithQueue(
|
processDocumentsWithQueue(
|
||||||
createdDocuments,
|
createdDocuments,
|
||||||
knowledgeBaseId,
|
knowledgeBaseId,
|
||||||
@@ -314,6 +332,21 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
// Silently fail
|
// Silently fail
|
||||||
}
|
}
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'knowledge_base_document_uploaded',
|
||||||
|
{
|
||||||
|
knowledge_base_id: knowledgeBaseId,
|
||||||
|
workspace_id: kbWorkspaceId ?? '',
|
||||||
|
document_count: 1,
|
||||||
|
upload_type: 'single',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...(kbWorkspaceId ? { groups: { workspace: kbWorkspaceId } } : {}),
|
||||||
|
setOnce: { first_document_uploaded_at: new Date().toISOString() },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
|
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
|
||||||
actorId: userId,
|
actorId: userId,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
KnowledgeBaseConflictError,
|
KnowledgeBaseConflictError,
|
||||||
type KnowledgeBaseScope,
|
type KnowledgeBaseScope,
|
||||||
} from '@/lib/knowledge/service'
|
} from '@/lib/knowledge/service'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
|
|
||||||
const logger = createLogger('KnowledgeBaseAPI')
|
const logger = createLogger('KnowledgeBaseAPI')
|
||||||
|
|
||||||
@@ -115,6 +116,20 @@ export async function POST(req: NextRequest) {
|
|||||||
// Telemetry should not fail the operation
|
// Telemetry should not fail the operation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
session.user.id,
|
||||||
|
'knowledge_base_created',
|
||||||
|
{
|
||||||
|
knowledge_base_id: newKnowledgeBase.id,
|
||||||
|
workspace_id: validatedData.workspaceId,
|
||||||
|
name: validatedData.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groups: { workspace: validatedData.workspaceId },
|
||||||
|
setOnce: { first_kb_created_at: new Date().toISOString() },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`[${requestId}] Knowledge base created: ${newKnowledgeBase.id} for user ${session.user.id}`
|
`[${requestId}] Knowledge base created: ${newKnowledgeBase.id} for user ${session.user.id}`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
createMcpSuccessResponse,
|
createMcpSuccessResponse,
|
||||||
generateMcpServerId,
|
generateMcpServerId,
|
||||||
} from '@/lib/mcp/utils'
|
} from '@/lib/mcp/utils'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
|
|
||||||
const logger = createLogger('McpServersAPI')
|
const logger = createLogger('McpServersAPI')
|
||||||
|
|
||||||
@@ -180,6 +181,20 @@ export const POST = withMcpAuth('write')(
|
|||||||
// Silently fail
|
// Silently fail
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sourceParam = body.source as string | undefined
|
||||||
|
const source =
|
||||||
|
sourceParam === 'settings' || sourceParam === 'tool_input' ? sourceParam : undefined
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'mcp_server_connected',
|
||||||
|
{ workspace_id: workspaceId, server_name: body.name, transport: body.transport, source },
|
||||||
|
{
|
||||||
|
groups: { workspace: workspaceId },
|
||||||
|
setOnce: { first_mcp_connected_at: new Date().toISOString() },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: userId,
|
actorId: userId,
|
||||||
@@ -214,6 +229,9 @@ export const DELETE = withMcpAuth('admin')(
|
|||||||
try {
|
try {
|
||||||
const { searchParams } = new URL(request.url)
|
const { searchParams } = new URL(request.url)
|
||||||
const serverId = searchParams.get('serverId')
|
const serverId = searchParams.get('serverId')
|
||||||
|
const sourceParam = searchParams.get('source')
|
||||||
|
const source =
|
||||||
|
sourceParam === 'settings' || sourceParam === 'tool_input' ? sourceParam : undefined
|
||||||
|
|
||||||
if (!serverId) {
|
if (!serverId) {
|
||||||
return createMcpErrorResponse(
|
return createMcpErrorResponse(
|
||||||
@@ -242,6 +260,13 @@ export const DELETE = withMcpAuth('admin')(
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Successfully deleted MCP server: ${serverId}`)
|
logger.info(`[${requestId}] Successfully deleted MCP server: ${serverId}`)
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'mcp_server_disconnected',
|
||||||
|
{ workspace_id: workspaceId, server_name: deletedServer.name, source },
|
||||||
|
{ groups: { workspace: workspaceId } }
|
||||||
|
)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: userId,
|
actorId: userId,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
createUnauthorizedResponse,
|
createUnauthorizedResponse,
|
||||||
} from '@/lib/copilot/request-helpers'
|
} from '@/lib/copilot/request-helpers'
|
||||||
import { taskPubSub } from '@/lib/copilot/task-events'
|
import { taskPubSub } from '@/lib/copilot/task-events'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
|
|
||||||
const logger = createLogger('MothershipChatAPI')
|
const logger = createLogger('MothershipChatAPI')
|
||||||
|
|
||||||
@@ -142,12 +143,41 @@ export async function PATCH(
|
|||||||
return NextResponse.json({ success: false, error: 'Chat not found' }, { status: 404 })
|
return NextResponse.json({ success: false, error: 'Chat not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (title !== undefined && updatedChat.workspaceId) {
|
if (updatedChat.workspaceId) {
|
||||||
taskPubSub?.publishStatusChanged({
|
if (title !== undefined) {
|
||||||
workspaceId: updatedChat.workspaceId,
|
taskPubSub?.publishStatusChanged({
|
||||||
chatId,
|
workspaceId: updatedChat.workspaceId,
|
||||||
type: 'renamed',
|
chatId,
|
||||||
})
|
type: 'renamed',
|
||||||
|
})
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'task_renamed',
|
||||||
|
{ workspace_id: updatedChat.workspaceId },
|
||||||
|
{
|
||||||
|
groups: { workspace: updatedChat.workspaceId },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (isUnread === false) {
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'task_marked_read',
|
||||||
|
{ workspace_id: updatedChat.workspaceId },
|
||||||
|
{
|
||||||
|
groups: { workspace: updatedChat.workspaceId },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else if (isUnread === true) {
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'task_marked_unread',
|
||||||
|
{ workspace_id: updatedChat.workspaceId },
|
||||||
|
{
|
||||||
|
groups: { workspace: updatedChat.workspaceId },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
@@ -203,6 +233,14 @@ export async function DELETE(
|
|||||||
chatId,
|
chatId,
|
||||||
type: 'deleted',
|
type: 'deleted',
|
||||||
})
|
})
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'task_deleted',
|
||||||
|
{ workspace_id: deletedChat.workspaceId },
|
||||||
|
{
|
||||||
|
groups: { workspace: deletedChat.workspaceId },
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
createUnauthorizedResponse,
|
createUnauthorizedResponse,
|
||||||
} from '@/lib/copilot/request-helpers'
|
} from '@/lib/copilot/request-helpers'
|
||||||
import { taskPubSub } from '@/lib/copilot/task-events'
|
import { taskPubSub } from '@/lib/copilot/task-events'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||||
|
|
||||||
const logger = createLogger('MothershipChatsAPI')
|
const logger = createLogger('MothershipChatsAPI')
|
||||||
@@ -95,6 +96,15 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
taskPubSub?.publishStatusChanged({ workspaceId, chatId: chat.id, type: 'created' })
|
taskPubSub?.publishStatusChanged({ workspaceId, chatId: chat.id, type: 'created' })
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'task_created',
|
||||||
|
{ workspace_id: workspaceId },
|
||||||
|
{
|
||||||
|
groups: { workspace: workspaceId },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return NextResponse.json({ success: true, id: chat.id })
|
return NextResponse.json({ success: true, id: chat.id })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { z } from 'zod'
|
|||||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { deleteSkill, listSkills, upsertSkills } from '@/lib/workflows/skills/operations'
|
import { deleteSkill, listSkills, upsertSkills } from '@/lib/workflows/skills/operations'
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ const SkillSchema = z.object({
|
|||||||
})
|
})
|
||||||
),
|
),
|
||||||
workspaceId: z.string().optional(),
|
workspaceId: z.string().optional(),
|
||||||
|
source: z.enum(['settings', 'tool_input']).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
/** GET - Fetch all skills for a workspace */
|
/** GET - Fetch all skills for a workspace */
|
||||||
@@ -75,7 +77,7 @@ export async function POST(req: NextRequest) {
|
|||||||
const body = await req.json()
|
const body = await req.json()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { skills, workspaceId } = SkillSchema.parse(body)
|
const { skills, workspaceId, source } = SkillSchema.parse(body)
|
||||||
|
|
||||||
if (!workspaceId) {
|
if (!workspaceId) {
|
||||||
logger.warn(`[${requestId}] Missing workspaceId in request body`)
|
logger.warn(`[${requestId}] Missing workspaceId in request body`)
|
||||||
@@ -107,6 +109,12 @@ export async function POST(req: NextRequest) {
|
|||||||
resourceName: skill.name,
|
resourceName: skill.name,
|
||||||
description: `Created/updated skill "${skill.name}"`,
|
description: `Created/updated skill "${skill.name}"`,
|
||||||
})
|
})
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'skill_created',
|
||||||
|
{ skill_id: skill.id, skill_name: skill.name, workspace_id: workspaceId, source },
|
||||||
|
{ groups: { workspace: workspaceId } }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true, data: resultSkills })
|
return NextResponse.json({ success: true, data: resultSkills })
|
||||||
@@ -137,6 +145,9 @@ export async function DELETE(request: NextRequest) {
|
|||||||
const searchParams = request.nextUrl.searchParams
|
const searchParams = request.nextUrl.searchParams
|
||||||
const skillId = searchParams.get('id')
|
const skillId = searchParams.get('id')
|
||||||
const workspaceId = searchParams.get('workspaceId')
|
const workspaceId = searchParams.get('workspaceId')
|
||||||
|
const sourceParam = searchParams.get('source')
|
||||||
|
const source =
|
||||||
|
sourceParam === 'settings' || sourceParam === 'tool_input' ? sourceParam : undefined
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
@@ -180,6 +191,13 @@ export async function DELETE(request: NextRequest) {
|
|||||||
description: `Deleted skill`,
|
description: `Deleted skill`,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'skill_deleted',
|
||||||
|
{ skill_id: skillId, workspace_id: workspaceId, source },
|
||||||
|
{ groups: { workspace: workspaceId } }
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(`[${requestId}] Deleted skill: ${skillId}`)
|
logger.info(`[${requestId}] Deleted skill: ${skillId}`)
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import {
|
import {
|
||||||
deleteTable,
|
deleteTable,
|
||||||
NAME_PATTERN,
|
NAME_PATTERN,
|
||||||
@@ -183,6 +184,13 @@ export async function DELETE(request: NextRequest, { params }: TableRouteParams)
|
|||||||
|
|
||||||
await deleteTable(tableId, requestId)
|
await deleteTable(tableId, requestId)
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
authResult.userId,
|
||||||
|
'table_deleted',
|
||||||
|
{ table_id: tableId, workspace_id: table.workspaceId },
|
||||||
|
{ groups: { workspace: table.workspaceId } }
|
||||||
|
)
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import {
|
import {
|
||||||
createTable,
|
createTable,
|
||||||
getWorkspaceTableLimits,
|
getWorkspaceTableLimits,
|
||||||
@@ -141,6 +142,20 @@ export async function POST(request: NextRequest) {
|
|||||||
requestId
|
requestId
|
||||||
)
|
)
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
authResult.userId,
|
||||||
|
'table_created',
|
||||||
|
{
|
||||||
|
table_id: table.id,
|
||||||
|
workspace_id: params.workspaceId,
|
||||||
|
column_count: params.schema.columns.length,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groups: { workspace: params.workspaceId },
|
||||||
|
setOnce: { first_table_created_at: new Date().toISOString() },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { z } from 'zod'
|
|||||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { upsertCustomTools } from '@/lib/workflows/custom-tools/operations'
|
import { upsertCustomTools } from '@/lib/workflows/custom-tools/operations'
|
||||||
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
@@ -34,6 +35,7 @@ const CustomToolSchema = z.object({
|
|||||||
})
|
})
|
||||||
),
|
),
|
||||||
workspaceId: z.string().optional(),
|
workspaceId: z.string().optional(),
|
||||||
|
source: z.enum(['settings', 'tool_input']).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
// GET - Fetch all custom tools for the workspace
|
// GET - Fetch all custom tools for the workspace
|
||||||
@@ -135,7 +137,7 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Validate the request body
|
// Validate the request body
|
||||||
const { tools, workspaceId } = CustomToolSchema.parse(body)
|
const { tools, workspaceId, source } = CustomToolSchema.parse(body)
|
||||||
|
|
||||||
if (!workspaceId) {
|
if (!workspaceId) {
|
||||||
logger.warn(`[${requestId}] Missing workspaceId in request body`)
|
logger.warn(`[${requestId}] Missing workspaceId in request body`)
|
||||||
@@ -168,6 +170,16 @@ export async function POST(req: NextRequest) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
for (const tool of resultTools) {
|
for (const tool of resultTools) {
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'custom_tool_saved',
|
||||||
|
{ tool_id: tool.id, workspace_id: workspaceId, tool_name: tool.title, source },
|
||||||
|
{
|
||||||
|
groups: { workspace: workspaceId },
|
||||||
|
setOnce: { first_custom_tool_saved_at: new Date().toISOString() },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: userId,
|
actorId: userId,
|
||||||
@@ -205,6 +217,9 @@ export async function DELETE(request: NextRequest) {
|
|||||||
const searchParams = request.nextUrl.searchParams
|
const searchParams = request.nextUrl.searchParams
|
||||||
const toolId = searchParams.get('id')
|
const toolId = searchParams.get('id')
|
||||||
const workspaceId = searchParams.get('workspaceId')
|
const workspaceId = searchParams.get('workspaceId')
|
||||||
|
const sourceParam = searchParams.get('source')
|
||||||
|
const source =
|
||||||
|
sourceParam === 'settings' || sourceParam === 'tool_input' ? sourceParam : undefined
|
||||||
|
|
||||||
if (!toolId) {
|
if (!toolId) {
|
||||||
logger.warn(`[${requestId}] Missing tool ID for deletion`)
|
logger.warn(`[${requestId}] Missing tool ID for deletion`)
|
||||||
@@ -278,6 +293,14 @@ export async function DELETE(request: NextRequest) {
|
|||||||
// Delete the tool
|
// Delete the tool
|
||||||
await db.delete(customTools).where(eq(customTools.id, toolId))
|
await db.delete(customTools).where(eq(customTools.id, toolId))
|
||||||
|
|
||||||
|
const toolWorkspaceId = tool.workspaceId ?? workspaceId ?? ''
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'custom_tool_deleted',
|
||||||
|
{ tool_id: toolId, workspace_id: toolWorkspaceId, source },
|
||||||
|
toolWorkspaceId ? { groups: { workspace: toolWorkspaceId } } : undefined
|
||||||
|
)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
workspaceId: tool.workspaceId || undefined,
|
workspaceId: tool.workspaceId || undefined,
|
||||||
actorId: userId,
|
actorId: userId,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|||||||
import { validateInteger } from '@/lib/core/security/input-validation'
|
import { validateInteger } from '@/lib/core/security/input-validation'
|
||||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { cleanupExternalWebhook } from '@/lib/webhooks/provider-subscriptions'
|
import { cleanupExternalWebhook } from '@/lib/webhooks/provider-subscriptions'
|
||||||
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
||||||
|
|
||||||
@@ -274,6 +275,19 @@ export async function DELETE(
|
|||||||
request,
|
request,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const wsId = webhookData.workflow.workspaceId || undefined
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'webhook_trigger_deleted',
|
||||||
|
{
|
||||||
|
webhook_id: id,
|
||||||
|
workflow_id: webhookData.workflow.id,
|
||||||
|
provider: foundWebhook.provider || 'generic',
|
||||||
|
workspace_id: wsId ?? '',
|
||||||
|
},
|
||||||
|
wsId ? { groups: { workspace: wsId } } : undefined
|
||||||
|
)
|
||||||
|
|
||||||
return NextResponse.json({ success: true }, { status: 200 })
|
return NextResponse.json({ success: true }, { status: 200 })
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error(`[${requestId}] Error deleting webhook`, {
|
logger.error(`[${requestId}] Error deleting webhook`, {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getSession } from '@/lib/auth'
|
|||||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { getProviderIdFromServiceId } from '@/lib/oauth'
|
import { getProviderIdFromServiceId } from '@/lib/oauth'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { resolveEnvVarsInObject } from '@/lib/webhooks/env-resolver'
|
import { resolveEnvVarsInObject } from '@/lib/webhooks/env-resolver'
|
||||||
import {
|
import {
|
||||||
cleanupExternalWebhook,
|
cleanupExternalWebhook,
|
||||||
@@ -763,6 +764,19 @@ export async function POST(request: NextRequest) {
|
|||||||
metadata: { provider, workflowId },
|
metadata: { provider, workflowId },
|
||||||
request,
|
request,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const wsId = workflowRecord.workspaceId || undefined
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'webhook_trigger_created',
|
||||||
|
{
|
||||||
|
webhook_id: savedWebhook.id,
|
||||||
|
workflow_id: workflowId,
|
||||||
|
provider: provider || 'generic',
|
||||||
|
workspace_id: wsId ?? '',
|
||||||
|
},
|
||||||
|
wsId ? { groups: { workspace: wsId } } : undefined
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = targetWebhookId ? 200 : 201
|
const status = targetWebhookId ? 200 : 201
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { performFullDeploy, performFullUndeploy } from '@/lib/workflows/orchestration'
|
import { performFullDeploy, performFullUndeploy } from '@/lib/workflows/orchestration'
|
||||||
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
|
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
|
||||||
import {
|
import {
|
||||||
@@ -96,6 +97,16 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Workflow deployed successfully: ${id}`)
|
logger.info(`[${requestId}] Workflow deployed successfully: ${id}`)
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
actorUserId,
|
||||||
|
'workflow_deployed',
|
||||||
|
{ workflow_id: id, workspace_id: workflowData!.workspaceId ?? '' },
|
||||||
|
{
|
||||||
|
groups: workflowData!.workspaceId ? { workspace: workflowData!.workspaceId } : undefined,
|
||||||
|
setOnce: { first_workflow_deployed_at: new Date().toISOString() },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const responseApiKeyInfo = workflowData!.workspaceId
|
const responseApiKeyInfo = workflowData!.workspaceId
|
||||||
? 'Workspace API keys'
|
? 'Workspace API keys'
|
||||||
: 'Personal API keys'
|
: 'Personal API keys'
|
||||||
@@ -118,7 +129,11 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||||||
const { id } = await params
|
const { id } = await params
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { error, session } = await validateWorkflowPermissions(id, requestId, 'admin')
|
const {
|
||||||
|
error,
|
||||||
|
session,
|
||||||
|
workflow: workflowData,
|
||||||
|
} = await validateWorkflowPermissions(id, requestId, 'admin')
|
||||||
if (error) {
|
if (error) {
|
||||||
return createErrorResponse(error.message, error.status)
|
return createErrorResponse(error.message, error.status)
|
||||||
}
|
}
|
||||||
@@ -148,6 +163,14 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Updated isPublicApi for workflow ${id} to ${isPublicApi}`)
|
logger.info(`[${requestId}] Updated isPublicApi for workflow ${id} to ${isPublicApi}`)
|
||||||
|
|
||||||
|
const wsId = workflowData?.workspaceId
|
||||||
|
captureServerEvent(
|
||||||
|
session!.user.id,
|
||||||
|
'workflow_public_api_toggled',
|
||||||
|
{ workflow_id: id, workspace_id: wsId ?? '', is_public: isPublicApi },
|
||||||
|
wsId ? { groups: { workspace: wsId } } : undefined
|
||||||
|
)
|
||||||
|
|
||||||
return createSuccessResponse({ isPublicApi })
|
return createSuccessResponse({ isPublicApi })
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Failed to update deployment settings'
|
const message = error instanceof Error ? error.message : 'Failed to update deployment settings'
|
||||||
@@ -164,7 +187,11 @@ export async function DELETE(
|
|||||||
const { id } = await params
|
const { id } = await params
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { error, session } = await validateWorkflowPermissions(id, requestId, 'admin')
|
const {
|
||||||
|
error,
|
||||||
|
session,
|
||||||
|
workflow: workflowData,
|
||||||
|
} = await validateWorkflowPermissions(id, requestId, 'admin')
|
||||||
if (error) {
|
if (error) {
|
||||||
return createErrorResponse(error.message, error.status)
|
return createErrorResponse(error.message, error.status)
|
||||||
}
|
}
|
||||||
@@ -179,6 +206,14 @@ export async function DELETE(
|
|||||||
return createErrorResponse(result.error || 'Failed to undeploy workflow', 500)
|
return createErrorResponse(result.error || 'Failed to undeploy workflow', 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const wsId = workflowData?.workspaceId
|
||||||
|
captureServerEvent(
|
||||||
|
session!.user.id,
|
||||||
|
'workflow_undeployed',
|
||||||
|
{ workflow_id: id, workspace_id: wsId ?? '' },
|
||||||
|
wsId ? { groups: { workspace: wsId } } : undefined
|
||||||
|
)
|
||||||
|
|
||||||
return createSuccessResponse({
|
return createSuccessResponse({
|
||||||
isDeployed: false,
|
isDeployed: false,
|
||||||
deployedAt: null,
|
deployedAt: null,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { NextRequest } from 'next/server'
|
|||||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { env } from '@/lib/core/config/env'
|
import { env } from '@/lib/core/config/env'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
|
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||||
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
|
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
|
||||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||||
@@ -78,7 +79,6 @@ export async function POST(
|
|||||||
loops: deployedState.loops || {},
|
loops: deployedState.loops || {},
|
||||||
parallels: deployedState.parallels || {},
|
parallels: deployedState.parallels || {},
|
||||||
lastSaved: Date.now(),
|
lastSaved: Date.now(),
|
||||||
deploymentStatuses: deployedState.deploymentStatuses || {},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!saveResult.success) {
|
if (!saveResult.success) {
|
||||||
@@ -104,6 +104,19 @@ export async function POST(
|
|||||||
logger.error('Error sending workflow reverted event to socket server', e)
|
logger.error('Error sending workflow reverted event to socket server', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
session!.user.id,
|
||||||
|
'workflow_deployment_reverted',
|
||||||
|
{
|
||||||
|
workflow_id: id,
|
||||||
|
workspace_id: workflowRecord?.workspaceId ?? '',
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
workflowRecord?.workspaceId
|
||||||
|
? { groups: { workspace: workflowRecord.workspaceId } }
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
workspaceId: workflowRecord?.workspaceId ?? null,
|
workspaceId: workflowRecord?.workspaceId ?? null,
|
||||||
actorId: session!.user.id,
|
actorId: session!.user.id,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { and, eq } from 'drizzle-orm'
|
|||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { performActivateVersion } from '@/lib/workflows/orchestration'
|
import { performActivateVersion } from '@/lib/workflows/orchestration'
|
||||||
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
|
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
|
||||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||||
@@ -174,6 +175,14 @@ export async function PATCH(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const wsId = (workflowData as { workspaceId?: string } | null)?.workspaceId
|
||||||
|
captureServerEvent(
|
||||||
|
actorUserId,
|
||||||
|
'deployment_version_activated',
|
||||||
|
{ workflow_id: id, workspace_id: wsId ?? '', version: versionNum },
|
||||||
|
wsId ? { groups: { workspace: wsId } } : undefined
|
||||||
|
)
|
||||||
|
|
||||||
return createSuccessResponse({
|
return createSuccessResponse({
|
||||||
success: true,
|
success: true,
|
||||||
deployedAt: activateResult.deployedAt,
|
deployedAt: activateResult.deployedAt,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
|||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { duplicateWorkflow } from '@/lib/workflows/persistence/duplicate'
|
import { duplicateWorkflow } from '@/lib/workflows/persistence/duplicate'
|
||||||
|
|
||||||
const logger = createLogger('WorkflowDuplicateAPI')
|
const logger = createLogger('WorkflowDuplicateAPI')
|
||||||
@@ -60,6 +61,17 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
// Telemetry should not fail the operation
|
// Telemetry should not fail the operation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'workflow_duplicated',
|
||||||
|
{
|
||||||
|
source_workflow_id: sourceWorkflowId,
|
||||||
|
new_workflow_id: result.id,
|
||||||
|
workspace_id: workspaceId ?? '',
|
||||||
|
},
|
||||||
|
workspaceId ? { groups: { workspace: workspaceId } } : undefined
|
||||||
|
)
|
||||||
|
|
||||||
const elapsed = Date.now() - startTime
|
const elapsed = Date.now() - startTime
|
||||||
logger.info(
|
logger.info(
|
||||||
`[${requestId}] Successfully duplicated workflow ${sourceWorkflowId} to ${result.id} in ${elapsed}ms`
|
`[${requestId}] Successfully duplicated workflow ${sourceWorkflowId} to ${result.id} in ${elapsed}ms`
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
|||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { markExecutionCancelled } from '@/lib/execution/cancellation'
|
import { markExecutionCancelled } from '@/lib/execution/cancellation'
|
||||||
import { abortManualExecution } from '@/lib/execution/manual-cancellation'
|
import { abortManualExecution } from '@/lib/execution/manual-cancellation'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
||||||
|
|
||||||
const logger = createLogger('CancelExecutionAPI')
|
const logger = createLogger('CancelExecutionAPI')
|
||||||
@@ -60,6 +61,16 @@ export async function POST(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cancellation.durablyRecorded || locallyAborted) {
|
||||||
|
const workspaceId = workflowAuthorization.workflow?.workspaceId
|
||||||
|
captureServerEvent(
|
||||||
|
auth.userId,
|
||||||
|
'workflow_execution_cancelled',
|
||||||
|
{ workflow_id: workflowId, workspace_id: workspaceId ?? '' },
|
||||||
|
workspaceId ? { groups: { workspace: workspaceId } } : undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: cancellation.durablyRecorded || locallyAborted,
|
success: cancellation.durablyRecorded || locallyAborted,
|
||||||
executionId,
|
executionId,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
|||||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { restoreWorkflow } from '@/lib/workflows/lifecycle'
|
import { restoreWorkflow } from '@/lib/workflows/lifecycle'
|
||||||
import { getWorkflowById } from '@/lib/workflows/utils'
|
import { getWorkflowById } from '@/lib/workflows/utils'
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
@@ -58,6 +59,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|||||||
request,
|
request,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
auth.userId,
|
||||||
|
'workflow_restored',
|
||||||
|
{ workflow_id: workflowId, workspace_id: workflowData.workspaceId ?? '' },
|
||||||
|
workflowData.workspaceId ? { groups: { workspace: workflowData.workspaceId } } : undefined
|
||||||
|
)
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[${requestId}] Error restoring workflow ${workflowId}`, error)
|
logger.error(`[${requestId}] Error restoring workflow ${workflowId}`, error)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { AuthType, checkHybridAuth, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
import { AuthType, checkHybridAuth, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { performDeleteWorkflow } from '@/lib/workflows/orchestration'
|
import { performDeleteWorkflow } from '@/lib/workflows/orchestration'
|
||||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||||
import { authorizeWorkflowByWorkspacePermission, getWorkflowById } from '@/lib/workflows/utils'
|
import { authorizeWorkflowByWorkspacePermission, getWorkflowById } from '@/lib/workflows/utils'
|
||||||
@@ -88,7 +89,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|||||||
const finalWorkflowData = {
|
const finalWorkflowData = {
|
||||||
...workflowData,
|
...workflowData,
|
||||||
state: {
|
state: {
|
||||||
deploymentStatuses: {},
|
|
||||||
blocks: normalizedData.blocks,
|
blocks: normalizedData.blocks,
|
||||||
edges: normalizedData.edges,
|
edges: normalizedData.edges,
|
||||||
loops: normalizedData.loops,
|
loops: normalizedData.loops,
|
||||||
@@ -114,7 +114,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|||||||
const emptyWorkflowData = {
|
const emptyWorkflowData = {
|
||||||
...workflowData,
|
...workflowData,
|
||||||
state: {
|
state: {
|
||||||
deploymentStatuses: {},
|
|
||||||
blocks: {},
|
blocks: {},
|
||||||
edges: [],
|
edges: [],
|
||||||
loops: {},
|
loops: {},
|
||||||
@@ -225,6 +224,13 @@ export async function DELETE(
|
|||||||
return NextResponse.json({ error: result.error }, { status })
|
return NextResponse.json({ error: result.error }, { status })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'workflow_deleted',
|
||||||
|
{ workflow_id: workflowId, workspace_id: workflowData.workspaceId ?? '' },
|
||||||
|
workflowData.workspaceId ? { groups: { workspace: workflowData.workspaceId } } : undefined
|
||||||
|
)
|
||||||
|
|
||||||
const elapsed = Date.now() - startTime
|
const elapsed = Date.now() - startTime
|
||||||
logger.info(`[${requestId}] Successfully archived workflow ${workflowId} in ${elapsed}ms`)
|
logger.info(`[${requestId}] Successfully archived workflow ${workflowId} in ${elapsed}ms`)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
|||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
||||||
import type { Variable } from '@/stores/panel/variables/types'
|
import type { Variable } from '@/stores/variables/types'
|
||||||
|
|
||||||
const logger = createLogger('WorkflowVariablesAPI')
|
const logger = createLogger('WorkflowVariablesAPI')
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { z } from 'zod'
|
|||||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { getNextWorkflowColor } from '@/lib/workflows/colors'
|
import { getNextWorkflowColor } from '@/lib/workflows/colors'
|
||||||
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
|
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
|
||||||
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
|
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||||
@@ -274,6 +275,16 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Successfully created workflow ${workflowId} with default blocks`)
|
logger.info(`[${requestId}] Successfully created workflow ${workflowId} with default blocks`)
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'workflow_created',
|
||||||
|
{ workflow_id: workflowId, workspace_id: workspaceId ?? '', name },
|
||||||
|
{
|
||||||
|
groups: workspaceId ? { workspace: workspaceId } : undefined,
|
||||||
|
setOnce: { first_workflow_created_at: new Date().toISOString() },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: userId,
|
actorId: userId,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { z } from 'zod'
|
|||||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
|
|
||||||
const logger = createLogger('WorkspaceApiKeyAPI')
|
const logger = createLogger('WorkspaceApiKeyAPI')
|
||||||
@@ -145,6 +146,13 @@ export async function DELETE(
|
|||||||
|
|
||||||
const deletedKey = deletedRows[0]
|
const deletedKey = deletedRows[0]
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'api_key_revoked',
|
||||||
|
{ workspace_id: workspaceId, key_name: deletedKey.name },
|
||||||
|
{ groups: { workspace: workspaceId } }
|
||||||
|
)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: userId,
|
actorId: userId,
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
|||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { getUserEntityPermissions, getWorkspaceById } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions, getWorkspaceById } from '@/lib/workspaces/permissions/utils'
|
||||||
|
|
||||||
const logger = createLogger('WorkspaceApiKeysAPI')
|
const logger = createLogger('WorkspaceApiKeysAPI')
|
||||||
|
|
||||||
const CreateKeySchema = z.object({
|
const CreateKeySchema = z.object({
|
||||||
name: z.string().trim().min(1, 'Name is required'),
|
name: z.string().trim().min(1, 'Name is required'),
|
||||||
|
source: z.enum(['settings', 'deploy_modal']).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const DeleteKeysSchema = z.object({
|
const DeleteKeysSchema = z.object({
|
||||||
@@ -101,7 +103,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { name } = CreateKeySchema.parse(body)
|
const { name, source } = CreateKeySchema.parse(body)
|
||||||
|
|
||||||
const existingKey = await db
|
const existingKey = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -158,6 +160,16 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|||||||
// Telemetry should not fail the operation
|
// Telemetry should not fail the operation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'api_key_created',
|
||||||
|
{ workspace_id: workspaceId, key_name: name, source },
|
||||||
|
{
|
||||||
|
groups: { workspace: workspaceId },
|
||||||
|
setOnce: { first_api_key_created_at: new Date().toISOString() },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(`[${requestId}] Created workspace API key: ${name} in workspace ${workspaceId}`)
|
logger.info(`[${requestId}] Created workspace API key: ${name} in workspace ${workspaceId}`)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
|||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
|
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { getUserEntityPermissions, getWorkspaceById } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions, getWorkspaceById } from '@/lib/workspaces/permissions/utils'
|
||||||
|
|
||||||
const logger = createLogger('WorkspaceBYOKKeysAPI')
|
const logger = createLogger('WorkspaceBYOKKeysAPI')
|
||||||
@@ -201,6 +202,16 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Created BYOK key for ${providerId} in workspace ${workspaceId}`)
|
logger.info(`[${requestId}] Created BYOK key for ${providerId} in workspace ${workspaceId}`)
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'byok_key_added',
|
||||||
|
{ workspace_id: workspaceId, provider_id: providerId },
|
||||||
|
{
|
||||||
|
groups: { workspace: workspaceId },
|
||||||
|
setOnce: { first_byok_key_added_at: new Date().toISOString() },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: userId,
|
actorId: userId,
|
||||||
@@ -272,6 +283,13 @@ export async function DELETE(
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Deleted BYOK key for ${providerId} from workspace ${workspaceId}`)
|
logger.info(`[${requestId}] Deleted BYOK key for ${providerId} from workspace ${workspaceId}`)
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
userId,
|
||||||
|
'byok_key_removed',
|
||||||
|
{ workspace_id: workspaceId, provider_id: providerId },
|
||||||
|
{ groups: { workspace: workspaceId } }
|
||||||
|
)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: userId,
|
actorId: userId,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
|||||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import {
|
import {
|
||||||
FileConflictError,
|
FileConflictError,
|
||||||
listWorkspaceFiles,
|
listWorkspaceFiles,
|
||||||
@@ -116,6 +117,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Uploaded workspace file: ${fileName}`)
|
logger.info(`[${requestId}] Uploaded workspace file: ${fileName}`)
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
session.user.id,
|
||||||
|
'file_uploaded',
|
||||||
|
{ workspace_id: workspaceId, file_type: rawFile.type || 'application/octet-stream' },
|
||||||
|
{ groups: { workspace: workspaceId } }
|
||||||
|
)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: session.user.id,
|
actorId: session.user.id,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { z } from 'zod'
|
|||||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { encryptSecret } from '@/lib/core/security/encryption'
|
import { encryptSecret } from '@/lib/core/security/encryption'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
import { MAX_EMAIL_RECIPIENTS, MAX_WORKFLOW_IDS } from '../constants'
|
import { MAX_EMAIL_RECIPIENTS, MAX_WORKFLOW_IDS } from '../constants'
|
||||||
|
|
||||||
@@ -342,6 +343,17 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
|||||||
request,
|
request,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
session.user.id,
|
||||||
|
'notification_channel_deleted',
|
||||||
|
{
|
||||||
|
notification_id: notificationId,
|
||||||
|
notification_type: deletedSubscription.notificationType,
|
||||||
|
workspace_id: workspaceId,
|
||||||
|
},
|
||||||
|
{ groups: { workspace: workspaceId } }
|
||||||
|
)
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error deleting notification', { error })
|
logger.error('Error deleting notification', { error })
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { z } from 'zod'
|
|||||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { encryptSecret } from '@/lib/core/security/encryption'
|
import { encryptSecret } from '@/lib/core/security/encryption'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
import { MAX_EMAIL_RECIPIENTS, MAX_NOTIFICATIONS_PER_TYPE, MAX_WORKFLOW_IDS } from './constants'
|
import { MAX_EMAIL_RECIPIENTS, MAX_NOTIFICATIONS_PER_TYPE, MAX_WORKFLOW_IDS } from './constants'
|
||||||
|
|
||||||
@@ -256,6 +257,17 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|||||||
type: data.notificationType,
|
type: data.notificationType,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
session.user.id,
|
||||||
|
'notification_channel_created',
|
||||||
|
{
|
||||||
|
workspace_id: workspaceId,
|
||||||
|
notification_type: data.notificationType,
|
||||||
|
alert_rule: data.alertConfig?.rule ?? null,
|
||||||
|
},
|
||||||
|
{ groups: { workspace: workspaceId } }
|
||||||
|
)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: session.user.id,
|
actorId: session.user.id,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { z } from 'zod'
|
|||||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { syncWorkspaceEnvCredentials } from '@/lib/credentials/environment'
|
import { syncWorkspaceEnvCredentials } from '@/lib/credentials/environment'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import {
|
import {
|
||||||
getUsersWithPermissions,
|
getUsersWithPermissions,
|
||||||
hasWorkspaceAdminAccess,
|
hasWorkspaceAdminAccess,
|
||||||
@@ -188,6 +189,13 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||||||
const updatedUsers = await getUsersWithPermissions(workspaceId)
|
const updatedUsers = await getUsersWithPermissions(workspaceId)
|
||||||
|
|
||||||
for (const update of body.updates) {
|
for (const update of body.updates) {
|
||||||
|
captureServerEvent(
|
||||||
|
session.user.id,
|
||||||
|
'workspace_member_role_changed',
|
||||||
|
{ workspace_id: workspaceId, new_role: update.permissions },
|
||||||
|
{ groups: { workspace: workspaceId } }
|
||||||
|
)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: session.user.id,
|
actorId: session.user.id,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { archiveWorkspace } from '@/lib/workspaces/lifecycle'
|
import { archiveWorkspace } from '@/lib/workspaces/lifecycle'
|
||||||
|
|
||||||
const logger = createLogger('WorkspaceByIdAPI')
|
const logger = createLogger('WorkspaceByIdAPI')
|
||||||
@@ -292,6 +293,13 @@ export async function DELETE(
|
|||||||
request,
|
request,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
session.user.id,
|
||||||
|
'workspace_deleted',
|
||||||
|
{ workspace_id: workspaceId, workflow_count: workflowIds.length },
|
||||||
|
{ groups: { workspace: workspaceId } }
|
||||||
|
)
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error deleting workspace ${workspaceId}:`, error)
|
logger.error(`Error deleting workspace ${workspaceId}:`, error)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { PlatformEvents } from '@/lib/core/telemetry'
|
|||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
import { sendEmail } from '@/lib/messaging/email/mailer'
|
import { sendEmail } from '@/lib/messaging/email/mailer'
|
||||||
import { getFromEmailAddress } from '@/lib/messaging/email/utils'
|
import { getFromEmailAddress } from '@/lib/messaging/email/utils'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { getWorkspaceById } from '@/lib/workspaces/permissions/utils'
|
import { getWorkspaceById } from '@/lib/workspaces/permissions/utils'
|
||||||
import {
|
import {
|
||||||
InvitationsNotAllowedError,
|
InvitationsNotAllowedError,
|
||||||
@@ -214,6 +215,16 @@ export async function POST(req: NextRequest) {
|
|||||||
// Telemetry should not fail the operation
|
// Telemetry should not fail the operation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
session.user.id,
|
||||||
|
'workspace_member_invited',
|
||||||
|
{ workspace_id: workspaceId, invitee_role: permission },
|
||||||
|
{
|
||||||
|
groups: { workspace: workspaceId },
|
||||||
|
setOnce: { first_invitation_sent_at: new Date().toISOString() },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
await sendInvitationEmail({
|
await sendInvitationEmail({
|
||||||
to: email,
|
to: email,
|
||||||
inviterName: session.user.name || session.user.email || 'A user',
|
inviterName: session.user.name || session.user.email || 'A user',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { z } from 'zod'
|
|||||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { revokeWorkspaceCredentialMemberships } from '@/lib/credentials/access'
|
import { revokeWorkspaceCredentialMemberships } from '@/lib/credentials/access'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils'
|
import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils'
|
||||||
|
|
||||||
const logger = createLogger('WorkspaceMemberAPI')
|
const logger = createLogger('WorkspaceMemberAPI')
|
||||||
@@ -105,6 +106,13 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
|||||||
|
|
||||||
await revokeWorkspaceCredentialMemberships(workspaceId, userId)
|
await revokeWorkspaceCredentialMemberships(workspaceId, userId)
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
session.user.id,
|
||||||
|
'workspace_member_removed',
|
||||||
|
{ workspace_id: workspaceId, is_self_removal: isSelf },
|
||||||
|
{ groups: { workspace: workspaceId } }
|
||||||
|
)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: session.user.id,
|
actorId: session.user.id,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { z } from 'zod'
|
|||||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||||
|
import { captureServerEvent } from '@/lib/posthog/server'
|
||||||
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
|
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
|
||||||
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
|
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||||
import { getRandomWorkspaceColor } from '@/lib/workspaces/colors'
|
import { getRandomWorkspaceColor } from '@/lib/workspaces/colors'
|
||||||
@@ -96,6 +97,16 @@ export async function POST(req: Request) {
|
|||||||
|
|
||||||
const newWorkspace = await createWorkspace(session.user.id, name, skipDefaultWorkflow, color)
|
const newWorkspace = await createWorkspace(session.user.id, name, skipDefaultWorkflow, color)
|
||||||
|
|
||||||
|
captureServerEvent(
|
||||||
|
session.user.id,
|
||||||
|
'workspace_created',
|
||||||
|
{ workspace_id: newWorkspace.id, name: newWorkspace.name },
|
||||||
|
{
|
||||||
|
groups: { workspace: newWorkspace.id },
|
||||||
|
setOnce: { first_workspace_created_at: new Date().toISOString() },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
recordAudit({
|
recordAudit({
|
||||||
workspaceId: newWorkspace.id,
|
workspaceId: newWorkspace.id,
|
||||||
actorId: session.user.id,
|
actorId: session.user.id,
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sidebar width
|
// Sidebar width
|
||||||
|
var defaultSidebarWidth = '248px';
|
||||||
try {
|
try {
|
||||||
var stored = localStorage.getItem('sidebar-state');
|
var stored = localStorage.getItem('sidebar-state');
|
||||||
if (stored) {
|
if (stored) {
|
||||||
@@ -108,11 +109,15 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
document.documentElement.style.setProperty('--sidebar-width', width + 'px');
|
document.documentElement.style.setProperty('--sidebar-width', width + 'px');
|
||||||
} else if (width > maxSidebarWidth) {
|
} else if (width > maxSidebarWidth) {
|
||||||
document.documentElement.style.setProperty('--sidebar-width', maxSidebarWidth + 'px');
|
document.documentElement.style.setProperty('--sidebar-width', maxSidebarWidth + 'px');
|
||||||
|
} else {
|
||||||
|
document.documentElement.style.setProperty('--sidebar-width', defaultSidebarWidth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
document.documentElement.style.setProperty('--sidebar-width', defaultSidebarWidth);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Fallback handled by CSS defaults
|
document.documentElement.style.setProperty('--sidebar-width', defaultSidebarWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Panel width and active tab
|
// Panel width and active tab
|
||||||
|
|||||||
@@ -108,8 +108,6 @@ function normalizeWorkflowState(input?: any): WorkflowState | null {
|
|||||||
lastUpdate: input.lastUpdate,
|
lastUpdate: input.lastUpdate,
|
||||||
metadata: input.metadata,
|
metadata: input.metadata,
|
||||||
variables: input.variables,
|
variables: input.variables,
|
||||||
deploymentStatuses: input.deploymentStatuses,
|
|
||||||
needsRedeployment: input.needsRedeployment,
|
|
||||||
dragStartPosition: input.dragStartPosition ?? null,
|
dragStartPosition: input.dragStartPosition ?? null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
} from '@/components/emcn'
|
} from '@/components/emcn'
|
||||||
import { client, useSession } from '@/lib/auth/auth-client'
|
import { client, useSession } from '@/lib/auth/auth-client'
|
||||||
import type { OAuthReturnContext } from '@/lib/credentials/client-state'
|
import type { OAuthReturnContext } from '@/lib/credentials/client-state'
|
||||||
import { writeOAuthReturnContext } from '@/lib/credentials/client-state'
|
import { ADD_CONNECTOR_SEARCH_PARAM, writeOAuthReturnContext } from '@/lib/credentials/client-state'
|
||||||
import {
|
import {
|
||||||
getCanonicalScopesForProvider,
|
getCanonicalScopesForProvider,
|
||||||
getProviderIdFromServiceId,
|
getProviderIdFromServiceId,
|
||||||
@@ -59,8 +59,8 @@ type OAuthModalConnectProps = OAuthModalBaseProps & {
|
|||||||
workspaceId: string
|
workspaceId: string
|
||||||
credentialCount: number
|
credentialCount: number
|
||||||
} & (
|
} & (
|
||||||
| { workflowId: string; knowledgeBaseId?: never }
|
| { workflowId: string; knowledgeBaseId?: never; connectorType?: never }
|
||||||
| { workflowId?: never; knowledgeBaseId: string }
|
| { workflowId?: never; knowledgeBaseId: string; connectorType?: string }
|
||||||
)
|
)
|
||||||
|
|
||||||
interface OAuthModalReauthorizeProps extends OAuthModalBaseProps {
|
interface OAuthModalReauthorizeProps extends OAuthModalBaseProps {
|
||||||
@@ -81,6 +81,7 @@ export function OAuthModal(props: OAuthModalProps) {
|
|||||||
const workspaceId = isConnect ? props.workspaceId : ''
|
const workspaceId = isConnect ? props.workspaceId : ''
|
||||||
const workflowId = isConnect ? props.workflowId : undefined
|
const workflowId = isConnect ? props.workflowId : undefined
|
||||||
const knowledgeBaseId = isConnect ? props.knowledgeBaseId : undefined
|
const knowledgeBaseId = isConnect ? props.knowledgeBaseId : undefined
|
||||||
|
const connectorType = isConnect ? props.connectorType : undefined
|
||||||
const toolName = !isConnect ? props.toolName : ''
|
const toolName = !isConnect ? props.toolName : ''
|
||||||
const requiredScopes = !isConnect ? (props.requiredScopes ?? EMPTY_SCOPES) : EMPTY_SCOPES
|
const requiredScopes = !isConnect ? (props.requiredScopes ?? EMPTY_SCOPES) : EMPTY_SCOPES
|
||||||
const newScopes = !isConnect ? (props.newScopes ?? EMPTY_SCOPES) : EMPTY_SCOPES
|
const newScopes = !isConnect ? (props.newScopes ?? EMPTY_SCOPES) : EMPTY_SCOPES
|
||||||
@@ -172,7 +173,7 @@ export function OAuthModal(props: OAuthModalProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const returnContext: OAuthReturnContext = knowledgeBaseId
|
const returnContext: OAuthReturnContext = knowledgeBaseId
|
||||||
? { ...baseContext, origin: 'kb-connectors' as const, knowledgeBaseId }
|
? { ...baseContext, origin: 'kb-connectors' as const, knowledgeBaseId, connectorType }
|
||||||
: { ...baseContext, origin: 'workflow' as const, workflowId: workflowId! }
|
: { ...baseContext, origin: 'workflow' as const, workflowId: workflowId! }
|
||||||
|
|
||||||
writeOAuthReturnContext(returnContext)
|
writeOAuthReturnContext(returnContext)
|
||||||
@@ -205,7 +206,11 @@ export function OAuthModal(props: OAuthModalProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.oauth2.link({ providerId, callbackURL: window.location.href })
|
const callbackURL = new URL(window.location.href)
|
||||||
|
if (connectorType) {
|
||||||
|
callbackURL.searchParams.set(ADD_CONNECTOR_SEARCH_PARAM, connectorType)
|
||||||
|
}
|
||||||
|
await client.oauth2.link({ providerId, callbackURL: callbackURL.toString() })
|
||||||
handleClose()
|
handleClose()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Failed to initiate OAuth connection', { error: err })
|
logger.error('Failed to initiate OAuth connection', { error: err })
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export function NavTour() {
|
|||||||
steps: navTourSteps,
|
steps: navTourSteps,
|
||||||
triggerEvent: START_NAV_TOUR_EVENT,
|
triggerEvent: START_NAV_TOUR_EVENT,
|
||||||
tourName: 'Navigation tour',
|
tourName: 'Navigation tour',
|
||||||
|
tourType: 'nav',
|
||||||
disabled: isWorkflowPage,
|
disabled: isWorkflowPage,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
|
import { usePostHog } from 'posthog-js/react'
|
||||||
import { ACTIONS, type CallBackProps, EVENTS, STATUS, type Step } from 'react-joyride'
|
import { ACTIONS, type CallBackProps, EVENTS, STATUS, type Step } from 'react-joyride'
|
||||||
|
import { captureEvent } from '@/lib/posthog/client'
|
||||||
|
|
||||||
const logger = createLogger('useTour')
|
const logger = createLogger('useTour')
|
||||||
|
|
||||||
@@ -16,6 +18,8 @@ interface UseTourOptions {
|
|||||||
triggerEvent?: string
|
triggerEvent?: string
|
||||||
/** Identifier for logging */
|
/** Identifier for logging */
|
||||||
tourName?: string
|
tourName?: string
|
||||||
|
/** Analytics tour type for PostHog events */
|
||||||
|
tourType?: 'nav' | 'workflow'
|
||||||
/** When true, stops a running tour (e.g. navigating away from the relevant page) */
|
/** When true, stops a running tour (e.g. navigating away from the relevant page) */
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
@@ -45,8 +49,10 @@ export function useTour({
|
|||||||
steps,
|
steps,
|
||||||
triggerEvent,
|
triggerEvent,
|
||||||
tourName = 'tour',
|
tourName = 'tour',
|
||||||
|
tourType,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}: UseTourOptions): UseTourReturn {
|
}: UseTourOptions): UseTourReturn {
|
||||||
|
const posthog = usePostHog()
|
||||||
const [run, setRun] = useState(false)
|
const [run, setRun] = useState(false)
|
||||||
const [stepIndex, setStepIndex] = useState(0)
|
const [stepIndex, setStepIndex] = useState(0)
|
||||||
const [tourKey, setTourKey] = useState(0)
|
const [tourKey, setTourKey] = useState(0)
|
||||||
@@ -152,6 +158,9 @@ export function useTour({
|
|||||||
setRun(true)
|
setRun(true)
|
||||||
logger.info(`${tourName} triggered via event`)
|
logger.info(`${tourName} triggered via event`)
|
||||||
scheduleReveal()
|
scheduleReveal()
|
||||||
|
if (tourType) {
|
||||||
|
captureEvent(posthog, 'tour_started', { tour_type: tourType })
|
||||||
|
}
|
||||||
}, 50)
|
}, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +190,13 @@ export function useTour({
|
|||||||
if (status === STATUS.FINISHED || status === STATUS.SKIPPED) {
|
if (status === STATUS.FINISHED || status === STATUS.SKIPPED) {
|
||||||
stopTour()
|
stopTour()
|
||||||
logger.info(`${tourName} ended`, { status })
|
logger.info(`${tourName} ended`, { status })
|
||||||
|
if (tourType) {
|
||||||
|
if (status === STATUS.FINISHED) {
|
||||||
|
captureEvent(posthog, 'tour_completed', { tour_type: tourType })
|
||||||
|
} else {
|
||||||
|
captureEvent(posthog, 'tour_skipped', { tour_type: tourType, step_index: index })
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +204,9 @@ export function useTour({
|
|||||||
if (action === ACTIONS.CLOSE) {
|
if (action === ACTIONS.CLOSE) {
|
||||||
stopTour()
|
stopTour()
|
||||||
logger.info(`${tourName} closed by user`)
|
logger.info(`${tourName} closed by user`)
|
||||||
|
if (tourType) {
|
||||||
|
captureEvent(posthog, 'tour_skipped', { tour_type: tourType, step_index: index })
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +222,7 @@ export function useTour({
|
|||||||
transitionToStep(nextIndex)
|
transitionToStep(nextIndex)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[stopTour, transitionToStep, steps, tourName]
|
[stopTour, transitionToStep, steps, tourName, tourType, posthog]
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export function WorkflowTour() {
|
|||||||
steps: workflowTourSteps,
|
steps: workflowTourSteps,
|
||||||
triggerEvent: START_WORKFLOW_TOUR_EVENT,
|
triggerEvent: START_WORKFLOW_TOUR_EVENT,
|
||||||
tourName: 'Workflow tour',
|
tourName: 'Workflow tour',
|
||||||
|
tourType: 'workflow',
|
||||||
})
|
})
|
||||||
|
|
||||||
const tourState = useMemo<TourState>(
|
const tourState = useMemo<TourState>(
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Loader2 } from 'lucide-react'
|
|
||||||
|
|
||||||
export default function FileViewLoading() {
|
|
||||||
return (
|
|
||||||
<div className='fixed inset-0 z-50 flex items-center justify-center bg-[var(--bg)]'>
|
|
||||||
<Loader2 className='h-[20px] w-[20px] animate-spin text-[var(--text-tertiary)]' />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -353,7 +353,17 @@ const TemplateCard = memo(function TemplateCard({ template, onSelect }: Template
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
onClick={() => onSelect(template.prompt)}
|
onClick={() => {
|
||||||
|
import('@/lib/posthog/client')
|
||||||
|
.then(({ captureClientEvent }) => {
|
||||||
|
captureClientEvent('template_used', {
|
||||||
|
template_title: template.title,
|
||||||
|
template_modules: template.modules.join(' '),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
onSelect(template.prompt)
|
||||||
|
}}
|
||||||
aria-label={`Select template: ${template.title}`}
|
aria-label={`Select template: ${template.title}`}
|
||||||
className='group flex cursor-pointer flex-col text-left'
|
className='group flex cursor-pointer flex-col text-left'
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { useParams, useRouter } from 'next/navigation'
|
import { useParams, useRouter } from 'next/navigation'
|
||||||
|
import { usePostHog } from 'posthog-js/react'
|
||||||
import { PanelLeft } from '@/components/emcn/icons'
|
import { PanelLeft } from '@/components/emcn/icons'
|
||||||
import { useSession } from '@/lib/auth/auth-client'
|
import { useSession } from '@/lib/auth/auth-client'
|
||||||
import {
|
import {
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
type LandingWorkflowSeed,
|
type LandingWorkflowSeed,
|
||||||
LandingWorkflowSeedStorage,
|
LandingWorkflowSeedStorage,
|
||||||
} from '@/lib/core/utils/browser-storage'
|
} from '@/lib/core/utils/browser-storage'
|
||||||
|
import { captureEvent } from '@/lib/posthog/client'
|
||||||
import { persistImportedWorkflow } from '@/lib/workflows/operations/import-export'
|
import { persistImportedWorkflow } from '@/lib/workflows/operations/import-export'
|
||||||
import { useChatHistory, useMarkTaskRead } from '@/hooks/queries/tasks'
|
import { useChatHistory, useMarkTaskRead } from '@/hooks/queries/tasks'
|
||||||
import type { ChatContext } from '@/stores/panel'
|
import type { ChatContext } from '@/stores/panel'
|
||||||
@@ -27,6 +29,8 @@ export function Home({ chatId }: HomeProps = {}) {
|
|||||||
const { workspaceId } = useParams<{ workspaceId: string }>()
|
const { workspaceId } = useParams<{ workspaceId: string }>()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
|
const posthog = usePostHog()
|
||||||
|
const posthogRef = useRef(posthog)
|
||||||
const [initialPrompt, setInitialPrompt] = useState('')
|
const [initialPrompt, setInitialPrompt] = useState('')
|
||||||
const hasCheckedLandingStorageRef = useRef(false)
|
const hasCheckedLandingStorageRef = useRef(false)
|
||||||
const initialViewInputRef = useRef<HTMLDivElement>(null)
|
const initialViewInputRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -199,11 +203,21 @@ export function Home({ chatId }: HomeProps = {}) {
|
|||||||
return () => cancelAnimationFrame(id)
|
return () => cancelAnimationFrame(id)
|
||||||
}, [resources])
|
}, [resources])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
posthogRef.current = posthog
|
||||||
|
}, [posthog])
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(text: string, fileAttachments?: FileAttachmentForApi[], contexts?: ChatContext[]) => {
|
(text: string, fileAttachments?: FileAttachmentForApi[], contexts?: ChatContext[]) => {
|
||||||
const trimmed = text.trim()
|
const trimmed = text.trim()
|
||||||
if (!trimmed && !(fileAttachments && fileAttachments.length > 0)) return
|
if (!trimmed && !(fileAttachments && fileAttachments.length > 0)) return
|
||||||
|
|
||||||
|
captureEvent(posthogRef.current, 'task_message_sent', {
|
||||||
|
has_attachments: !!(fileAttachments && fileAttachments.length > 0),
|
||||||
|
has_contexts: !!(contexts && contexts.length > 0),
|
||||||
|
is_new_task: !chatId,
|
||||||
|
})
|
||||||
|
|
||||||
if (initialViewInputRef.current) {
|
if (initialViewInputRef.current) {
|
||||||
setIsInputEntering(true)
|
setIsInputEntering(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1407,17 +1407,6 @@ export function useChat(
|
|||||||
const output = tc.result?.output as Record<string, unknown> | undefined
|
const output = tc.result?.output as Record<string, unknown> | undefined
|
||||||
const deployedWorkflowId = (output?.workflowId as string) ?? undefined
|
const deployedWorkflowId = (output?.workflowId as string) ?? undefined
|
||||||
if (deployedWorkflowId && typeof output?.isDeployed === 'boolean') {
|
if (deployedWorkflowId && typeof output?.isDeployed === 'boolean') {
|
||||||
const isDeployed = output.isDeployed as boolean
|
|
||||||
const serverDeployedAt = output.deployedAt
|
|
||||||
? new Date(output.deployedAt as string)
|
|
||||||
: undefined
|
|
||||||
useWorkflowRegistry
|
|
||||||
.getState()
|
|
||||||
.setDeploymentStatus(
|
|
||||||
deployedWorkflowId,
|
|
||||||
isDeployed,
|
|
||||||
isDeployed ? (serverDeployedAt ?? new Date()) : undefined
|
|
||||||
)
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: deploymentKeys.info(deployedWorkflowId),
|
queryKey: deploymentKeys.info(deployedWorkflowId),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Skeleton } from '@/components/emcn'
|
|
||||||
|
|
||||||
const SKELETON_LINE_COUNT = 4
|
|
||||||
|
|
||||||
export default function HomeLoading() {
|
|
||||||
return (
|
|
||||||
<div className='flex h-full flex-col bg-[var(--bg)]'>
|
|
||||||
<div className='min-h-0 flex-1 overflow-hidden px-6 py-4'>
|
|
||||||
<div className='mx-auto max-w-[42rem] space-y-[10px] pt-3'>
|
|
||||||
{Array.from({ length: SKELETON_LINE_COUNT }).map((_, i) => (
|
|
||||||
<Skeleton key={i} className='h-[16px]' style={{ width: `${120 + (i % 4) * 48}px` }} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex-shrink-0 px-[24px] pb-[16px]'>
|
|
||||||
<div className='mx-auto max-w-[42rem]'>
|
|
||||||
<Skeleton className='h-[48px] w-full rounded-[12px]' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import { AlertCircle, Loader2, Pencil, Plus, Tag, X } from 'lucide-react'
|
import { AlertCircle, Loader2, Pencil, Plus, Tag, X } from 'lucide-react'
|
||||||
import { useParams, useRouter } from 'next/navigation'
|
import { useParams, usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||||
|
import { usePostHog } from 'posthog-js/react'
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -24,10 +25,12 @@ import {
|
|||||||
import { Database, DatabaseX } from '@/components/emcn/icons'
|
import { Database, DatabaseX } from '@/components/emcn/icons'
|
||||||
import { SearchHighlight } from '@/components/ui/search-highlight'
|
import { SearchHighlight } from '@/components/ui/search-highlight'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
|
import { ADD_CONNECTOR_SEARCH_PARAM } from '@/lib/credentials/client-state'
|
||||||
import { ALL_TAG_SLOTS, type AllTagSlot, getFieldTypeForSlot } from '@/lib/knowledge/constants'
|
import { ALL_TAG_SLOTS, type AllTagSlot, getFieldTypeForSlot } from '@/lib/knowledge/constants'
|
||||||
import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types'
|
import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types'
|
||||||
import { type FilterFieldType, getOperatorsForFieldType } from '@/lib/knowledge/filters/types'
|
import { type FilterFieldType, getOperatorsForFieldType } from '@/lib/knowledge/filters/types'
|
||||||
import type { DocumentData } from '@/lib/knowledge/types'
|
import type { DocumentData } from '@/lib/knowledge/types'
|
||||||
|
import { captureEvent } from '@/lib/posthog/client'
|
||||||
import { formatFileSize } from '@/lib/uploads/utils/file-utils'
|
import { formatFileSize } from '@/lib/uploads/utils/file-utils'
|
||||||
import type {
|
import type {
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
@@ -190,6 +193,19 @@ export function KnowledgeBase({
|
|||||||
}: KnowledgeBaseProps) {
|
}: KnowledgeBaseProps) {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const workspaceId = propWorkspaceId || (params.workspaceId as string)
|
const workspaceId = propWorkspaceId || (params.workspaceId as string)
|
||||||
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const pathname = usePathname()
|
||||||
|
const addConnectorParam = searchParams.get(ADD_CONNECTOR_SEARCH_PARAM)
|
||||||
|
const posthog = usePostHog()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
captureEvent(posthog, 'knowledge_base_opened', {
|
||||||
|
knowledge_base_id: id,
|
||||||
|
knowledge_base_name: passedKnowledgeBaseName ?? 'Unknown',
|
||||||
|
})
|
||||||
|
}, [id, passedKnowledgeBaseName, posthog])
|
||||||
|
|
||||||
useOAuthReturnForKBConnectors(id)
|
useOAuthReturnForKBConnectors(id)
|
||||||
const { removeKnowledgeBase } = useKnowledgeBasesList(workspaceId, { enabled: false })
|
const { removeKnowledgeBase } = useKnowledgeBasesList(workspaceId, { enabled: false })
|
||||||
const userPermissions = useUserPermissionsContext()
|
const userPermissions = useUserPermissionsContext()
|
||||||
@@ -267,7 +283,29 @@ export function KnowledgeBase({
|
|||||||
const [contextMenuDocument, setContextMenuDocument] = useState<DocumentData | null>(null)
|
const [contextMenuDocument, setContextMenuDocument] = useState<DocumentData | null>(null)
|
||||||
const [showRenameModal, setShowRenameModal] = useState(false)
|
const [showRenameModal, setShowRenameModal] = useState(false)
|
||||||
const [documentToRename, setDocumentToRename] = useState<DocumentData | null>(null)
|
const [documentToRename, setDocumentToRename] = useState<DocumentData | null>(null)
|
||||||
const [showAddConnectorModal, setShowAddConnectorModal] = useState(false)
|
const showAddConnectorModal = addConnectorParam != null
|
||||||
|
const searchParamsRef = useRef(searchParams)
|
||||||
|
searchParamsRef.current = searchParams
|
||||||
|
const updateAddConnectorParam = useCallback(
|
||||||
|
(value: string | null) => {
|
||||||
|
const current = searchParamsRef.current
|
||||||
|
const currentValue = current.get(ADD_CONNECTOR_SEARCH_PARAM)
|
||||||
|
if (value === currentValue || (value === null && currentValue === null)) return
|
||||||
|
const next = new URLSearchParams(current.toString())
|
||||||
|
if (value === null) {
|
||||||
|
next.delete(ADD_CONNECTOR_SEARCH_PARAM)
|
||||||
|
} else {
|
||||||
|
next.set(ADD_CONNECTOR_SEARCH_PARAM, value)
|
||||||
|
}
|
||||||
|
const qs = next.toString()
|
||||||
|
router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false })
|
||||||
|
},
|
||||||
|
[pathname, router]
|
||||||
|
)
|
||||||
|
const setShowAddConnectorModal = useCallback(
|
||||||
|
(open: boolean) => updateAddConnectorParam(open ? '' : null),
|
||||||
|
[updateAddConnectorParam]
|
||||||
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isOpen: isContextMenuOpen,
|
isOpen: isContextMenuOpen,
|
||||||
@@ -329,8 +367,6 @@ export function KnowledgeBase({
|
|||||||
prevHadSyncingRef.current = hasSyncingConnectors
|
prevHadSyncingRef.current = hasSyncingConnectors
|
||||||
}, [hasSyncingConnectors, refreshKnowledgeBase, refreshDocuments])
|
}, [hasSyncingConnectors, refreshKnowledgeBase, refreshDocuments])
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const knowledgeBaseName = knowledgeBase?.name || passedKnowledgeBaseName || 'Knowledge Base'
|
const knowledgeBaseName = knowledgeBase?.name || passedKnowledgeBaseName || 'Knowledge Base'
|
||||||
const error = knowledgeBaseError || documentsError
|
const error = knowledgeBaseError || documentsError
|
||||||
|
|
||||||
@@ -1243,7 +1279,13 @@ export function KnowledgeBase({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{showAddConnectorModal && (
|
{showAddConnectorModal && (
|
||||||
<AddConnectorModal open onOpenChange={setShowAddConnectorModal} knowledgeBaseId={id} />
|
<AddConnectorModal
|
||||||
|
open
|
||||||
|
onOpenChange={setShowAddConnectorModal}
|
||||||
|
onConnectorTypeChange={updateAddConnectorParam}
|
||||||
|
knowledgeBaseId={id}
|
||||||
|
initialConnectorType={addConnectorParam || undefined}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{documentToRename && (
|
{documentToRename && (
|
||||||
|
|||||||
@@ -44,14 +44,22 @@ const CONNECTOR_ENTRIES = Object.entries(CONNECTOR_REGISTRY)
|
|||||||
interface AddConnectorModalProps {
|
interface AddConnectorModalProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
|
onConnectorTypeChange?: (connectorType: string | null) => void
|
||||||
knowledgeBaseId: string
|
knowledgeBaseId: string
|
||||||
|
initialConnectorType?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
type Step = 'select-type' | 'configure'
|
type Step = 'select-type' | 'configure'
|
||||||
|
|
||||||
export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddConnectorModalProps) {
|
export function AddConnectorModal({
|
||||||
const [step, setStep] = useState<Step>('select-type')
|
open,
|
||||||
const [selectedType, setSelectedType] = useState<string | null>(null)
|
onOpenChange,
|
||||||
|
onConnectorTypeChange,
|
||||||
|
knowledgeBaseId,
|
||||||
|
initialConnectorType,
|
||||||
|
}: AddConnectorModalProps) {
|
||||||
|
const [step, setStep] = useState<Step>(() => (initialConnectorType ? 'configure' : 'select-type'))
|
||||||
|
const [selectedType, setSelectedType] = useState<string | null>(initialConnectorType ?? null)
|
||||||
const [sourceConfig, setSourceConfig] = useState<Record<string, string>>({})
|
const [sourceConfig, setSourceConfig] = useState<Record<string, string>>({})
|
||||||
const [syncInterval, setSyncInterval] = useState(1440)
|
const [syncInterval, setSyncInterval] = useState(1440)
|
||||||
const [selectedCredentialId, setSelectedCredentialId] = useState<string | null>(null)
|
const [selectedCredentialId, setSelectedCredentialId] = useState<string | null>(null)
|
||||||
@@ -151,6 +159,7 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
|
|||||||
setError(null)
|
setError(null)
|
||||||
setSearchTerm('')
|
setSearchTerm('')
|
||||||
setStep('configure')
|
setStep('configure')
|
||||||
|
onConnectorTypeChange?.(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFieldChange = useCallback(
|
const handleFieldChange = useCallback(
|
||||||
@@ -286,7 +295,10 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
|
|||||||
<Button
|
<Button
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
className='mr-2 h-6 w-6 p-0'
|
className='mr-2 h-6 w-6 p-0'
|
||||||
onClick={() => setStep('select-type')}
|
onClick={() => {
|
||||||
|
setStep('select-type')
|
||||||
|
onConnectorTypeChange?.('')
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ArrowLeft className='h-4 w-4' />
|
<ArrowLeft className='h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -565,6 +577,7 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
|
|||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
knowledgeBaseId={knowledgeBaseId}
|
knowledgeBaseId={knowledgeBaseId}
|
||||||
credentialCount={credentials.length}
|
credentialCount={credentials.length}
|
||||||
|
connectorType={selectedType ?? undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
|
import { usePostHog } from 'posthog-js/react'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,6 +12,12 @@ export function WorkspaceScopeSync() {
|
|||||||
const { workspaceId } = useParams<{ workspaceId: string }>()
|
const { workspaceId } = useParams<{ workspaceId: string }>()
|
||||||
const hydrationWorkspaceId = useWorkflowRegistry((state) => state.hydration.workspaceId)
|
const hydrationWorkspaceId = useWorkflowRegistry((state) => state.hydration.workspaceId)
|
||||||
const switchToWorkspace = useWorkflowRegistry((state) => state.switchToWorkspace)
|
const switchToWorkspace = useWorkflowRegistry((state) => state.switchToWorkspace)
|
||||||
|
const posthog = usePostHog()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!workspaceId) return
|
||||||
|
posthog?.group('workspace', workspaceId)
|
||||||
|
}, [posthog, workspaceId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workspaceId || hydrationWorkspaceId === workspaceId) {
|
if (!workspaceId || hydrationWorkspaceId === workspaceId) {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect } from 'react'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import { useSearchParams } from 'next/navigation'
|
import { useSearchParams } from 'next/navigation'
|
||||||
|
import { usePostHog } from 'posthog-js/react'
|
||||||
import { Skeleton } from '@/components/emcn'
|
import { Skeleton } from '@/components/emcn'
|
||||||
import { useSession } from '@/lib/auth/auth-client'
|
import { useSession } from '@/lib/auth/auth-client'
|
||||||
|
import { captureEvent } from '@/lib/posthog/client'
|
||||||
import { AdminSkeleton } from '@/app/workspace/[workspaceId]/settings/components/admin/admin-skeleton'
|
import { AdminSkeleton } from '@/app/workspace/[workspaceId]/settings/components/admin/admin-skeleton'
|
||||||
import { ApiKeysSkeleton } from '@/app/workspace/[workspaceId]/settings/components/api-keys/api-key-skeleton'
|
import { ApiKeysSkeleton } from '@/app/workspace/[workspaceId]/settings/components/api-keys/api-key-skeleton'
|
||||||
import { BYOKSkeleton } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton'
|
import { BYOKSkeleton } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton'
|
||||||
@@ -160,6 +163,7 @@ export function SettingsPage({ section }: SettingsPageProps) {
|
|||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const mcpServerId = searchParams.get('mcpServerId')
|
const mcpServerId = searchParams.get('mcpServerId')
|
||||||
const { data: session, isPending: sessionLoading } = useSession()
|
const { data: session, isPending: sessionLoading } = useSession()
|
||||||
|
const posthog = usePostHog()
|
||||||
|
|
||||||
const isAdminRole = session?.user?.role === 'admin'
|
const isAdminRole = session?.user?.role === 'admin'
|
||||||
const effectiveSection =
|
const effectiveSection =
|
||||||
@@ -174,6 +178,11 @@ export function SettingsPage({ section }: SettingsPageProps) {
|
|||||||
const label =
|
const label =
|
||||||
allNavigationItems.find((item) => item.id === effectiveSection)?.label ?? effectiveSection
|
allNavigationItems.find((item) => item.id === effectiveSection)?.label ?? effectiveSection
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sessionLoading) return
|
||||||
|
captureEvent(posthog, 'settings_tab_viewed', { section: effectiveSection })
|
||||||
|
}, [effectiveSection, sessionLoading, posthog])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className='mb-7 font-medium text-[22px] text-[var(--text-primary)]'>{label}</h2>
|
<h2 className='mb-7 font-medium text-[22px] text-[var(--text-primary)]'>{label}</h2>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ interface CreateApiKeyModalProps {
|
|||||||
allowPersonalApiKeys?: boolean
|
allowPersonalApiKeys?: boolean
|
||||||
canManageWorkspaceKeys?: boolean
|
canManageWorkspaceKeys?: boolean
|
||||||
defaultKeyType?: 'personal' | 'workspace'
|
defaultKeyType?: 'personal' | 'workspace'
|
||||||
|
source?: 'settings' | 'deploy_modal'
|
||||||
onKeyCreated?: (key: ApiKey) => void
|
onKeyCreated?: (key: ApiKey) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ export function CreateApiKeyModal({
|
|||||||
allowPersonalApiKeys = true,
|
allowPersonalApiKeys = true,
|
||||||
canManageWorkspaceKeys = false,
|
canManageWorkspaceKeys = false,
|
||||||
defaultKeyType = 'personal',
|
defaultKeyType = 'personal',
|
||||||
|
source = 'settings',
|
||||||
onKeyCreated,
|
onKeyCreated,
|
||||||
}: CreateApiKeyModalProps) {
|
}: CreateApiKeyModalProps) {
|
||||||
const [keyName, setKeyName] = useState('')
|
const [keyName, setKeyName] = useState('')
|
||||||
@@ -74,6 +76,7 @@ export function CreateApiKeyModal({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
name: trimmedName,
|
name: trimmedName,
|
||||||
keyType,
|
keyType,
|
||||||
|
source,
|
||||||
})
|
})
|
||||||
|
|
||||||
setNewKey(data.key)
|
setNewKey(data.key)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { GripVertical } from 'lucide-react'
|
import { GripVertical } from 'lucide-react'
|
||||||
import { useParams, useRouter } from 'next/navigation'
|
import { useParams, useRouter } from 'next/navigation'
|
||||||
|
import { usePostHog } from 'posthog-js/react'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -39,6 +40,7 @@ import {
|
|||||||
TypeText,
|
TypeText,
|
||||||
} from '@/components/emcn/icons'
|
} from '@/components/emcn/icons'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
|
import { captureEvent } from '@/lib/posthog/client'
|
||||||
import type { ColumnDefinition, Filter, SortDirection, TableRow as TableRowType } from '@/lib/table'
|
import type { ColumnDefinition, Filter, SortDirection, TableRow as TableRowType } from '@/lib/table'
|
||||||
import type { ColumnOption, SortConfig } from '@/app/workspace/[workspaceId]/components'
|
import type { ColumnOption, SortConfig } from '@/app/workspace/[workspaceId]/components'
|
||||||
import { ResourceHeader, ResourceOptionsBar } from '@/app/workspace/[workspaceId]/components'
|
import { ResourceHeader, ResourceOptionsBar } from '@/app/workspace/[workspaceId]/components'
|
||||||
@@ -177,6 +179,12 @@ export function Table({
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const workspaceId = propWorkspaceId || (params.workspaceId as string)
|
const workspaceId = propWorkspaceId || (params.workspaceId as string)
|
||||||
const tableId = propTableId || (params.tableId as string)
|
const tableId = propTableId || (params.tableId as string)
|
||||||
|
const posthog = usePostHog()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!tableId || !workspaceId) return
|
||||||
|
captureEvent(posthog, 'table_opened', { table_id: tableId, workspace_id: workspaceId })
|
||||||
|
}, [tableId, workspaceId, posthog])
|
||||||
|
|
||||||
const [queryOptions, setQueryOptions] = useState<QueryOptions>({
|
const [queryOptions, setQueryOptions] = useState<QueryOptions>({
|
||||||
filter: null,
|
filter: null,
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Loader2 } from 'lucide-react'
|
|
||||||
|
|
||||||
export default function TaskLoading() {
|
|
||||||
return (
|
|
||||||
<div className='flex h-full bg-[var(--bg)]'>
|
|
||||||
<div className='flex h-full min-w-0 flex-1 flex-col'>
|
|
||||||
<div className='flex min-h-0 flex-1 items-center justify-center'>
|
|
||||||
<Loader2 className='h-[20px] w-[20px] animate-spin text-[var(--text-tertiary)]' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -111,8 +111,6 @@ function normalizeWorkflowState(input?: any): WorkflowState | null {
|
|||||||
lastUpdate: input.lastUpdate,
|
lastUpdate: input.lastUpdate,
|
||||||
metadata: input.metadata,
|
metadata: input.metadata,
|
||||||
variables: input.variables,
|
variables: input.variables,
|
||||||
deploymentStatuses: input.deploymentStatuses,
|
|
||||||
needsRedeployment: input.needsRedeployment,
|
|
||||||
dragStartPosition: input.dragStartPosition ?? null,
|
dragStartPosition: input.dragStartPosition ?? null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import { useWorkflowMap } from '@/hooks/queries/workflows'
|
|||||||
import { useWorkspaceSettings } from '@/hooks/queries/workspace'
|
import { useWorkspaceSettings } from '@/hooks/queries/workspace'
|
||||||
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
||||||
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
|
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
|
||||||
import { mergeSubblockState } from '@/stores/workflows/utils'
|
import { mergeSubblockState } from '@/stores/workflows/utils'
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||||
@@ -90,10 +89,7 @@ export function DeployModal({
|
|||||||
const params = useParams()
|
const params = useParams()
|
||||||
const workspaceId = params?.workspaceId as string
|
const workspaceId = params?.workspaceId as string
|
||||||
const { navigateToSettings } = useSettingsNavigation()
|
const { navigateToSettings } = useSettingsNavigation()
|
||||||
const deploymentStatus = useWorkflowRegistry((state) =>
|
const isDeployed = isDeployedProp
|
||||||
state.getWorkflowDeploymentStatus(workflowId)
|
|
||||||
)
|
|
||||||
const isDeployed = deploymentStatus?.isDeployed ?? isDeployedProp
|
|
||||||
const { data: workflowMap = {} } = useWorkflowMap(workspaceId)
|
const { data: workflowMap = {} } = useWorkflowMap(workspaceId)
|
||||||
const workflowMetadata = workflowId ? workflowMap[workflowId] : undefined
|
const workflowMetadata = workflowId ? workflowMap[workflowId] : undefined
|
||||||
const workflowWorkspaceId = workflowMetadata?.workspaceId ?? null
|
const workflowWorkspaceId = workflowMetadata?.workspaceId ?? null
|
||||||
@@ -381,8 +377,6 @@ export function DeployModal({
|
|||||||
|
|
||||||
invalidateDeploymentQueries(queryClient, workflowId)
|
invalidateDeploymentQueries(queryClient, workflowId)
|
||||||
|
|
||||||
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(workflowId, false)
|
|
||||||
|
|
||||||
if (chatSuccessTimeoutRef.current) {
|
if (chatSuccessTimeoutRef.current) {
|
||||||
clearTimeout(chatSuccessTimeoutRef.current)
|
clearTimeout(chatSuccessTimeoutRef.current)
|
||||||
}
|
}
|
||||||
@@ -915,6 +909,7 @@ export function DeployModal({
|
|||||||
allowPersonalApiKeys={allowPersonalApiKeys}
|
allowPersonalApiKeys={allowPersonalApiKeys}
|
||||||
canManageWorkspaceKeys={canManageWorkspaceKeys}
|
canManageWorkspaceKeys={canManageWorkspaceKeys}
|
||||||
defaultKeyType={defaultKeyType}
|
defaultKeyType={defaultKeyType}
|
||||||
|
source='deploy_modal'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{workflowId && (
|
{workflowId && (
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
useDeployment,
|
useDeployment,
|
||||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks'
|
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks'
|
||||||
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
|
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
|
||||||
import { useDeployedWorkflowState } from '@/hooks/queries/deployments'
|
import { useDeployedWorkflowState, useDeploymentInfo } from '@/hooks/queries/deployments'
|
||||||
import type { WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
|
import type { WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
|
|
||||||
@@ -25,10 +25,10 @@ export function Deploy({ activeWorkflowId, userPermissions, className }: DeployP
|
|||||||
const isRegistryLoading = hydrationPhase === 'idle' || hydrationPhase === 'state-loading'
|
const isRegistryLoading = hydrationPhase === 'idle' || hydrationPhase === 'state-loading'
|
||||||
const { hasBlocks } = useCurrentWorkflow()
|
const { hasBlocks } = useCurrentWorkflow()
|
||||||
|
|
||||||
const deploymentStatus = useWorkflowRegistry((state) =>
|
const { data: deploymentInfo } = useDeploymentInfo(activeWorkflowId, {
|
||||||
state.getWorkflowDeploymentStatus(activeWorkflowId)
|
enabled: !isRegistryLoading,
|
||||||
)
|
})
|
||||||
const isDeployed = deploymentStatus?.isDeployed || false
|
const isDeployed = deploymentInfo?.isDeployed ?? false
|
||||||
|
|
||||||
const isDeployedStateEnabled = Boolean(activeWorkflowId) && isDeployed && !isRegistryLoading
|
const isDeployedStateEnabled = Boolean(activeWorkflowId) && isDeployed && !isRegistryLoading
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { hasWorkflowChanged } from '@/lib/workflows/comparison'
|
import { hasWorkflowChanged } from '@/lib/workflows/comparison'
|
||||||
import { mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
|
import { mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
|
||||||
import { useVariablesStore } from '@/stores/panel/variables/store'
|
import { useVariablesStore } from '@/stores/variables/store'
|
||||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||||
|
|||||||
@@ -59,14 +59,10 @@ interface ComboBoxProps {
|
|||||||
/** Configuration for the sub-block */
|
/** Configuration for the sub-block */
|
||||||
config: SubBlockConfig
|
config: SubBlockConfig
|
||||||
/** Async function to fetch options dynamically */
|
/** Async function to fetch options dynamically */
|
||||||
fetchOptions?: (
|
fetchOptions?: (blockId: string) => Promise<Array<{ label: string; id: string }>>
|
||||||
blockId: string,
|
|
||||||
subBlockId: string
|
|
||||||
) => Promise<Array<{ label: string; id: string }>>
|
|
||||||
/** Async function to fetch a single option's label by ID (for hydration) */
|
/** Async function to fetch a single option's label by ID (for hydration) */
|
||||||
fetchOptionById?: (
|
fetchOptionById?: (
|
||||||
blockId: string,
|
blockId: string,
|
||||||
subBlockId: string,
|
|
||||||
optionId: string
|
optionId: string
|
||||||
) => Promise<{ label: string; id: string } | null>
|
) => Promise<{ label: string; id: string } | null>
|
||||||
/** Field dependencies that trigger option refetch when changed */
|
/** Field dependencies that trigger option refetch when changed */
|
||||||
@@ -135,7 +131,7 @@ export const ComboBox = memo(function ComboBox({
|
|||||||
setIsLoadingOptions(true)
|
setIsLoadingOptions(true)
|
||||||
setFetchError(null)
|
setFetchError(null)
|
||||||
try {
|
try {
|
||||||
const options = await fetchOptions(blockId, subBlockId)
|
const options = await fetchOptions(blockId)
|
||||||
setFetchedOptions(options)
|
setFetchedOptions(options)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch options'
|
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch options'
|
||||||
@@ -144,7 +140,7 @@ export const ComboBox = memo(function ComboBox({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoadingOptions(false)
|
setIsLoadingOptions(false)
|
||||||
}
|
}
|
||||||
}, [fetchOptions, blockId, subBlockId, isPreview, disabled])
|
}, [fetchOptions, blockId, isPreview, disabled])
|
||||||
|
|
||||||
// Determine the active value based on mode (preview vs. controlled vs. store)
|
// Determine the active value based on mode (preview vs. controlled vs. store)
|
||||||
const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
|
const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
|
||||||
@@ -363,7 +359,7 @@ export const ComboBox = memo(function ComboBox({
|
|||||||
let isActive = true
|
let isActive = true
|
||||||
|
|
||||||
// Fetch the hydrated option
|
// Fetch the hydrated option
|
||||||
fetchOptionById(blockId, subBlockId, valueToHydrate)
|
fetchOptionById(blockId, valueToHydrate)
|
||||||
.then((option) => {
|
.then((option) => {
|
||||||
if (isActive) setHydratedOption(option)
|
if (isActive) setHydratedOption(option)
|
||||||
})
|
})
|
||||||
@@ -378,7 +374,6 @@ export const ComboBox = memo(function ComboBox({
|
|||||||
fetchOptionById,
|
fetchOptionById,
|
||||||
value,
|
value,
|
||||||
blockId,
|
blockId,
|
||||||
subBlockId,
|
|
||||||
isPreview,
|
isPreview,
|
||||||
disabled,
|
disabled,
|
||||||
fetchedOptions,
|
fetchedOptions,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { createElement, useCallback, useMemo, useState } from 'react'
|
import { createElement, useCallback, useMemo, useState } from 'react'
|
||||||
import { ExternalLink, Users } from 'lucide-react'
|
import { ExternalLink, KeyRound, Users } from 'lucide-react'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import { Button, Combobox } from '@/components/emcn/components'
|
import { Button, Combobox } from '@/components/emcn/components'
|
||||||
import { getSubscriptionAccessState } from '@/lib/billing/client'
|
import { getSubscriptionAccessState } from '@/lib/billing/client'
|
||||||
@@ -22,7 +22,7 @@ import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
|
|||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { CREDENTIAL_SET } from '@/executor/constants'
|
import { CREDENTIAL_SET } from '@/executor/constants'
|
||||||
import { useCredentialSets } from '@/hooks/queries/credential-sets'
|
import { useCredentialSets } from '@/hooks/queries/credential-sets'
|
||||||
import { useWorkspaceCredential } from '@/hooks/queries/credentials'
|
import { useWorkspaceCredential, useWorkspaceCredentials } from '@/hooks/queries/credentials'
|
||||||
import { useOAuthCredentials } from '@/hooks/queries/oauth/oauth-credentials'
|
import { useOAuthCredentials } from '@/hooks/queries/oauth/oauth-credentials'
|
||||||
import { useOrganizations } from '@/hooks/queries/organization'
|
import { useOrganizations } from '@/hooks/queries/organization'
|
||||||
import { useSubscriptionData } from '@/hooks/queries/subscription'
|
import { useSubscriptionData } from '@/hooks/queries/subscription'
|
||||||
@@ -60,6 +60,7 @@ export function CredentialSelector({
|
|||||||
const requiredScopes = subBlock.requiredScopes || []
|
const requiredScopes = subBlock.requiredScopes || []
|
||||||
const label = subBlock.placeholder || 'Select credential'
|
const label = subBlock.placeholder || 'Select credential'
|
||||||
const serviceId = subBlock.serviceId || ''
|
const serviceId = subBlock.serviceId || ''
|
||||||
|
const isAllCredentials = !serviceId
|
||||||
const supportsCredentialSets = subBlock.supportsCredentialSets || false
|
const supportsCredentialSets = subBlock.supportsCredentialSets || false
|
||||||
|
|
||||||
const { data: organizationsData } = useOrganizations()
|
const { data: organizationsData } = useOrganizations()
|
||||||
@@ -101,14 +102,22 @@ export function CredentialSelector({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
data: rawCredentials = [],
|
data: rawCredentials = [],
|
||||||
isFetching: credentialsLoading,
|
isFetching: oauthCredentialsLoading,
|
||||||
refetch: refetchCredentials,
|
refetch: refetchCredentials,
|
||||||
} = useOAuthCredentials(effectiveProviderId, {
|
} = useOAuthCredentials(effectiveProviderId, {
|
||||||
enabled: Boolean(effectiveProviderId),
|
enabled: !isAllCredentials && Boolean(effectiveProviderId),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
workflowId: activeWorkflowId || undefined,
|
workflowId: activeWorkflowId || undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: allWorkspaceCredentials = [],
|
||||||
|
isFetching: allCredentialsLoading,
|
||||||
|
refetch: refetchAllCredentials,
|
||||||
|
} = useWorkspaceCredentials({ workspaceId, enabled: isAllCredentials })
|
||||||
|
|
||||||
|
const credentialsLoading = isAllCredentials ? allCredentialsLoading : oauthCredentialsLoading
|
||||||
|
|
||||||
const credentials = useMemo(
|
const credentials = useMemo(
|
||||||
() =>
|
() =>
|
||||||
isTriggerMode
|
isTriggerMode
|
||||||
@@ -122,9 +131,17 @@ export function CredentialSelector({
|
|||||||
[credentials, selectedId]
|
[credentials, selectedId]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const selectedAllCredential = useMemo(
|
||||||
|
() =>
|
||||||
|
isAllCredentials ? (allWorkspaceCredentials.find((c) => c.id === selectedId) ?? null) : null,
|
||||||
|
[isAllCredentials, allWorkspaceCredentials, selectedId]
|
||||||
|
)
|
||||||
|
|
||||||
const isServiceAccount = useMemo(
|
const isServiceAccount = useMemo(
|
||||||
() => selectedCredential?.type === 'service_account',
|
() =>
|
||||||
[selectedCredential]
|
selectedCredential?.type === 'service_account' ||
|
||||||
|
selectedAllCredential?.type === 'service_account',
|
||||||
|
[selectedCredential, selectedAllCredential]
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectedCredentialSet = useMemo(
|
const selectedCredentialSet = useMemo(
|
||||||
@@ -134,37 +151,45 @@ export function CredentialSelector({
|
|||||||
|
|
||||||
const { data: inaccessibleCredential } = useWorkspaceCredential(
|
const { data: inaccessibleCredential } = useWorkspaceCredential(
|
||||||
selectedId || undefined,
|
selectedId || undefined,
|
||||||
Boolean(selectedId) && !selectedCredential && !credentialsLoading && Boolean(workspaceId)
|
Boolean(selectedId) &&
|
||||||
|
!selectedCredential &&
|
||||||
|
!selectedAllCredential &&
|
||||||
|
!credentialsLoading &&
|
||||||
|
Boolean(workspaceId)
|
||||||
)
|
)
|
||||||
const inaccessibleCredentialName = inaccessibleCredential?.displayName ?? null
|
const inaccessibleCredentialName = inaccessibleCredential?.displayName ?? null
|
||||||
|
|
||||||
const resolvedLabel = useMemo(() => {
|
const resolvedLabel = useMemo(() => {
|
||||||
if (selectedCredentialSet) return selectedCredentialSet.name
|
if (selectedCredentialSet) return selectedCredentialSet.name
|
||||||
|
if (selectedAllCredential) return selectedAllCredential.displayName
|
||||||
if (selectedCredential) return selectedCredential.name
|
if (selectedCredential) return selectedCredential.name
|
||||||
if (inaccessibleCredentialName) return inaccessibleCredentialName
|
if (inaccessibleCredentialName) return inaccessibleCredentialName
|
||||||
return ''
|
return ''
|
||||||
}, [selectedCredentialSet, selectedCredential, inaccessibleCredentialName])
|
}, [selectedCredentialSet, selectedAllCredential, selectedCredential, inaccessibleCredentialName])
|
||||||
|
|
||||||
const displayValue = isEditing ? editingValue : resolvedLabel
|
const displayValue = isEditing ? editingValue : resolvedLabel
|
||||||
|
|
||||||
useCredentialRefreshTriggers(refetchCredentials, effectiveProviderId, workspaceId)
|
const refetch = useCallback(
|
||||||
|
() => (isAllCredentials ? refetchAllCredentials() : refetchCredentials()),
|
||||||
|
[isAllCredentials, refetchAllCredentials, refetchCredentials]
|
||||||
|
)
|
||||||
|
|
||||||
|
useCredentialRefreshTriggers(refetch, effectiveProviderId, workspaceId)
|
||||||
|
|
||||||
const handleOpenChange = useCallback(
|
const handleOpenChange = useCallback(
|
||||||
(isOpen: boolean) => {
|
(isOpen: boolean) => {
|
||||||
if (isOpen) {
|
if (isOpen) void refetch()
|
||||||
void refetchCredentials()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[refetchCredentials]
|
[refetch]
|
||||||
)
|
)
|
||||||
|
|
||||||
const hasSelection = Boolean(selectedCredential)
|
const hasOAuthSelection = Boolean(selectedCredential)
|
||||||
const missingRequiredScopes = hasSelection
|
const missingRequiredScopes = hasOAuthSelection
|
||||||
? getMissingRequiredScopes(selectedCredential!, requiredScopes || [])
|
? getMissingRequiredScopes(selectedCredential!, requiredScopes || [])
|
||||||
: []
|
: []
|
||||||
|
|
||||||
const needsUpdate =
|
const needsUpdate =
|
||||||
hasSelection &&
|
hasOAuthSelection &&
|
||||||
!isServiceAccount &&
|
!isServiceAccount &&
|
||||||
missingRequiredScopes.length > 0 &&
|
missingRequiredScopes.length > 0 &&
|
||||||
!effectiveDisabled &&
|
!effectiveDisabled &&
|
||||||
@@ -218,6 +243,12 @@ export function CredentialSelector({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const { comboboxOptions, comboboxGroups } = useMemo(() => {
|
const { comboboxOptions, comboboxGroups } = useMemo(() => {
|
||||||
|
if (isAllCredentials) {
|
||||||
|
const oauthCredentials = allWorkspaceCredentials.filter((c) => c.type === 'oauth')
|
||||||
|
const options = oauthCredentials.map((cred) => ({ label: cred.displayName, value: cred.id }))
|
||||||
|
return { comboboxOptions: options, comboboxGroups: undefined }
|
||||||
|
}
|
||||||
|
|
||||||
const pollingProviderId = getPollingProviderFromOAuth(effectiveProviderId)
|
const pollingProviderId = getPollingProviderFromOAuth(effectiveProviderId)
|
||||||
// Handle both old ('gmail') and new ('google-email') provider IDs for backwards compatibility
|
// Handle both old ('gmail') and new ('google-email') provider IDs for backwards compatibility
|
||||||
const matchesProvider = (csProviderId: string | null) => {
|
const matchesProvider = (csProviderId: string | null) => {
|
||||||
@@ -281,6 +312,8 @@ export function CredentialSelector({
|
|||||||
|
|
||||||
return { comboboxOptions: options, comboboxGroups: undefined }
|
return { comboboxOptions: options, comboboxGroups: undefined }
|
||||||
}, [
|
}, [
|
||||||
|
isAllCredentials,
|
||||||
|
allWorkspaceCredentials,
|
||||||
credentials,
|
credentials,
|
||||||
provider,
|
provider,
|
||||||
effectiveProviderId,
|
effectiveProviderId,
|
||||||
@@ -306,6 +339,17 @@ export function CredentialSelector({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAllCredentials && selectedAllCredential) {
|
||||||
|
return (
|
||||||
|
<div className='flex w-full items-center truncate'>
|
||||||
|
<div className='mr-2 flex-shrink-0 opacity-90'>
|
||||||
|
<KeyRound className='h-3 w-3' />
|
||||||
|
</div>
|
||||||
|
<span className='truncate'>{displayValue}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex w-full items-center truncate'>
|
<div className='flex w-full items-center truncate'>
|
||||||
<div className='mr-2 flex-shrink-0 opacity-90'>
|
<div className='mr-2 flex-shrink-0 opacity-90'>
|
||||||
@@ -320,7 +364,8 @@ export function CredentialSelector({
|
|||||||
selectedCredentialProvider,
|
selectedCredentialProvider,
|
||||||
isCredentialSetSelected,
|
isCredentialSetSelected,
|
||||||
selectedCredentialSet,
|
selectedCredentialSet,
|
||||||
isServiceAccount,
|
isAllCredentials,
|
||||||
|
selectedAllCredential,
|
||||||
])
|
])
|
||||||
|
|
||||||
const handleComboboxChange = useCallback(
|
const handleComboboxChange = useCallback(
|
||||||
@@ -339,7 +384,9 @@ export function CredentialSelector({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchedCred = credentials.find((c) => c.id === value)
|
const matchedCred = (
|
||||||
|
isAllCredentials ? allWorkspaceCredentials.filter((c) => c.type === 'oauth') : credentials
|
||||||
|
).find((c) => c.id === value)
|
||||||
if (matchedCred) {
|
if (matchedCred) {
|
||||||
handleSelect(value)
|
handleSelect(value)
|
||||||
return
|
return
|
||||||
@@ -348,7 +395,15 @@ export function CredentialSelector({
|
|||||||
setIsEditing(true)
|
setIsEditing(true)
|
||||||
setEditingValue(value)
|
setEditingValue(value)
|
||||||
},
|
},
|
||||||
[credentials, credentialSets, handleAddCredential, handleSelect, handleCredentialSetSelect]
|
[
|
||||||
|
isAllCredentials,
|
||||||
|
allWorkspaceCredentials,
|
||||||
|
credentials,
|
||||||
|
credentialSets,
|
||||||
|
handleAddCredential,
|
||||||
|
handleSelect,
|
||||||
|
handleCredentialSetSelect,
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -52,14 +52,10 @@ interface DropdownProps {
|
|||||||
/** Enable multi-select mode */
|
/** Enable multi-select mode */
|
||||||
multiSelect?: boolean
|
multiSelect?: boolean
|
||||||
/** Async function to fetch options dynamically */
|
/** Async function to fetch options dynamically */
|
||||||
fetchOptions?: (
|
fetchOptions?: (blockId: string) => Promise<Array<{ label: string; id: string }>>
|
||||||
blockId: string,
|
|
||||||
subBlockId: string
|
|
||||||
) => Promise<Array<{ label: string; id: string }>>
|
|
||||||
/** Async function to fetch a single option's label by ID (for hydration) */
|
/** Async function to fetch a single option's label by ID (for hydration) */
|
||||||
fetchOptionById?: (
|
fetchOptionById?: (
|
||||||
blockId: string,
|
blockId: string,
|
||||||
subBlockId: string,
|
|
||||||
optionId: string
|
optionId: string
|
||||||
) => Promise<{ label: string; id: string } | null>
|
) => Promise<{ label: string; id: string } | null>
|
||||||
/** Field dependencies that trigger option refetch when changed */
|
/** Field dependencies that trigger option refetch when changed */
|
||||||
@@ -160,7 +156,7 @@ export const Dropdown = memo(function Dropdown({
|
|||||||
setIsLoadingOptions(true)
|
setIsLoadingOptions(true)
|
||||||
setFetchError(null)
|
setFetchError(null)
|
||||||
try {
|
try {
|
||||||
const options = await fetchOptions(blockId, subBlockId)
|
const options = await fetchOptions(blockId)
|
||||||
setFetchedOptions(options)
|
setFetchedOptions(options)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch options'
|
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch options'
|
||||||
@@ -169,7 +165,7 @@ export const Dropdown = memo(function Dropdown({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoadingOptions(false)
|
setIsLoadingOptions(false)
|
||||||
}
|
}
|
||||||
}, [fetchOptions, blockId, subBlockId, isPreview, disabled])
|
}, [fetchOptions, blockId, isPreview, disabled])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles combobox open state changes to trigger option fetching
|
* Handles combobox open state changes to trigger option fetching
|
||||||
@@ -430,7 +426,7 @@ export const Dropdown = memo(function Dropdown({
|
|||||||
let isActive = true
|
let isActive = true
|
||||||
|
|
||||||
// Fetch the hydrated option
|
// Fetch the hydrated option
|
||||||
fetchOptionById(blockId, subBlockId, valueToHydrate)
|
fetchOptionById(blockId, valueToHydrate)
|
||||||
.then((option) => {
|
.then((option) => {
|
||||||
if (isActive) setHydratedOption(option)
|
if (isActive) setHydratedOption(option)
|
||||||
})
|
})
|
||||||
@@ -446,7 +442,6 @@ export const Dropdown = memo(function Dropdown({
|
|||||||
singleValue,
|
singleValue,
|
||||||
multiSelect,
|
multiSelect,
|
||||||
blockId,
|
blockId,
|
||||||
subBlockId,
|
|
||||||
isPreview,
|
isPreview,
|
||||||
disabled,
|
disabled,
|
||||||
fetchedOptions,
|
fetchedOptions,
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/
|
|||||||
import { getBlock } from '@/blocks'
|
import { getBlock } from '@/blocks'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
import { normalizeName } from '@/executor/constants'
|
import { normalizeName } from '@/executor/constants'
|
||||||
import type { Variable } from '@/stores/panel'
|
import { useVariablesStore } from '@/stores/variables/store'
|
||||||
import { useVariablesStore } from '@/stores/panel'
|
import type { Variable } from '@/stores/variables/types'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import {
|
|||||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||||
import type { Variable } from '@/stores/panel'
|
import { useVariablesStore } from '@/stores/variables/store'
|
||||||
import { useVariablesStore } from '@/stores/panel'
|
import type { Variable } from '@/stores/variables/types'
|
||||||
|
|
||||||
interface VariableAssignment {
|
interface VariableAssignment {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { useParams } from 'next/navigation'
|
|||||||
import { SELECTOR_CONTEXT_FIELDS } from '@/lib/workflows/subblocks/context'
|
import { SELECTOR_CONTEXT_FIELDS } from '@/lib/workflows/subblocks/context'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { extractEnvVarName, isEnvVarReference, isReference } from '@/executor/constants'
|
import { extractEnvVarName, isEnvVarReference, isReference } from '@/executor/constants'
|
||||||
|
import { usePersonalEnvironment } from '@/hooks/queries/environment'
|
||||||
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
|
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
|
||||||
import { useEnvironmentStore } from '@/stores/settings/environment'
|
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { useDependsOnGate } from './use-depends-on-gate'
|
import { useDependsOnGate } from './use-depends-on-gate'
|
||||||
import { useSubBlockValue } from './use-sub-block-value'
|
import { useSubBlockValue } from './use-sub-block-value'
|
||||||
@@ -32,7 +32,7 @@ export function useSelectorSetup(
|
|||||||
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
|
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
|
||||||
const workflowId = (params?.workflowId as string) || activeWorkflowId || ''
|
const workflowId = (params?.workflowId as string) || activeWorkflowId || ''
|
||||||
|
|
||||||
const envVariables = useEnvironmentStore((s) => s.variables)
|
const { data: envVariables = {} } = usePersonalEnvironment()
|
||||||
|
|
||||||
const { finalDisabled, dependencyValues, canonicalIndex } = useDependsOnGate(
|
const { finalDisabled, dependencyValues, canonicalIndex } = useDependsOnGate(
|
||||||
blockId,
|
blockId,
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ import {
|
|||||||
Unlock,
|
Unlock,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
|
import { usePostHog } from 'posthog-js/react'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
import { useStoreWithEqualityFn } from 'zustand/traditional'
|
import { useStoreWithEqualityFn } from 'zustand/traditional'
|
||||||
import { Button, Tooltip } from '@/components/emcn'
|
import { Button, Tooltip } from '@/components/emcn'
|
||||||
|
import { captureEvent } from '@/lib/posthog/client'
|
||||||
import {
|
import {
|
||||||
buildCanonicalIndex,
|
buildCanonicalIndex,
|
||||||
evaluateSubBlockCondition,
|
evaluateSubBlockCondition,
|
||||||
@@ -106,6 +108,7 @@ export function Editor() {
|
|||||||
|
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const workspaceId = params.workspaceId as string
|
const workspaceId = params.workspaceId as string
|
||||||
|
const posthog = usePostHog()
|
||||||
|
|
||||||
const subBlocksRef = useRef<HTMLDivElement>(null)
|
const subBlocksRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
@@ -298,7 +301,11 @@ export function Editor() {
|
|||||||
const handleOpenDocs = useCallback(() => {
|
const handleOpenDocs = useCallback(() => {
|
||||||
const docsLink = isSubflow ? subflowConfig?.docsLink : blockConfig?.docsLink
|
const docsLink = isSubflow ? subflowConfig?.docsLink : blockConfig?.docsLink
|
||||||
window.open(docsLink || 'https://docs.sim.ai/quick-reference', '_blank', 'noopener,noreferrer')
|
window.open(docsLink || 'https://docs.sim.ai/quick-reference', '_blank', 'noopener,noreferrer')
|
||||||
}, [isSubflow, subflowConfig?.docsLink, blockConfig?.docsLink])
|
captureEvent(posthog, 'docs_opened', {
|
||||||
|
source: 'editor_button',
|
||||||
|
block_type: currentBlock?.type,
|
||||||
|
})
|
||||||
|
}, [isSubflow, subflowConfig?.docsLink, blockConfig?.docsLink, posthog, currentBlock?.type])
|
||||||
|
|
||||||
const childWorkflowId = isWorkflowBlock ? blockSubBlockValues?.workflowId : null
|
const childWorkflowId = isWorkflowBlock ? blockSubBlockValues?.workflowId : null
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { Search } from 'lucide-react'
|
import { Search } from 'lucide-react'
|
||||||
|
import { usePostHog } from 'posthog-js/react'
|
||||||
import { Button } from '@/components/emcn'
|
import { Button } from '@/components/emcn'
|
||||||
|
import { captureEvent } from '@/lib/posthog/client'
|
||||||
import {
|
import {
|
||||||
getBlocksForSidebar,
|
getBlocksForSidebar,
|
||||||
getTriggersForSidebar,
|
getTriggersForSidebar,
|
||||||
@@ -348,6 +350,7 @@ export const Toolbar = memo(
|
|||||||
triggersHeaderRef,
|
triggersHeaderRef,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const posthog = usePostHog()
|
||||||
const { filterBlocks } = usePermissionConfig()
|
const { filterBlocks } = usePermissionConfig()
|
||||||
const sandboxAllowedBlocks = useSandboxBlockConstraints()
|
const sandboxAllowedBlocks = useSandboxBlockConstraints()
|
||||||
|
|
||||||
@@ -541,8 +544,12 @@ export const Toolbar = memo(
|
|||||||
const handleViewDocumentation = useCallback(() => {
|
const handleViewDocumentation = useCallback(() => {
|
||||||
if (activeItemInfo?.docsLink) {
|
if (activeItemInfo?.docsLink) {
|
||||||
window.open(activeItemInfo.docsLink, '_blank', 'noopener,noreferrer')
|
window.open(activeItemInfo.docsLink, '_blank', 'noopener,noreferrer')
|
||||||
|
captureEvent(posthog, 'docs_opened', {
|
||||||
|
source: 'toolbar_context_menu',
|
||||||
|
block_type: activeItemInfo.type,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [activeItemInfo])
|
}, [activeItemInfo, posthog])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle clicks outside the context menu to close it
|
* Handle clicks outside the context menu to close it
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
|
|||||||
import { useChatStore } from '@/stores/chat/store'
|
import { useChatStore } from '@/stores/chat/store'
|
||||||
import { useNotificationStore } from '@/stores/notifications/store'
|
import { useNotificationStore } from '@/stores/notifications/store'
|
||||||
import type { ChatContext, PanelTab } from '@/stores/panel'
|
import type { ChatContext, PanelTab } from '@/stores/panel'
|
||||||
import { usePanelStore, useVariablesStore as usePanelVariablesStore } from '@/stores/panel'
|
import { usePanelStore } from '@/stores/panel'
|
||||||
|
import { useVariablesModalStore } from '@/stores/variables/modal'
|
||||||
import { useVariablesStore } from '@/stores/variables/store'
|
import { useVariablesStore } from '@/stores/variables/store'
|
||||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||||
import { captureBaselineSnapshot } from '@/stores/workflow-diff/utils'
|
import { captureBaselineSnapshot } from '@/stores/workflow-diff/utils'
|
||||||
@@ -205,7 +206,7 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
|
|||||||
setIsChatOpen: state.setIsChatOpen,
|
setIsChatOpen: state.setIsChatOpen,
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
const { isOpen: isVariablesOpen, setIsOpen: setVariablesOpen } = useVariablesStore(
|
const { isOpen: isVariablesOpen, setIsOpen: setVariablesOpen } = useVariablesModalStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
isOpen: state.isOpen,
|
isOpen: state.isOpen,
|
||||||
setIsOpen: state.setIsOpen,
|
setIsOpen: state.setIsOpen,
|
||||||
@@ -410,6 +411,17 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
|
|||||||
setHasHydrated(true)
|
setHasHydrated(true)
|
||||||
}, [setHasHydrated])
|
}, [setHasHydrated])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: Event) => {
|
||||||
|
const message = (e as CustomEvent<{ message: string }>).detail?.message
|
||||||
|
if (!message) return
|
||||||
|
setActiveTab('copilot')
|
||||||
|
copilotSendMessage(message)
|
||||||
|
}
|
||||||
|
window.addEventListener('mothership-send-message', handler)
|
||||||
|
return () => window.removeEventListener('mothership-send-message', handler)
|
||||||
|
}, [setActiveTab, copilotSendMessage])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles tab click events
|
* Handles tab click events
|
||||||
*/
|
*/
|
||||||
@@ -482,7 +494,7 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
|
|||||||
throw new Error('No workflow state found')
|
throw new Error('No workflow state found')
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowVariables = usePanelVariablesStore
|
const workflowVariables = useVariablesStore
|
||||||
.getState()
|
.getState()
|
||||||
.getVariablesByWorkflowId(activeWorkflowId)
|
.getVariablesByWorkflowId(activeWorkflowId)
|
||||||
|
|
||||||
|
|||||||
@@ -27,15 +27,15 @@ import {
|
|||||||
usePreventZoom,
|
usePreventZoom,
|
||||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
|
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
|
||||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||||
import { useVariablesStore as usePanelVariablesStore } from '@/stores/panel'
|
|
||||||
import {
|
import {
|
||||||
getVariablesPosition,
|
getVariablesPosition,
|
||||||
MAX_VARIABLES_HEIGHT,
|
MAX_VARIABLES_HEIGHT,
|
||||||
MAX_VARIABLES_WIDTH,
|
MAX_VARIABLES_WIDTH,
|
||||||
MIN_VARIABLES_HEIGHT,
|
MIN_VARIABLES_HEIGHT,
|
||||||
MIN_VARIABLES_WIDTH,
|
MIN_VARIABLES_WIDTH,
|
||||||
useVariablesStore,
|
useVariablesModalStore,
|
||||||
} from '@/stores/variables/store'
|
} from '@/stores/variables/modal'
|
||||||
|
import { useVariablesStore } from '@/stores/variables/store'
|
||||||
import type { Variable } from '@/stores/variables/types'
|
import type { Variable } from '@/stores/variables/types'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ export function Variables() {
|
|||||||
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
|
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
|
||||||
|
|
||||||
const { isOpen, position, width, height, setIsOpen, setPosition, setDimensions } =
|
const { isOpen, position, width, height, setIsOpen, setPosition, setDimensions } =
|
||||||
useVariablesStore(
|
useVariablesModalStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
isOpen: s.isOpen,
|
isOpen: s.isOpen,
|
||||||
position: s.position,
|
position: s.position,
|
||||||
@@ -108,7 +108,7 @@ export function Variables() {
|
|||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
const variables = usePanelVariablesStore((s) => s.variables)
|
const variables = useVariablesStore((s) => s.variables)
|
||||||
|
|
||||||
const { collaborativeUpdateVariable, collaborativeAddVariable, collaborativeDeleteVariable } =
|
const { collaborativeUpdateVariable, collaborativeAddVariable, collaborativeDeleteVariable } =
|
||||||
useCollaborativeWorkflow()
|
useCollaborativeWorkflow()
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import { useSkills } from '@/hooks/queries/skills'
|
|||||||
import { useTablesList } from '@/hooks/queries/tables'
|
import { useTablesList } from '@/hooks/queries/tables'
|
||||||
import { useWorkflowMap } from '@/hooks/queries/workflows'
|
import { useWorkflowMap } from '@/hooks/queries/workflows'
|
||||||
import { useSelectorDisplayName } from '@/hooks/use-selector-display-name'
|
import { useSelectorDisplayName } from '@/hooks/use-selector-display-name'
|
||||||
import { useVariablesStore } from '@/stores/panel'
|
import { useVariablesStore } from '@/stores/variables/store'
|
||||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||||
import { wouldCreateCycle } from '@/stores/workflows/workflow/utils'
|
import { wouldCreateCycle } from '@/stores/workflows/workflow/utils'
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useMemo } from 'react'
|
|||||||
import type { Edge } from 'reactflow'
|
import type { Edge } from 'reactflow'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||||
import type { DeploymentStatus } from '@/stores/workflows/registry/types'
|
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||||
import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
|
import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
|
||||||
|
|
||||||
@@ -16,8 +15,6 @@ export interface CurrentWorkflow {
|
|||||||
loops: Record<string, Loop>
|
loops: Record<string, Loop>
|
||||||
parallels: Record<string, Parallel>
|
parallels: Record<string, Parallel>
|
||||||
lastSaved?: number
|
lastSaved?: number
|
||||||
deploymentStatuses?: Record<string, DeploymentStatus>
|
|
||||||
needsRedeployment?: boolean
|
|
||||||
|
|
||||||
// Mode information
|
// Mode information
|
||||||
isDiffMode: boolean
|
isDiffMode: boolean
|
||||||
@@ -48,8 +45,6 @@ export function useCurrentWorkflow(): CurrentWorkflow {
|
|||||||
loops: state.loops,
|
loops: state.loops,
|
||||||
parallels: state.parallels,
|
parallels: state.parallels,
|
||||||
lastSaved: state.lastSaved,
|
lastSaved: state.lastSaved,
|
||||||
deploymentStatuses: state.deploymentStatuses,
|
|
||||||
needsRedeployment: state.needsRedeployment,
|
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -78,8 +73,6 @@ export function useCurrentWorkflow(): CurrentWorkflow {
|
|||||||
loops: activeWorkflow.loops || {},
|
loops: activeWorkflow.loops || {},
|
||||||
parallels: activeWorkflow.parallels || {},
|
parallels: activeWorkflow.parallels || {},
|
||||||
lastSaved: activeWorkflow.lastSaved,
|
lastSaved: activeWorkflow.lastSaved,
|
||||||
deploymentStatuses: activeWorkflow.deploymentStatuses,
|
|
||||||
needsRedeployment: activeWorkflow.needsRedeployment,
|
|
||||||
|
|
||||||
// Mode information - update to reflect ready state
|
// Mode information - update to reflect ready state
|
||||||
isDiffMode: hasActiveDiff && isShowingDiff,
|
isDiffMode: hasActiveDiff && isShowingDiff,
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ import { useExecutionStream } from '@/hooks/use-execution-stream'
|
|||||||
import { WorkflowValidationError } from '@/serializer'
|
import { WorkflowValidationError } from '@/serializer'
|
||||||
import { useCurrentWorkflowExecution, useExecutionStore } from '@/stores/execution'
|
import { useCurrentWorkflowExecution, useExecutionStore } from '@/stores/execution'
|
||||||
import { useNotificationStore } from '@/stores/notifications'
|
import { useNotificationStore } from '@/stores/notifications'
|
||||||
import { useVariablesStore } from '@/stores/panel'
|
|
||||||
import { useEnvironmentStore } from '@/stores/settings/environment'
|
|
||||||
import {
|
import {
|
||||||
clearExecutionPointer,
|
clearExecutionPointer,
|
||||||
consolePersistence,
|
consolePersistence,
|
||||||
@@ -45,6 +43,7 @@ import {
|
|||||||
saveExecutionPointer,
|
saveExecutionPointer,
|
||||||
useTerminalConsoleStore,
|
useTerminalConsoleStore,
|
||||||
} from '@/stores/terminal'
|
} from '@/stores/terminal'
|
||||||
|
import { useVariablesStore } from '@/stores/variables/store'
|
||||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
|
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { mergeSubblockState } from '@/stores/workflows/utils'
|
import { mergeSubblockState } from '@/stores/workflows/utils'
|
||||||
@@ -120,7 +119,6 @@ export function useWorkflowExecution() {
|
|||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
const hasHydrated = useTerminalConsoleStore((s) => s._hasHydrated)
|
const hasHydrated = useTerminalConsoleStore((s) => s._hasHydrated)
|
||||||
const getAllVariables = useEnvironmentStore((s) => s.getAllVariables)
|
|
||||||
const { getVariablesByWorkflowId, variables } = useVariablesStore(
|
const { getVariablesByWorkflowId, variables } = useVariablesStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
getVariablesByWorkflowId: s.getVariablesByWorkflowId,
|
getVariablesByWorkflowId: s.getVariablesByWorkflowId,
|
||||||
@@ -744,7 +742,6 @@ export function useWorkflowExecution() {
|
|||||||
activeWorkflowId,
|
activeWorkflowId,
|
||||||
currentWorkflow,
|
currentWorkflow,
|
||||||
toggleConsole,
|
toggleConsole,
|
||||||
getAllVariables,
|
|
||||||
getVariablesByWorkflowId,
|
getVariablesByWorkflowId,
|
||||||
setIsExecuting,
|
setIsExecuting,
|
||||||
setIsDebugging,
|
setIsDebugging,
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Loader2 } from 'lucide-react'
|
|
||||||
|
|
||||||
export default function WorkflowLoading() {
|
|
||||||
return (
|
|
||||||
<div className='flex h-full w-full flex-col overflow-hidden bg-[var(--bg)]'>
|
|
||||||
<div className='relative flex h-full w-full flex-1 items-center justify-center bg-[var(--bg)]'>
|
|
||||||
<Loader2 className='h-[20px] w-[20px] animate-spin text-[var(--text-tertiary)]' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -124,8 +124,7 @@ export async function applyAutoLayoutAndUpdateStore(
|
|||||||
try {
|
try {
|
||||||
useWorkflowStore.getState().updateLastSaved()
|
useWorkflowStore.getState().updateLastSaved()
|
||||||
|
|
||||||
const { deploymentStatuses, needsRedeployment, dragStartPosition, ...stateToSave } =
|
const { dragStartPosition, ...stateToSave } = newWorkflowState
|
||||||
newWorkflowState
|
|
||||||
|
|
||||||
const cleanedWorkflowState = {
|
const cleanedWorkflowState = {
|
||||||
...stateToSave,
|
...stateToSave,
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ import { useSearchModalStore } from '@/stores/modals/search/store'
|
|||||||
import { useNotificationStore } from '@/stores/notifications'
|
import { useNotificationStore } from '@/stores/notifications'
|
||||||
import { usePanelEditorStore } from '@/stores/panel'
|
import { usePanelEditorStore } from '@/stores/panel'
|
||||||
import { useUndoRedoStore } from '@/stores/undo-redo'
|
import { useUndoRedoStore } from '@/stores/undo-redo'
|
||||||
import { useVariablesStore } from '@/stores/variables/store'
|
import { useVariablesModalStore } from '@/stores/variables/modal'
|
||||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { getUniqueBlockName, prepareBlockState } from '@/stores/workflows/utils'
|
import { getUniqueBlockName, prepareBlockState } from '@/stores/workflows/utils'
|
||||||
@@ -265,7 +265,7 @@ const WorkflowContent = React.memo(
|
|||||||
const { fitViewToBounds, getViewportCenter } = useCanvasViewport(reactFlowInstance, {
|
const { fitViewToBounds, getViewportCenter } = useCanvasViewport(reactFlowInstance, {
|
||||||
embedded,
|
embedded,
|
||||||
})
|
})
|
||||||
const { emitCursorUpdate } = useSocket()
|
const { emitCursorUpdate, joinWorkflow, leaveWorkflow } = useSocket()
|
||||||
useDynamicHandleRefresh()
|
useDynamicHandleRefresh()
|
||||||
|
|
||||||
const workspaceId = propWorkspaceId || (params.workspaceId as string)
|
const workspaceId = propWorkspaceId || (params.workspaceId as string)
|
||||||
@@ -273,6 +273,14 @@ const WorkflowContent = React.memo(
|
|||||||
|
|
||||||
const addNotification = useNotificationStore((state) => state.addNotification)
|
const addNotification = useNotificationStore((state) => state.addNotification)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!embedded || !workflowIdParam) return
|
||||||
|
joinWorkflow(workflowIdParam)
|
||||||
|
return () => {
|
||||||
|
leaveWorkflow()
|
||||||
|
}
|
||||||
|
}, [embedded, workflowIdParam, joinWorkflow, leaveWorkflow])
|
||||||
|
|
||||||
useOAuthReturnForWorkflow(workflowIdParam)
|
useOAuthReturnForWorkflow(workflowIdParam)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -337,7 +345,7 @@ const WorkflowContent = React.memo(
|
|||||||
autoConnectRef.current = isAutoConnectEnabled
|
autoConnectRef.current = isAutoConnectEnabled
|
||||||
|
|
||||||
// Panel open states for context menu
|
// Panel open states for context menu
|
||||||
const isVariablesOpen = useVariablesStore((state) => state.isOpen)
|
const isVariablesOpen = useVariablesModalStore((state) => state.isOpen)
|
||||||
const isChatOpen = useChatStore((state) => state.isChatOpen)
|
const isChatOpen = useChatStore((state) => state.isChatOpen)
|
||||||
|
|
||||||
const snapGrid: [number, number] = useMemo(
|
const snapGrid: [number, number] = useMemo(
|
||||||
@@ -1374,7 +1382,7 @@ const WorkflowContent = React.memo(
|
|||||||
}, [router, workspaceId, workflowIdParam])
|
}, [router, workspaceId, workflowIdParam])
|
||||||
|
|
||||||
const handleContextToggleVariables = useCallback(() => {
|
const handleContextToggleVariables = useCallback(() => {
|
||||||
const { isOpen, setIsOpen } = useVariablesStore.getState()
|
const { isOpen, setIsOpen } = useVariablesModalStore.getState()
|
||||||
setIsOpen(!isOpen)
|
setIsOpen(!isOpen)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -2144,12 +2152,9 @@ const WorkflowContent = React.memo(
|
|||||||
|
|
||||||
const handleCanvasPointerMove = useCallback(
|
const handleCanvasPointerMove = useCallback(
|
||||||
(event: React.PointerEvent<Element>) => {
|
(event: React.PointerEvent<Element>) => {
|
||||||
const target = event.currentTarget as HTMLElement
|
|
||||||
const bounds = target.getBoundingClientRect()
|
|
||||||
|
|
||||||
const position = screenToFlowPosition({
|
const position = screenToFlowPosition({
|
||||||
x: event.clientX - bounds.left,
|
x: event.clientX,
|
||||||
y: event.clientY - bounds.top,
|
y: event.clientY,
|
||||||
})
|
})
|
||||||
|
|
||||||
emitCursorUpdate(position)
|
emitCursorUpdate(position)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { DELETED_WORKFLOW_LABEL } from '@/app/workspace/[workspaceId]/logs/utils
|
|||||||
import { getDisplayValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block'
|
import { getDisplayValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block'
|
||||||
import { getBlock } from '@/blocks'
|
import { getBlock } from '@/blocks'
|
||||||
import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types'
|
import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types'
|
||||||
import { useVariablesStore } from '@/stores/panel/variables/store'
|
import { useVariablesStore } from '@/stores/variables/store'
|
||||||
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
|
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
|
||||||
|
|
||||||
/** Execution status for blocks in preview mode */
|
/** Execution status for blocks in preview mode */
|
||||||
|
|||||||
@@ -343,7 +343,11 @@ export function SearchModal({
|
|||||||
'-translate-x-1/2 fixed top-[15%] z-50 w-[500px] rounded-xl border-[4px] border-black/[0.06] bg-[var(--bg)] shadow-[0_24px_80px_-16px_rgba(0,0,0,0.15)] dark:border-white/[0.06] dark:shadow-[0_24px_80px_-16px_rgba(0,0,0,0.4)]',
|
'-translate-x-1/2 fixed top-[15%] z-50 w-[500px] rounded-xl border-[4px] border-black/[0.06] bg-[var(--bg)] shadow-[0_24px_80px_-16px_rgba(0,0,0,0.15)] dark:border-white/[0.06] dark:shadow-[0_24px_80px_-16px_rgba(0,0,0,0.4)]',
|
||||||
open ? 'visible opacity-100' : 'invisible opacity-0'
|
open ? 'visible opacity-100' : 'invisible opacity-0'
|
||||||
)}
|
)}
|
||||||
style={{ left: 'calc(var(--sidebar-width) / 2 + 50%)' }}
|
style={{
|
||||||
|
left: isOnWorkflowPage
|
||||||
|
? 'calc(50% + (var(--sidebar-width) - var(--panel-width)) / 2)'
|
||||||
|
: 'calc(var(--sidebar-width) / 2 + 50%)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Command label='Search' shouldFilter={false}>
|
<Command label='Search' shouldFilter={false}>
|
||||||
<div className='mx-2 mt-2 mb-1 flex items-center gap-1.5 rounded-lg border border-[var(--border-1)] bg-[var(--surface-5)] px-2 dark:bg-[var(--surface-4)]'>
|
<div className='mx-2 mt-2 mb-1 flex items-center gap-1.5 rounded-lg border border-[var(--border-1)] bg-[var(--surface-5)] px-2 dark:bg-[var(--surface-4)]'>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Compass, MoreHorizontal } from 'lucide-react'
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useParams, usePathname, useRouter } from 'next/navigation'
|
import { useParams, usePathname, useRouter } from 'next/navigation'
|
||||||
|
import { usePostHog } from 'posthog-js/react'
|
||||||
import {
|
import {
|
||||||
Blimp,
|
Blimp,
|
||||||
Button,
|
Button,
|
||||||
@@ -39,6 +40,7 @@ import { useSession } from '@/lib/auth/auth-client'
|
|||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
import { isMacPlatform } from '@/lib/core/utils/platform'
|
import { isMacPlatform } from '@/lib/core/utils/platform'
|
||||||
import { buildFolderTree } from '@/lib/folders/tree'
|
import { buildFolderTree } from '@/lib/folders/tree'
|
||||||
|
import { captureEvent } from '@/lib/posthog/client'
|
||||||
import {
|
import {
|
||||||
START_NAV_TOUR_EVENT,
|
START_NAV_TOUR_EVENT,
|
||||||
START_WORKFLOW_TOUR_EVENT,
|
START_WORKFLOW_TOUR_EVENT,
|
||||||
@@ -315,6 +317,7 @@ export const Sidebar = memo(function Sidebar() {
|
|||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const posthog = usePostHog()
|
||||||
const { data: sessionData, isPending: sessionLoading } = useSession()
|
const { data: sessionData, isPending: sessionLoading } = useSession()
|
||||||
const { canEdit } = useUserPermissionsContext()
|
const { canEdit } = useUserPermissionsContext()
|
||||||
const { config: permissionConfig, filterBlocks } = usePermissionConfig()
|
const { config: permissionConfig, filterBlocks } = usePermissionConfig()
|
||||||
@@ -1092,10 +1095,10 @@ export const Sidebar = memo(function Sidebar() {
|
|||||||
|
|
||||||
const handleOpenHelpFromMenu = useCallback(() => setIsHelpModalOpen(true), [])
|
const handleOpenHelpFromMenu = useCallback(() => setIsHelpModalOpen(true), [])
|
||||||
|
|
||||||
const handleOpenDocs = useCallback(
|
const handleOpenDocs = useCallback(() => {
|
||||||
() => window.open('https://docs.sim.ai', '_blank', 'noopener,noreferrer'),
|
window.open('https://docs.sim.ai', '_blank', 'noopener,noreferrer')
|
||||||
[]
|
captureEvent(posthog, 'docs_opened', { source: 'help_menu' })
|
||||||
)
|
}, [posthog])
|
||||||
|
|
||||||
const handleTaskRenameBlur = useCallback(
|
const handleTaskRenameBlur = useCallback(
|
||||||
() => void taskFlyoutRename.saveRename(),
|
() => void taskFlyoutRename.saveRename(),
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Loader2 } from 'lucide-react'
|
|
||||||
|
|
||||||
export default function WorkflowsLoading() {
|
|
||||||
return (
|
|
||||||
<div className='flex h-full w-full flex-col overflow-hidden bg-[var(--bg)]'>
|
|
||||||
<div className='relative flex h-full w-full flex-1 items-center justify-center bg-[var(--bg)]'>
|
|
||||||
<Loader2 className='h-[20px] w-[20px] animate-spin text-[var(--text-tertiary)]' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -90,6 +90,7 @@ interface SocketContextType {
|
|||||||
onSelectionUpdate: (handler: (data: any) => void) => void
|
onSelectionUpdate: (handler: (data: any) => void) => void
|
||||||
onWorkflowDeleted: (handler: (data: any) => void) => void
|
onWorkflowDeleted: (handler: (data: any) => void) => void
|
||||||
onWorkflowReverted: (handler: (data: any) => void) => void
|
onWorkflowReverted: (handler: (data: any) => void) => void
|
||||||
|
onWorkflowUpdated: (handler: (data: any) => void) => void
|
||||||
onOperationConfirmed: (handler: (data: any) => void) => void
|
onOperationConfirmed: (handler: (data: any) => void) => void
|
||||||
onOperationFailed: (handler: (data: any) => void) => void
|
onOperationFailed: (handler: (data: any) => void) => void
|
||||||
}
|
}
|
||||||
@@ -118,6 +119,7 @@ const SocketContext = createContext<SocketContextType>({
|
|||||||
onSelectionUpdate: () => {},
|
onSelectionUpdate: () => {},
|
||||||
onWorkflowDeleted: () => {},
|
onWorkflowDeleted: () => {},
|
||||||
onWorkflowReverted: () => {},
|
onWorkflowReverted: () => {},
|
||||||
|
onWorkflowUpdated: () => {},
|
||||||
onOperationConfirmed: () => {},
|
onOperationConfirmed: () => {},
|
||||||
onOperationFailed: () => {},
|
onOperationFailed: () => {},
|
||||||
})
|
})
|
||||||
@@ -155,6 +157,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
|||||||
selectionUpdate?: (data: any) => void
|
selectionUpdate?: (data: any) => void
|
||||||
workflowDeleted?: (data: any) => void
|
workflowDeleted?: (data: any) => void
|
||||||
workflowReverted?: (data: any) => void
|
workflowReverted?: (data: any) => void
|
||||||
|
workflowUpdated?: (data: any) => void
|
||||||
operationConfirmed?: (data: any) => void
|
operationConfirmed?: (data: any) => void
|
||||||
operationFailed?: (data: any) => void
|
operationFailed?: (data: any) => void
|
||||||
}>({})
|
}>({})
|
||||||
@@ -334,7 +337,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
|||||||
socketInstance.on('join-workflow-success', ({ workflowId, presenceUsers }) => {
|
socketInstance.on('join-workflow-success', ({ workflowId, presenceUsers }) => {
|
||||||
isRejoiningRef.current = false
|
isRejoiningRef.current = false
|
||||||
// Ignore stale success responses from previous navigation
|
// Ignore stale success responses from previous navigation
|
||||||
if (workflowId !== urlWorkflowIdRef.current) {
|
if (urlWorkflowIdRef.current && workflowId !== urlWorkflowIdRef.current) {
|
||||||
logger.debug(`Ignoring stale join-workflow-success for ${workflowId}`)
|
logger.debug(`Ignoring stale join-workflow-success for ${workflowId}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -382,6 +385,11 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
|||||||
eventHandlers.current.workflowReverted?.(data)
|
eventHandlers.current.workflowReverted?.(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
socketInstance.on('workflow-updated', (data) => {
|
||||||
|
logger.info(`Workflow ${data.workflowId} has been updated externally`)
|
||||||
|
eventHandlers.current.workflowUpdated?.(data)
|
||||||
|
})
|
||||||
|
|
||||||
const rehydrateWorkflowStores = async (workflowId: string, workflowState: any) => {
|
const rehydrateWorkflowStores = async (workflowId: string, workflowState: any) => {
|
||||||
const [
|
const [
|
||||||
{ useOperationQueueStore },
|
{ useOperationQueueStore },
|
||||||
@@ -424,7 +432,6 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
|||||||
loops: workflowState.loops || {},
|
loops: workflowState.loops || {},
|
||||||
parallels: workflowState.parallels || {},
|
parallels: workflowState.parallels || {},
|
||||||
lastSaved: workflowState.lastSaved || Date.now(),
|
lastSaved: workflowState.lastSaved || Date.now(),
|
||||||
deploymentStatuses: workflowState.deploymentStatuses || {},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
useSubBlockStore.setState((state: any) => ({
|
useSubBlockStore.setState((state: any) => ({
|
||||||
@@ -804,6 +811,10 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
|||||||
eventHandlers.current.workflowReverted = handler
|
eventHandlers.current.workflowReverted = handler
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const onWorkflowUpdated = useCallback((handler: (data: any) => void) => {
|
||||||
|
eventHandlers.current.workflowUpdated = handler
|
||||||
|
}, [])
|
||||||
|
|
||||||
const onOperationConfirmed = useCallback((handler: (data: any) => void) => {
|
const onOperationConfirmed = useCallback((handler: (data: any) => void) => {
|
||||||
eventHandlers.current.operationConfirmed = handler
|
eventHandlers.current.operationConfirmed = handler
|
||||||
}, [])
|
}, [])
|
||||||
@@ -837,6 +848,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
|||||||
onSelectionUpdate,
|
onSelectionUpdate,
|
||||||
onWorkflowDeleted,
|
onWorkflowDeleted,
|
||||||
onWorkflowReverted,
|
onWorkflowReverted,
|
||||||
|
onWorkflowUpdated,
|
||||||
onOperationConfirmed,
|
onOperationConfirmed,
|
||||||
onOperationFailed,
|
onOperationFailed,
|
||||||
}),
|
}),
|
||||||
@@ -864,6 +876,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
|||||||
onSelectionUpdate,
|
onSelectionUpdate,
|
||||||
onWorkflowDeleted,
|
onWorkflowDeleted,
|
||||||
onWorkflowReverted,
|
onWorkflowReverted,
|
||||||
|
onWorkflowUpdated,
|
||||||
onOperationConfirmed,
|
onOperationConfirmed,
|
||||||
onOperationFailed,
|
onOperationFailed,
|
||||||
]
|
]
|
||||||
|
|||||||
65
apps/sim/background/lifecycle-email.ts
Normal file
65
apps/sim/background/lifecycle-email.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { db } from '@sim/db'
|
||||||
|
import { user } from '@sim/db/schema'
|
||||||
|
import { createLogger } from '@sim/logger'
|
||||||
|
import { task } from '@trigger.dev/sdk'
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
import { getEmailSubject, renderOnboardingFollowupEmail } from '@/components/emails'
|
||||||
|
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
|
||||||
|
import { checkEnterprisePlan } from '@/lib/billing/subscriptions/utils'
|
||||||
|
import { sendEmail } from '@/lib/messaging/email/mailer'
|
||||||
|
import { getPersonalEmailFrom } from '@/lib/messaging/email/utils'
|
||||||
|
import { LIFECYCLE_EMAIL_TASK_ID, type LifecycleEmailType } from '@/lib/messaging/lifecycle'
|
||||||
|
|
||||||
|
const logger = createLogger('LifecycleEmail')
|
||||||
|
|
||||||
|
interface LifecycleEmailParams {
|
||||||
|
userId: string
|
||||||
|
type: LifecycleEmailType
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendLifecycleEmail({ userId, type }: LifecycleEmailParams): Promise<void> {
|
||||||
|
const [userData] = await db.select().from(user).where(eq(user.id, userId)).limit(1)
|
||||||
|
|
||||||
|
if (!userData?.email) {
|
||||||
|
logger.warn('[lifecycle-email] User not found or has no email', { userId, type })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscription = await getHighestPrioritySubscription(userId)
|
||||||
|
if (checkEnterprisePlan(subscription)) {
|
||||||
|
logger.info('[lifecycle-email] Skipping lifecycle email for enterprise user', { userId, type })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { from, replyTo } = getPersonalEmailFrom()
|
||||||
|
|
||||||
|
let html: string
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'onboarding-followup':
|
||||||
|
html = await renderOnboardingFollowupEmail(userData.name || undefined)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
logger.warn('[lifecycle-email] Unknown lifecycle email type', { type })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendEmail({
|
||||||
|
to: userData.email,
|
||||||
|
subject: getEmailSubject(type),
|
||||||
|
html,
|
||||||
|
from,
|
||||||
|
replyTo,
|
||||||
|
emailType: 'notifications',
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info('[lifecycle-email] Sent lifecycle email', { userId, type })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const lifecycleEmailTask = task({
|
||||||
|
id: LIFECYCLE_EMAIL_TASK_ID,
|
||||||
|
retry: { maxAttempts: 2 },
|
||||||
|
run: async (params: LifecycleEmailParams) => {
|
||||||
|
await sendLifecycleEmail(params)
|
||||||
|
},
|
||||||
|
})
|
||||||
151
apps/sim/blocks/blocks/credential.ts
Normal file
151
apps/sim/blocks/blocks/credential.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { CredentialIcon } from '@/components/icons'
|
||||||
|
import { getServiceConfigByProviderId } from '@/lib/oauth/utils'
|
||||||
|
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
|
||||||
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { fetchWorkspaceCredentialList, workspaceCredentialKeys } from '@/hooks/queries/credentials'
|
||||||
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
|
|
||||||
|
interface CredentialBlockOutput {
|
||||||
|
success: boolean
|
||||||
|
output: {
|
||||||
|
credentialId: string
|
||||||
|
displayName: string
|
||||||
|
providerId: string
|
||||||
|
credentials: Array<{
|
||||||
|
credentialId: string
|
||||||
|
displayName: string
|
||||||
|
providerId: string
|
||||||
|
}>
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CredentialBlock: BlockConfig<CredentialBlockOutput> = {
|
||||||
|
type: 'credential',
|
||||||
|
name: 'Credential',
|
||||||
|
description: 'Select or list OAuth credentials',
|
||||||
|
longDescription:
|
||||||
|
'Select an OAuth credential once and pipe its ID into any downstream block that requires authentication, or list all OAuth credentials in the workspace for iteration. No secrets are ever exposed — only credential IDs and metadata.',
|
||||||
|
bestPractices: `
|
||||||
|
- Use "Select Credential" to define an OAuth credential once and reference <CredentialBlock.credentialId> in multiple downstream blocks instead of repeating credential IDs.
|
||||||
|
- Use "List Credentials" with a ForEach loop to iterate over all OAuth accounts (e.g. all Gmail accounts).
|
||||||
|
- Use the Provider filter to narrow results to specific services (e.g. Gmail, Slack).
|
||||||
|
- The outputs are credential ID references, not secret values — they are safe to log and inspect.
|
||||||
|
- To switch credentials across environments, replace the single Credential block rather than updating every downstream block.
|
||||||
|
`,
|
||||||
|
docsLink: 'https://docs.sim.ai/blocks/credential',
|
||||||
|
bgColor: '#6366F1',
|
||||||
|
icon: CredentialIcon,
|
||||||
|
category: 'blocks',
|
||||||
|
subBlocks: [
|
||||||
|
{
|
||||||
|
id: 'operation',
|
||||||
|
title: 'Operation',
|
||||||
|
type: 'dropdown',
|
||||||
|
options: [
|
||||||
|
{ label: 'Select Credential', id: 'select' },
|
||||||
|
{ label: 'List Credentials', id: 'list' },
|
||||||
|
],
|
||||||
|
value: () => 'select',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'providerFilter',
|
||||||
|
title: 'Provider',
|
||||||
|
type: 'dropdown',
|
||||||
|
multiSelect: true,
|
||||||
|
options: [],
|
||||||
|
condition: { field: 'operation', value: 'list' },
|
||||||
|
fetchOptions: async () => {
|
||||||
|
const workspaceId = useWorkflowRegistry.getState().hydration.workspaceId
|
||||||
|
if (!workspaceId) return []
|
||||||
|
|
||||||
|
const credentials = await getQueryClient().fetchQuery({
|
||||||
|
queryKey: workspaceCredentialKeys.list(workspaceId),
|
||||||
|
queryFn: () => fetchWorkspaceCredentialList(workspaceId),
|
||||||
|
staleTime: 60 * 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const options: Array<{ label: string; id: string }> = []
|
||||||
|
|
||||||
|
for (const cred of credentials) {
|
||||||
|
if (cred.type === 'oauth' && cred.providerId && !seen.has(cred.providerId)) {
|
||||||
|
seen.add(cred.providerId)
|
||||||
|
const serviceConfig = getServiceConfigByProviderId(cred.providerId)
|
||||||
|
options.push({ label: serviceConfig?.name ?? cred.providerId, id: cred.providerId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
|
},
|
||||||
|
fetchOptionById: async (_blockId: string, optionId: string) => {
|
||||||
|
const serviceConfig = getServiceConfigByProviderId(optionId)
|
||||||
|
const label = serviceConfig?.name ?? optionId
|
||||||
|
return { label, id: optionId }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'credential',
|
||||||
|
title: 'Credential',
|
||||||
|
type: 'oauth-input',
|
||||||
|
required: { field: 'operation', value: 'select' },
|
||||||
|
mode: 'basic',
|
||||||
|
placeholder: 'Select a credential',
|
||||||
|
canonicalParamId: 'credentialId',
|
||||||
|
condition: { field: 'operation', value: 'select' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'manualCredential',
|
||||||
|
title: 'Credential ID',
|
||||||
|
type: 'short-input',
|
||||||
|
required: { field: 'operation', value: 'select' },
|
||||||
|
mode: 'advanced',
|
||||||
|
placeholder: 'Enter credential ID',
|
||||||
|
canonicalParamId: 'credentialId',
|
||||||
|
condition: { field: 'operation', value: 'select' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tools: {
|
||||||
|
access: [],
|
||||||
|
},
|
||||||
|
inputs: {
|
||||||
|
operation: { type: 'string', description: "'select' or 'list'" },
|
||||||
|
credentialId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The OAuth credential ID to resolve (select operation)',
|
||||||
|
},
|
||||||
|
providerFilter: {
|
||||||
|
type: 'json',
|
||||||
|
description:
|
||||||
|
'Array of OAuth provider IDs to filter by (e.g. ["google-email", "slack"]). Leave empty to return all OAuth credentials.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outputs: {
|
||||||
|
credentialId: {
|
||||||
|
type: 'string',
|
||||||
|
description: "Credential ID — pipe into other blocks' credential fields",
|
||||||
|
condition: { field: 'operation', value: 'select' },
|
||||||
|
},
|
||||||
|
displayName: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Human-readable name of the credential',
|
||||||
|
condition: { field: 'operation', value: 'select' },
|
||||||
|
},
|
||||||
|
providerId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'OAuth provider ID (e.g. google-email, slack)',
|
||||||
|
condition: { field: 'operation', value: 'select' },
|
||||||
|
},
|
||||||
|
credentials: {
|
||||||
|
type: 'json',
|
||||||
|
description:
|
||||||
|
'Array of OAuth credential objects, each with credentialId, displayName, and providerId',
|
||||||
|
condition: { field: 'operation', value: 'list' },
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Number of credentials returned',
|
||||||
|
condition: { field: 'operation', value: 'list' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import { ClerkBlock } from '@/blocks/blocks/clerk'
|
|||||||
import { CloudflareBlock } from '@/blocks/blocks/cloudflare'
|
import { CloudflareBlock } from '@/blocks/blocks/cloudflare'
|
||||||
import { ConditionBlock } from '@/blocks/blocks/condition'
|
import { ConditionBlock } from '@/blocks/blocks/condition'
|
||||||
import { ConfluenceBlock, ConfluenceV2Block } from '@/blocks/blocks/confluence'
|
import { ConfluenceBlock, ConfluenceV2Block } from '@/blocks/blocks/confluence'
|
||||||
|
import { CredentialBlock } from '@/blocks/blocks/credential'
|
||||||
import { CursorBlock, CursorV2Block } from '@/blocks/blocks/cursor'
|
import { CursorBlock, CursorV2Block } from '@/blocks/blocks/cursor'
|
||||||
import { DatabricksBlock } from '@/blocks/blocks/databricks'
|
import { DatabricksBlock } from '@/blocks/blocks/databricks'
|
||||||
import { DatadogBlock } from '@/blocks/blocks/datadog'
|
import { DatadogBlock } from '@/blocks/blocks/datadog'
|
||||||
@@ -243,6 +244,7 @@ export const registry: Record<string, BlockConfig> = {
|
|||||||
clay: ClayBlock,
|
clay: ClayBlock,
|
||||||
clerk: ClerkBlock,
|
clerk: ClerkBlock,
|
||||||
condition: ConditionBlock,
|
condition: ConditionBlock,
|
||||||
|
credential: CredentialBlock,
|
||||||
confluence: ConfluenceBlock,
|
confluence: ConfluenceBlock,
|
||||||
confluence_v2: ConfluenceV2Block,
|
confluence_v2: ConfluenceV2Block,
|
||||||
cursor: CursorBlock,
|
cursor: CursorBlock,
|
||||||
|
|||||||
@@ -421,15 +421,11 @@ export interface SubBlockConfig {
|
|||||||
triggerId?: string
|
triggerId?: string
|
||||||
// Dropdown/Combobox: Function to fetch options dynamically
|
// Dropdown/Combobox: Function to fetch options dynamically
|
||||||
// Works with both 'dropdown' (select-only) and 'combobox' (editable with expression support)
|
// Works with both 'dropdown' (select-only) and 'combobox' (editable with expression support)
|
||||||
fetchOptions?: (
|
fetchOptions?: (blockId: string) => Promise<Array<{ label: string; id: string }>>
|
||||||
blockId: string,
|
|
||||||
subBlockId: string
|
|
||||||
) => Promise<Array<{ label: string; id: string }>>
|
|
||||||
// Dropdown/Combobox: Function to fetch a single option's label by ID (for hydration)
|
// Dropdown/Combobox: Function to fetch a single option's label by ID (for hydration)
|
||||||
// Called when component mounts with a stored value to display the correct label before options load
|
// Called when component mounts with a stored value to display the correct label before options load
|
||||||
fetchOptionById?: (
|
fetchOptionById?: (
|
||||||
blockId: string,
|
blockId: string,
|
||||||
subBlockId: string,
|
|
||||||
optionId: string
|
optionId: string
|
||||||
) => Promise<{ label: string; id: string } | null>
|
) => Promise<{ label: string; id: string } | null>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -267,3 +267,24 @@ export const baseStyles = {
|
|||||||
margin: '8px 0',
|
margin: '8px 0',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Styles for plain personal emails (no branding, no EmailLayout) */
|
||||||
|
export const plainEmailStyles = {
|
||||||
|
body: {
|
||||||
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
margin: '0',
|
||||||
|
padding: '0',
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
maxWidth: '560px',
|
||||||
|
margin: '40px auto',
|
||||||
|
padding: '0 24px',
|
||||||
|
},
|
||||||
|
p: {
|
||||||
|
fontSize: '15px',
|
||||||
|
lineHeight: '1.6',
|
||||||
|
color: '#1a1a1a',
|
||||||
|
margin: '0 0 16px',
|
||||||
|
},
|
||||||
|
} as const
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { baseStyles, colors, spacing, typography } from './base'
|
export { baseStyles, colors, plainEmailStyles, spacing, typography } from './base'
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user