Minibeak 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: MinibeakConfig
Example 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): MinibeakHookResult
React 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): MinibeakResult
Processes 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);
}
})