Skip to content

React components

@cratis/showrunner.boxoffice.react is the user interface of Box Office. It tells the signed-in user what their tenant may do, keeps the product from showing what the tenant has no module for, and gives the product’s maintainers a seats page, a usage page and a plan page. It is built on @cratis/components and the generated frontend proxies, so every dialog and table is a Cratis Components one bound to Box Office’s commands and queries.

The backend refuses what a tenant may not do whatever the frontend shows; see Module gating, Suspension and Seat licensing. The components are what keep a user from meeting those refusals.

Box Office needs the backend, the provider and, where they fit, the banners and pages.

Backend. Reference Cratis.Showrunner.BoxOffice and call AddBoxOffice; see Receiving and entitlements. To let people buy, try and change plan, set the customer portal’s address and signing key as well; see The customer portal.

Frontend. Install the package and its peers:

Terminal window
yarn add @cratis/showrunner.boxoffice.react @cratis/showrunner.boxoffice @cratis/arc @cratis/arc.react @cratis/components @cratis/fundamentals

Import the stylesheet once, after the Cratis Components stylesheets:

@import '@cratis/components/tokens';
@import '@cratis/components/styles';
@import '@cratis/components/theme';
@import '@cratis/showrunner.boxoffice.react/styles';

Wrap the application in BoxOfficeProvider, inside <Arc>, the dialog registration and a CratisComponentsProvider with a toaster:

import { Arc } from '@cratis/arc.react';
import { DialogComponents } from '@cratis/arc.react/dialogs';
import { CratisComponentsProvider } from '@cratis/components';
import { BusyIndicatorDialog, ConfirmationDialog } from '@cratis/components/Dialogs';
import { BoxOfficeProvider, NoLicense, SuspensionBanner, TrialBanner } from '@cratis/showrunner.boxoffice.react';
export const App = () => (
<CratisComponentsProvider toaster>
<Arc>
<DialogComponents confirmation={ConfirmationDialog} busyIndicator={BusyIndicatorDialog}>
<BoxOfficeProvider>
<TrialBanner />
<SuspensionBanner />
<NoLicense />
<Application />
</BoxOfficeProvider>
</DialogComponents>
</Arc>
</CratisComponentsProvider>
);

BoxOfficeProvider asks the backend what the tenant may do with the CurrentEntitlements query and where the user stands with the seats with Mine, once, and shares the answer with everything below it.

PropDefaultMeaning
labelsEnglishThe texts to replace; see Texts and localization.
localethe browser’sThe locale dates, money and quantities are formatted for.
openLinkopens a new tabWhat to do with a link to the customer portal. Replace it to open the portal in the same window or inside a desktop shell.
entitlements, seat, portalLinknoneFixed answers instead of asking the backend. For specifications and previews; leave them out in the application.

useEntitlements answers from the one place the provider asked:

import { useEntitlements } from '@cratis/showrunner.boxoffice.react';
const entitlements = useEntitlements();
entitlements.state; // 'loading' | 'suspended' | 'trialClosed' | 'trial' | 'licensed' | 'unlicensed'
entitlements.isReadOnly; // can read, cannot change
entitlements.seats; // seats the tenant has today
entitlements.trial?.daysLeft; // 0 on the last day, negative after it
entitlements.has('analytics'); // module in full or on trial; a preview does not count
PropertyMeaning
isLoadingNothing is known yet. Everything that belongs to a module is hidden until it is, so nothing flashes up and is taken away.
stateWhere the tenant stands with its license. A suspension and a closed trial come before the rest, because they hold for everyone in the tenant.
readOnlyReasonWhy the tenant is read-only, as in the TenantStanding of Suspension.
trialThe last day of the trial, the days left and whether it has closed. Present when the tenant is or was on a trial.
modules, has, accessTo, standingOfWhat the tenant may do with each module the product declares: none, preview, trial or full, and until when.
canOpenPortalWhether the product has a customer portal.

The backend decides a module’s access from the clock, so a trial that ended is none the next day without anything having been recorded.

Hide what belongs to a module the tenant lacks

Section titled “Hide what belongs to a module the tenant lacks”

Give each navigation entry or page the module that owns it and filter:

import { useModuleFilter } from '@cratis/showrunner.boxoffice.react';
const entries = [
{ path: '/', title: 'Home' },
{ path: '/reports', title: 'Reports', module: 'analytics' },
];
const visible = useModuleFilter(entries);

An entry with no module belongs to the product and always shows. filterByModule(items, entitlements) is the same without a hook.

To guard content on a page, wrap it in RequiresModule:

import { RequiresModule } from '@cratis/showrunner.boxoffice.react';
<RequiresModule module='analytics'>
<Reports />
</RequiresModule>
<RequiresModule module='analytics' mode='explain'>
<Reports />
</RequiresModule>

By default the content is hidden, or replaced by fallback. In explain mode the user is told the module is not part of their plan and offered to try or buy it, as far as the product has a customer portal.

ComponentShows
TrialBannerHow many days the trial has left, as a warning in its last week and as an error once it has closed, with a button to buy. Nothing for a tenant that is not on a trial.
SuspensionBannerThat the operator suspended the tenant, the cause the operator gave, and that everything is read-only until it is lifted. Asks CurrentStanding for the cause only while the tenant is suspended.
NoLicenseWhy a user cannot use the product: the tenant is suspended, its trial closed, the user holds no seat, or the tenant holds more seats than it has bought. Nothing to a user with a license.

Render them once, above the pages. SuspensionBannerView is the banner without the query, for a page that already has the cause.

PortalButton opens the operator’s customer portal with a link the backend signs, for what the user wants to do:

import { PortalButton } from '@cratis/showrunner.boxoffice.react';
import { PortalIntent } from '@cratis/showrunner.boxoffice';
<PortalButton intent={PortalIntent.buy} label='Buy more seats' />
<PortalButton intent={PortalIntent.try} label='Try Analytics' />
<PortalButton intent={PortalIntent.changePlan} offer={offer.id} label='Change plan' />

It is not rendered when the product has no customer portal, so a button never leads nowhere. A failure to get a link is a toast, not an exception. How the link is made and what the portal can rely on is in The customer portal.

The three pages are for the product’s maintainers, the people the backend lets manage seats and compute. Mount them on routes of your own; each is a plain component and works in any router:

import { PlanPage, SeatsPage, UsagePage } from '@cratis/showrunner.boxoffice.react';
<Route path='/settings/seats' element={<SeatsPage />} />
<Route path='/settings/usage' element={<UsagePage />} />
<Route path='/settings/plan' element={<PlanPage />} />
PageWhat it does
SeatsPageLists who holds a seat, oldest first, marking the ones the tenant’s seats do not cover. Assigns a seat with a dialog, and releases one after a confirmation. Both are disabled while the tenant is read-only. Reads CurrentRoster, so it needs the BoxOffice.ManageSeats policy; see Seat licensing.
UsagePageOne row per meter the product declares: what is used against the allowance in the period it is in, when the period starts over, what happens beyond the allowance, the budget and, for a meter that has one, the concurrency limit. Reads CurrentUsage, so it needs the BoxOffice.ManageCompute policy; see Compute admission.
PlanPageWhere the tenant stands, every module and its access, and the offers on sale with their prices, each with a button to buy, try or change plan. Open to every signed-in user.

SeatsView, UsageView and PlanView are the same pages without their data, for an application that fetches it elsewhere.

  • Every button has an accessible name, and a row’s release button names the person it releases.
  • The usage bars are progress bars named after their meter, and a meter that has used its allowance or budget says so in text as well as color.
  • The assign dialog opens with focus in its content, not on Assign, so a held Enter key does not give a seat nobody chose.
  • Banners are Cratis Components messages, announced to assistive technology when they appear.

Every text Box Office shows or announces comes from labels. Pass only the texts you replace; the rest keep their English defaults:

<BoxOfficeProvider labels={{
trialBanner: { running: 'Prøveperioden din slutter om {days} dager.', buy: 'Kjøp' },
seats: { title: 'Seter' }
}}>

The full set is the BoxOfficeLabels type, and defaultBoxOfficeLabels holds the English texts. Placeholders in braces, such as {days} and {held}, are filled in by the components.

The models behind the components are exported: entitlementsOf, trialBannerModel, noLicenseModel, seatRows, meterUsageModel and moduleRows. They are pure functions of what the backend said, so a product with its own design can use the decisions and bring its own markup.