Vertesia Documentation

Embedding in applications

A persisted View can be opened with no application code, embedded as a React component, or executed directly through the client. Applications can also contribute Views in code so they install alongside the app.

The generic route

The generic Studio route renders a persisted View without application-specific page code:

/view/sales-orders

Use it for internal tools, quick links, and anywhere you do not need custom page chrome.

The ViewExperience component

Most applications embed ViewExperience, which loads the authoritative configuration, executes the View, renders navigation and results, manages pagination, and synchronizes state with the URL. Inside a Vertesia session it self-fetches — pass only a viewId:

Sales Order Console

Search orders and browse them by geography or collection.

Loading

SalesOrdersPage.tsx

import type { ViewHit } from '@vertesia/common';
import { ViewExperience } from '@vertesia/ui/features';
import { useNavigate } from '@vertesia/ui/router';

export function SalesOrdersPage() {
    const navigate = useNavigate();
    const openSalesOrder = (hit: ViewHit) => {
        navigate(`/store/objects/${encodeURIComponent(hit.id)}`, { isBasePathNested: false });
    };
    return <ViewExperience viewId="sales-orders" onOpenHit={openSalesOrder} />;
}

ViewExperience uses client.views.execute from the active Vertesia session. Outside a session provider, supply an execute function:

EmbeddedSalesOrders.tsx

<ViewExperience
    viewId="sales-orders"
    execute={(request) => client.views.execute('sales-orders', request)}
    showHeader={false}
/>

Built-in system Views

Two built-in Views are available without creating a project resource:

IDPurpose
sys:ContentLibraryDeterministic content browsing with folder, collection, type, and status navigation; list, table, and card displays; selection, export/delete, and upload-on-drop.
sys:AgenticDocumentExplorerDemonstrates natural-language Elasticsearch query, adaptive presentation planning, bounded result reranking, and why-match guidance over the always-available sys:GenericDocument type, while retaining folder and collection navigation.

Open them through the generic route (the colon may be URL-encoded) or embed them exactly like a persisted View:

<ViewExperience viewId="sys:AgenticDocumentExplorer" onOpenHit={openContent} />

The agentic system View inherits the project model configured for the Content Search system interaction and reuses it for reranking. It does not pin a model or environment. It scopes results to sys:GenericDocument; try queries such as compare contracts about termination clauses, show documents by Alice as cards, or group presentations updated last month. System Views are read-only in-code definitions; create a project View when you need to customize their configuration.

Execute directly

The underlying content API is POST /api/v1/view-executions/:id/execute. Clients send only user-controlled state, never a complete Elasticsearch request:

Execute directly

const result = await client.views.execute('sales-orders', {
    query: 'orders delayed by weather',
    key_terms: { customer: ['Acme'] },
    navigation: { sold_to_state: ['CA', 'NV'], status: ['open'] },
    navigation_queries: { collections: 'renewals' },
    display: 'table',
    sort: 'recent',
    offset: 0,
    limit: 25,
});

The response contains the authoritative View definition, normalized content hits, total count, navigation nodes and counts, the applied display and sort, search mode and warnings, and execution time.

With URL synchronization enabled, ViewExperience serializes the user-visible state:

ParameterMeaning
qNatural-language or deterministic text query
t.<id>Structured key-term value
n.<id>Navigation selection
nq.<id>Server-side navigation-node filter, currently supported for collections
displayConfigured display ID
sortConfigured sort ID
offsetResult offset

Multiple values use repeated parameters. For example:

/view/sales-orders?q=orders+delayed+by+weather&t.customer=Acme&n.sold_to_state=CA&nq.collections=renewals&display=table&sort=recent

The component preserves unrelated host query parameters and responds to browser back/forward navigation. After execution it removes stale key-term or navigation IDs and uses the display and sort accepted by the authoritative runtime. Generated Elasticsearch DSL and security filters are never placed in the URL.

Set syncUrl={false} for a preview, modal, or embedded surface that should not modify the host route.

Custom renderers

The built-in renderers cover common search, navigation, and result layouts. An embedded application can replace any of them with a typed React component:

  1. Set a stable renderer name on search, a navigation item, or a result display.
  2. Pass components under matching names in the renderers registry.
  3. Keep data access in the View runtime; custom renderers receive normalized state and callbacks.

First, declare the renderer names in the View configuration:

sales-order-view.ts

import type { ViewExperienceConfiguration } from '@vertesia/common';

export const salesOrderView = {
    name: 'Sales orders',
    description: 'Find and manage sales orders.',
    search: {
        mode: 'deterministic',
        renderer: 'sales-order-search',
    },
    navigation: [
        {
            id: 'status',
            label: 'Status',
            source: 'terms',
            field: 'properties.status',
            renderer: 'status-pipeline',
        },
    ],
    results: {
        default_display: 'cards',
        displays: [
            {
                id: 'cards',
                label: 'Cards',
                type: 'cards',
                renderer: 'sales-order-cards',
                title: { field: 'name' },
            },
        ],
    },
} satisfies ViewExperienceConfiguration;

Then register components under those exact names in the application:

Renderer registry

import type {
    ViewNavigationRendererProps,
    ViewResultsRendererProps,
    ViewSearchRendererProps,
} from '@vertesia/ui/features';
import { ViewExperience } from '@vertesia/ui/features';

function SalesOrderSearch(props: ViewSearchRendererProps) {
    // Render inputs, then call props.onQueryChange and props.onSubmit.
    return <MySalesOrderSearch {...props} />;
}
function StatusPipeline(props: ViewNavigationRendererProps) {
    // Render props.result.nodes, then call props.onChange with selected node IDs.
    return <MyStatusPipeline {...props} />;
}
function SalesOrderCards(props: ViewResultsRendererProps) {
    // Render props.result.hits and use props.onOpenHit for host navigation.
    return <MySalesOrderCards {...props} />;
}

const renderers = {
    search: { 'sales-order-search': SalesOrderSearch },
    navigation: { 'status-pipeline': StatusPipeline },
    results: { 'sales-order-cards': SalesOrderCards },
};

export function SalesOrders() {
    return <ViewExperience viewId="sales-orders" renderers={renderers} />;
}

Persisted configuration stores renderer names, not executable code. When a renderer name is missing from the application registry — including on the generic /view/<id> route — the component safely falls back to the built-in renderer for that section. Treat renderer names as an application compatibility contract: deploy the new renderer before updating a persisted View to reference it, and keep the old name registered while older View revisions or deep links may still use it.

Actions and drop contributions

The built-in result renderer supports single or multiple selection. Persisted View JSON may enable the standard export and delete actions, and may declare app action metadata by a stable handler name. The application supplies the executable handler through contributions.actions:

Contributed View actions

import { ViewExperience } from '@vertesia/ui/features';

export function SalesOrders() {
    return (
        <ViewExperience
            viewId="sales-orders"
            contributions={{
                actions: {
                    'approve-orders': {
                        isAvailable: ({ result }) => result.total > 0,
                        run: async ({ action, hits, refresh, clearSelection }) => {
                            await approveOrders({
                                ids: hits.map((hit) => hit.id),
                                status: action.params?.status,
                            });
                            clearSelection();
                            await refresh();
                        },
                    },
                },
            }}
        />
    );
}

The contribution receives normalized hits, the authoritative View definition and result, the current request, and refresh/clearSelection callbacks. No executable code is stored in the View. Custom result renderers receive the selection controller in props.selection and can use the exported useViewActions hook for app-specific action UI.

Persisted JSON supports one drop handler: the standard upload flow. For any other behavior, contribute drop code directly from the application:

Code-only custom drop

<ViewExperience
    viewId="case-inbox"
    contributions={{
        drop: {
            run: async ({ files, definition, request, refresh }) => {
                await importCaseBundle({ files, view: definition.name, query: request.query });
                await refresh();
            },
        },
    }}
/>

A code contribution works even when the View has no results.drop entry and overrides the built-in upload target when both are present. Because it is application code, the app is responsible for checking any domain-specific permissions. The generic /view/<id> route has no app contribution registry, so it only runs the persisted built-in upload behavior.

In-code (app-contributed) Views

An application or plugin can ship a View in its own code so it installs with the app, instead of being persisted through the Studio API. Declare InCodeViewDefinition entries in the tool server's views configuration:

plugin: an in-code View

import type { InCodeViewDefinition } from '@vertesia/common';

export const documentLibraryView = {
    id: 'document-library',
    name: 'document_library',
    title: 'Document Library',
    description: 'Browse and search indexed documents by type with full-text search.',
    definition: {
        name: 'Document Library',
        navigation: [{ id: 'type', label: 'Type', source: 'terms', field: 'type.name' }],
        search: { mode: 'deterministic' },
        results: {
            default_display: 'list',
            displays: [{ id: 'list', label: 'List', type: 'list', title: { field: 'name' } }],
        },
    },
} satisfies InCodeViewDefinition;

When the app is installed, Studio resolves the View as app:<app-name>:<view-id> — for example app:my-plugin:document-library. It renders through the same /view/<id> route and ViewExperience component as a persisted View; the app-scoped ID is the only difference:

<ViewExperience viewId="app:my-plugin:document-library" />

App-contributed Views are read-only at runtime: edit them in the app's source, not through the Studio CRUD API.

Was this page helpful?