Examplary
Start for free
Developer docs

Dashboard pages

A dashboard page adds a page of your own to Examplary, with a menu item in the sidebar. The page renders one of your app's components, which can call your app's server-side code through actions.

Loading diagram…

Defining a page

capabilities/avatar-page.ts
import { DashboardPage } from "@examplary/apps";
 
export default new DashboardPage({
  id: "avatar",
  menuTitle: { en: "My avatar", nl: "Mijn avatar" },
  location: "sidebar",
  component: {
    path: "./components/avatar-page.tsx",
    translations: {
      title: { en: "Pick an avatar", nl: "Kies een avatar" },
    },
  },
  actions: {
    shuffle: async (settings, { seed }) => {
      const response = await fetch(`https://avatars.example.com/${seed}`, {
        headers: { Authorization: `Bearer ${settings.apiToken}` },
      });
      return response.json();
    },
  },
});
PropertyDescription
idUnique within your app.
menuTitleThe label of the page's menu item.
locationWhere the menu item goes, see below. The main sidebar when left out.
componentThe component that renders the page.
actionsHooks your component can call with action(name, input), see below.

Locations

LocationMenu itemWho sees it
sidebarIn the main sidebar, below Library.Teachers and admins
account.personalIn the account settings, under the user's own settings.Teachers and admins
account.workspaceIn the account settings, under the workspace's settings.Admins
account.advancedIn the account settings, under the advanced settings.Admins

The page shows up in every workspace that installed your app.

The component

Besides the usual props, the page's component gets action(name, input), which runs one of your actions and returns its result. Use the FrontendPageComponent type from @examplary/ui:

components/avatar-page.tsx
import { useState } from "react";
 
import { Button, FrontendPageComponent } from "@examplary/ui";
 
const AvatarPage: FrontendPageComponent = ({ t, action }) => {
  const [avatar, setAvatar] = useState<string>();
 
  const shuffle = async () => {
    const { url } = await action("shuffle", { seed: crypto.randomUUID() });
    setAvatar(url);
  };
 
  return (
    <>
      <h1>{t("title")}</h1>
      {avatar && <img src={avatar} alt="" />}
      <Button onClick={shuffle}>Shuffle</Button>
    </>
  );
};
 
export default AvatarPage;

When all you need is Examplary's own API, you don't need an action: call it from the component with api, like api.patch("/me", { ... }).

Actions

Actions are hooks: they run on our servers, in a sandbox, so they can use your app's secrets and talk to your own platform without exposing credentials to the browser.

Each action gets:

  • settings: the workspace's settings for your app, merged with the user's own.
  • input: whatever the component passed to action().

Return anything that can be sent as JSON. context.user tells you who called it. If you throw an error, its message is shown to the user, and action() rejects.

Calling the Examplary API

In an action, context.examplary is an Examplary SDK client that acts as the user who called the action, with exactly their permissions in this workspace:

import { context, DashboardPage } from "@examplary/apps";
 
export default new DashboardPage({
  id: "sync",
  menuTitle: { en: "Sync to Acme" },
  component: "./components/sync-page.tsx",
  actions: {
    syncTests: async (settings) => {
      const tests = await context.examplary.exams.list();
      await fetch("https://api.acme.com/tests", {
        method: "POST",
        headers: { Authorization: `Bearer ${settings.apiToken}` },
        body: JSON.stringify(tests.map(({ id, name }) => ({ id, name }))),
      });
      return { synced: tests.length };
    },
  },
});

Its methods take a single object, as described in the SDK's README. Your code never sees the user's credentials: the client only works while the action runs, and only for this workspace. Account security settings, like the user's email address and two-factor authentication, can't be changed through it.

Actions of admin pages

Actions of pages in account.workspace and account.advanced can only be called by admins, just like the pages themselves.

Every call of an action is recorded in the workspace's audit log.