Skip to content

Commit 323acd0

Browse files
author
Joey Marshment-Howell
authored
Merge pull request #4 from airbytehq/teal/post-message-iframe
feat: send token via postMessage to widget
2 parents 6ca34ca + e936250 commit 323acd0

13 files changed

+254
-196
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ yarn-debug.log*
1717
yarn-error.log*
1818
pnpm-debug.log*
1919

20-
playwright-report/
20+
playwright-report/
21+
test-results/

README.md

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Airbyte Embedded Widget
22

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

55
## Features
66

@@ -34,7 +34,7 @@ pnpm install
3434
pnpm dev
3535
```
3636

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

3939
## Building the Library
4040

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

4949
## Usage
5050

51+
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:
52+
53+
```
54+
curl --location '$AIRBYTE_BASE_URL/api/public/v1/embedded/widget' \
55+
--header 'Content-Type: application/json' \
56+
--header 'Accept: text/plain' \
57+
--data '{
58+
"workspaceId": "$CUSTOMER_WORKSPACE_ID",
59+
"allowedOrigin": "$EMBEDDING_ORIGIN"
60+
}'
61+
```
62+
63+
`AIRBYTE_BASE_URL`: where your Airbyte instance is deployed
64+
`CUSTOMER_WORKSPACE_ID`: the workspace you have associated with this customer
65+
`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.
66+
67+
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.
68+
69+
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.
70+
71+
These values should be passed to where you initializze the widget like so:
72+
5173
```typescript
5274
import { EmbeddedWidget } from "airbyte-embedded-widget";
5375

5476
// Initialize the widget
5577
const widget = new EmbeddedWidget({
56-
organizationId: "your_organization_id",
57-
workspaceId: "your_customer_workspace_id",
58-
token: "your_api_token",
59-
// Additional configuration options
78+
token: res.token,
6079
});
6180

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

7796
```env
78-
VITE_API_TOKEN=your_api_token_here
97+
VITE_AB_EMBEDDED_TOKEN=""
7998
```
8099

100+
You can fetch an Airbyte Embedded token using the curl request example above.
101+
102+
You can then run the demo app using `pnpm dev` and access a very simple example UI at `https://localhost:3003` in your browser.
103+
81104
## Publishing
82105

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

94-
and then push those changes to the main branch. Don't forget the tags!
117+
and then push those changes to the main branch. Don't forget the tags!
95118

96119
```bash
97120
git push origin main --tags && git push

demo/.env.test

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
VITE_AB_BASE_URL="https://test.airbyte.com"
2-
VITE_AB_API_CLIENT_ID="test-client-id"
3-
VITE_AB_API_CLIENT_SECRET="test-client-secret"
4-
VITE_AB_WORKSPACE_ID="test-workspace"
5-
VITE_AB_ORGANIZATION_ID="test-org"
1+
VITE_AB_EMBEDDED_TOKEN="eyJ0b2tlbiI6ICJtb2NrLXRva2VuIiwgIndpZGdldFVybCI6ICJodHRwczovL2Zvby5haXJieXRlLmNvbS9lbWJlZGRlZC13aWRnZXQmd29ya3NwYWNlSWQ9Zm9vJmFsbG93ZWRPcmlnaW49aHR0cHMlM0ElMkYlMkZsb2NhbGhvc3QlM0EzMDAzIn0="

demo/README.md

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,37 @@ This is a demo application showcasing the usage of the Airbyte Embedded Widget.
1818
pnpm install
1919
```
2020

21-
2. Create a `.env` file in this directory with the following:
21+
2. Fetch embedded token
22+
23+
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:
2224

23-
```env
24-
VITE_AB_API_CLIENT_ID=
25-
VITE_AB_API_CLIENT_SECRET=
26-
VITE_AB_ORGANIZATION_ID=
27-
VITE_AB_WORKSPACE_ID=
28-
VITE_AB_BASE_URL=
25+
```
26+
curl --location '$AIRBYTE_BASE_URL/api/public/v1/embedded/widget' \
27+
--header 'Content-Type: application/json' \
28+
--header 'Accept: text/plain' \
29+
--data '{
30+
"workspaceId": "$CUSTOMER_WORKSPACE_ID",
31+
"allowedOrigin": "$EMBEDDING_ORIGIN"
32+
}'
2933
```
3034

31-
## Development
35+
`AIRBYTE_BASE_URL`: where your Airbyte instance is deployed
36+
`CUSTOMER_WORKSPACE_ID`: the workspace you have associated with this customer
37+
`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.
3238

33-
Start the development server:
39+
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.
3440

35-
```bash
36-
pnpm dev
41+
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.
42+
43+
3. Create a `.env` file in the `/demo` directory:
44+
45+
```env
46+
VITE_AB_EMBEDDED_TOKEN=""
3747
```
3848

39-
The server will start at `https://localhost:3000`. You may need to accept the self-signed certificate warning in your browser.
49+
You can fetch an Airbyte Embedded token using the curl request example above.
50+
51+
4. Run the demo app using `pnpm dev` and access a very simple example UI at `https://localhost:3003` in your browser.
4052

4153
## Project Structure
4254

demo/index.html

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -33,40 +33,8 @@ <h1>Airbyte Widget Demo</h1>
3333

3434
async function initializeWidget() {
3535
try {
36-
// Show loading state
37-
loadingEl.style.display = "block";
38-
errorEl.textContent = "";
39-
40-
console.log("Fetching token with credentials:", {
41-
client_id: import.meta.env.VITE_AB_API_CLIENT_ID || "",
42-
client_secret: import.meta.env.VITE_AB_API_CLIENT_SECRET || "",
43-
});
44-
45-
// Note: This should be an API call to the customer's backend server, not Airbyte directly
46-
const response = await fetch(`${import.meta.env.VITE_AB_BASE_URL}/api/v1/applications/token`, {
47-
method: "POST",
48-
headers: {
49-
"Content-Type": "application/json",
50-
},
51-
body: JSON.stringify({
52-
client_id: import.meta.env.VITE_AB_API_CLIENT_ID,
53-
client_secret: import.meta.env.VITE_AB_API_CLIENT_SECRET,
54-
}),
55-
});
56-
57-
if (!response.ok) {
58-
const errorText = await response.text();
59-
throw new Error(`Failed to fetch token: ${response.status} ${response.statusText}\n${errorText}`);
60-
}
61-
62-
const { access_token } = await response.json();
63-
console.log("Received token:", access_token);
64-
6536
new EmbeddedWidget({
66-
workspaceId: import.meta.env.VITE_AB_WORKSPACE_ID,
67-
organizationId: import.meta.env.VITE_AB_ORGANIZATION_ID,
68-
token: access_token,
69-
baseUrl: import.meta.env.VITE_AB_BASE_URL,
37+
token: import.meta.env.VITE_AB_EMBEDDED_TOKEN,
7038
});
7139

7240
// Hide loading state on success

demo/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@playwright/test": "^1.42.1",
1515
"@types/node": "^22.13.14",
1616
"@vitejs/plugin-basic-ssl": "^2.0.0",
17+
"dotenv": "^16.4.7",
1718
"prettier": "^3.5.3",
1819
"typescript": "^5.3.3",
1920
"vite": "^5.0.0"

demo/playwright.config.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { defineConfig, devices } from "@playwright/test";
2-
import { loadEnv } from "vite";
2+
import dotenv from "dotenv";
3+
import path from "path";
4+
import fs from "fs";
35

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

79
export default defineConfig({
810
testDir: "./tests",
@@ -12,8 +14,10 @@ export default defineConfig({
1214
workers: process.env.CI ? 1 : undefined,
1315
reporter: "html",
1416
use: {
15-
baseURL: "https://localhost:3000",
17+
baseURL: "https://localhost:3003",
1618
trace: "on-first-retry",
19+
screenshot: "only-on-failure",
20+
video: "on-first-retry",
1721
ignoreHTTPSErrors: true,
1822
},
1923
projects: [
@@ -24,17 +28,18 @@ export default defineConfig({
2428
],
2529
webServer: {
2630
command: "NODE_ENV=test pnpm dev",
27-
url: "https://localhost:3000",
31+
url: "https://localhost:3003",
2832
reuseExistingServer: !process.env.CI,
29-
timeout: 120 * 1000, // 120 seconds
30-
ignoreHTTPSErrors: true,
33+
stdout: "pipe",
34+
stderr: "pipe",
35+
timeout: 60000,
3136
env: {
37+
// Set NODE_ENV for the server process
3238
NODE_ENV: "test",
33-
VITE_AB_BASE_URL: env.VITE_AB_BASE_URL,
34-
VITE_AB_API_CLIENT_ID: env.VITE_AB_API_CLIENT_ID,
35-
VITE_AB_API_CLIENT_SECRET: env.VITE_AB_API_CLIENT_SECRET,
36-
VITE_AB_WORKSPACE_ID: env.VITE_AB_WORKSPACE_ID,
37-
VITE_AB_ORGANIZATION_ID: env.VITE_AB_ORGANIZATION_ID,
39+
40+
// Forward all VITE_ variables from process.env
41+
...Object.fromEntries(Object.entries(process.env).filter(([key]) => key.startsWith("VITE_"))),
3842
},
43+
ignoreHTTPSErrors: true,
3944
},
4045
});

demo/tests/widget.spec.ts

Lines changed: 13 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -2,101 +2,23 @@ import { test, expect } from "@playwright/test";
22

33
test.describe("Airbyte Widget", () => {
44
test.beforeEach(async ({ page }) => {
5-
// Enable verbose logging
6-
page.on("console", (msg) => console.log("Browser console:", msg.text()));
7-
page.on("pageerror", (err) => console.error("Browser error:", err));
8-
9-
// Mock all required API endpoints
10-
await page.route("**/api/v1/applications/token", async (route) => {
11-
console.log("Token request intercepted");
12-
await route.fulfill({
13-
status: 200,
14-
contentType: "application/json",
15-
body: JSON.stringify({ access_token: "test-token" }),
16-
});
17-
});
18-
19-
// Mock config templates endpoint
20-
await page.route("**/api/v1/config_templates/list", async (route) => {
21-
console.log("Config templates request intercepted");
22-
await route.fulfill({
23-
status: 200,
24-
contentType: "application/json",
25-
body: JSON.stringify({
26-
configTemplates: [],
27-
totalCount: 0,
28-
}),
29-
});
5+
page.on("console", (msg) => console.log(`BROWSER LOG: ${msg.type()}: ${msg.text()}`));
6+
page.on("pageerror", (err) => console.error("BROWSER ERROR:", err.message));
7+
page.on("request", (request) => console.log(`>> ${request.method()} ${request.url()}`));
8+
page.on("response", (response) => console.log(`<< ${response.status()} ${response.url()}`));
9+
10+
await page.goto("/", {
11+
timeout: 30000,
12+
waitUntil: "domcontentloaded",
3013
});
3114

32-
// Mock any other API endpoints that might be called
33-
await page.route("**/api/v1/**", async (route) => {
34-
console.log(`API request intercepted: ${route.request().url()}`);
35-
await route.fulfill({
36-
status: 200,
37-
contentType: "application/json",
38-
body: JSON.stringify({}),
39-
});
40-
});
41-
42-
// Navigate to the page and wait for network idle
43-
console.log("Navigating to page...");
44-
await page.goto("/", { waitUntil: "networkidle" });
45-
console.log("Page loaded");
46-
47-
// Wait for either the widget button to appear or an error message
48-
console.log("Waiting for widget initialization...");
49-
await Promise.race([
50-
page.waitForSelector("button.airbyte-widget-button:has-text('Open Airbyte')", { timeout: 10000 }),
51-
page.waitForSelector("#error", { timeout: 10000 }),
52-
]);
53-
54-
// Check for any error messages
55-
const errorEl = page.locator("#error");
56-
const errorText = await errorEl.textContent();
57-
if (errorText && errorText.trim()) {
58-
console.error("Error on page:", errorText);
59-
throw new Error(`Widget initialization failed: ${errorText}`);
60-
}
15+
await page.waitForLoadState("networkidle", { timeout: 10000 });
6116

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

68-
test("widget opens and closes correctly", async ({ page }) => {
69-
console.log("Starting widget test...");
70-
71-
// Click the button to open the widget
72-
const button = page.locator("button.airbyte-widget-button:has-text('Open Airbyte')");
73-
await button.click();
74-
console.log("Widget button clicked");
75-
76-
// Check if the dialog is visible
77-
const dialog = page.locator("dialog.airbyte-widget-dialog");
78-
await expect(dialog).toBeVisible();
79-
console.log("Dialog is visible");
80-
81-
// Check if the iframe is present and visible
82-
const iframeElement = page.locator("iframe.airbyte-widget-iframe");
83-
await expect(iframeElement).toBeVisible();
84-
console.log("Iframe is visible");
85-
86-
// Verify iframe source contains required parameters
87-
const iframeSrc = await iframeElement.getAttribute("src");
88-
console.log("Iframe src:", iframeSrc);
89-
expect(iframeSrc).toContain("workspaceId=");
90-
expect(iframeSrc).toContain("organizationId=");
91-
expect(iframeSrc).toContain("auth=");
92-
93-
// Close the dialog
94-
const closeButton = page.locator("button.airbyte-widget-close");
95-
await closeButton.click();
96-
console.log("Dialog closed");
97-
98-
// Verify dialog is closed
99-
await expect(dialog).not.toBeVisible();
100-
console.log("Dialog is not visible");
20+
test("widget loads on the page", async ({ page }) => {
21+
await page.waitForSelector('button:has-text("Open Airbyte")', { timeout: 20000 });
22+
console.log("Found button by text content");
10123
});
10224
});

demo/vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default defineConfig(({ mode }) => {
66
return {
77
plugins: [basicSsl()],
88
server: {
9-
port: 3000,
9+
port: 3003,
1010
https: {},
1111
},
1212
define: {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@airbyte-embedded/airbyte-embedded-widget",
3-
"version": "0.1.1",
3+
"version": "0.2.0",
44
"description": "Embedded widget for Airbyte",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

0 commit comments

Comments
 (0)