Skip to main content
Version: v3.0

User Interface

Rhino provides a set of components that are used to construct a default interface for your application and can be used to build a custom user interface for your application. These components are designed to be used with React, Reactstrap and React Table along with the View Controllers to provide a complete user interface.

Models and Attributes

In the frontend, models represent backend resources and each model has attributes which represent properties in the backend.

Attributes for a model are read from the OpenAPI description and come with a set of additional information to help define the attribute for instance a number may have a minimum and a maximum range. The basis of these additional pieces of information is the OpenAPI data types specification however Rhino provides extensions to the OpenAPI specification in order to provide more detailed information.

For instance if the resources are defined as follows:

The a portion of the OpenAPI description for the blog_post model may look like the following:

{
"openapi": "3.0.3",
"components": {
"schemas": {
// Mapped to a frontend model
"blog_post": {
// Rhino specific extensions for models
"x-rhino-model": {
"model": "blog_post",
"modelPlural": "blog_posts",
"name": "blogPost",
"pluralName": "blogPosts",
"readableName": "Blog post",
"pluralReadableName": "Blog posts",
"ownedBy": "blog",
"singular": false,
"path": "/api/blog_posts",
"searchable": false
},
"type": "object",
"properties": {
// Mapped to a frontend attribute for blog post - this one is read only
"id": {
// Rhino specific extensions for attributes
"x-rhino-attribute": {
"name": "id",
"readableName": "Id",
"readable": true,
"creatable": false,
"updatable": false
},
"readOnly": true,
"nullable": false,
"type": "identifier"
},
// Mapped to a frontend attribute for blog post
"title": {
"x-rhino-attribute": {
"name": "title",
"readableName": "Title",
"readable": true,
"creatable": true,
"updatable": true
},
"nullable": false,
"type": "string"
},
// Mapped to a frontend attribute for blog post
"body": {
"x-rhino-attribute": {
"name": "body",
"readableName": "Body",
"readable": true,
"creatable": true,
"updatable": true
},
"nullable": false,
"type": "text"
}
}
}
}
}
}

Types and formats

An attribute type is based on the OpenAPI data types https://swagger.io/docs/specification/data-models/data-types/ and may include format modifiers, this information is typically used to determine how to render the attribute in the frontend in various contexts such as a form or a table.

Paths

Paths are used to determine what to display and in which order in components such as forms and tables. Paths are array of strings and valid react components or function that returns such an array. Strings and react components can be mixed in the same array.

const rhinoConfig = {
version: 1,
components: {
blog: {
ModelIndexTable: {
props: {
paths: ["title", "body", <MyCustomComponent />],
},
},
},
},
};
info

This is demonstrating setting paths for ModelIndexTable for the blog model using the Global Overrides feature.

Strings

An attribute can be referenced starting from a particular model using a string path. For instance if the base model is Blog, path is title.

A path may lead through multiple models (via references) before ultimately coming to the final attribute for example blog post may have a reference attribute blog and blog may have a reference attribute user. The path to the user name from the BlogPost model would then be blog.user.name.

const rhinoConfig = {
version: 1,
components: {
blog_post: {
ModelIndexTable: {
props: {
paths: ["title", "blog.user.name"],
},
},
},
},
};

Elements

An element path is a react component that will be rendered in a component like a table or a form. It can be used to display a modified instance of a built-in component or a completely custom component.

const rhinoConfig = {
version: 1,
components: {
blog_post: {
ModelIndexTable: {
props: {
paths: [<ModelCellLink path="title" />, <FullyCustomCell />],
},
},
},
},
};

Roles

If the paths are passed as an object, the keys of the object will be used to specify paths based on the user's roles for the current base owner.

For example, if it was desired to show different fields for the admin and editor roles:

const rhinoConfig = {
version: 1,
components: {
blog: {
ModelIndexTable: {
props: {
paths: {
admin: ["title", "category", "published_at", "author"],
editor: ["title", "author"],
},
},
},
},
},
};

Paths functions

If the paths are passed as a function, it will receive as arguments the current user's roles and the resources in context. For instance, paths related to create would receive null as resource, whereas the edit case would lead to resource being the current record. For index, the list of resources would be passed.

For example, if it was desired to hide some fields from non-admin users:

const rhinoConfig = {
version: 1,
components: {
blog: {
ModelIndexTable: {
props: {
paths: (roles, resource) => {
if (roles.includes("admin")) {
return ["title", "category", "published_at", "author"];
}
return [["title", "author"]];
},
},
},
},
},
};

It's also possible to show/hide fields based on the resource state:

const rhinoConfig = {
version: 1,
components: {
blog: {
ModelIndexTable: {
props: {
paths: (roles, resource) => {
if (resource.published_at != null) {
return ["title", "category", "author"];
}
return ["title", "category", "published_at", "author"];
},
},
},
},
},
};

Component Types

There are several broad categories of components that work in specific ways and can be used in different contexts.

Global Components

These are components that can be configured globally (including per model and per attribute) with overrides. ModelIndexTable is an example of a global component.

Base Components

These are the implementation of global components. They are useful for composing more specific components or avoiding global overrides. ModelIndexTableBase is an example of a base component.

Simple Components

These are components that provide the context but no user interface. They are useful for creating arbitrary user interfaces while retaining the power of the view controllers. ModelIndexSimple is an example of a simple component.

Abstract Components

These are components that render a more specific component based on the model and attribute data. ModelCell is an example of an abstract component.

Composite Components

These are components that layout or groups multiple components together, typically the components they compose are overridable. FilterLayoutVertical is an example of a composite component that lays out filters with a vertical form label and a filter field.

Convenience Components

Provides common overrides scenarios via useMergedOverrides to merge the overrides for the composed component with the overrides for the convenience component. FieldGroupPassword is an example of a convenience component, with the same overrides API as FieldGroup

Overrides

Overrides are a system that allow developers to customize the UI of Rhino. Overrides provide the ability to:

  • Alter the props of a component
  • Wrap an existing component
  • Replacing a component
  • Remove a component from the UI

Overrides are based on Overrides Sample Library and Better Reusable React Components with the Overrides Pattern

Both global and local overrides are configured using the same API. The primary difference is that global overrides are configured in src/rhino.config.js and local overrides are configured as a prop to the component.

Global Overrides

Global overrides are configured in src/rhino.config.js and can be applied to all instances of the component or to a specific model or attribute. For example removing the table footer from all index pages can be done for all models:

src/rhino.config.js
const rhinoConfig = {
version: 1,
components: {
ModelFooter: null,
},
};

just for a specific model:

src/rhino.config.js
const rhinoConfig = {
version: 1,
components: {
ModelFooter: null,
},
};

or just for a specific attribute:

src/rhino.config.js
const rhinoConfig = {
version: 1,
components: {
blog: {
title: {
ModelFooter: null,
},
},
},
};

Local Overrides

Local overrides are configured as a prop to a component:

const overrides = {
ModelIndexActions: null,
};

const MyComponent = () => {
// Do not show the actions for this view
return <ModelIndex overrides={overrides} />;
};

Altering props

Props can be altered by using the props key of the overrides. Named props will replace existing props, but other props will be preserved.This can be done both globally:

src/rhino.config.js
const rhinoConfig = {
version: 1,
components: {
blog: {
ModelSort: {
props: { paths: ["title", "created_at"] },
},
},
},
};

and locally:

const overrides = {
ModelIndexHeader: {
ModelSort: {
props: { paths: ["title", "created_at"] },
},
},
};

const MyComponent = () => {
// Specify
return <ModelIndex overrides={overrides} />;
};

Wrapping a component

An existing component can be wrapped by using the shorthand syntax or component key of the overrides. Globally this can be done as follows:

src/rhino.config.js
const rhinoConfig = {
version: 1,
components: {
blog: {
ModelIndex: (props) => (
<div style={{ backgroundColor: "red" }}>
<ModelIndexBase {...props} />
</div>
),
ModelEdit: {
component: (props) => (
<div style={{ backgroundColor: "red" }}>
<ModelEditBase {...props} />
</div>
),
},
},
},
};
warning

Do not wrap the global version of the component as this could lead to an infinite loop with the overrides, always use the base component.

Locally there is really no need to use overrides for this, simply wrap the component:

const MyIndexComponent = () => {
<div style={{ backgroundColor: "red" }}>
<ModelIndexBase {...props} />
</div>;
};

Replacing a component

A component can be replaced in the interface with either the shorthand or the component key of the overrides. Globally this can be done as follows:

src/rhino.config.js
const rhinoConfig = {
version: 1,
components: {
blog: {
// Shorthand
ModelSort: MyCustomSort,
// Component key
ModelPager: { component: MyCustomPager },
},
},
};

and locally:

const overrides = {
ModelIndexHeader: {
ModelSort: MyCustomSort,
ModelPager: { component: MyCustomPager },
},
};

const MyComponent = () => {
// Customize the sort and pager components
return <ModelIndex overrides={overrides} />;
};

Removing a component

A component can be removed from the interface by setting it to null with either the shorthand or the component key of the overrides. Globally this can be done as follows:

src/rhino.config.js
const rhinoConfig = {
version: 1,
components: {
blog: {
// Shorthand
ModelSort: null,
// Component key
ModelPager: { component: null },
},
},
};

and locally:

const overrides = {
ModelIndexHeader: {
ModelSort: null,
ModelPager: { component: null },
},
};

const MyComponent = () => {
// Remove the sort and pager components
return <ModelIndex overrides={overrides} />;
};

Read more