Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

playwright-report/
playwright-report/
test-results/
39 changes: 31 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Airbyte Embedded Widget

A lightweight, embeddable widget for integrating Airbyte's data synchronization capabilities into your application.
An embeddable widget for integrating Airbyte's data synchronization capabilities into your application.

## Features

Expand Down Expand Up @@ -34,7 +34,7 @@ pnpm install
pnpm dev
```

The demo server will start at `https://localhost:3000`. You may need to accept the self-signed certificate warning in your browser.
The demo server will start at `https://localhost:3003`. You may need to accept the self-signed certificate warning in your browser.

## Building the Library

Expand All @@ -48,15 +48,34 @@ The built files will be in the `dist` directory.

## Usage

To use this library, you will first need to fetch an Airbyte Embedded token. You should do this in your server, though if you are simply testing this locally, you can use:

```
curl --location '$AIRBYTE_BASE_URL/api/public/v1/embedded/widget' \
--header 'Content-Type: application/json' \
--header 'Accept: text/plain' \
--data '{
"workspaceId": "$CUSTOMER_WORKSPACE_ID",
"allowedOrigin": "$EMBEDDING_ORIGIN"
}'
```

`AIRBYTE_BASE_URL`: where your Airbyte instance is deployed
`CUSTOMER_WORKSPACE_ID`: the workspace you have associated with this customer
`EMBEDDING_ORIGIN` here refers to where you are adding this widget to. It will be used to generate an `allowedOrigin` parameter for the webapp to open communications with the widget. If you are running the widget locally using our demo app, the allowed origin should be `https://localhost:3003`, for example.

You can also, optionally, send an `externalUserId` in your request and we will attach it to the jwt encoded within the Airbyte Embedded token for provenance purposes.

Embedded tokens are short-lived (15-minutes) and only allow an end user to create and edit Airbyte source configurations within the workspace you have created for them.

These values should be passed to where you initializze the widget like so:

```typescript
import { EmbeddedWidget } from "airbyte-embedded-widget";

// Initialize the widget
const widget = new EmbeddedWidget({
organizationId: "your_organization_id",
workspaceId: "your_customer_workspace_id",
token: "your_api_token",
// Additional configuration options
token: res.token,
});

// Mount the widget
Expand All @@ -75,9 +94,13 @@ The demo application in the `/demo` directory shows a complete example of integr
To configure the demo, create a `.env` file in the `/demo` directory:

```env
VITE_API_TOKEN=your_api_token_here
VITE_AB_EMBEDDED_TOKEN=""
```

You can fetch an Airbyte Embedded token using the curl request example above.

You can then run the demo app using `pnpm dev` and access a very simple example UI at `https://localhost:3003` in your browser.

## Publishing

This repository is configured to publish to npmjs.org whenever:
Expand All @@ -91,7 +114,7 @@ To create a new version, you can use the following command:
pnpm version <major|minor|patch>
```

and then push those changes to the main branch. Don't forget the tags!
and then push those changes to the main branch. Don't forget the tags!

```bash
git push origin main --tags && git push
Expand Down
6 changes: 1 addition & 5 deletions demo/.env.test
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
VITE_AB_BASE_URL="https://test.airbyte.com"
VITE_AB_API_CLIENT_ID="test-client-id"
VITE_AB_API_CLIENT_SECRET="test-client-secret"
VITE_AB_WORKSPACE_ID="test-workspace"
VITE_AB_ORGANIZATION_ID="test-org"
VITE_AB_EMBEDDED_TOKEN="eyJ0b2tlbiI6ICJtb2NrLXRva2VuIiwgIndpZGdldFVybCI6ICJodHRwczovL2Zvby5haXJieXRlLmNvbS9lbWJlZGRlZC13aWRnZXQmd29ya3NwYWNlSWQ9Zm9vJmFsbG93ZWRPcmlnaW49aHR0cHMlM0ElMkYlMkZsb2NhbGhvc3QlM0EzMDAzIn0="
36 changes: 24 additions & 12 deletions demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,37 @@ This is a demo application showcasing the usage of the Airbyte Embedded Widget.
pnpm install
```

2. Create a `.env` file in this directory with the following:
2. Fetch embedded token

To use this library, you will first need to fetch an Airbyte Embedded token. You should do this in your server, though if you are simply testing this locally, you can use:

```env
VITE_AB_API_CLIENT_ID=
VITE_AB_API_CLIENT_SECRET=
VITE_AB_ORGANIZATION_ID=
VITE_AB_WORKSPACE_ID=
VITE_AB_BASE_URL=
```
curl --location '$AIRBYTE_BASE_URL/api/public/v1/embedded/widget' \
--header 'Content-Type: application/json' \
--header 'Accept: text/plain' \
--data '{
"workspaceId": "$CUSTOMER_WORKSPACE_ID",
"allowedOrigin": "$EMBEDDING_ORIGIN"
}'
```

## Development
`AIRBYTE_BASE_URL`: where your Airbyte instance is deployed
`CUSTOMER_WORKSPACE_ID`: the workspace you have associated with this customer
`EMBEDDING_ORIGIN` here refers to where you are adding this widget to. It will be used to generate an `allowedOrigin` parameter for the webapp to open communications with the widget. If you are running the widget locally using our demo app, the allowed origin should be `https://localhost:3003`, for example.

Start the development server:
You can also, optionally, send an `externalUserId` in your request and we will attach it to the jwt encoded within the Airbyte Embedded token for provenance purposes.

```bash
pnpm dev
Embedded tokens are short-lived (15-minutes) and only allow an end user to create and edit Airbyte source configurations within the workspace you have created for them.

3. Create a `.env` file in the `/demo` directory:

```env
VITE_AB_EMBEDDED_TOKEN=""
```

The server will start at `https://localhost:3000`. You may need to accept the self-signed certificate warning in your browser.
You can fetch an Airbyte Embedded token using the curl request example above.

4. Run the demo app using `pnpm dev` and access a very simple example UI at `https://localhost:3003` in your browser.

## Project Structure

Expand Down
34 changes: 1 addition & 33 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,40 +33,8 @@ <h1>Airbyte Widget Demo</h1>

async function initializeWidget() {
try {
// Show loading state
loadingEl.style.display = "block";
errorEl.textContent = "";

console.log("Fetching token with credentials:", {
client_id: import.meta.env.VITE_AB_API_CLIENT_ID || "",
client_secret: import.meta.env.VITE_AB_API_CLIENT_SECRET || "",
});

// Note: This should be an API call to the customer's backend server, not Airbyte directly
const response = await fetch(`${import.meta.env.VITE_AB_BASE_URL}/api/v1/applications/token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: import.meta.env.VITE_AB_API_CLIENT_ID,
client_secret: import.meta.env.VITE_AB_API_CLIENT_SECRET,
}),
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to fetch token: ${response.status} ${response.statusText}\n${errorText}`);
}

const { access_token } = await response.json();
console.log("Received token:", access_token);

new EmbeddedWidget({
workspaceId: import.meta.env.VITE_AB_WORKSPACE_ID,
organizationId: import.meta.env.VITE_AB_ORGANIZATION_ID,
token: access_token,
baseUrl: import.meta.env.VITE_AB_BASE_URL,
token: import.meta.env.VITE_AB_EMBEDDED_TOKEN,
});

// Hide loading state on success
Expand Down
1 change: 1 addition & 0 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@playwright/test": "^1.42.1",
"@types/node": "^22.13.14",
"@vitejs/plugin-basic-ssl": "^2.0.0",
"dotenv": "^16.4.7",
"prettier": "^3.5.3",
"typescript": "^5.3.3",
"vite": "^5.0.0"
Expand Down
29 changes: 17 additions & 12 deletions demo/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { defineConfig, devices } from "@playwright/test";
import { loadEnv } from "vite";
import dotenv from "dotenv";
import path from "path";
import fs from "fs";

// Load test environment variables
const env = loadEnv("test", process.cwd(), "");
// Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, ".env.test") });

export default defineConfig({
testDir: "./tests",
Expand All @@ -12,8 +14,10 @@ export default defineConfig({
workers: process.env.CI ? 1 : undefined,
reporter: "html",
use: {
baseURL: "https://localhost:3000",
baseURL: "https://localhost:3003",
trace: "on-first-retry",
screenshot: "only-on-failure",
video: "on-first-retry",
ignoreHTTPSErrors: true,
},
projects: [
Expand All @@ -24,17 +28,18 @@ export default defineConfig({
],
webServer: {
command: "NODE_ENV=test pnpm dev",
url: "https://localhost:3000",
url: "https://localhost:3003",
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000, // 120 seconds
ignoreHTTPSErrors: true,
stdout: "pipe",
stderr: "pipe",
timeout: 60000,
env: {
// Set NODE_ENV for the server process
NODE_ENV: "test",
VITE_AB_BASE_URL: env.VITE_AB_BASE_URL,
VITE_AB_API_CLIENT_ID: env.VITE_AB_API_CLIENT_ID,
VITE_AB_API_CLIENT_SECRET: env.VITE_AB_API_CLIENT_SECRET,
VITE_AB_WORKSPACE_ID: env.VITE_AB_WORKSPACE_ID,
VITE_AB_ORGANIZATION_ID: env.VITE_AB_ORGANIZATION_ID,

// Forward all VITE_ variables from process.env
...Object.fromEntries(Object.entries(process.env).filter(([key]) => key.startsWith("VITE_"))),
},
ignoreHTTPSErrors: true,
},
});
104 changes: 13 additions & 91 deletions demo/tests/widget.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,101 +2,23 @@ import { test, expect } from "@playwright/test";

test.describe("Airbyte Widget", () => {
test.beforeEach(async ({ page }) => {
// Enable verbose logging
page.on("console", (msg) => console.log("Browser console:", msg.text()));
page.on("pageerror", (err) => console.error("Browser error:", err));

// Mock all required API endpoints
await page.route("**/api/v1/applications/token", async (route) => {
console.log("Token request intercepted");
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ access_token: "test-token" }),
});
});

// Mock config templates endpoint
await page.route("**/api/v1/config_templates/list", async (route) => {
console.log("Config templates request intercepted");
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
configTemplates: [],
totalCount: 0,
}),
});
page.on("console", (msg) => console.log(`BROWSER LOG: ${msg.type()}: ${msg.text()}`));
page.on("pageerror", (err) => console.error("BROWSER ERROR:", err.message));
page.on("request", (request) => console.log(`>> ${request.method()} ${request.url()}`));
page.on("response", (response) => console.log(`<< ${response.status()} ${response.url()}`));

await page.goto("/", {
timeout: 30000,
waitUntil: "domcontentloaded",
});

// Mock any other API endpoints that might be called
await page.route("**/api/v1/**", async (route) => {
console.log(`API request intercepted: ${route.request().url()}`);
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({}),
});
});

// Navigate to the page and wait for network idle
console.log("Navigating to page...");
await page.goto("/", { waitUntil: "networkidle" });
console.log("Page loaded");

// Wait for either the widget button to appear or an error message
console.log("Waiting for widget initialization...");
await Promise.race([
page.waitForSelector("button.airbyte-widget-button:has-text('Open Airbyte')", { timeout: 10000 }),
page.waitForSelector("#error", { timeout: 10000 }),
]);

// Check for any error messages
const errorEl = page.locator("#error");
const errorText = await errorEl.textContent();
if (errorText && errorText.trim()) {
console.error("Error on page:", errorText);
throw new Error(`Widget initialization failed: ${errorText}`);
}
await page.waitForLoadState("networkidle", { timeout: 10000 });

// Verify widget button is present
const button = page.locator("button.airbyte-widget-button:has-text('Open Airbyte')");
await expect(button).toBeVisible();
console.log("Widget button is visible");
const hasWidgetButton = (await page.locator("button.airbyte-widget-button").count()) > 0;
});

test("widget opens and closes correctly", async ({ page }) => {
console.log("Starting widget test...");

// Click the button to open the widget
const button = page.locator("button.airbyte-widget-button:has-text('Open Airbyte')");
await button.click();
console.log("Widget button clicked");

// Check if the dialog is visible
const dialog = page.locator("dialog.airbyte-widget-dialog");
await expect(dialog).toBeVisible();
console.log("Dialog is visible");

// Check if the iframe is present and visible
const iframeElement = page.locator("iframe.airbyte-widget-iframe");
await expect(iframeElement).toBeVisible();
console.log("Iframe is visible");

// Verify iframe source contains required parameters
const iframeSrc = await iframeElement.getAttribute("src");
console.log("Iframe src:", iframeSrc);
expect(iframeSrc).toContain("workspaceId=");
expect(iframeSrc).toContain("organizationId=");
expect(iframeSrc).toContain("auth=");

// Close the dialog
const closeButton = page.locator("button.airbyte-widget-close");
await closeButton.click();
console.log("Dialog closed");

// Verify dialog is closed
await expect(dialog).not.toBeVisible();
console.log("Dialog is not visible");
test("widget loads on the page", async ({ page }) => {
await page.waitForSelector('button:has-text("Open Airbyte")', { timeout: 20000 });
console.log("Found button by text content");
});
});
2 changes: 1 addition & 1 deletion demo/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default defineConfig(({ mode }) => {
return {
plugins: [basicSsl()],
server: {
port: 3000,
port: 3003,
https: {},
},
define: {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@airbyte-embedded/airbyte-embedded-widget",
"version": "0.1.1",
"version": "0.2.0",
"description": "Embedded widget for Airbyte",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
Loading
Loading