diff --git a/.env.example b/.env.example index cefe550f9c..6aa389ff74 100644 --- a/.env.example +++ b/.env.example @@ -97,6 +97,7 @@ GOOGLE_APPLICATION_CREDENTIALS= # Key to send email MAILGUN_API_KEY= +# MAILGUN_URL= # Redis (optional) #NANGO_REDIS_URL= diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index df37f11b90..95017accff 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -6,6 +6,7 @@ on: - master - staging/** pull_request: + merge_group: jobs: build-image: diff --git a/.github/workflows/build-images.yaml b/.github/workflows/build-images.yaml index 4d8b358503..2f61d6059e 100644 --- a/.github/workflows/build-images.yaml +++ b/.github/workflows/build-images.yaml @@ -5,6 +5,7 @@ on: - master - staging/** pull_request: + merge_group: concurrency: group: pulls/${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/cli-verification.yaml b/.github/workflows/cli-verification.yaml index c2cce9dc64..3629ea8125 100644 --- a/.github/workflows/cli-verification.yaml +++ b/.github/workflows/cli-verification.yaml @@ -6,6 +6,7 @@ on: - master - staging/** pull_request: + merge_group: concurrency: group: verify-cli-${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 11f5acecec..9df7b85616 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -53,9 +53,6 @@ jobs: with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Push tag - run: | - docker buildx imagetools create nangohq/nango-jobs:${{ github.sha }} --tag nangohq/nango-jobs:${{ inputs.stage }} - name: Deploy jobs run: | SERVICE_ID=${{ fromJson('{ production: "srv-clvvtdug1b2c73cklps0", staging: "srv-clthttda73kc73ejflg0" }')[inputs.stage] }} diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 06879bf225..dc63bc0d48 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -6,6 +6,7 @@ on: - master - staging/** pull_request: + merge_group: concurrency: group: docker-${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/lint-pr.yaml b/.github/workflows/lint-pr.yaml index dbdf7cbc91..629e986e75 100644 --- a/.github/workflows/lint-pr.yaml +++ b/.github/workflows/lint-pr.yaml @@ -6,6 +6,7 @@ on: - opened - edited - synchronize + merge_group: permissions: pull-requests: read diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 766504c57b..d19e6e8e9b 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -6,6 +6,7 @@ on: - master - staging/** pull_request: + merge_group: jobs: lint-code: diff --git a/.github/workflows/tests-cli-windows.yaml b/.github/workflows/tests-cli-windows.yaml index 10c576bc60..d8989a6006 100644 --- a/.github/workflows/tests-cli-windows.yaml +++ b/.github/workflows/tests-cli-windows.yaml @@ -6,6 +6,7 @@ on: - master - staging/** pull_request: + merge_group: concurrency: group: tests-windows/${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/tests-node-client.yaml b/.github/workflows/tests-node-client.yaml index 7fdcef29dd..01a1d576ad 100644 --- a/.github/workflows/tests-node-client.yaml +++ b/.github/workflows/tests-node-client.yaml @@ -6,6 +6,7 @@ on: - master - staging/** pull_request: + merge_group: concurrency: group: tests-node-client/${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index cf40489b27..dd53b37a92 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -6,6 +6,7 @@ on: - master - staging/** pull_request: + merge_group: concurrency: group: tests-${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 18b4f19ebf..2cdd24f659 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -6,6 +6,7 @@ on: - master - staging/** pull_request: + merge_group: concurrency: group: validation-${{ github.event.pull_request.number || github.ref }} diff --git a/README.md b/README.md index c6b7ee7d60..e9c2157bb2 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,11 @@ Nango is a single API to interact with all other external APIs. It should be the -## 👩‍💻 Sample code +# 📺 Demo video + +[![what-is-nango](/docs-v2/images/video-thumbnail.png)](https://www.youtube.com/watch?v=pvUpbi04IjQ) + +# 👩‍💻 Sample code Initiate a new OAuth flow from your frontend: @@ -47,7 +51,7 @@ nango.listRecords({ }); ``` -# 👩🏻‍🔧 Choose your level of service +# 👩🏻‍🔧 Pre-built and custom integrations Nango's flexibility ensures it supports any API integration: diff --git a/docs-v2/images/overview.png b/docs-v2/images/overview.png index 75d28e525b..5c30095e93 100644 Binary files a/docs-v2/images/overview.png and b/docs-v2/images/overview.png differ diff --git a/docs-v2/images/video-thumbnail.png b/docs-v2/images/video-thumbnail.png new file mode 100644 index 0000000000..82fac3a285 Binary files /dev/null and b/docs-v2/images/video-thumbnail.png differ diff --git a/docs-v2/introduction.mdx b/docs-v2/introduction.mdx index dabbdf8781..a4b4ebd51d 100644 --- a/docs-v2/introduction.mdx +++ b/docs-v2/introduction.mdx @@ -10,6 +10,19 @@ Nango is a single API to interact with all other external APIs. It should be the +# 📺 Demo video + +
+ +
+ # 👩‍💻 Sample code Initiate a new OAuth flow from your frontend: @@ -53,14 +66,6 @@ Nango's flexibility ensures it supports any API integration: But remember, Nango can work with **any API and any use-case**! -# 📺 Demo video - - - -
- -
- # 🚀 Get started Sign up for free and try the interactive demo: diff --git a/packages/connect-ui/src/views/Go.tsx b/packages/connect-ui/src/views/Go.tsx index e0adbaa5fb..605c101fe5 100644 --- a/packages/connect-ui/src/views/Go.tsx +++ b/packages/connect-ui/src/views/Go.tsx @@ -347,7 +347,7 @@ export const Go: React.FC = () => { {shouldAutoTrigger && (
We will connect you to {provider.display_name} - {provider.auth_mode === 'OAUTH2' && ". A popup will open, please make sure your browser don't block popup"} + {provider.auth_mode === 'OAUTH2' && ". A popup will open, please make sure your browser doesn't block popups"}
)}
diff --git a/packages/logs/lib/models/helpers.ts b/packages/logs/lib/models/helpers.ts index 38e4e98db1..ff8199379f 100644 --- a/packages/logs/lib/models/helpers.ts +++ b/packages/logs/lib/models/helpers.ts @@ -138,5 +138,5 @@ export const operationTypeToMessage: Record = { 'sync:run': 'Sync execution', 'sync:unpause': 'Sync schedule started', 'webhook:incoming': 'Received a webhook', - 'webhook:outgoing': 'Forwarding Webhook' + 'webhook:forward': 'Forwarding Webhook' }; diff --git a/packages/logs/lib/models/insights.ts b/packages/logs/lib/models/insights.ts index 43395fa6f9..5f4eebf465 100644 --- a/packages/logs/lib/models/insights.ts +++ b/packages/logs/lib/models/insights.ts @@ -1,17 +1,24 @@ import type { estypes } from '@elastic/elasticsearch'; import { indexMessages } from '../es/schema.js'; import { client } from '../es/client.js'; -import type { InsightsHistogramEntry } from '@nangohq/types'; +import type { ConcatOperationList, InsightsHistogramEntry, OperationList } from '@nangohq/types'; -export async function retrieveInsights(opts: { accountId: number; environmentId: number; type: string }) { +export async function retrieveInsights(opts: { accountId: number; environmentId: number; type: OperationList['type'] | ConcatOperationList }) { const query: estypes.QueryDslQueryContainer = { bool: { - must: [{ term: { accountId: opts.accountId } }, { term: { environmentId: opts.environmentId } }, { term: { 'operation.type': opts.type } }], + must: [{ term: { accountId: opts.accountId } }, { term: { environmentId: opts.environmentId } }], must_not: { exists: { field: 'parentId' } }, should: [] } }; + if (opts.type.includes(':')) { + const split = opts.type.split(':'); + (query.bool!.must! as estypes.QueryDslQueryContainer[]).push({ term: { 'operation.type': split[0] } }, { term: { 'operation.action': split[1] } }); + } else { + (query.bool!.must! as estypes.QueryDslQueryContainer[]).push({ term: { 'operation.type': opts.type } }); + } + const res = await client.search< never, { diff --git a/packages/server/lib/clients/email.client.ts b/packages/server/lib/clients/email.client.ts index d8ea1c8db4..c55e492285 100644 --- a/packages/server/lib/clients/email.client.ts +++ b/packages/server/lib/clients/email.client.ts @@ -1,14 +1,15 @@ import formData from 'form-data'; import Mailgun from 'mailgun.js'; import { getLogger } from '@nangohq/utils'; +import type Client from 'mailgun.js/client'; const logger = getLogger('Server.EmailClient'); export class EmailClient { private static instance: EmailClient | undefined; - private client: any; + private client: Client; - private constructor(config: { username: string; key: string }) { + private constructor(config: { username: string; key: string; url?: string }) { const mailgun = new Mailgun(formData); this.client = mailgun.client(config); } @@ -17,7 +18,8 @@ export class EmailClient { if (!EmailClient.instance) { EmailClient.instance = new EmailClient({ username: 'api', - key: process.env['MAILGUN_API_KEY'] || 'EMPTY' + key: process.env['MAILGUN_API_KEY'] || 'EMPTY', + url: process.env['MAILGUN_URL'] || '' }); } return EmailClient.instance; @@ -31,6 +33,7 @@ export class EmailClient { logger.info(html); return; } + return this.client.messages.create('email.nango.dev', { from: 'Nango ', to: [email], diff --git a/packages/server/lib/controllers/v1/logs/postInsights.ts b/packages/server/lib/controllers/v1/logs/postInsights.ts index 516e4fe4c6..6d6e5731c5 100644 --- a/packages/server/lib/controllers/v1/logs/postInsights.ts +++ b/packages/server/lib/controllers/v1/logs/postInsights.ts @@ -6,7 +6,7 @@ import { envs, modelOperations } from '@nangohq/logs'; const validation = z .object({ - type: z.enum(['sync', 'action', 'proxy', 'webhook_external']) + type: z.enum(['sync', 'action', 'proxy', 'webhook:incoming']) }) .strict(); diff --git a/packages/types/lib/logs/api.ts b/packages/types/lib/logs/api.ts index efbbac9382..9bfbbb1eea 100644 --- a/packages/types/lib/logs/api.ts +++ b/packages/types/lib/logs/api.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-redundant-type-constituents */ import type { Endpoint } from '../api'; +import type { PickFromUnion } from '../utils'; import type { MessageRow, MessageState, OperationList, OperationRow } from './messages'; type Concat = T extends { action: string } ? `${T['type']}:${T['action']}` : never; @@ -81,7 +82,7 @@ export type PostInsights = Endpoint<{ Path: '/api/v1/logs/insights'; Querystring: { env: string }; Body: { - type: 'action' | 'sync' | 'proxy' | 'webhook_external'; + type: PickFromUnion; }; Success: { data: { diff --git a/packages/types/lib/logs/messages.ts b/packages/types/lib/logs/messages.ts index 6b3ba24417..bc798da161 100644 --- a/packages/types/lib/logs/messages.ts +++ b/packages/types/lib/logs/messages.ts @@ -53,7 +53,7 @@ export interface OperationAdmin { } export interface OperationWebhook { type: 'webhook'; - action: 'incoming' | 'outgoing'; + action: 'incoming' | 'forward'; } export interface OperationDeploy { type: 'deploy'; diff --git a/packages/types/lib/utils.ts b/packages/types/lib/utils.ts index f8078de42c..69bd2ffff7 100644 --- a/packages/types/lib/utils.ts +++ b/packages/types/lib/utils.ts @@ -9,3 +9,6 @@ export interface Logger { debug: LogMethod; child: (...message: any[]) => Logger; } + +type ValidateSelection = U extends T ? U : never; +export type PickFromUnion = ValidateSelection; diff --git a/packages/utils/lib/environment/parse.ts b/packages/utils/lib/environment/parse.ts index c235231236..35b8262810 100644 --- a/packages/utils/lib/environment/parse.ts +++ b/packages/utils/lib/environment/parse.ts @@ -78,6 +78,7 @@ export const ENVS = z.object({ // Mailgun MAILGUN_API_KEY: z.string().optional(), + MAILGUN_URL: z.string().url().optional(), // Postgres NANGO_DATABASE_URL: z.string().url().optional(), diff --git a/packages/webapp/src/components/CopyText.tsx b/packages/webapp/src/components/CopyText.tsx new file mode 100644 index 0000000000..9becdfae90 --- /dev/null +++ b/packages/webapp/src/components/CopyText.tsx @@ -0,0 +1,67 @@ +import { useState, useEffect, useRef } from 'react'; +import type { ClassValue } from 'clsx'; +import { Tooltip, TooltipContent, TooltipTrigger } from './ui/Tooltip'; +import { IconCopy } from '@tabler/icons-react'; +import { cn } from '../utils/utils'; + +export const CopyText: React.FC<{ text: string; showOnHover?: boolean; className?: ClassValue }> = ({ text, showOnHover, className }) => { + const [tooltipText, setTooltipText] = useState('Copy'); + const triggerRef = useRef(null); + + const copyToClipboard = async (e: React.MouseEvent) => { + try { + e.stopPropagation(); + e.preventDefault(); + await navigator.clipboard.writeText(text); + setTooltipText('Copied'); + } catch (err) { + //this should never happen! + console.error('Failed to copy:', err); + } + }; + + useEffect(() => { + const timer = setTimeout(() => { + setTooltipText('Copy'); + }, 1000); + + return () => { + clearTimeout(timer); + }; + }, [tooltipText]); + + return ( + + + + + { + // Radix assume a click on a tooltip should close it + // https://github.com/radix-ui/primitives/issues/2029 + if (event.target === triggerRef.current || (event.target as any).parentNode === triggerRef.current) { + event.preventDefault(); + } + }} + > + {tooltipText} + + + ); +}; diff --git a/packages/webapp/src/pages/Connection/Authorization.tsx b/packages/webapp/src/pages/Connection/Authorization.tsx index 85946b52ea..3a3f036e5f 100644 --- a/packages/webapp/src/pages/Connection/Authorization.tsx +++ b/packages/webapp/src/pages/Connection/Authorization.tsx @@ -4,7 +4,6 @@ import PrismPlus from '../../components/ui/prism/PrismPlus'; import type { ActiveLog, ApiConnectionFull, ApiEndUser } from '@nangohq/types'; import { formatDateToShortUSFormat } from '../../utils/utils'; import SecretInput from '../../components/ui/input/SecretInput'; -import { CopyButton } from '../../components/ui/button/CopyButton'; import TagsInput from '../../components/ui/input/TagsInput'; import type React from 'react'; import { apiRefreshConnection } from '../../hooks/useConnections'; @@ -15,6 +14,7 @@ import { mutate } from 'swr'; import { getLogsUrl } from '../../utils/logs'; import { Info } from '../../components/Info'; import { Link } from 'react-router-dom'; +import { CopyText } from '../../components/CopyText'; interface AuthorizationProps { connection: ApiConnectionFull; @@ -58,6 +58,13 @@ export const Authorization: React.FC = ({ connection, errorL
)}
+
+ Connection ID +
+ +
+
+ {endUser?.id && (
User ID @@ -110,13 +117,6 @@ export const Authorization: React.FC = ({ connection, errorL {formatDateToShortUSFormat(connection.credentials.expires_at.toString())}
)} -
- Connection ID -
- {connection.connection_id} - -
-
{connection.credentials && diff --git a/packages/webapp/src/pages/Connection/List.tsx b/packages/webapp/src/pages/Connection/List.tsx index 4291d11a6e..bcbe849ab0 100644 --- a/packages/webapp/src/pages/Connection/List.tsx +++ b/packages/webapp/src/pages/Connection/List.tsx @@ -32,6 +32,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, DropdownMenuIte import { IconChevronDown } from '@tabler/icons-react'; import { useToast } from '../../hooks/useToast'; import type { ApiConnectionSimple } from '@nangohq/types'; +import { CopyText } from '../../components/CopyText'; const defaultFilter = ['all']; const filterErrors = [ @@ -83,6 +84,14 @@ const columns: ColumnDef[] = [ ); } }, + { + accessorKey: 'connection_id', + header: 'Connection ID', + size: 130, + cell: ({ row }) => { + return ; + } + }, { accessorKey: 'created_at', header: 'Created', diff --git a/packages/webapp/src/pages/Homepage/Show.tsx b/packages/webapp/src/pages/Homepage/Show.tsx index 55a19d3eb2..1db610e5b0 100644 --- a/packages/webapp/src/pages/Homepage/Show.tsx +++ b/packages/webapp/src/pages/Homepage/Show.tsx @@ -50,9 +50,9 @@ export const Homepage: React.FC = () => {
{globalEnv.features.scripts && ( No sync executions in the last 14 days.{' '} @@ -65,9 +65,9 @@ export const Homepage: React.FC = () => { )} {globalEnv.features.scripts && ( No action executions in the last 14 days.{' '} @@ -79,12 +79,12 @@ export const Homepage: React.FC = () => { /> )} - No proxy calls in the last 14 days.{' '} + No proxy requests sent in the last 14 days.{' '} Learn more @@ -93,12 +93,12 @@ export const Homepage: React.FC = () => { /> {globalEnv.features.scripts && ( - No external webhooks received in the last 14 days.{' '} + No webhook executions in the last 14 days.{' '} Learn more diff --git a/packages/webhooks/lib/forward.ts b/packages/webhooks/lib/forward.ts index 811f9a313e..88e56a3e45 100644 --- a/packages/webhooks/lib/forward.ts +++ b/packages/webhooks/lib/forward.ts @@ -30,7 +30,7 @@ export const forwardWebhook = async ({ } const logCtx = await logContextGetter.create( - { operation: { type: 'webhook', action: 'outgoing' }, expiresAt: new Date(Date.now() + 15 * 60 * 1000).toISOString() }, + { operation: { type: 'webhook', action: 'forward' }, expiresAt: new Date(Date.now() + 15 * 60 * 1000).toISOString() }, { account, environment, @@ -60,7 +60,7 @@ export const forwardWebhook = async ({ incomingHeaders: webhookOriginalHeaders }); - result ? await logCtx.success() : await logCtx.failed(); + result.isOk() ? await logCtx.success() : await logCtx.failed(); return; } @@ -79,7 +79,7 @@ export const forwardWebhook = async ({ incomingHeaders: webhookOriginalHeaders }); - if (!result) { + if (result.isErr()) { success = false; } }