Quickstart - frontend
Frontend module is single npm packace with standard package.json
descriptor and CzechIdM module descriptor:
package.json
{ "name": "czechidm-example", "version": "7.3.0", "description": "Example module for CzechIdM devstack. This module can be duplicated and rename for create new optional CzechIdM module.", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ "CzechIdM", "example", "IdM" ], "author": "BCV solutions s.r.o", "license": "MIT" }
module-descriptor.js
module.exports = { 'id': 'example', 'npmName': 'czechidm-example', 'backendId': 'example', 'name': 'Example module for CzechIdM devstack.', 'description': 'Example module for CzechIdM devstack. This module can be duplicated and renamed for create new optional CzechIdM module.' };
Module is built together with czechidm-app module using gulp
. See frontend installation guide.
The simplest way to create new module is to copy example module skeleton.
Routes
Each module could expose new urls with contents. We need to register routes.js
descriptor in module descriptor:
module.exports = { 'id': 'example', ... 'mainRouteFile': 'routes.js', ... };
Then we can add routes descriptor routes.js
into the same folder as module descriptor with new route definition:
module.exports = { module: 'example', childRoutes: [ { path: '/example/content', component: require('./src/content/ExampleContent') } ] };
Content
After route is definied, then we can add new content to path <module>/src/content/ExampleContent.js
as route says:
import React from 'react'; import Helmet from 'react-helmet'; // import { Basic } from 'czechidm-core'; /** * Example content (page) */ export default class ExampleContent extends Basic.AbstractContent { constructor(props, context) { super(props, context); } /** * "Shorcut" for localization */ getContentKey() { return 'example:content.example'; } render() { return ( <div> <Helmet title={this.i18n('title')} /> <Basic.PageHeader> <Basic.Icon value="link"/> {' '} {this.i18n('header')} </Basic.PageHeader> <Basic.Panel> <Basic.PanelBody> { this.i18n('text') } </Basic.PanelBody> </Basic.Panel> </div> ); } }
This content will be available on url <server>/example/content
. Basic
and Advanced
component usage is recommended, when contents are created. Each component has their own readme with description and notes (e.g. AbstractComponent).
Localization
Localization keys are used in contents and components. Localization location needs to be registered in module descriptor:
module.exports = { 'id': 'example', ... 'mainLocalePath': 'src/locales/', ... };
We can create localization json file with name en.json
:
{ "module": { "name": "Example module", "author": "BCV solutions s.r.o." }, "content": { "example": { "header": "Example content", "label": "Example content", "title": "Example content available from navigation, added by czechidm-example module.", "text": "New example content." } } }
See getContentKey()
method above in ExampleContent
(page) and the json structure - it fits together.
Navigation
Now is localized content available from url. We can add item with link to navigation - add navigation item into module descriptor:
module.exports = { 'id': 'example', ... 'navigation': { 'items': [ { 'id': 'example-main-menu', 'labelKey': 'example:content.examples.label', 'titleKey': 'example:content.examples.title', 'icon': 'gift', 'iconColor': '#FF8A80', 'order': 9 } ] } ... };
Services (rest)
Service consumes rest endpoints. This is the only place when url of rest endpoint can be defined. Services simply make calls to rest api - isomorphic-fetch is used.
Base services
RestApiService
- calls http methods to given endpoint and wraps authentication tokens (xsrf).AbstractService
- basic CRUD method, whid are commons for all endpoints- Other services define concrete rest endpoint and add custom endpoint methods
Service example
Service with example product CRUD operations.
import { Services } from 'czechidm-core'; import { Domain } from 'czechidm-core'; /** * Example products */ export default class ExampleProductService extends Services.AbstractService { getApiPath() { return '/example-products'; } getNiceLabel(entity) { if (!entity) { return ''; } return `${entity.name} (${entity.code})`; } /** * Agenda supports authorization policies */ supportsAuthorization() { return true; } /** * Group permission - all base permissions (`READ`, `WRITE`, ...) will be evaluated under this group */ getGroupPermission() { return 'EXAMPLEPRODUCT'; } /** * Almost all dtos doesn§t support rest `patch` method */ supportsPatch() { return false; } /** * Returns default searchParameters for current entity type * * @return {object} searchParameters */ getDefaultSearchParameters() { return super.getDefaultSearchParameters().setName(Domain.SearchParameters.NAME_QUICK).clearSort().setSort('name'); } }
Managers (redux)
The business logic is implemented inside manager. Manager use underlying service and adds redux state usage - holds application state and provide actions (encapsulated to managers) to change application state.
Manager example
/** * Example product manager */ export default class ExampleProductManager extends Managers.EntityManager { constructor() { super(); this.service = new ExampleProductService(); } getModule() { return 'example'; } getService() { return this.service; } /** * Controlled entity */ getEntityType() { return 'ExampleProduct'; } /** * Collection name in search / find response */ getCollectionType() { return 'exampleProducts'; } }
We can register services and managers in index.js
files placed in folders, when services and managers are defined - it simpler to import services from one file:
... import { ExampleProductService } from '../services'; ...
where relative path leeds to module service's index.js
:
import ExampleProductService from './ExampleProductService'; const ServiceRoot = { ExampleProductService }; ServiceRoot.version = '0.1.0'; module.exports = ServiceRoot;
Created manager can be used together with Basic.AbstractTableContent
component to provide CRUD methods for persisted entity - see ExampleProductTable.
Security
Authorities and permissions are primarily evaluated on backend, but if we need to hide some navigation items, routes and contents on frontend, then SecurityManager
can be used for this purpose.
Secure route
SecurityManager
is not called directly, but each route can have access
property, which is controlled by SecurityManager#checkAccess
method on the backgroud:
module.exports = { module: 'example', childRoutes: [ ... { path: '/example/products', component: require('./src/content/example-product/ExampleProducts'), access: [ { 'type': 'HAS_ANY_AUTHORITY', 'authorities': ['EXAMPLEPRODUCT_READ'] } ] }, ... ] };
Url <server>/example/products
will be available only to logged identity with authority EXAMPLEPRODUCT_READ in this example.
Secure navigation item
Navigation item is secured the same way as route by access
parameter:
module.exports = { 'id': 'example', ... 'navigation': { 'items': [ { 'id': 'example-main-menu', 'labelKey': 'example:content.examples.label', 'titleKey': 'example:content.examples.title', 'icon': 'gift', 'iconColor': '#FF8A80', 'order': 9, 'items': [ { 'id': 'example-products', 'type': 'DYNAMIC', 'section': 'main', 'icon': 'gift', 'labelKey': 'example:content.example-products.label', 'titleKey': 'example:content.example-products.title', 'order': 20, 'path': '/example/products', 'access': [ { 'type': 'HAS_ANY_AUTHORITY', 'authorities': ['EXAMPLEPRODUCT_READ'] } ] } ] }, ... ] } };
Secure components
SecurityManager
can be used direcly for hide some component, e.g. button on some content:
... <Basic.Button type="submit" level="success" showLoadingIcon showLoadingText={ this.i18n('button.saving') } rendered={ SecurityManager.hasAuthority('ROLECATALOGUE_UPDATE') }> { this.i18n('button.save') } </Basic.Button> ...
Button will be shown only for logged identity with authority ROLECATALOGUE_UPDATE
.
Secure form
When agenda supports authorization policies = permissions for data, then form can be secured using manager's canRead
, canSave
, canDelete
methods. Permissions, what currently logged identity can do with selected record, has to be loaded at first. Permission are stored in redux store and are loaded together with entity by default (see EntityManager#fetchEntity
method) - full example.
Error codes
Backend returns error code response, when some error occurs. We can localize this error on frontend by adding error
section into localization file:
... "error": { "EXAMPLE_SERVER_ERROR": { "title": "Example server error", "message": "Example server with parameter [{{parameter}}]." }, "EXAMPLE_CLIENT_ERROR": { "title": "Example client error", "message": "Example client error, bad value given [{{parameter}}]." } }, ...
Registrable components
Component descriptor can be used for register component, which can be added into existing contents or which can override some existing component (e.g. when different behavior is needed).
We need to register component.js
descriptor in module descriptor:
module.exports = { 'id': 'example', ... 'mainComponentDescriptorFile': 'component-descriptor.js', ...
Example - dashboard component
Then we can add component-descriptor.js
into the same folder as module descriptor with new component definition:
module.exports = { 'id': 'example', 'name': 'Example', 'description': 'Components for Example module', 'components': [ { 'id': 'exampleDashboard', 'type': 'dashboard', 'span': '4', 'order': '4', 'component': require('./src/content/dashboards/ExampleDashboard') } ] };
And then we can add new content to path <module>/src/content/dashboards/ExampleDashboard.js
as component definition says:
/** * Example dashbord panel */ export default class ExampleDashboard extends Basic.AbstractContent { /** * "Shorcut" for localization */ getContentKey() { return 'example:content.dashboard.exampleDashboard'; } render() { return ( <Basic.Panel> <Basic.PanelHeader text={ this.i18n('header') }/> <Basic.PanelBody> <Basic.Panel className="panel-warning"> <Basic.PanelHeader> <Basic.Row> <Basic.Col lg={ 3 }> <Basic.Icon type="fa" icon="dashboard" className="fa-5x"/> </Basic.Col> <Basic.Col lg={ 9 }> <div><strong>{ this.i18n('title') }</strong></div> <div>{ this.i18n('text') }</div> </Basic.Col> </Basic.Row> </Basic.PanelHeader> </Basic.Panel> </Basic.PanelBody> </Basic.Panel> ); } }
Theme
Custom theme can be created in module. Theme contains less files and images. Core less variables can be overriden.
We start with copy core default theme into new module's theme folder and rename theme folder from default
to example
. Then we select new theme in frontend configuration under our state and profile (e.g. development.json
):
{ "env": "development", ... "theme": "czechidm-example/themes/example", ... }
Then we can change some colors and logo in module's theme <module>/themes/example/css/main.less
:
@brand-success: #A13432; @idm-base-color: #A13432; @idm-link-color: #A13432; header { .home { background-image: url("../images/logo.png"); } }
and replace logo image on path <module>/themes/example/images/logo.png
.
New theme will be used after application is built (gulp build
) or started (gulp
) under our stage and profile.