a basic understanding of the extension points concepts of extension points, for example @c8y/ngx-components.
Info
All recipes are written with a particular version of Web SDK for Angular. The version is mentioned at the top of the recipe. It is not recommended to use an older version for a recipe, as some of the mentioned features might not be available. If you use a newer version, you might face naming or import changes. We will update the recipes if there are conceptual revisions but not for small variations. Check out the tutorial application with c8ycli new my-app tutorial to have an up-to-date example of all concepts.
Version: 1017.0.23 | Packages: @c8y/cli, @c8y/apps and @c8y/ngx-components
If the widgets that are provided by the platform do not meet your requirements, you can create a custom widget and add it to a dashboard.
A typical dashboard looks like this, showing various widgets:
This recipe will show how to add a custom widget to a dashboard with the HOOK_COMPONENTS.
1. Initialize the example application
As a starting point, you need an application showing dashboards.
For this purpose, create a new Cockpit application using the c8ycli:
c8ycli new my-cockpit cockpit -a @c8y/apps@1017.0.23
Next, you must install all dependencies. Switch to the new folder and run npm install.
Info
The c8ycli new command has a -a flag which defines which package to use for scaffolding. This way you can also define which version of the application you want to scaffold, for example:
c8ycli new my-cockpit cockpit -a @c8y/apps@1017.0.23 will scaffold an application with the version 10.17.0.23
2. Create the widget components
Widgets usually consist of two parts:
Configuration: The component that is shown when the user wants to add a widget to a dashboard.
Widget: The component that is shown when it is added to the dashboard.
The component will show a configured text which is vertically mirrored via CSS.
You can do anything in it that you can also do in other Angular components.
It must have the config input to pass the configuration from the demo-widget-config.component.ts which is defined as follows:
To add the widget you must use the HOOK_COMPONENTS and define the created components as entryComponents.
To do so, add the following to your app.module.ts:
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
// --- 8< changed part ----
import { CoreModule, RouterModule,HOOK_COMPONENTS } from '@c8y/ngx-components';
// --- >8 ----
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { SubAssetsModule } from '@c8y/ngx-components/sub-assets';
import { ChildDevicesModule } from '@c8y/ngx-components/child-devices';
import { CockpitDashboardModule,ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';
import { SearchModule } from '@c8y/ngx-components/search';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitConfigModule } from '@c8y/ngx-components/cockpit-config';
import { DatapointLibraryModule } from '@c8y/ngx-components/datapoint-library';
import { WidgetsModule } from '@c8y/ngx-components/widgets';
import { PluginSetupStepperModule } from '@c8y/ngx-components/ecosystem/plugin-setup-stepper';
// --- 8< added part ----
import { WidgetDemo } from './demo-widget.component';
import { WidgetConfigDemo } from './demo-widget-config.component';
// --- >8 ----
@NgModule({
imports: [
// Upgrade module must be the first
UpgradeModule,
BrowserAnimationsModule,
RouterModule.forRoot(),
NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
CoreModule.forRoot(),
ReportsModule,
NgUpgradeModule,
AssetsNavigatorModule,
DashboardUpgradeModule,
CockpitDashboardModule,
SensorPhoneModule,
ReportDashboardModule,
BinaryFileDownloadModule,
SearchModule,
SubAssetsModule,
ChildDevicesModule,
CockpitConfigModule,
DatapointLibraryModule.forRoot(),
WidgetsModule,
PluginSetupStepperModule
],
// --- 8< added part ----
declarations: [WidgetDemo, WidgetConfigDemo], // 1.
entryComponents: [WidgetDemo, WidgetConfigDemo],
providers: [{
provide: HOOK_COMPONENTS, // 2.
multi: true,
useValue: [
{
id: 'acme.text.widget', // 3.
label: 'Text widget',
description: 'Can display a text',
component: WidgetDemo, // 4.
configComponent: WidgetConfigDemo,
}
]
}],
// --- >8 ----
})
exportclass AppModule extends HybridAppModule {
constructor(protected upgrade: NgUpgradeModule) {
super();
}
}
Explanation of the numbers above:
Define the components as entry components and declare them to make them accessible by this module.
Add a multi-provider hook with the HOOK_COMPONENTS. This hook is collected by the application and adds the widget based on the values you provide.
The ID must be unique as it identifies the data stored in the inventory. The label and description is shown as the title and in the widget dropdown.
These parts tell the hook to associate the previously defined components to the widget.
If you now start your application with npm start, you should be able to add your custom widget to a dashboard.
Once added to a dashboard, the widget looks similar to this:
Add a Jest-based unit test
Version: 1016.274.0 | Packages: @c8y/cli, @c8y/apps and @c8y/ngx-components
Unit testing is an essential part of every development process.
Since version 10.13.0.0, all new c8ycli scaffolded applications include the unit test framework Jest by default.
This tutorial shows you how to write and verify your first unit test.
1. Initialize the example application
You need an application, for example, the empty default application:
c8ycli new my-app application -a @c8y/apps@1016.274.0
However, any application supports unit tests in the same way. Next, you must install all dependencies.
Switch to the new folder and run npm install.
Info
The c8ycli new command has a -a flag which defines which package to use for scaffolding. This way you can also define which version of the application you want to scaffold, for example:
c8ycli new my-app application -a @c8y/apps@1016.274.0 will scaffold an application with the version 10.16.274.0.
2. Add a component
To test something, you first need a component that you can verify.
Therefore, add a new file called test.component.ts:
Add the newly created component to the declarations of your app.module.ts:
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as ngRouterModule } from '@angular/router';
import { CoreModule, BootstrapComponent, RouterModule } from '@c8y/ngx-components';
// --- 8< added part ----
import { TestComponent } from "./test.component";
// ---- >8 ----
@NgModule({
imports: [
BrowserAnimationsModule,
RouterModule.forRoot(),
ngRouterModule.forRoot([], { enableTracing: false, useHash: true }),
CoreModule.forRoot()
],
bootstrap: [BootstrapComponent],
// --- 8< added part ----
declarations: [
TestComponent
]
// --- >8 ----
})
exportclass AppModule {}
After the example component is added to the module, the component is ready for testing.
2. Add a unit test for the test component
Test files have the file extension .spec.ts.
There is an example spec file in the repository called app.module.spec.ts.
Rename this spec file to test.component.spec.ts and align the content to:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AppModule } from './app.module';
import { TestComponent } from './test.component';
describe('Test component test', () => {
let fixture: ComponentFixture<TestComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppModule]
});
fixture = TestBed.createComponent(TestComponent);
});
test('should be defined', () => {
expect(fixture).toBeDefined();
});
});
This is your first test file.
It configures an Angular testing module and checks if the TestComponent can be defined.
You can read more about Angular testing support on the Angular website.
To start the test, run npm test on your command line.
This executes the predefined script in the package.json which then starts Jest.
You should see the following test result:
PASS ./test.component.spec.ts (32.071 s)
Test component test
✓ should be defined (123 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 32.858 s
If the test says PASS, everything went well and your first component test was successful.
Now, you can add more detailed test cases to verify your component works as intended.
3. Use a snapshot test to verify the component template
This section provides you with additional information on other ways to verify the component template.
We use Jest instead of Karma as it comes with the option to use so called snapshot tests.
Snapshot tests allow the verification of the outcome of a test without defining all results.
The Jest function toMatchSnapshot() creates a file which contains the snapshot of the test on the first run.
Create another test, which will use snapshot testing, to verify the template of our TestComponent by adding the following to your test.component.spec.ts file:
test("should show a title tag", () => {
expect(fixture.debugElement.nativeElement).toMatchSnapshot();
});
Run npm test. The result should say that a snapshot is written:
PASS ./test.component.spec.ts
Test component test
✓ should be defined (94 ms)
✓ should show a title tag (29 ms)
› 1 snapshot written.
Snapshot Summary
› 1 snapshot written from 1 test suite.
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 1 written, 1 total
Time: 5.154 s
You can find and verify this snapshot in the newly created folder ./__snapshot__.
When the template changes, the test will fail and you must overwrite your test with npm test -- -u.
You can test this behavior by changing your template in the test.component.ts file.
Info
It is common practice to commit these snapshots with your code.
Conclusion
This tutorial showed you how to add tests to newly scaffolded applications via the c8ycli command.
The advanced snapshot testing has the option to verify templates quickly.
Add a tab to a details views with context routes
Version: 1017.0.23 | Packages: @c8y/cli, @c8y/apps and @c8y/ngx-components
It is a common use case that you want to show additional information to a user in a details view, for example, for a device or a group.
This how-to recipe explains how to create a new tab in the device details view:
In Web SDK for Angular, this kind of view is called ViewContext as it provides a view for a certain context.
There are a couple of context views, for example, Device, Group, User, Application and Tenant.
You can access them by navigating to a certain Route with the hash navigation.
For example, if you go to the route apps/cockpit/#/device/1234, the application tries to resolve the device with the ID 1234.
The details view usually shows a couple of Tabs, like the hello tab in the screenshot above.
It is referenced by another route called /hello but reuses the context of the device to show information about it.
In the following, we will guide you through the process of creating a new tab for this view that is accessible through the route apps/cockpit/#/device/:id/hello.
1. Initialize the example application
As a starting point, you need an application supporting context routes.
For this purpose, create a new Cockpit application using the c8ycli:
c8ycli new my-cockpit cockpit -a @c8y/apps@1017.0.23
Next, you must install all dependencies. Switch to the new folder and run npm install.
Info
The c8ycli new command has a -a flag which defines which package to use for scaffolding. This way you can also define which version of the application you want to scaffold, for example:
c8ycli new my-cockpit cockpit -a @c8y/apps@1017.0.23 will scaffold an application with the version 10.17.0.23
2. Add a new HOOK_ROUTE
The hook concept allows you to hook into the existing code.
In this example we want to add a so-called ChildRoute (by Angular) on the existing route device/:id.
To achieve this, add the following code to the app.module.ts:
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
// ---- 8< changed part ----
import { CoreModule, RouterModule, HOOK_ROUTE, ViewContext } from '@c8y/ngx-components';
// ---- >8 ----
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
@NgModule({
imports: [
// Upgrade module must be the first
UpgradeModule,
BrowserAnimationsModule,
RouterModule.forRoot(),
NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
CoreModule.forRoot(),
AssetsNavigatorModule,
ReportsModule,
NgUpgradeModule,
DashboardUpgradeModule,
CockpitDashboardModule,
SensorPhoneModule,
ReportDashboardModule,
BinaryFileDownloadModule
],
// ---- 8< added part ----
providers: [{
provide: HOOK_ROUTE, // 1.
useValue: [{ // 2.
context: ViewContext.Device, // 3.
path: 'hello', // 4.
component: HelloComponent, // 5.
label: 'hello', // 6.
priority: 100,
icon: 'rocket' }],
multi: true }]
// ---- >8 ----
})
exportclass AppModule extends HybridAppModule {
constructor(protected upgrade: NgUpgradeModule) {
super();
}
}
Explanation of the numbers above:
Provides the multi-provider hook HOOK_ROUTE. This tells the application to extend the current route configuration.
Specifies that we want to use a value to define the route hook. You can also use a class here, for example, if you want to resolve the routes asynchronously.
Defines the context of the route. Use the ViewContext enum to define it. For this example you want to extend the context of a device.
The path where it is going to be shown. It is added to the context path. For this example the complete path is: device/:id/hello.
Defines which component should be shown if the path is hit by a user.
The properties label and icon define what the tab should look like. The priority defines in which position it should be shown.
Info
The HOOK_ROUTE inherits the Angular Route type. All of its properties can be reused here.
After this alignment the route is registered but the application will fail to compile as the HelloComponent does not exist yet.
You will create it in the next section.
3. Add a component to display context data
The HelloComponent might want to display details about the device.
To do this, it needs information about the context it has been opened in.
The context route resolves the device upfront.
You can directly access it via the parent route.
This component injects the ActivatedRoute and accesses its parent data.
This is the key point: as the parent context route already has resolved the data of the device, this component will always show the detailed data of the current device.
Adding it to the entryComponents in app.module.ts will allow you to compile the application:
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
import { CoreModule, RouterModule, HOOK_ROUTE, ViewContext } from '@c8y/ngx-components';
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
// ---- 8< added part ----
import { HelloComponent } from './hello.component';
// ---- >8 ----
@NgModule({
imports: [
// Upgrade module must be the first
UpgradeModule,
BrowserAnimationsModule,
RouterModule.forRoot(),
NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
CoreModule.forRoot(),
AssetsNavigatorModule,
ReportsModule,
NgUpgradeModule,
DashboardUpgradeModule,
CockpitDashboardModule,
SensorPhoneModule,
ReportDashboardModule,
BinaryFileDownloadModule
],
// ---- 8< added part ----
declarations: [HelloComponent],
entryComponents: [HelloComponent],
// ---- >8 ----
providers: [{
provide: HOOK_ROUTE,
useValue: [{
context: ViewContext.Device,
path: 'hello',
component: HelloComponent,
label: 'hello',
priority: 100,
icon: 'rocket' }],
multi: true }]
})
exportclass AppModule extends HybridAppModule {
constructor(protected upgrade: NgUpgradeModule) {
super();
}
}
When you now start your application with npm start and navigate to a details view of a device it should look like this:
You have now added a tab to a device.
You can do the same for tenants, users or applications details views.
Next you will learn how to show this tab only if a condition is met.
4. Show the tab only if a condition is met (Optional)
In some cases, additional information is available only if a condition is met. For example, it only makes sense to show a location if the device has a location fragment associated.
To add such a condition, the context routes inherit the guard concept of Angular.
To add a guard, you simply must add the canActivate property to the route definition in app.module.ts:
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
import { CoreModule, RouterModule, HOOK_ROUTE, ViewContext } from '@c8y/ngx-components';
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { SubAssetsModule } from '@c8y/ngx-components/sub-assets';
import { ChildDevicesModule } from '@c8y/ngx-components/child-devices';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';
import { SearchModule } from '@c8y/ngx-components/search';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitConfigModule } from '@c8y/ngx-components/cockpit-config';
import { DatapointLibraryModule } from '@c8y/ngx-components/datapoint-library';
import { WidgetsModule } from '@c8y/ngx-components/widgets';
import { PluginSetupStepperModule } from '@c8y/ngx-components/ecosystem/plugin-setup-stepper';
import { HelloComponent } from './hello.component';
// ---- 8< added part ----
import { HelloGuard } from './hello.guard';
// ---- >8 ----
@NgModule({
imports: [
// Upgrade module must be the first
UpgradeModule,
BrowserAnimationsModule,
RouterModule.forRoot(),
NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
CoreModule.forRoot(),
AssetsNavigatorModule,
ReportsModule,
NgUpgradeModule,
DashboardUpgradeModule,
CockpitDashboardModule,
SensorPhoneModule,
ReportDashboardModule,
BinaryFileDownloadModule
],
declarations: [HelloComponent],
entryComponents: [HelloComponent],
providers: [
// ---- 8< added part ----
HelloGuard,
// ---- >8 ----
{
provide: HOOK_ROUTE,
useValue: [{
context: ViewContext.Device,
path: 'hello',
component: HelloComponent,
label: 'hello',
priority: 100,
icon: 'rocket',
// ---- 8< added part ----
canActivate: [HelloGuard]
// ---- >8 ----
}],
multi: true }]
})
exportclass AppModule extends HybridAppModule {
constructor(protected upgrade: NgUpgradeModule) {
super();
}
}
Now you can write a guard which checks a certain condition. If it resolves to true, the tab will be shown, otherwise not.
In order for the guard to check for a certain fragment on a device, create a new file called hello.guard.ts with the following information:
This is the only part which is not aligned with the Angular router. In a context route, CanActivate will be called twice, once when the parent route is activated and once when the child route is activated. The first call checks if the tab should be shown at all, while the second call checks if the user is allowed to navigate to it. Hence, the ActivatedRouteSnapshot is different in both calls and you must resolve the contextData in the second case from the parent.
Checks if the acme_HelloWorld fragment is set on the context.
If you now post a device with the fragment "acme_HelloWorld": {} to the API, the Hello tab will only be shown for that device and not for others.
Conclusion
Context routes help you to extend existing routes with further information.
At the same time, the concept allows the application to be consistent since the context is just resolved once and if the context is not found, it can be handled by the parent.
However, there is currently no default way of abstracting the context route concept and implementing your own.
Since the concept is heavily based on Angular routing you can implement the concept yourself.
Extend an existing application and use hooks
Version: 1017.0.23 | Packages: @c8y/cli, @c8y/apps and @c8y/ngx-components
It is a common use case to extend one of our existing applications like Cockpit or Device management.
This recipe explains step by step, how you can extend the Cockpit application with a custom route and hook this route into the navigator.
First, we will provide some background on what we call a hybrid application and what the @c8y/apps npm package contains.
Brief background
The default applications consist of three applications which are shipped with our platform by default.
As these applications are a result of several years of development, the code is mainly based on angularjs.
As we now use Angular for all of our build-in applications, we needed a solution to serve both frameworks.
The @c8y/cli allows scaffolding a default application which does exactly that.
It uses the Angular upgrade functionality to serve an Angular and angularjs application at the same time.
This enables us to develop new features in Angular while every angularjs plugin can be integrated.
This is what we call the hybrid mode.
The hybrid mode, however, comes with some limitations we will cover later in this recipe.
Due to these limitations, we decided to provide a pure Angular empty starter application which comes without the possibility to integrate angularjs plugins.
This pure version of the application comes in two flavors:
Angular CLI: When using the Angular CLI you benefit from the whole Angular ecosystem so that you can re-use many of the tools of the Angular CLI (for example testing).
@c8y/cli: This is our pre-caved way that integrates well with our tooling but will likely not support special cases.
There are three possibilities in total to start with the Web SDK:
Extending an existing hybrid application.
Building a pure Angular application with Angular CLI.
Building it with @c8y/cli.
Which one to choose heavily depends on the application you want to build.
For example, if you want an application that just follows the look and feel of the platform but want to use special dependencies for certain scenarios, for example, the Material framework, you are best set with the pure Angular CLI solution.
The most common use case is the extension of a hybrid application, which we will cover in this recipe.
First, take a look at the limitations of that approach to understand why the concepts are designed the way they are.
Hybrid mode limitations
As we must make sure that Angular and angularjs run side by side when running a hybrid application, there are some limitations:
It is not possible to access the index.html: The whole bootstrapping process must be handled by Cumulocity to make sure that all required elements for Angular and angularjs are in place. There is no possibility to change the bootstrapping template and you can only add routes.
As the services must be loaded first, you can also not inject any service in the root application module. You must provide them on a route or as providedIn: root at the deceleration of the service.
Routes in the router must be defined before the UPGRADED_ROUTES. This is because the Angular router has a ** path match for all angularjs routes which is defined in the UPGRADED_ROUTES. If you define a route after it, the ** will match before your defined route.
Every extension must be done via a hook. This is because Angular and angularjs are needed in hybrid applications and the hooks can be used by both.
Styling is limited to global styles. That means you can only extend the styling by applying a custom branding or by using inline styles. The styleUrls are, as of this version, not supported.
Now that you know the limitations you can start to extend the first application and develop your first extension hook.
To do so, you must scaffold a hybrid application.
@c8y/apps is a package which contains the default applications and their minimum setup.
The c8ycli uses this package every time you initialize an application with the new command.
The next section will explain the scaffolding process and how to extend a hybrid application step by step.
1. Initialize the example application
As a starting point, you need an application.
For this purpose, create a new Cockpit application using the c8ycli:
c8ycli new my-cockpit cockpit -a @c8y/apps@1017.0.23
Next, you must install all dependencies.
Switch to the new folder and run npm install.
Info
The c8ycli new command has a -a flag which defines which package to use for scaffolding. This way you can also define which version of the application you want to scaffold, for example:
c8ycli new my-cockpit cockpit -a @c8y/apps@1017.0.23 will scaffold an application with the version 10.17.0.23
2. Bind a custom component to a route
Routes can be added the same way as in Angular.
The only difference is that it must be defined before the UPGRADE_ROUTES because of the hybrid limitations.
Create the hello.component.ts file in our project with the following content:
This is a basic component.
Only the template uses a special feature called “content projection” to show a title.
Content projection is an Angular concept used to display content in other places than they are defined.
For more information on which components support content projection refer to the @c8y/ngx-components documentation.
We can now bind this custom component to a route by changing the app.module.ts in the following way:
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
import { CoreModule, RouterModule } from '@c8y/ngx-components';
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { SubAssetsModule } from '@c8y/ngx-components/sub-assets';
import { ChildDevicesModule } from '@c8y/ngx-components/child-devices';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';
import { SearchModule } from '@c8y/ngx-components/search';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitConfigModule } from '@c8y/ngx-components/cockpit-config';
import { DatapointLibraryModule } from '@c8y/ngx-components/datapoint-library';
import { WidgetsModule } from '@c8y/ngx-components/widgets';
import { PluginSetupStepperModule } from '@c8y/ngx-components/ecosystem/plugin-setup-stepper';
// --- 8< added part ----
import { HelloComponent } from './hello.component'; // 1
// --- >8 ----
@NgModule({
// --- 8< added part ----
declarations: [HelloComponent], // 2
// --- >8 ----
imports: [
// Upgrade module must be the first
UpgradeModule,
BrowserAnimationsModule,
RouterModule.forRoot(),
// --- 8< changed part ----
NgRouterModule.forRoot(
[{ path: "hello", component: HelloComponent }, ...UPGRADE_ROUTES],
{ enableTracing: false, useHash: true }
),
// --- >8 ----
CoreModule.forRoot(),
ReportsModule,
NgUpgradeModule,
AssetsNavigatorModule,
DashboardUpgradeModule,
CockpitDashboardModule,
SensorPhoneModule,
ReportDashboardModule,
BinaryFileDownloadModule,
SearchModule,
SubAssetsModule,
ChildDevicesModule,
CockpitConfigModule,
DatapointLibraryModule.forRoot(),
WidgetsModule,
PluginSetupStepperModule
],
})
exportclass AppModule extends HybridAppModule {
constructor(protected upgrade: NgUpgradeModule) {
super();
}
}
The changes here are straightforward.
First, you import the component (1.). Then you add it to the declarations (2.). Last, you must bind it to a path, for this example, hello (3.).
When you now spin up the application with the c8ycli server command and navigate to the URL by adding the right hash to it (http://localhost:9000/apps/cockpit/#/hello) you should see the custom component.
In the next step, you will hook the component in the navigator at the left.
3. Hooking a navigator node
To allow the user to navigate to your newly created hello.component.ts, add some navigation to the navigator on the left.
To do so, you will use a so-called hook.
Hooks are providers that are bound to a certain injection token.
To allow the addition of multiple providers, use the multi-provider concept of Angular.
Explaining it in detail goes beyond the scope of this tutorial.
Refer to the angular.io documentation.
The injection tokens can be received from the @c8y/ngx-components package by importing it.
They all start with HOOK_ followed by what they are used for.
For example, to add a navigator node, use the HOOK_NAVIGATOR_NODE in app.module.ts in the following way:
As you see in (1) you must take care of the typing on your own.
To avoid it, you can also use the hookX function, which allow the same but without taking care of the boilerplate code.
The following example uses these functions, to add a navigator node, using hookRoute and hookNavigatorNode:
import { NgModule } from "@angular/core";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { RouterModule as NgRouterModule } from "@angular/router";
import { UpgradeModule as NgUpgradeModule } from "@angular/upgrade/static";
// --- 8< changed part ----
import { CoreModule, RouterModule, hookNavigator, hookRoute } from "@c8y/ngx-components";
// --- >8 ----
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from "@c8y/ngx-components/upgrade";
import { SubAssetsModule } from "@c8y/ngx-components/sub-assets";
import { ChildDevicesModule } from "@c8y/ngx-components/child-devices";
import { CockpitDashboardModule, ReportDashboardModule } from "@c8y/ngx-components/context-dashboard";
import { ReportsModule } from "@c8y/ngx-components/reports";
import { SensorPhoneModule } from "@c8y/ngx-components/sensor-phone";
import { BinaryFileDownloadModule } from "@c8y/ngx-components/binary-file-download";
import { SearchModule } from "@c8y/ngx-components/search";
import { AssetsNavigatorModule } from "@c8y/ngx-components/assets-navigator";
import { CockpitConfigModule } from "@c8y/ngx-components/cockpit-config";
import { DatapointLibraryModule } from "@c8y/ngx-components/datapoint-library";
import { WidgetsModule } from "@c8y/ngx-components/widgets";
import { PluginSetupStepperModule } from "@c8y/ngx-components/ecosystem/plugin-setup-stepper";
import { HelloComponent } from "./hello.component";
@NgModule({
declarations: [HelloComponent],
imports: [
// Upgrade module must be the first
UpgradeModule,
BrowserAnimationsModule,
RouterModule.forRoot(),
NgRouterModule.forRoot(
[{ path: "hello", component: HelloComponent }, ...UPGRADE_ROUTES],
{ enableTracing: false, useHash: true }
),
CoreModule.forRoot(),
ReportsModule,
NgUpgradeModule,
AssetsNavigatorModule,
DashboardUpgradeModule,
CockpitDashboardModule,
SensorPhoneModule,
ReportDashboardModule,
BinaryFileDownloadModule,
SearchModule,
SubAssetsModule,
ChildDevicesModule,
CockpitConfigModule,
DatapointLibraryModule.forRoot(),
WidgetsModule,
PluginSetupStepperModule,
],
// --- 8< changed part ----
providers: [
hookRoute({ // 1
path: "hello",
component: HelloComponent,
}),
hookNavigator({ // 1, 2
priority: 1000,
path: "/hello", // 3
icon: "rocket",
label: "Hello", // 4
}),
],
// --- >8 ----
})
exportclass AppModule extends HybridAppModule {
constructor(protected upgrade: NgUpgradeModule) {
super();
}
}
Explanation of the above comment numbers:
You provide the hookRoute and hookNavigator.
You use a certain value. For complex cases you can also define a useClass and a get() function.
You provide a path to your application, should always start with /.
You define what the navigator node should look like.
After you implement this extension hook you get a new entry in the navigator which looks like this:
Note that the property priority of the NavigatorNode interface defines in which order the nodes are shown.
The hello.component.ts is now like a blank canvas inside the Cockpit application.
You can implement any kind of feature you need, while the given functionality of the Cockpit is not affected.
Conclusion
A hybrid application is limited because of its angularjs and Angular integration.
However, the hook concept and a custom route allow for additions to existing hybrid applications.
They are a powerful tool to extend the build-in applications.
Sometimes additional features are needed and a pure Angular application is a better fit.
This depends on the use case.
Remove login page and authentication
Version: 1016.274.0 | Packages: @c8y/cli, @c8y/apps and @c8y/ngx-components
Info
This technique exposes the username and password. Ensure that this user doesn’t have access to sensible data.
The default application always takes you to the login page for authentication before it allows you to access a page.
This recipe will explain how to remove the login authentication and use the application directly.
Brief background
The removal of all authentication is not possible.
In order to get around it you must pass default credentials that the application will read upon request.
Your goal is to trigger the login with the default credentials before the application requests the login page because it is not authenticated.
The login functionality is part of the CoreModule in the @c8y/ngx-components package which is loaded when Angular bootstraps the application.
The default credentials must be passed to the API before that happens.
The result will be that, when Angular loads the initial page, the user will be already authenticated and the login page will be skipped.
1. Initialize the example application
As a starting point, you need an application.
For this purpose, create a new application using the c8ycli:
c8ycli new my-cockpit cockpit -a @c8y/apps@1016.274.0
This will create a new application that is an exact copy of the Cockpit application.
Next, you must install all dependencies.
Switch to the new folder and run npm install.
Info
The c8ycli new command has a -a flag which defines which package to use for scaffolding. This way you can also define which version of the application you want to scaffold, for example:
c8ycli new my-cockpit cockpit -a @c8y/apps@1016.274.0 will scaffold an application with the version 10.16.274.0
2. Add logic for default authentication
First you must make sure to add the default authentication before Angular bootstraps your custom application.
For that reason you must add a new provider in the app.module.ts in the newly created custom Cockpit application, which will be triggered before the login.
For that, use Angular’s injection token APP_INITIALIZER.
This token will ensure that the application will not be initialized until the new functionality is being executed.
To login with your default credentials, you must call the login function from the service and pass the authentication method and the default credentials.
With that, the recipe is completed and authentication will be done behind the scenes:
// --- 8< changed part ----
import { APP_INITIALIZER, NgModule } from '@angular/core';
// --- >8 ----
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
// --- 8< changed part ----
import { CoreModule, LoginService, RouterModule } from '@c8y/ngx-components';
// --- >8 ----
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { SubAssetsModule } from '@c8y/ngx-components/sub-assets';
import { ChildDevicesModule } from '@c8y/ngx-components/child-devices';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';
import { SearchModule } from '@c8y/ngx-components/search';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitConfigModule } from '@c8y/ngx-components/cockpit-config';
import { DatapointLibraryModule } from '@c8y/ngx-components/datapoint-library';
import { WidgetsModule } from '@c8y/ngx-components/widgets';
import { PluginSetupStepperModule } from '@c8y/ngx-components/ecosystem/plugin-setup-stepper';
@NgModule({
imports: [
// Upgrade module must be the first
UpgradeModule,
BrowserAnimationsModule,
RouterModule.forRoot(),
NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
CoreModule.forRoot(),
ReportsModule,
NgUpgradeModule,
AssetsNavigatorModule,
DashboardUpgradeModule,
CockpitDashboardModule,
SensorPhoneModule,
ReportDashboardModule,
BinaryFileDownloadModule,
SearchModule,
SubAssetsModule,
ChildDevicesModule,
CockpitConfigModule,
DatapointLibraryModule.forRoot(),
WidgetsModule,
PluginSetupStepperModule
],
providers: [
{
provide: APP_INITIALIZER,
useFactory: initApp,
multi: true,
deps: [LoginService],
},
]
})
exportclass AppModule extends HybridAppModule {
constructor(protected upgrade: NgUpgradeModule) {
super();
}
}
exportfunction initApp(loginService: LoginService) {
return () => {
const credentials = {
tenant: "tenantName",
user: "admin",
password: "C8Yadmin",
};
const basicAuth = loginService.useBasicAuth(credentials);
return loginService.login(basicAuth, credentials);
};
}
Conclusion
This tutorial shows how to remove authentication when developing a custom application.
This kind of technique can be used if an application does not have confidential information.
If you need data protection you should avoid this technique.
Request data from a custom microservice
Version: 1017.0.23 | Packages: @c8y/cli, @c8y/apps and @c8y/ngx-components
In some situations, the UI needs data from a custom microservice.
While you can always read that data with any HTTP client, for example, Angular’s HttpModule, you might want authentication out of the box.
This recipe shows how to access custom endpoints with the @c8y/client and get authenticated automatically.
First, it will take a deeper look at the basics to explain how the client works in Angular applications.
Basic: How the client works
Let’s first look at how the @c8y/client works and what its benefits are.
The client handles HTTP requests from the browser or, if desired, from node.js to the platform.
As most platform APIs are secured it allows to set the authentication to use.
Currently, there are two options for the authentication method:
BasicAuth: Adds a header to each request with the authentication details. This is less secure, as the password can be read by JavaScript.
CookieAuth: Reads a cookie set by the backend which allows accessing the platform. As a cookie is sent on each HTTP request, there is no particular authentication handling for this method except that it also sets an XSRF-TOKEN header to prevent cross-site scripting attacks.
When you set the authentication method on a new client instance you can define which authentication to use.
The client returns an object with all common endpoints of the platform.
For example, the following example requests data from the inventory via BasicAuth:
Each of the pre-configured endpoints returns an object containing the data, an optional paging object and the res object.
The response is given by fetch, a next-generation XHR API which is implemented in all modern browsers (and can be polyfilled for IE11).
In conclusion, the @c8y/client is a helper library for JavaScript that abstracts fetch to allow easy authentication and direct access on the common platform APIs.
The next section shows how you can use that concept in an Angular application with the help of the Dependency Injection (DI) model of Angular.
Basic: Interaction between @c8y/client and an Angular application
@c8y/ngx-components is an Angular module that allows to spin up an application.
It is, for example, used in our basic applications like Cockpit, Administration and Device managament to display the login screen.
When you spin up a new Angular-based application the @c8y/client and the @c8y/ngx-components are always included.
Moreover the ngx-components have a subpackage which is called @c8y/ngx-components/api and which exports a DataModule.
That module already imports all common endpoint services, so that you can use the standard dependency injection of Angular to access data.
The example above in an Angular application would look like this:
Use dependency injection to use the desired service. The DI concept of Angular will take care of all dependencies needed when the DataModule has been imported correctly in your main module.
You can now request data. Authentication is already handled. When used directly in a constructor or as an EntryComponent, the request might fail unauthorized as the component is loaded previous to the login module. To avoid this, inject the AppStateService. It provides a currentUser observable that updates as soon as a user is logged in.
This covers the overview on how to use the common endpoints.
The following recipe shows how to add a custom endpoint.
1. Initialize the example application
As a starting point, you need an application showing dashboards.
For this purpose, create a new Cockpit application using the c8ycli:
c8ycli new my-cockpit cockpit -a @c8y/apps@1017.0.23
Next, you must install all dependencies.
Switch to the new folder and run npm install.
Info
The c8ycli new command has a -a flag which defines which package to use for scaffolding. This way you can also define which version of the application you want to scaffold, for example:
c8ycli new my-cockpit cockpit -a @c8y/apps@1017.0.23 will scaffold an application with the version 10.17.0.23
2. Request data directly with fetch
If you want to access data from the endpoint service/acme via HTTP GET, the easiest way to achieve this with authentication is to reuse the fetch implementation of the client.
Add a file to the application and call it acme.component.ts:
Inject the FetchClient which is the fetch abstraction used by the client.
Request the data via fetchClient.fetch. The function is identical to the Fetch API, as a second parameter it accepts, for example, the method or data, except that it adds the authentication to the platform.
Parse the data and set it onto your controller to display it in the template.
Next, add a route to your application where you can show the component.
The following code does this in the app.module.ts, also refer to our other tutorials for more details:
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
import { CoreModule, RouterModule } from '@c8y/ngx-components';
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { SubAssetsModule } from '@c8y/ngx-components/sub-assets';
import { ChildDevicesModule } from '@c8y/ngx-components/child-devices';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';
import { SearchModule } from '@c8y/ngx-components/search';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitConfigModule } from '@c8y/ngx-components/cockpit-config';
import { DatapointLibraryModule } from '@c8y/ngx-components/datapoint-library';
import { WidgetsModule } from '@c8y/ngx-components/widgets';
import { PluginSetupStepperModule } from '@c8y/ngx-components/ecosystem/plugin-setup-stepper';
// ---- 8< added part ----
import { AcmeComponent } from './acme.component';
// ---- >8 ----
@NgModule({
imports: [
// Upgrade module must be the first
UpgradeModule,
BrowserAnimationsModule,
RouterModule.forRoot(),
// ---- 8< added part ----
NgRouterModule.forRoot([
{ path: 'acme', component: AcmeComponent },
...UPGRADE_ROUTES],
{ enableTracing: false, useHash: true }),
// ---- >8 ----
CoreModule.forRoot(),
ReportsModule,
NgUpgradeModule,
AssetsNavigatorModule,
DashboardUpgradeModule,
CockpitDashboardModule,
SensorPhoneModule,
ReportDashboardModule,
BinaryFileDownloadModule,
SearchModule,
SubAssetsModule,
ChildDevicesModule,
CockpitConfigModule,
DatapointLibraryModule.forRoot(),
WidgetsModule,
PluginSetupStepperModule
],
// ---- 8< added part ----
declarations: [
AcmeComponent
]
// ---- >8 ----
})
exportclass AppModule extends HybridAppModule {
constructor(protected upgrade: NgUpgradeModule) {
super();
}
}
3. Run and verify the application
When you run the application with c8ycli server and point your browser to the path defined in the module http://localhost:9000/apps/cockpit/#/acme, you should see the following:
The request fails as we don’t have a microservice with this context path running.
However, as you can see in the developer tools the request has an authorization cookie attached.
If the microservice existed, the request would pass and the data would be displayed.
4. Bonus: Write a Service.ts abstraction
In the example above, you have used the underlying fetch abstraction to directly access a custom microservice.
You might want to achieve the same simplicity for the common service of the client. It handles the URL and the JSON parsing for you internally.
To do so, extend the Service class returned by the @c8y/client and override the necessary methods or properties.
Do this for the acme microservice example by creating a new file called acme.service.ts:
By extending the service you get the same capabilities as of all common services in @c8y/client. The generic type, in this case, is set to any, to keep the example as easy as possible. It is a common pattern to create an interface that reflects the data you are sending via this service and replace any by this interface.
The URLs are the main entry points for this service. The pattern is always <<url>>/<<baseUrl>>/<<listUrl>>/<id>. If your microservice follows a different structure, you can override the getUrl method of the service class.
The constructor needs the current FetchClient imported via dependency injection. It also needs to get it passed to the extended Service class via super(). If you want your endpoint to support real time, you also must inject the RealTime abstraction here and pass it.
You can now override the detail() or list() implementation. You can call the super method only, modify the result of the super call or write your own implementation. The choice depends on the implementation details of your microservice.
Now you can reuse the AcmeService in the acme.component.ts:
Inject the services (1.) and directly do a list request on the service (2.). The service will throw an error which is why you wrap the call in a try/catch block and on error show an alert by adding the exception to the addServerFailure method (3.).
Conclusion
The examples above show how to access custom microservices via the client. While it might be simpler to use a well-known client abstraction like Angular’s HttpModule, reusing the @c8y/client gives you authentication out of the box.
This solution is more robust against changes as you can update the @c8y/client without worrying about underlying changes.
Implementing internationalization
Version: 1016.0.321 | Packages: @c8y/cli, @c8y/apps and @c8y/ngx-components
Introduction
Cumulocity provides an integrated tool which allows you to translate your content. This tool is based on the ngx-translate library. The CoreModule exports a preconfigured instance of this tool, with Cumulocity already being integrated. Refer to the ngx-translate Github page for more information.
For this tutorial, create a new application with minimal configuration.
Setting up a new application
Start with creating a new application based on version 1016.0.321 or higher, and based on the basic application project.
Execute the following commands:
c8ycli new my-app-i18n
cd my-app-i18n
npm install
After the application is set up, create a new basic module that you can use to add and translate your content.
Now you can run the application.
Initially, the application displays a single Translations menu item, which renders a blank page with the text: Index.
Extending default translations
Cumulocity comes with a wide range of content that is already translated into multiple languages. These translations can be extended by adding a custom *.po file for a language. This allows for both adding new translations and modifying the existing ones.
Example
You can override one of the existing strings, for example, “User settings”, to display “User settings (de)” instead of the default “Benutzereinstellungen”, with the following steps:
Restart the server and the application. Now you can select German and the User settings label is changed to User settings (de), as defined in the de.po file.
Info
You can find *.po files with default translations under node_modules/@c8y/ngx-components/locales. To override these files, copy them to your locales directory and add an import statement to index.ts like the one for de.po above.
Adding new languages
To define new languages which are not supported by default, follow the example below, to add language specific User settings.
Create a new translation file translations/locales/it.po:
Now you can select Italian and the User settings label is changed to User settings (it), as defined in the it.po file.
Info
Note that the entire UI can only be localized if you provide the respective data in the .po file.
Basic text translation
There are multiple ways of translating content. The most common is the translate pipe and directive, which is explained in the following section.
Translate pipe
The translate pipe is the most common way to translate content that is present in your HTML views. The following example works assuming that you have added a custom it.po file as described in the previous section.
In your translations/text-translation.component.html file, add:
<div>{{ 'User settings' | translate }}</div>
If your language is set to Italian, reloading the application renders the content as User settings (it).
The translate pipe allows you to include parameters in the translated strings. Add a new entry to the following files:
translations/locales/it.po:
msgid "Mr. Smith is {{ age }} years old"
msgstr "Sig. Smith ha {{ age }} anni"
translations/text-translation.component.ts:
exportclass TextTranslationComponent {
textWithParam = gettext('Mr. Smith is {{ age }} years old');
}
If you put the text wrapped with {{ ... }} directly in the template, you must escape curly braces which are part of the text.
For example: <div>{{ 'Mr. Smith is \{\{ age \}\} years old' | translate:{ age: 40 } }}</div>
This avoids compilation issues. The string extraction tool does not support such cases currently and you must put such a string in *.po files yourself.
Translate directive
Another way of translating content is to use the attribute translate, as shown in the example for translations/text-translation.component.html:
<div class="card">
<div class="card-header separator">
<h4 class="card-title">Translate directive example</h4>
</div>
<div class="card-block">
This phrase will be translated:
<span class="m-r-4" translate>User settings</span>
</div>
</div>
Similar to the example with the translate pipe, the content of the span is translated to User settings (it).
You can use parameters with the translate directive in the following way:
translations/locales/it.po:
msgid "{{ filteredItemsCount }} of {{ allItemsCount }} items."
msgstr "{{ filteredItemsCount }} of {{ allItemsCount }} items. (it)"
translations/text-translation.component.html:
<div class="card">
<div class="card-header separator">
<h4 class="card-title">Translate directive with parameters example</h4>
</div>
<div class="card-block">
This sentence will be translated:
<span class="m-r-4" ngNonBindable translate [translateParams]="{filteredItemsCount: 10, allItemsCount: 100 }">
{{ filteredItemsCount }} of {{ allItemsCount }} items.
</span>
</div>
</div>
In the example above, you must use Angular’s ngNonBindable directive in addition to the translate directive, so that Angular ignores curly braces and lets the translation service handle them.
Furthermore, you can translate entire HTML code blocks, as shown in the example below:
translations/locales/it.po:
msgid "Read about your current language in <a href=\"#guide\">our guide</a>"
msgstr "Read about your Italian language in <a href=\"#italian-guide\">our Italian guide</a>"
translations/text-translation.component.html:
<div class="card">
<div class="card-header separator">
<h4 class="card-title">Translate directive used on html code</h4>
</div>
<div class="card-block">
<span class="m-r-4" translate ngNonBindable>
Read about your current language in <a href="#guide">our guide</a>
</span>
</div>
</div>
Important
In general we recommend you to have ngNonBindable present while translating HTML blocks, because the Angular compiler might otherwise interfere with the translation service.
Translating content of variables
Your content can be located in TypeScript as string variables.
It is possible to translate such variables, as in the example below:
msgid "Text inside variable that is translatable"
msgstr "Text inside variable that is translatable (it)"
Info
Wrap such strings with the gettext function. This enables automatic extraction of the strings to the locales/locales.po file. This also indicates that such strings are meant to be translated.
See Extracting strings for translation using the locale-extract tool for information about extracting strings for translation.
Manual translation in TypeScript code
It is also possible to translate strings manually in TypeScript code.
To do so, inject the TranslateService into the component and use its instant method to translate the content:
Translating content using the instant method is a one-time operation, so the translation won’t be updated if the user changes the language and decides not to reload the application.
If you want the translation to be updated in such a case, we recommend you to use the stream method instead.
All subscriptions must be unsubscribed in order to prevent memory leaks. This can be avoided by using Angular’s async pipe on observables instead.
Extracting strings for translation using the locale-extract tool
You can use the c8ycli locale-extract command to extract strings from:
node_modules/@c8y/locales - which contains all the default strings and translations, which makes it easier to edit or add new ones.
. - which contains your custom modules, components, templates, services and more.
After using the command, a new directory ./locales will be created if it doesn’t exist yet.
It contains:
Files with the .po extension for all available languages. You can copy these files to the ./translations/locales directory, add necessary imports, and edit the translations as needed.
The locales.pot file which contains all strings that were marked with the translate pipe, the translate directive or the gettext method in your own source code. That means it does not include any default strings from node_modules/@c8y/locales. You can append these values to your custom *.po files and translate them.
Translating dates
In order to display dates according to the current locale settings, use the Angular date pipe, as shown in the example below:
<div class="card">
<div class="card-header separator">
<h4 class="card-title">Angular date pipe example</h4>
</div>
<div class="card-block">This date will be translated: {{ currentDate | date: 'medium' }}.</div>
</div>
Alternatively, use the c8yDate pipe to return dates in medium format. This also works with values outside of the range supported by ECMAScript:
translations/text-translation.component.html:
<div class="card">
<div class="card-header separator">
<h4 class="card-title">Cumulocity date pipe example</h4>
</div>
<div class="card-block">
<div>This date will be translated: {{ currentDate | c8yDate }}.</div>
<div>
This date exceeding the range supported by ECMAScript will be translated:
{{ 8640000000000000 + 1 | c8yDate }}.
</div>
</div>
</div>