Skip to content
Open
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
4 changes: 4 additions & 0 deletions admin/server/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,10 @@ func (s *Server) GetIFrame(ctx context.Context, req *adminv1.GetIFrameRequest) (
iframeQuery["navigation"] = "true"
}

if req.HideNavigationBar {
iframeQuery["hide_navigation_bar"] = "true"
}

if req.State != "" {
iframeQuery["state"] = req.State
}
Expand Down
6 changes: 6 additions & 0 deletions cli/cmd/sudo/embed/embed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ func TestEmbedOpen(t *testing.T) {
require.Equal(t, 0, res.ExitCode, res.Output)
require.Contains(t, res.Output, "Open browser at:")

// The navigation bar can be hidden without disabling navigation.
res = u1.Run(t, "sudo", "embed", "open", "embed-test", "embed-project", "--no-open", "--navigation", "--hide-navigation-bar")
require.Equal(t, 0, res.ExitCode, res.Output)
require.Contains(t, res.Output, "hide_navigation_bar=true")
require.Contains(t, res.Output, "navigation=true")

// Normal user cannot get an embed URL for the project.
res = u3.Run(t, "sudo", "embed", "open", "embed-test", "embed-project", "--no-open", "--navigation")
require.NotEqual(t, 0, res.ExitCode)
Expand Down
3 changes: 3 additions & 0 deletions cli/cmd/sudo/embed/open.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func OpenCmd(ch *cmdutil.Helper) *cobra.Command {
var theme string
var themeMode string
var navigation bool
var hideNavigationBar bool
var query map[string]string
var noOpen bool

Expand All @@ -45,6 +46,7 @@ func OpenCmd(ch *cmdutil.Helper) *cobra.Command {
Theme: theme,
ThemeMode: themeMode,
Navigation: navigation,
HideNavigationBar: hideNavigationBar,
Query: query,
SuperuserForceAccess: true,
}
Expand Down Expand Up @@ -107,6 +109,7 @@ func OpenCmd(ch *cmdutil.Helper) *cobra.Command {
openCmd.Flags().StringVar(&theme, "theme", "", "Theme for the embedded resource")
openCmd.Flags().StringVar(&themeMode, "theme-mode", "", "Theme mode")
openCmd.Flags().BoolVar(&navigation, "navigation", false, "Enable navigation between resources")
openCmd.Flags().BoolVar(&hideNavigationBar, "hide-navigation-bar", false, "Hide the navigation bar while keeping navigation enabled")
openCmd.Flags().StringToStringVar(&query, "query", nil, "Additional query parameters (key=value)")
openCmd.Flags().BoolVar(&noOpen, "no-open", false, "Print the URL without opening the browser")

Expand Down
13 changes: 13 additions & 0 deletions docs/docs/developers/embed/iframe.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ The API accepts the following parameters:
| resource | The name of the dashboard to embed | No (if not specified, `navigation` should be set to `true`) |
| type | The type of the dashboard identified by `resource` (options: `explore`, `canvas`) | No (defaults to `explore`) |
| navigation | Boolean whether to enable navigation and allow users to navigate to other dashboards (`false` will hard embed and allow access to a single dashboard; `true` allows navigation) | No (defaults to `false`) |
| hide_navigation_bar | Boolean whether to hide the navigation bar (the home link and dashboard breadcrumbs) while keeping navigation enabled. Only meaningful when `navigation` is `true`; the bar is always hidden when `navigation` is `false`. In-dashboard navigation, such as the drill-through from a Canvas component to an Explore dashboard, remains available. | No (defaults to `false`) |
| theme | If [themes](/developers/build/dashboards/customization#changing-themes--colors) are being used, the specific theme to pass to the embedded dashboard | No (set to the name of the theme) |
| theme_mode | The theme mode to use for the embedded dashboard. Valid values: `light`, `dark`, `system`. When set to `system`, the dashboard will follow the user's system preference. | No (defaults to `light`) |
| user_email | Email of the end user accessing the embed. Used to populate attributes for use in security policies. | No (only one of `user_email` or `attributes` should be passed in) |
Expand Down Expand Up @@ -348,6 +349,18 @@ If you wish to still embed a dashboard _but allow navigation between dashboards_
}
```

If you wish to allow navigation _but hide the navigation bar_ — for example to embed a Canvas dashboard whose components can drill through to an Explore dashboard, without showing Rill's own breadcrumbs — then add `hide_navigation_bar`:
```json
{
"type": "canvas",
"resource": "dashboardName",
"navigation": true,
"hide_navigation_bar": true
}
```

Note that in this mode there is no in-iframe affordance for returning from a dashboard the user drilled into. Your application should listen for the [`navigation` notification](/developers/embed/postmessage) — still emitted when the navigation bar is hidden — and provide its own way back.

Finally, _if you wish to embed the project list view of dashboards instead (what you see when you first open a project in Rill Cloud)_, then you can simply omit the `resource` and appropriately set `navigation` in your payload:
```json
{
Expand Down
6 changes: 6 additions & 0 deletions proto/gen/rill/admin/v1/admin.swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1760,6 +1760,12 @@ paths:
navigation:
type: boolean
description: Navigation denotes whether navigation between different resources should be enabled in the embed.
hideNavigationBar:
type: boolean
description: |-
HideNavigationBar hides the embed's top navigation bar (the home link and dashboard breadcrumbs) without disabling navigation itself.
It is only meaningful when `navigation` is true; the bar is always hidden when `navigation` is false.
In-dashboard navigation, such as the canvas drill-through to an explore dashboard, remains available.
state:
type: string
description: Blob containing UI state for rendering the initial embed. Not currently supported.
Expand Down
16 changes: 15 additions & 1 deletion proto/gen/rill/admin/v1/api.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions proto/gen/rill/admin/v1/api.pb.validate.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions proto/gen/rill/admin/v1/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4589,6 +4589,12 @@ paths:
Optional ID for the external end user of the iframe. If set, the access token enables per-user state, such as AI chat history.
Cannot be combined with `user_id`. If `user_email` matches a Rill Cloud user, their attributes are used, but this ID takes precedence for per-user state.
type: string
hideNavigationBar:
description: |-
HideNavigationBar hides the embed's top navigation bar (the home link and dashboard breadcrumbs) without disabling navigation itself.
It is only meaningful when `navigation` is true; the bar is always hidden when `navigation` is false.
In-dashboard navigation, such as the canvas drill-through to an explore dashboard, remains available.
type: boolean
kind:
description: 'Deprecated: Alias for `type`.'
type: string
Expand Down
4 changes: 4 additions & 0 deletions proto/rill/admin/v1/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1767,6 +1767,10 @@ message GetIFrameRequest {
string theme_mode = 15 [(validate.rules).string = {ignore_empty: true, in: ["light", "dark", "system"]}];
// Navigation denotes whether navigation between different resources should be enabled in the embed.
bool navigation = 13;
// HideNavigationBar hides the embed's top navigation bar (the home link and dashboard breadcrumbs) without disabling navigation itself.
// It is only meaningful when `navigation` is true; the bar is always hidden when `navigation` is false.
// In-dashboard navigation, such as the canvas drill-through to an explore dashboard, remains available.
bool hide_navigation_bar = 18;
// Blob containing UI state for rendering the initial embed. Not currently supported.
string state = 7;
// DEPRECATED: Additional parameters to set outright in the generated URL query.
Expand Down
4 changes: 4 additions & 0 deletions web-admin/src/client/gen/index.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2136,6 +2136,10 @@ Cannot be combined with `user_id`. If `user_email` matches a Rill Cloud user, th
themeMode?: string;
/** Navigation denotes whether navigation between different resources should be enabled in the embed. */
navigation?: boolean;
/** HideNavigationBar hides the embed's top navigation bar (the home link and dashboard breadcrumbs) without disabling navigation itself.
It is only meaningful when `navigation` is true; the bar is always hidden when `navigation` is false.
In-dashboard navigation, such as the canvas drill-through to an explore dashboard, remains available. */
hideNavigationBar?: boolean;
/** Blob containing UI state for rendering the initial embed. Not currently supported. */
state?: string;
/** DEPRECATED: Additional parameters to set outright in the generated URL query. */
Expand Down
6 changes: 3 additions & 3 deletions web-admin/src/features/embeds/EmbedHeader.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@
import { m } from "@rilldata/web-common/lib/i18n/gen/messages";

export let activeResource: V1ResourceName;
export let navigationEnabled: boolean = true;
export let navigationBarEnabled: boolean = true;

const runtimeClient = useRuntimeClient();
const { twoTieredNavigation, dashboardChat } = featureFlags;

$: onProjectPage = !activeResource;
$: showDashboardChat = $dashboardChat && !onProjectPage;

$: shouldRender = navigationEnabled || showDashboardChat;
$: shouldRender = navigationBarEnabled || showDashboardChat;

// Dashboard breadcrumb
$: dashboardsQuery = useValidDashboards(runtimeClient);
Expand Down Expand Up @@ -55,7 +55,7 @@

{#if $isErrorStoreEmpty && shouldRender}
<div class="flex items-center w-full">
{#if navigationEnabled}
{#if navigationBarEnabled}
<nav class="flex-1">
<ol class="flex items-center pl-4">
{#if !onProjectPage}
Expand Down
1 change: 1 addition & 0 deletions web-admin/src/features/embeds/init-embed-public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ const EmbedParams = [
"type",
"kind",
"navigation",
"hide_navigation_bar",
"theme",
"theme_mode",
"external_user_id",
Expand Down
5 changes: 3 additions & 2 deletions web-admin/src/routes/-/embed/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
instanceId,
missingRequireParams,
navigationEnabled,
navigationBarEnabled,
runtimeHost,
accessToken,
} = data;
Expand Down Expand Up @@ -62,7 +63,7 @@
| ResourceKind.Canvas
| undefined);

$: showTopBar = navigationEnabled || showDashboardChat;
$: showTopBar = navigationBarEnabled || showDashboardChat;

// Suppress browser back/forward
beforeNavigate((nav) => {
Expand Down Expand Up @@ -150,7 +151,7 @@
class="flex items-center w-full pr-4 py-1 min-h-[2.5rem] bg-surface-subtle"
class:border-b={!onProjectPage}
>
<EmbedHeader {activeResource} {navigationEnabled} />
<EmbedHeader {activeResource} {navigationBarEnabled} />
</div>
</ThemeProvider>
{/if}
Expand Down
2 changes: 2 additions & 0 deletions web-admin/src/routes/-/embed/+layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const load = async ({ url }) => {
accessToken,
missingRequireParams,
navigationEnabled,
navigationBarEnabled,
visibleExplores,
} = embedStore;

Expand All @@ -40,6 +41,7 @@ export const load = async ({ url }) => {
accessToken,
missingRequireParams,
navigationEnabled,
navigationBarEnabled,
visibleExplores,
};
};
52 changes: 52 additions & 0 deletions web-admin/tests/embeds.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,58 @@ test.describe("Embeds", () => {
});
});

test.describe("embedded canvas with a hidden navigation bar", () => {
test.use({
embeddedResourceName: "bids_canvas",
embeddedResourceType: "rill.runtime.v1.Canvas",
embeddedHideNavigationBar: true,
});

test("hides the navigation bar but keeps the drill-through to explore", async ({
embedPage,
}) => {
const recorder = new EmbedMessageRecorder(embedPage);
await recorder.waitForReady();
const frame = embedPage.frameLocator("iframe");

const kpi = frame.getByLabel("overall_spend KPI data");
await expect(kpi).toContainText("Advertising Spend Overall");

// The navigation bar is gone. Assert on DOM presence rather than
// visibility, since Playwright's locators also match hidden elements.
await expect(frame.getByLabel("Breadcrumb dropdown")).toHaveCount(0);
await expect(frame.getByText("Home", { exact: true })).toHaveCount(0);

// The component's drill-through to an explore dashboard is still offered.
// Every explore-linkable component on this canvas renders its own toolbar,
// so scope the link to the component being hovered rather than taking the
// canvas's first one.
const kpiComponent = frame
.locator("article.component-card")
.filter({ has: kpi });
const exploreLink = kpiComponent.getByLabel(
"Go to Programmatic Ads Bids",
);
await expect(exploreLink).toHaveCount(1);

// The toolbar is revealed on hover.
await kpiComponent.hover();
await expect(exploreLink).toBeVisible();

await exploreLink.click();

// The explore dashboard renders, and the host is still notified of the
// navigation even though the navigation bar is hidden.
await recorder.expectContaining(
`{"method":"navigation","params":{"from":"bids_canvas","to":"bids_explore"}}`,
);
await expect(frame.getByLabel("Select time range")).toBeVisible();

// The navigation bar stays hidden on the dashboard we drilled into.
await expect(frame.getByLabel("Breadcrumb dropdown")).toHaveCount(0);
});
});

test("navigation works as expected", async ({ embedPage }) => {
const recorder = new EmbedMessageRecorder(embedPage);
await recorder.waitForReady();
Expand Down
52 changes: 52 additions & 0 deletions web-common/src/features/embeds/embed-store.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, expect, it } from "vitest";
import { EmbedStore } from "./embed-store";

const REQUIRED_PARAMS =
"instance_id=inst&runtime_host=https%3A%2F%2Fruntime.example.com&access_token=token";

function initEmbedStore(params: string) {
EmbedStore.init(
new URL(`https://ui.rilldata.com/-/embed?${REQUIRED_PARAMS}&${params}`),
);
return EmbedStore.getInstance()!;
}

describe("EmbedStore", () => {
describe("navigationBarEnabled", () => {
it("renders the navigation bar when navigation is enabled", () => {
expect(initEmbedStore("navigation=true").navigationBarEnabled).toBe(true);
});

it("hides the navigation bar when hide_navigation_bar is set", () => {
const embedStore = initEmbedStore(
"navigation=true&hide_navigation_bar=true",
);
expect(embedStore.navigationBarEnabled).toBe(false);
// Navigation itself stays enabled, so in-dashboard drill-through still works.
expect(embedStore.navigationEnabled).toBe(true);
});

it("hides the navigation bar when navigation is disabled", () => {
expect(initEmbedStore("navigation=false").navigationBarEnabled).toBe(
false,
);
});

it("hides the navigation bar when navigation is disabled, regardless of hide_navigation_bar", () => {
expect(
initEmbedStore("navigation=false&hide_navigation_bar=false")
.navigationBarEnabled,
).toBe(false);
expect(
initEmbedStore("navigation=false&hide_navigation_bar=true")
.navigationBarEnabled,
).toBe(false);
});

it("defaults to hidden when neither param is present", () => {
const embedStore = initEmbedStore("");
expect(embedStore.navigationEnabled).toBe(false);
expect(embedStore.navigationBarEnabled).toBe(false);
});
});
});
10 changes: 10 additions & 0 deletions web-common/src/features/embeds/embed-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ export class EmbedStore {
*/
public readonly missingRequireParams: string[] = [];
public readonly navigationEnabled: boolean;
/**
* Whether the embed's top navigation bar should render.
* The bar is always hidden when navigation is disabled, regardless of `hide_navigation_bar`.
* `hide_navigation_bar` only suppresses the bar in the navigation-enabled case, leaving
* in-dashboard navigation such as the canvas drill-through to an explore dashboard intact.
*/
public readonly navigationBarEnabled: boolean;
public readonly theme: string | null;
public readonly themeMode: string | null;
public readonly externalUserId: string | null;
Expand Down Expand Up @@ -46,6 +53,9 @@ export class EmbedStore {
this.runtimeHost = url.searchParams.get("runtime_host") ?? "";
this.accessToken = url.searchParams.get("access_token") ?? "";
this.navigationEnabled = url.searchParams.get("navigation") === "true";
this.navigationBarEnabled =
this.navigationEnabled &&
url.searchParams.get("hide_navigation_bar") !== "true";
this.theme = url.searchParams.get("theme");
this.themeMode = url.searchParams.get("theme_mode");
this.externalUserId = url.searchParams.get("external_user_id");
Expand Down
Loading
Loading