minibeakMinibeak helps you:
Differences from Kolibri resources:
Should you use minibeak instead of Kolibri resources?
This example code lets you log in and out of profile, and select employments. It demonstrates some different ways to use minibeak.
import { Button, TextInputBlock, Well } from "@dossier/salvia-ui";
import { ReactNode } from "hoist-non-react-statics/node_modules/@types/react";
import React, {
Component,
ErrorInfo,
createContext,
useContext,
useState,
} from "react";
import { FaKey, FaUser } from "react-icons/fa";
import {
callProFile,
defaultConfig,
MinibeakHookResult,
MinibeakResourceLookup,
StrictProFileResource,
useProFile,
useProFileIncrementally,
} from "@dossier/minibeak";
// Initialization. Set default api url for requests:
defaultConfig.apiUrl = "/profile/jsonapi";
// Actions:
// These do the same as action creators in kolibri,
// except that they can be called directly. They do
// not need to be bound first.
interface SessionResource extends StrictProFileResource {
activeDelegateEmploymentDelegations?: EmploymentDelegationResource[];
loggedInUser?: ProFileUserResource;
employment?: EmploymentResource;
enabledEmployments?: EmploymentResource[];
}
interface EmploymentDelegationResource extends StrictProFileResource {
delegatorEmployment?: EmploymentResource;
delegateEmployment?: EmploymentResource;
}
interface EmploymentResource extends StrictProFileResource {
person?: PersonResource;
employmentInfo?: EmploymentInfoResource;
departments?: DepartmentResource[];
}
interface EmploymentInfoResource extends StrictProFileResource {}
interface PersonResource extends StrictProFileResource {
fullName: string;
}
interface ProFileUserResource extends StrictProFileResource {
person?: PersonResource;
}
interface DepartmentResource extends StrictProFileResource {
name: string;
}
interface ProFileResourceLookup extends MinibeakResourceLookup {
sessions?: Record<string, SessionResource>;
employments?: Record<string, EmploymentResource>;
employmentDelegations?: Record<string, EmploymentDelegationResource>;
employmentInfos?: Record<string, EmploymentInfoResource>;
persons?: Record<string, PersonResource>;
users?: Record<string, ProFileUserResource>;
}
function doLogin(
username: string,
password: string,
organizationId: string,
locale: string
) {
return callProFile({
...defaultConfig,
path: "/session",
method: "post",
body: {
data: {
attributes: {
username,
password,
permanentLogin: false,
organizationId,
locale,
},
},
},
});
}
function doSelectEmployment(
employmentId: string,
delegatorEmploymentId?: string
) {
return callProFile({
...defaultConfig,
path: "/session/selectEmployment",
method: "post",
body: {
data: {
type: "session",
attributes: {
employmentId,
delegatorEmploymentId,
},
},
},
});
}
function doLogout() {
return callProFile({
...defaultConfig,
path: "/session/logout",
method: "post",
});
}
// Components
const SessionContext = createContext<
MinibeakHookResult<SessionResource, ProFileResourceLookup>
>(null);
function SessionProvider(props: { children?: ReactNode }) {
const session = useProFile<SessionResource, ProFileResourceLookup>({
path: "/session/singleton",
include: [
"employment.departments",
"loggedInUser.actualLoggedInUser.person",
"loggedInUser.person",
"translationValues",
"enabledEmployments.employmentInfo.employmentPositionTypeNames",
"enabledEmployments.departments",
"user",
"anonymousUser",
],
minibeakCache: 10, // seconds
// throwErrors: true,
});
// Session is an object that can have these fields:
// { isReady, isFetching, data, error, resources, response, refresh }
// Prevent rendering if session fetching crashed:
if (session.error) {
return (
<Well
variant="dark"
intent="danger"
style={{ margin: "2rem", maxWidth: "20rem" }}
>
<p>Session request failed. Is backend running? Maybe it has crashed?</p>
</Well>
);
}
// Pass the session down to children so they can know who is logged in:
return (
<SessionContext.Provider value={session}>
{props.children}
</SessionContext.Provider>
);
}
function EmploymentSelector() {
// Get the session that was passed down
const session = useContext(SessionContext);
const [
isWatingForSelectEmployment,
setIsWaitingForSelectEmployment,
] = useState(false);
// We need to read the active delegates employment delegations,
// but if we try to fetch them before we are logged in, we crash.
// So we must first wait for the user to be logged in:
const delegationsSession = useProFile<SessionResource, ProFileResourceLookup>(
{
// Wait for session. As long as path is falsey, the call will not happen
path: !!session.data?.loggedInUser && "/session/singleton",
include: [
"activeDelegateEmploymentDelegations.delegatorEmployment.employmentInfo.employmentPositionTypeNames",
"activeDelegateEmploymentDelegations.delegatorEmployment.employmentInfo.orgUnitNames",
"activeDelegateEmploymentDelegations.delegatorEmployment.person.pictureUrl",
"activeDelegateEmploymentDelegations.delegateEmployment.person.pictureUrl",
],
minibeakCache: 10, // seconds
}
);
// const orgUnits = useProFile({
// path: !!session.data?.loggedInUser && "/orgUnits",
// minibeakCache: true,
// });
// console.log(orgUnits
// Handle loading and error states:
if (!session.data || !delegationsSession.data) return <p>Loading...</p>;
if (delegationsSession.error) return <p>Error fetching session!</p>;
const { employment, enabledEmployments } = session.data;
const { activeDelegateEmploymentDelegations } = delegationsSession.data;
return (
<>
{enabledEmployments.map((enabledEmployment) => (
<React.Fragment key={enabledEmployment.id}>
<Button
elementSize="large"
intent={
enabledEmployment.id === employment?.id ? "primary" : "neutral"
}
disabled={isWatingForSelectEmployment}
onClick={async () => {
setIsWaitingForSelectEmployment(true);
await doSelectEmployment(enabledEmployment.id);
await session.refresh(); // Refresh the session in SessionProvider
setIsWaitingForSelectEmployment(false);
}}
>
{enabledEmployment.person.fullName}
</Button>
{(activeDelegateEmploymentDelegations || [])
.filter(
(delegation) =>
(delegation.$relationships.delegateEmployment.data as any)
.id === enabledEmployment.id
)
.map((delegation) => (
<Button
key={delegation.id}
elementSize="large"
intent={
delegation.delegatorEmployment.id === employment?.id
? "primary"
: "neutral"
}
disabled={isWatingForSelectEmployment}
onClick={async () => {
setIsWaitingForSelectEmployment(true);
await doSelectEmployment(
delegation.delegateEmployment.id,
delegation.delegatorEmployment.id
);
await session.refresh(); // Refresh the session in SessionProvider
setIsWaitingForSelectEmployment(false);
}}
>
{delegation.delegatorEmployment.person.fullName}
</Button>
))}
</React.Fragment>
))}
</>
);
}
const errorMessages = { 401: "Invalid credentials." };
function LoginUi() {
const session = useContext(SessionContext);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [isWaitingForLogin, setIsWaitingForLogin] = useState(false);
const [loginError, setLoginError] = useState(null);
if (
!["localhost", "127.0.0.1"].find(
(host) => window.location.hostname === host
)
) {
return (
<p>
Unfortunately, because of CORS, you can only try minibeak against
profile by running a local dev server. No worries, just{" "}
<a href="https://bitbucket.org/dossiersolutions/minibeak/src/master/src/">
clone the repo
</a>
.
</p>
);
}
if (!session.data) return <p>Authenticating...</p>;
if (!session.data.loggedInUser)
return (
<form>
{!session.data.loggedInUser && (
<>
{loginError && (
<Well intent="danger">
{errorMessages[loginError.response?.status] ||
"Error while logging in."}
</Well>
)}
<TextInputBlock
elementSize="large"
placeholder="User..."
rightContent={<FaUser />}
value={username}
onChange={(event) => setUsername(event.target.value)}
/>
<TextInputBlock
elementSize="large"
placeholder="Password..."
type="password"
rightContent={<FaKey />}
value={password}
onChange={(event) => setPassword(event.target.value)}
/>
<Button
elementSize="large"
intent="primary"
disabled={isWaitingForLogin}
onClick={async (event) => {
event.preventDefault();
setIsWaitingForLogin(true);
try {
await doLogin(username, password, "109106", "nb-NO");
await session.refresh(); // Refresh the session in SessionProvider
setLoginError(null); // clear old login errors
} catch (error) {
console.log(error);
setLoginError(error);
}
setIsWaitingForLogin(false);
}}
>
{isWaitingForLogin ? "Logging in..." : "Sign in"}
</Button>
</>
)}
</form>
);
if (!session.data.employment) return <EmploymentSelector />;
return (
<>
<p>
Logged in as {session.data.loggedInUser?.person?.fullName} (
{(session.data.employment.departments || [])
.map((department) => department.name)
.join(", ")}
).
</p>
<EmploymentSelector />
<Button
elementSize="large"
onClick={async () => {
await doLogout();
await session.refresh(); // Refresh the session in SessionProvider
}}
>
Sign out
</Button>
<pre>
<code>{JSON.stringify(session.data.employment.person, null, 2)}</code>
</pre>
</>
);
}
function PaginationUi() {
const {
results,
loadNextPage,
totalResults,
fetchedResults,
remainingResults,
} = useProFileIncrementally({
path: `/coworkerModuleStatusTableRows`,
queryParams: {
mode: "COWORKER_DATA",
max: 20,
// sort: convertFieldSortSpecToOrderForQuery(fieldSortSpec),
// filter: generateFilterStringFromMap(debouncedFieldFilterSpec),
includeSubOrgUnits: false,
},
include: [
"employment.person",
"employment.employmentInfo.employmentPositionTypeNames",
"cells.moduleStatus",
],
});
return (
<div>
{results.map((result, i) =>
result.isFetching ? (
<p key={i}>Loading...</p>
) : (
<Well key={i}>
<Button intent="primary" onClick={result.refresh}>
Refresh
</Button>
{result?.data?.map((resource) => {
return (
<p key={resource.id}>
{resource?.employment?.person?.firstName}
</p>
);
})}
</Well>
)
)}
{remainingResults && (
<Button
onClick={() => {
loadNextPage();
}}
>
Load more (showing {fetchedResults} of {totalResults})
</Button>
)}
</div>
);
}
export default function ReactExample() {
return (
<ErrorBoundary>
<SessionProvider>
<LoginUi />
<PaginationUi />
</SessionProvider>
</ErrorBoundary>
);
}
interface Props {
children?: ReactNode;
}
interface State {
hasError: boolean;
}
class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
};
public static getDerivedStateFromError(_: Error): State {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("Uncaught error:", error, errorInfo);
}
public render() {
if (this.state.hasError) {
return <h1>Error boundary triggered!</h1>;
}
return this.props.children;
}
}
defaultConfig: MinibeakConfigExample initialization:
import {defaultConfig} from "@dossier/minibeak";
defaultConfig.apiUrl = "http://localhost:8080/profile/jsonapi";
Let's you set default configuration that will be merged into the parameters passed to useProFile.
Does not affect callProFile– you must pass configuration manually there.
Implements MinibeakConfig.
useProFile(config: MinibeakConfig): MinibeakHookResultReact hook to fetch data from ProFile's json-api. Refreshes automatically when config changes. Must be refreshed manually when resources are modified, by calling .refresh() on the returned object.
Returns a MinibeakHookResult.
useProFile is only intended to be used to GET resources. If you need to create, update, delete or do some other action, use callProFile instead.
useProFile uses callProFile and processProFileResult internally.
// Inside your functional component:
const achievementTypes = useProFile<AchievementTypeResource[]>({
path: "/achievementTypes/editable",
queryParams: {
max,
offset,
sort: orderByField + " " + (orderReverse ? "DESC" : "ASC"),
filter: debouncedFilterString
},
include: [
"shares",
"drafts",
"usedInPlanTypesCount",
"category",
"orgUnit"
]
});
if (!achievmentTypes.isReady) {
return <LoadingComponent/>;
}
// For pagination:
const totalCount = achievementTypes.response?.links.total;
console.log("Result of call, with linked resources:", achievmentTypes.data);
console.log("All included resources by type and id:", achievmentTypes.resources);
// to refresh, call achievementTypes.refresh()
callProFile(config: MinibeakConfig): Promise<ProFileRawResponse|Response>Performs a call against json-api based on the given configuration.
Returns a promise resolving to the response returned by the api. When the response Content-Type is json, the parsed json value is returned. Otherwise the fetch response is returned directly.
Fails with ResponseNotOkError when the response does not have an http status in the 200-series.
import { useProFileApi, callProFile, defaultConfig } from "@dossier/minibeak";
async function doSelectEmployment(employmentId, delegatorEmploymentId) {
return callProFile({
...defaultConfig,
path: "/session/selectEmployment",
method: "post",
body: {
data: {
type: "session",
attributes: {
employmentId,
delegatorEmploymentId,
permanentLogin: false,
},
},
},
});
}
// You don't need to bind this, you can just use it.
// For example:
function MyFunctionalComponent(props) {
const session = useProFile(/* ... */);
async function handleSelectEmployment(employmentId, delegatorEmploymentId) {
// Usually you don't use the result of actions.
// Instead you just refresh affected resources:
await doSelectEmployment(employmentId, delegatorEmploymentId)
await session.refresh();
}
// If you want to use the result, you can do this:
async function handleSelectEmploymentAndUseResult(employmentId, delegatorEmploymentId) {
const rawResult = await doSelectEmployment(employmentId, delegatorEmploymentId)
const selectEmploymentResult = processProFileResult(rawResult);
console.log("Result of call, with linked resources:", selectEmploymentResult.data);
console.log("All included resources by type and id:", selectEmploymentResult.resources);
await session.refresh();
}
/* ... */
}processProFileResult(result: ProFileRawResponse): MinibeakResultProcesses a callProFile result.
Returns a MinibeakResult with proper resource links, easier access to attributes etc.
When using React, you will usually not be interested in processing the result, because you will be refreshing instances of useProFile instead.
type CacheClearable = string | MinibeakConfig;
clearCallCache(configsOrTags?: boolean | CacheClearable | Array<CacheClearable>)When called without parameters, or with false: Clears the entire call cache.
When called with a string: Clears all caches with this cache tag.
When called with a MinibeakConfig: clears the cache for this specific call.
Can also be called with an array of tags or configs.
| Field | Type | Default | Description |
|---|---|---|---|
apiPath | string | /profile/jsonapi | Path to the api server |
path | string | Relative path to the specific endpoint with leading slash, for example "/orgUnits" | |
queryParams | Map of keys to values | JavaScript object that will be serialized into the URL as query params | |
include | array of strings | List of linked resources to include | |
minibeakCache | boolean or cache tag string or number of timeout seconds, or array of some combination | false | Enables request caching |
body | json-like data | | Body of the request (for POST etc). For example: {data: {type: "session", attributes: {employmentId, delegatorEmploymentId}} |
| Also supports any other fields supported by the HTTP fetch request API, including: | |||
method | string | "GET" | Which HTTP method to use |
shouldExecute | boolean | true | If true, the request will be executed. If false, it will be delayed until it is set to true. |
| Field | Type | Description |
|---|---|---|
data | Resource or array of resources | The resource or resources returned directly by the call. |
resources | Map of resource types to maps of resource ids to resources | All of the resources returned by the call, including included resources. |
| Field | Type | Description |
|---|---|---|
isReady | boolean | Whether the requested resource is available yet. This is always true after the initial fetch. |
isFetching | boolean | Whether this resource is currently being fetched or re-fetched. |
data | Resource or array of resources | The resource or resources returned directly by the call. |
resources | Map of resource types to maps of resource ids to resources | All of the resources returned by the call, including included resources. |
response | Json response object | The unprocessed json returned from ProFile. Useful for pagination. |
error | Error | Any errors that happened during the request. If the HTTP status was not in the 200-range, this will be a ResponseNotOkError, with a .response field containing the response itself. |
refresh | () => void | A function that can be called to run the fetch again, to refresh the data. This can be called manually when a resource is changed somewhere else. |
You're going to want proper typings for the resources that you fetch. Since these types are not known at compile time, we have to cast the response to the expected type for each call. Because the ProFile json api types will change over time, those types are not included in minibeak itself. However, minibeak contains generics and inheritable interfaces that help with the casting.
Start by implementing resource types that inherit from ProFileResource, for example an AchievementResource that extends ProFileResource. Now you can use the generic versions of useProFile and processProFileResult:
interface AchievementResource extends ProFileResource {
name: string;
// ...
}
// Single resource
useProFile<AchievementResource>({...});
// Collection
useProFile<AchievementResource[]>({...});
// Same for processProFileResult:
processProFileResult<AchievementResource>(response);
// Or for a collection:
processProFileResult<AchievementResource[]>(response);
The ProFileResource interface will let you read fields without an explicit typing as the any type. If you want stricter typechecking on resources, you can inherit from StrictProFileResource instead. This will only let you read fields that are defined in the type.
You can also implement a ProFileResourceLookup type for the returned resource lookup (the field called resources on MinibeakResult). This type should be the same for all backend calls. For example:
interface ProFileResourceLookup extends MinibeakResourceLookup {
achievementTypes?: Record<string, AchievementTypeResource>;
achievements?: Record<string, AchievementResource>;
// and all of the other types...
}
useProFile<AchievementResource, ProFileResourceLookup>({...})
useProFile<AchievementResource[], ProFileResourceLookup>({...})
processProFileResult<AchievementResource, ProFileResourceLookup>(response)
processProFileResult<AchievementResource[], ProFileResourceLookup>(response)Raw responses from callProFile can be cast to anything, but you can use RawProFileResource or RawProFileResource[] together with ProFileRawResponse to get the basic fields like id, type, etc:
callProFile<ProFileRawResponse<RawProFileResource>>({...});
// Or for a collection:
callProFile<ProFileRawResponse<RawProFileResource[]>>({...});Note that RawProFileResource is different from ProFileResource. The first is returned from callProFile (it is the raw json from backend). The second is a processed resource returned from useProFile or processProFileResult. All of your normal resource type definitions for useProFile and processProFileResult should inherit from ProFileResource.
Thrown when the http response has a status code not in the 200-series. For example, when authentication failed, access was denied, or the resource did not exist.
This error has a .response field containing the response.
These mini-guides explain how to do different things with minibeak. Most of them are also covered by the example code above.
You can use the useProFile hook to fetch resources from ProFile. For example, look at how it is used in the example code above. For non-react usage, see Without React.
useProFile returns a MinibeakHookResult that contains a bunch of different useful stuff. You can check the value of .isReady or .isFetching to see if the requested resource is available, and if it is being fetched. Or you can just check if .data or .error is present.
Some times you don't have all the data needed to make a call with useProFile. For example, maybe you need to wait for another call to finish first. You can prevent useProFile from actually making any call by passing path: undefined or shouldExecute: false to MinibeakConfig until the data become available.
You can make calls to ProFile by calling callProFile directly. There is no need to make action creators and bind them, like in Kolibri. See how callProFile is used in the example code above.
Backend calls may fail for several reasons. The request itself may fail, or it may have the wrong status code (for example, if authentication failed or access was denied).
useProFile:
useProFile returns a MinibeakHookResult that has a field called .error when there are any errors during the request. You can inspect this error to find out what failed. ResponseNotOkError provides the .request property. Errors shuold be responded to in some way. See the example code above.
callProFile:
callProFile returns a promise. When there is an error, this fails with the given error. You can see examples of this in the try-catch blocks in the event handlers in the example code.
Minibeak does not have a central resource store. This means that when a call changes some resource that was populated elsewhere, that useProFile result must be manually refreshed. You can see examples of this in the event handlers in the example code.
useProFile refreshes automatically when its configuration changes.
useProFile and callProFile support a lot of different configuration parameters. See MinibeakConfig for more.
It also includes all of the options supported by the HTTP fetch API.
For example, to set a custom header, you can just set the headers field on the request config to an array of the headers you want.
Experimental Minibeak has built-in backend call caching. This can be used to cache the results of calls based on their config (until the browser is refreshed). This is useful when users are expected to keep revisiting expensive fetches.
Caching can be enabled simply by setting minibeakCache to true in the MinibeakConfig.
minibeakCache can also be set to a number of seconds, after which the cache will time out.
It can also be set to a string cache tag, that can later be used with clearCallCache to clear all caches with that tag.
It can also be set to an array combining several cache tags and a timeout.
callProFile and processProFileResult can be used without React:
import {defaultConfig, callProFile, processProFileResult} from "@dossier/minibeak";
async function getStatusTableRows() {
const requestResult = await callProFile({
...defaultConfig,
path: "/coworkerModuleStatusTableRows",
include: [
"employment.person",
"employment.person.pictureUrl",
"employment.employmentInfos.employmentPositionTypeNames",
],
queryParams: {
mode: "MANAGED_COWORKER_DATA",
offset: 0,
max: 20,
includeSubOrgUnits: false,
},
});
const processedResult = processProFileResult(requestResult);
return processedResult;
}
getStatusTableRows().then((result) => {
if (result.error) {
console.log("Error:", result.error)
}
else {
console.log("Data:", result.data);
console.log("All resources:", result.resources);
console.log("Response object:", result.response);
}
})