<!-- Source: https://docs.squirro.com/en/latest/technical/neo/dev-guide/building-dashboards.html -->
# Building Dashboard Components

> **Warning**
>
> Project Neo is currently in [Technical Preview](../../../a-z/squirro-glossary.md#term-Technical-Preview). Features described in this section may change before general availability.

Every dashboard in your bundle is a standard React component. This page covers the component contract, available libraries, navigation, and TypeScript usage.

## Component Signature

Every dashboard component must accept a `projectId` prop. The host injects it automatically when rendering your dashboard.

```jsx
interface Props {
  projectId: string;
}

export default function Analytics({ projectId }: Props) {
  return (
    <div className="p-6">
      <h1 className="text-xl font-semibold">Analytics</h1>
      <p className="text-sm text-muted-foreground">
        Showing data for project: {projectId}
      </p>
    </div>
  );
}
```

`projectId` is the identifier of the active Squirro project. Use it to scope API calls, display project-specific data, and construct URLs.

## Project and User Context

`projectId` is the only prop injected by the host. Project metadata such as the title, the workspace, or your role in the project is not passed directly. Fetch it from the Squirro API using the shared `@tanstack/react-query` library. The identity of the signed-in user is available from a hook, described further below.

### Fetching Project Metadata

A single API call to `GET /v0/projects/{projectId}` returns the most commonly needed project fields:

| Field | Description |
| --- | --- |
| `title` | Human-readable project name. |
| `description` | Project description, if set. |
| `workspace` | Workspace domain (used to construct API URLs). |
| `workspace_title` | Human-readable workspace name. |
| `project_role` | Role of the current user in this project (for example, `admin`, `editor`, `viewer`). |
| `permissions` | List of permission strings granted to the current user for this project. |

Example using `useQuery`:

```jsx
import { useQuery } from '@tanstack/react-query';

interface Project {
  id: string;
  title: string;
  description?: string;
  workspace?: string;
  workspace_title?: string;
  project_role?: string;
  permissions?: string[];
}

function Analytics({ projectId }: { projectId: string }) {
  const { data: project, isPending, isError } = useQuery<Project>({
    queryKey: ['project', projectId],
    queryFn: async () => {
      const response = await fetch(`/v0/projects/${projectId}`);
      if (!response.ok) throw new Error('Failed to fetch project');
      return response.json();
    },
  });

  if (isPending) return <p>Loading...</p>;
  if (isError) return <p>Failed to load project.</p>;

  return (
    <div>
      <h1>{project.title}</h1>
      <p>Workspace: {project.workspace_title}</p>
      <p>Your role: {project.project_role}</p>
    </div>
  );
}
```

### Reading the Current User

Call the `useCurrentUser()` hook from `@squirro/neo-core/api` to read the signed-in user. The hook returns `null` until authentication resolves, and it re-renders the component when the user changes:

```jsx
import { useCurrentUser } from '@squirro/neo-core/api';

export default function Analytics() {
  const user = useCurrentUser();

  if (!user) return <p>Loading...</p>;

  return (
    <p>
      Signed in as {user.full_name ?? user.email} ({user.role})
    </p>
  );
}
```

The hook returns the following fields:

| Field | Description |
| --- | --- |
| `id` | Identifier of the user. |
| `tenant` | Tenant the user belongs to. |
| `email` | Email address of the user. |
| `role` | Role of the user, for example `admin`. |
| `role_permissions` | List of permission strings granted by that role. |
| `full_name` | Display name. Optional, because the backend omits it when it is not set. |
| `groups` | Groups the user belongs to, each with an `id` and a `name`. |

The `groups` field is `undefined` when the membership is unknown, which is not the same as an empty array. An empty array means the user is known to belong to no group. Treat the unknown case as undecided rather than as an absence of membership.

Those fields are a deliberately narrow view of the user profile. For anything outside them, call the endpoint directly with the `fetch` escape hatch of `useSquirroApi()`, described on the [Squirro API](squirro-api.md#neo-extensions-api) page. For the available user fields, see the [Microservices APIs](../../api/services/index.md#api-services) page.

> **Note**
>
> The hook needs a host that reports the user identity. On an older host, it raises an error naming the missing support instead of returning a user.

## Restricting a Dashboard to Specific Groups

A user group is defined once for the whole Squirro instance, not per project. An administrator creates groups in the Server space under Groups, then gives a group a role in a project under Setup → Settings → Project Members. Where single sign-on is used, membership can be mapped from the groups of the identity provider. For more information about creating and managing groups, see the [Managing User Groups](../../ui/user-groups.md#ui-user-groups) page.

Project roles are a separate concept. `allowedGroups` matches the name or the ID of a group, so a role such as `Administrator`, `Member`, or `Reader` in that list matches nothing and leaves the dashboard hidden from everyone.

Add the `allowedGroups` field to the manifest entry of a dashboard to show it only to members of specific user groups:

```typescript
{
  id: 'exec-summary',
  title: 'Executive Summary',
  icon: 'chart-line',
  route: 'exec-summary',
  component: () => import('./dashboards/ExecSummary'),
  allowedGroups: ['Executives'],
}
```

- Each entry matches either the name or the ID of a group. To list the groups of your instance with both values, run `neo-ui groups`.
- The dashboard is shown to viewers who belong to at least one of the listed groups.
- Omit the field, or leave the array empty, to show the dashboard to everyone.
- If the group membership of the viewer cannot be determined, because the identity has not resolved yet or the host does not report it, the dashboard stays hidden.

`neo-ui create dashboard` offers a group picker when it creates the dashboard, and `--groups` sets the same field without prompting. Both are described on the [CLI Reference](cli-reference.md#neo-extensions-cli-reference) page. The restriction can be changed at any time by editing the manifest entry.

To remove an item from the sidebar for every viewer, including a core item of the host application, use the `nav` field of the manifest instead. The two fields compose, so an item appears only when it passes both. For more information, see the [The Bundle Manifest](manifest.md#neo-extensions-manifest) page.

A restricted dashboard is left out of the sidebar, and it cannot be opened by URL either. A viewer who tries is redirected to the first dashboard available to them, or to the project home page when no dashboard is available.

> **Warning**
>
> `allowedGroups` tailors the interface to the viewer. It is not a security boundary. The field removes an entry point from the sidebar and blocks a route in the browser, but it does not restrict the underlying data. Data access is enforced by the Squirro platform, so a dashboard that presents sensitive content still requires the matching project and item permissions.

## Available Shared Libraries

The following libraries are provided by the host application at runtime. You do not need to install them in your project.

### Installed by the Scaffold (Do Not Bump These)

These packages are included in the scaffolded `package.json` so that TypeScript can resolve their types during development. Do not change their versions independently. They must match the versions bundled into `@squirro/neo-core`.

| Package | Description |
| --- | --- |
| `react` | JSX, hooks, and components. |
| `react-dom` | DOM rendering. |
| `react-router-dom` | `useNavigate`, `Link`, `useParams`, and other routing hooks. |

Those three are listed in `package.json` so that TypeScript types resolve and the standalone preview on port 3001 works, but they are still consumed from the shared scope of the host at runtime. Duplicate copies break Module Federation, so leave the versions where the scaffold puts them.

The scaffold also installs `lucide-react` for icons. Unlike the three packages above, it is not shared with the host. It is bundled into your own output from your `node_modules`, so you can upgrade it independently.

### Provided Only at Runtime (Do Not Add at All)

These packages are not in the scaffolded `package.json`. The host supplies them exclusively at runtime via Module Federation. Do not add them to your `package.json`. Adding them causes duplicate versions and runtime errors.

| Package | Description |
| --- | --- |
| `zustand` | Lightweight global state. |
| `@tanstack/react-query` | Data fetching, caching, and loading and error states. |
| `i18next` and `react-i18next` | Translations. For more information, see the [Translations](translations.md#neo-extensions-translations) page. |

Those libraries run as singletons shared with the host. Your dashboards share the same router context, query cache, and i18n instance as the host application, giving them full access to standard navigation and any cached data.

## Navigation

Use `react-router-dom` hooks for navigation within the host application:

```jsx
import { useNavigate, Link } from 'react-router-dom';

function Analytics({ projectId }: { projectId: string }) {
  const navigate = useNavigate();

  return (
    <div>
      {/* Link to another dashboard in your bundle */}
      <Link to={`/${projectId}/dashboard/reports`}>Go to Reports</Link>

      {/* Programmatic navigation */}
      <button type="button" onClick={() => navigate(`/${projectId}/dashboard/reports`)}>
        Open Reports
      </button>
    </div>
  );
}
```

## TypeScript

TypeScript is fully configured in the generated project. TypeScript type-checks dashboard components against the shared library types automatically.

To check types manually:

```bash
npx tsc --noEmit
```

Avoid `as` for type casting. Use type guards or proper type narrowing instead:

```typescript
// Avoid
const item = data as SearchResult;

// Prefer
function isSearchResult(value: unknown): value is SearchResult {
  return typeof value === 'object' && value !== null && 'id' in value;
}
```

## Browser Support

Project Neo targets the same browsers as the classic Squirro interface: the latest versions of Chrome, Edge, Firefox, and Safari. When choosing JavaScript or Web APIs for your dashboard components, you can rely on anything supported across those four browsers at their current release.

For the full client requirements, see the [System Requirements](../../../getting/install/system-requirements.md#install-system-requirements) page.

## Runtime Environment

Bundles run directly inside the host application’s JavaScript context via Module Federation. There is no iframe or Worker-based sandbox between bundle code and the host: dashboards share the host’s React instance, router, query cache, and origin. Plan for the implications below before writing code that crosses a trust boundary.

### Browser Storage

Bundles share the same origin as the host and have full read and write access to `localStorage` and `sessionStorage`. Use a namespaced key prefix to avoid collisions with host data or other bundles:

```typescript
localStorage.setItem('my-bundle:user-preference', value);
const pref = localStorage.getItem('my-bundle:user-preference');
```

> **Warning**
>
> Because all bundles share the same origin, any code running on the page can read any key in `localStorage` and `sessionStorage`. Do not store tokens, credentials, or other sensitive data there.

### External APIs

The Squirro instance is the only origin a dashboard can reach without additional setup. To call a third-party service, route the request through a server-side proxy you control rather than calling the external API directly from the browser. There is no built-in pass-through proxy for bundle use.

### Browser Permissions

The host denies access to geolocation, microphone, and camera via the `Permissions-Policy` header. Bundles cannot request those browser APIs regardless of user consent.
