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: 3 additions & 0 deletions visual-embed/add-filter-from-liveboard/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
dist
.env.local
66 changes: 66 additions & 0 deletions visual-embed/add-filter-from-liveboard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Add Filter (Edit Mode) Demo

Embeds a Liveboard and adds an **Add Filter** button outside the ThoughtSpot UI. Clicking it:

1. Triggers `HostEvent.Edit` to switch the Liveboard into edit mode.
2. Once that's acknowledged, triggers `HostEvent.OpenAddFilterModal` to open the Add Filter modal — the same modal the in-app "Add filter" button opens.

Two equivalent implementations of the same flow live side by side:

- `vanilla/` — plain JS + Vite, no framework.
- `react/` — the same logic ported to React (official `@thoughtspot/visual-embed-sdk/react` `LiveboardEmbed` component + hooks).

## Run it

```bash
cd vanilla # or: cd react
npm install
npm run dev
```

Open http://localhost:8642 for `vanilla/`, or http://localhost:8643 for `react/`.

Set your cluster host, credentials, and Liveboard ID in `vanilla/.env` or `react/.env`:

```
VITE_THOUGHTSPOT_HOST=https://training.thoughtspot.cloud
VITE_THOUGHTSPOT_USERNAME=code-sandbox
VITE_THOUGHTSPOT_PASSWORD="PASSWORD"
VITE_LIVEBOARD_ID=b504e160-3025-4508-a76a-1beb1f4b5eed
```

`AuthType.Basic` is already configured with the credentials above. Swap it for your org's real auth type (SSO/trusted auth).

The logged-in user must have edit permission on the Liveboard for the Edit/OpenAddFilterModal sequence to succeed.

## How the race condition is avoided

- **`isLiveboardReady` guard** — the button stays disabled until `EmbedEvent.LiveboardRendered` fires, so no host event is sent before the embed has registered handlers for it.
- **Sequenced on the SDK's own readiness signal, not on `trigger()`** — `HostEvent.OpenAddFilterModal` is only fired once the embedded app has confirmed (via `embed.subscribedEvent`) that it's registered a handler for it. See the **Gotcha** below for why we don't rely on the `trigger()` promise for this.
- **`isTransitioning` guard** — blocks re-entrant clicks (e.g. rapid double-click) from firing a second overlapping Edit/OpenAddFilterModal sequence while the first is still in flight; it's reset in a `finally` block so a failed sequence doesn't leave the button permanently disabled.

## Gotcha: don't `await trigger()` to know when a host event took effect

`liveboardEmbed.trigger(hostEvent)`'s promise only resolves once the embedded app explicitly acknowledges it — and for `HostEvent.Edit` / `HostEvent.OpenAddFilterModal`, that acknowledgment never comes, so the promise just rides out the full 30s timeout and then resolves anyway (not rejects) with an `Error` object. Awaiting it tells you nothing useful about whether the action happened.

Instead: fire the trigger without awaiting it, and rely on the SDK's own signals — `EmbedEvent`s and `embed.subscribedEvent(hostEvent)` (fires when the embedded app registers a handler for that host event) — to know when it's safe to sequence the next step. See `isOpenAddFilterModalSubscribed(Ref)` in `vanilla/src/main.js` / `react/src/App.jsx`.

### Caveat: `Subscribed` behavior differs by version

- **26.10+** — `Subscribed` fires each time the embedded app (re-)registers a handler for `OpenAddFilterModal`, so `isOpenAddFilterModalSubscribed(Ref)` is reset on `EmbedEvent.Cancel` and set again the next time Edit mode is entered.
- **Pre-26.8** — `Subscribed` is memoized and only ever fires once per embed lifetime, so it won't refire after a Cancel. Both implementations fall back to triggering `OpenAddFilterModal` unconditionally after `OPEN_ADD_FILTER_MODAL_FALLBACK_TIMEOUT_MS` if the flag hasn't flipped back to true.

## Files

### `vanilla/`

- `index.html` — layout: embed container + button + loader overlay.
- `src/main.js` — SDK init, embed setup, and the click handler described above.
- `package.json` — `@thoughtspot/visual-embed-sdk` dependency + Vite dev/build scripts.

### `react/`

- `index.html` — page shell: styles + `#root` mount point.
- `src/main.jsx` — React entry point, mounts `<App />`.
- `src/App.jsx` — same SDK init, embed setup, and click handler, ported to hooks. Uses the official `@thoughtspot/visual-embed-sdk/react` `LiveboardEmbed` component and `useEmbedRef` instead of the raw `LiveboardEmbed` class. Calls `init()` directly at module load rather than via the `useInit` hook — see the comment above that call in `App.jsx` for why.
- `package.json` — `@thoughtspot/visual-embed-sdk`, React, and Vite dev/build scripts (dev server on port 8643 to avoid clashing with `vanilla/`).
4 changes: 4 additions & 0 deletions visual-embed/add-filter-from-liveboard/react/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
VITE_THOUGHTSPOT_HOST=http://localhost:5001
VITE_THOUGHTSPOT_USERNAME=tsadmin
VITE_THOUGHTSPOT_PASSWORD="PASSWORD"
VITE_LIVEBOARD_ID=9bd202f5-d431-44bf-9a07-b4f7be372125
89 changes: 89 additions & 0 deletions visual-embed/add-filter-from-liveboard/react/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>TSE — Add Filter (Edit Mode) Demo</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f5f6f8;
color: #1a1a1a;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: #ffffff;
border-bottom: 1px solid #e2e4e8;
}
header h1 {
font-size: 16px;
margin: 0;
}
#addFilterBtn {
padding: 8px 16px;
font-size: 14px;
font-weight: 600;
color: #fff;
background: #4285f4;
border: none;
border-radius: 6px;
cursor: pointer;
}
#addFilterBtn:disabled {
background: #a9c3f5;
cursor: not-allowed;
}
#layout {
display: flex;
height: calc(100vh - 53px);
}
#embed-wrapper {
position: relative;
flex: 1 1 auto;
height: 100%;
}
.ts-embed {
width: 100%;
height: 100%;
}
#embed-loader {
position: absolute;
inset: 0;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
background: rgba(255, 255, 255, 0.85);
}
#embed-loader[hidden] {
display: none;
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid #d7dce3;
border-top-color: #4285f4;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
#embed-loader-text {
font-size: 13px;
color: #4a4f57;
}
</style>
</head>
<body>
<div id="root"></div>

<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Loading
Loading