Skip to content

Commit 4a1b382

Browse files
authored
feat: support for multiple module formats, remove default button (#18)
1 parent 6efb883 commit 4a1b382

16 files changed

+969
-380
lines changed

.github/workflows/widget-tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
branches: [main]
88

99
jobs:
10-
jest:
10+
test:
1111
runs-on: ubuntu-latest
1212
steps:
1313
- uses: actions/checkout@v4
@@ -20,11 +20,11 @@ jobs:
2020
cache: "pnpm"
2121
- name: Install dependencies
2222
run: pnpm install
23-
- name: Run Jest tests
23+
- name: Run tests
2424
run: pnpm test
2525
- uses: actions/upload-artifact@v4
2626
if: always()
2727
with:
28-
name: jest-report
28+
name: test-coverage
2929
path: coverage/
3030
retention-days: 30

README.md

Lines changed: 99 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,63 @@ An embeddable widget for integrating Airbyte's data synchronization capabilities
44

55
## Features
66

7-
Easy, lightweight integration for Airbyte Embedded customers to enable user configurations with any web application
7+
Easy, lightweight integration for Airbyte Embedded customers to enable user configurations within any web application
88

9-
## Installation
9+
## Usage
1010

11-
```bash
12-
# Using pnpm (recommended)
13-
pnpm install
11+
You can use the widget in two ways:
1412

15-
# Using npm
16-
npm install
13+
### Option 1: Via npm (React/Vue/etc.)
1714

18-
# Using yarn
19-
yarn install
15+
Install the package:
16+
17+
```bash
18+
pnpm add @airbyte-embedded/airbyte-embedded-widget
19+
# or npm install / yarn add
2020
```
2121

22-
## Project Structure
22+
Use it in your application:
2323

24-
- `/src` - The widget library source code
25-
- `/demo` - A demo application showcasing the widget usage
24+
```html
2625

27-
## Building the Library
26+
<button id="open-airbyte">Open Airbyte</button>
2827

29-
To build the widget library:
28+
<script type="module">
29+
import { EmbeddedWidget } from "@airbyte-embedded/airbyte-embedded-widget";
3030
31-
```bash
32-
pnpm build
31+
const widget = new EmbeddedWidget({
32+
token: "<your-token-here>",
33+
onEvent: (event) => {
34+
console.log("Widget event:", event);
35+
},
36+
});
37+
38+
document.getElementById("open-airbyte").addEventListener("click", () => widget.open());
39+
</script>
3340
```
3441

35-
The built files will be in the `dist` directory.
42+
---
3643

37-
## Usage
44+
### Option 2: Via `<script>` tag (CDN)
45+
46+
Load the widget via a CDN:
47+
48+
```html
49+
<button id="open-airbyte">Open Airbyte</button>
50+
51+
<script src="https://cdn.jsdelivr.net/npm/@airbyte-embedded/airbyte-embedded-widget"></script>
52+
<script>
53+
const widget = new AirbyteEmbeddedWidget({
54+
token: "<your-token-here>",
55+
onEvent: (event) => {
56+
console.log("Widget event:", event);
57+
},
58+
});
59+
60+
document.getElementById("open-airbyte").addEventListener("click", () => widget.open());
61+
</script>
62+
63+
---
3864

3965
### Authentication
4066

@@ -50,51 +76,55 @@ curl --location '$AIRBYTE_BASE_URL/api/public/v1/embedded/widget' \
5076
}'
5177
```
5278

53-
`AIRBYTE_BASE_URL`: where your Airbyte instance is deployed
54-
`CUSTOMER_WORKSPACE_ID`: the workspace you have associated with this customer
55-
`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.
79+
- `AIRBYTE_BASE_URL`: where your Airbyte instance is deployed
80+
- `CUSTOMER_WORKSPACE_ID`: the workspace you have associated with this customer
81+
- `EMBEDDING_ORIGIN`: the origin where you're embedding the widget (used for CORS validation)
5682

57-
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.
83+
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.
5884

59-
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.
85+
Embedded tokens are short-lived (15 minutes) and only allow the user to create/edit configurations within the scoped workspace.
86+
87+
---
6088

6189
### Event Callbacks
6290

63-
The widget also accepts an `onEvent` function as an argument. This function, if provided, will be executed when a user completes the integration setup or update flow. These events have the following format:
91+
You can pass an `onEvent` callback to receive messages when the user completes actions in the widget:
6492

65-
```typescript
66-
// successful events:
93+
```ts
6794
{
6895
type: "end_user_action_result";
6996
message: "partial_user_config_created" | "partial_user_config_updated";
70-
data: PartialUserConfigRead; // an object containing all data related to the user's configuration, including an actorId identifying the source created/updated
97+
data: PartialUserConfigRead;
7198
}
99+
```
100+
101+
Or, in case of error:
72102

73-
// errored events:
103+
```ts
74104
{
75105
type: "end_user_action_result";
76106
message: "partial_user_config_update_error" | "partial_user_config_create_error";
77-
error: Error; // an error object, including a message property
107+
error: Error;
78108
}
79109
```
80110

81-
This allows you to trigger operations based upon different types of results.
111+
Use this to trigger actions like refreshing your UI or storing source IDs.
82112

83-
### Configuration
113+
---
84114

85-
These values should be passed to where you initialize the widget like so:
115+
### Configuration Example
86116

87-
```typescript
88-
import { EmbeddedWidget } from "airbyte-embedded-widget";
117+
```html
118+
<button id="open-airbyte">Open Airbyte</button>
89119

90-
// Initialize the widget
91-
const widget = new EmbeddedWidget({
92-
token: res.token,
93-
onEvent: yourEventFunction,
94-
});
120+
<script>
121+
const widget = new EmbeddedWidget({
122+
token: "<your-token-here>",
123+
onEvent: handleWidgetEvent,
124+
});
95125
96-
// Mount the widget
97-
widget.mount("#widget-container");
126+
document.getElementById("open-airbyte").addEventListener("click", () => widget.open());
127+
</script>
98128
```
99129

100130
## Demo Application
@@ -105,6 +135,34 @@ The demo application in the `/demo` directory shows a complete example of integr
105135
- Basic styling and layout
106136
- API token handling
107137

138+
## Project Structure
139+
140+
- `/src` - The widget library source code
141+
- `/demo` - A demo application showcasing the widget usage
142+
143+
## Installation
144+
145+
```bash
146+
# Using pnpm (highly recommended)
147+
pnpm install
148+
149+
# Using npm
150+
npm install
151+
152+
# Using yarn
153+
yarn install
154+
```
155+
156+
## Building the Library
157+
158+
To build the widget library:
159+
160+
```bash
161+
pnpm build
162+
```
163+
164+
The built files will be in the `dist` directory.
165+
108166
## Publishing
109167

110168
This repository is configured to publish to npmjs.org whenever:

dev/app/api/widget_token/route.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const debugLog = (message: string, data?: any) => {
1818
};
1919

2020
const BASE_URL = process.env.NEXT_PUBLIC_AIRBYTE_PUBLIC_API_URL || "https://local.airbyte.dev/api/public";
21-
const AIRBYTE_WIDGET_URL = `${BASE_URL}/v1/embedded/widget`;
21+
const AIRBYTE_WIDGET_URL = `${BASE_URL}/v1/embedded/widget_token`;
2222
const AIRBYTE_ACCESS_TOKEN_URL = `${BASE_URL}/v1/applications/token`;
2323
const CLIENT_ID = process.env.NEXT_PUBLIC_CLIENT_ID;
2424
const CLIENT_SECRET = process.env.NEXT_PUBLIC_CLIENT_SECRET;
@@ -109,9 +109,9 @@ export async function GET(request: NextRequest) {
109109
return NextResponse.json({ error: "Failed to fetch embedded token" }, { status: 500 });
110110
}
111111

112-
const widgetToken = await widgetTokenResponse.text();
113-
114-
if (!!process.env.WEBAPP_URL) {
112+
const widgetTokenJson = await widgetTokenResponse.json();
113+
const widgetToken = widgetTokenJson.token;
114+
if (!!process.env.NEXT_PUBLIC_WEBAPP_URL) {
115115
// Decode the base64 token for debugging (it's a JSON object)
116116
try {
117117
const decodedToken = JSON.parse(Buffer.from(widgetToken, "base64").toString());

dev/app/components/EmbeddedWidgetComponent.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,13 @@ export function EmbeddedWidgetComponent({ onEvent, className }: EmbeddedWidgetCo
1717
const [loading, setLoading] = useState(true);
1818
const [error, setError] = useState<string | null>(null);
1919
const [initialized, setInitialized] = useState(false);
20-
const containerRef = useRef<HTMLDivElement>(null);
2120
const widgetRef = useRef<EmbeddedWidget | null>(null);
2221

2322
useEffect(() => {
2423
let isMounted = true;
2524

2625
const initializeWidget = async () => {
27-
if (!containerRef.current || initialized) {
26+
if (initialized) {
2827
return;
2928
}
3029

@@ -44,11 +43,6 @@ export function EmbeddedWidgetComponent({ onEvent, className }: EmbeddedWidgetCo
4443
throw new Error("Missing token in response");
4544
}
4645

47-
// Check again if containerRef is still valid and component still mounted
48-
if (!containerRef.current || !isMounted) {
49-
return;
50-
}
51-
5246
try {
5347
widgetRef.current = new EmbeddedWidget({
5448
token: data.token,
@@ -64,9 +58,6 @@ export function EmbeddedWidgetComponent({ onEvent, className }: EmbeddedWidgetCo
6458
},
6559
});
6660

67-
// Mount the widget to the container element
68-
widgetRef.current.mount(containerRef.current);
69-
7061
if (isMounted) {
7162
setInitialized(true);
7263
setLoading(false);
@@ -93,11 +84,24 @@ export function EmbeddedWidgetComponent({ onEvent, className }: EmbeddedWidgetCo
9384
};
9485
}, [onEvent]);
9586

87+
const handleOpenWidget = () => {
88+
if (widgetRef.current) {
89+
widgetRef.current.open();
90+
}
91+
};
92+
9693
return (
9794
<div className={styles.widgetComponentWrapper}>
9895
{loading && <div className={styles.loading}>Loading widget...</div>}
9996
{error && <div className={styles.error}>Error: {error}</div>}
100-
<div ref={containerRef} className={`${styles.widgetMount} ${className || ""}`} />
97+
{initialized && (
98+
<button
99+
onClick={handleOpenWidget}
100+
className={`${styles.widgetButton} ${className || ""}`}
101+
>
102+
Open Airbyte
103+
</button>
104+
)}
101105
</div>
102106
);
103107
}

0 commit comments

Comments
 (0)