diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/README.md b/README.md new file mode 100644 index 0000000..9d79675 --- /dev/null +++ b/README.md @@ -0,0 +1,362 @@ +# Cumulocity Plugin Demo +This is an inofficial demo showing how to build a simple plugin for the Cumulocity UI. + +> In this example the `widget-plugin` app was scaffold in version 1015.164.0. It is recommended to use your platform version. You can do this by running `c8ycli new` and select your preferred version. However, for plugins it is recommended to use at least 1016.0.0, as it is only in beta mode in 1015.x.x. + +## Prepare + - node 14 + - npm i -g @c8y/cli@next + - clone this repository + - switch to cumulocity-plugin-demo + +## 1. Step +Add your tenant and shell to npm start command. Run `npm start` and checkout the two application started. + + - shows exports/remotes in package.json + - shows debugging with `--shell` + +
+ Diff + +``` +diff --git a/package.json b/package.json +index b100dfe..a2dfd14 100644 +--- a/package.json ++++ b/package.json +@@ -3,7 +3,7 @@ + "version": "1.0.0", + "description": "This is the Cumulocity module federation plugin. Plugins can be developed like any Cumulocity application, but can be used at runtime by other applications. Therefore, they export an Angular module which can then be imported by any other application. The exports are defined in `package.json`:", + "scripts": { +- "start": "c8ycli server", ++ "start": "c8ycli server -u http://demos.cumulocity.com --shell cockpit", + "build": "c8ycli build", + "deploy": "c8ycli deploy", + "postinstall": "ngcc" + +``` + +
+ + > If not working -> checkout `step1` branch + +## 2. Step +Add a new plugin adding a navigator node + + - shows how simple a new plugin can be added + - shows how to add a navigator node with useClass + +
+ Diff + +``` +diff --git a/bookmarks/bookmarks.module.ts b/bookmarks/bookmarks.module.ts +new file mode 100644 +index 0000000..9907e4a +--- /dev/null ++++ b/bookmarks/bookmarks.module.ts +@@ -0,0 +1,15 @@ ++import { NgModule } from "@angular/core"; ++import { CommonModule } from "@angular/common"; ++import { BookmarksService } from "./bookmarks.service"; ++import { HOOK_NAVIGATOR_NODES } from "@c8y/ngx-components"; ++ ++@NgModule({ ++ declarations: [], ++ imports: [CommonModule], ++ exports: [], ++ providers: [ ++ BookmarksService, ++ { provide: HOOK_NAVIGATOR_NODES, useClass: BookmarksService, multi: true }, ++ ], ++}) ++export class BookmarksModule {} +diff --git a/bookmarks/bookmarks.service.ts b/bookmarks/bookmarks.service.ts +new file mode 100644 +index 0000000..a3e0765 +--- /dev/null ++++ b/bookmarks/bookmarks.service.ts +@@ -0,0 +1,15 @@ ++import { Injectable } from '@angular/core'; ++import { NavigatorNode } from '@c8y/ngx-components'; ++ ++@Injectable({ ++ providedIn: 'root' ++}) ++export class BookmarksService { ++ get() { ++ return new NavigatorNode({ ++ label: 'Bookmarks', ++ icon: 'bookmark', ++ priority: -1000 ++ }); ++ } ++} +\ No newline at end of file +diff --git a/package.json b/package.json +index a2dfd14..bd81906 100644 +--- a/package.json ++++ b/package.json +@@ -62,11 +62,17 @@ + "module": "WidgetPluginModule", + "path": "./widget/widget-plugin.module.ts", + "description": "Adds a custom widget to the shell application" ++ }, ++ { ++ "name": "Device bookmarks", ++ "module": "BookmarksModule", ++ "path": "./bookmarks/bookmarks.module.ts", ++ "description": "Allows you to bookmark your favorite device" + } + ], + "remotes": { + "summit-ui-demo": [ +- "WidgetPluginModule" ++ "WidgetPluginModule", "BookmarksModule" + ] + } + } + +``` + +
+ + > If not working -> checkout `step2` branch + + + ## 3. Step +Add logic and injection to the HOOK + + - injection can be tricky. Here it's quite simple as all is on the root injector. + +
+ Diff + +``` +diff --git a/bookmarks/bookmarks.service.ts b/bookmarks/bookmarks.service.ts +index a3e0765..00c23fc 100644 +--- a/bookmarks/bookmarks.service.ts ++++ b/bookmarks/bookmarks.service.ts +@@ -1,15 +1,30 @@ +-import { Injectable } from '@angular/core'; +-import { NavigatorNode } from '@c8y/ngx-components'; ++import { Injectable } from "@angular/core"; ++import { InventoryService } from "@c8y/client"; ++import { NavigatorNode } from "@c8y/ngx-components"; ++import { AssetNode, AssetNodeService } from "@c8y/ngx-components/assets-navigator"; + + @Injectable({ +- providedIn: 'root' ++ providedIn: "root", + }) + export class BookmarksService { +- get() { +- return new NavigatorNode({ +- label: 'Bookmarks', +- icon: 'bookmark', +- priority: -1000 ++ constructor(private inventory: InventoryService, private assetService: AssetNodeService) {} ++ ++ async get() { ++ const { data: bookmarkedDevices } = await this.inventory.list({ ++ fragmentType: "c8y_IsBookmarked", ++ pageSize: 2000 ++ }); ++ ++ const children = bookmarkedDevices.map((mo) => new AssetNode(this.assetService, { mo })); ++ ++ const rootNode = new NavigatorNode({ ++ label: "Bookmarks", ++ icon: "bookmark", ++ priority: -1000, + }); ++ ++ rootNode.children = children; ++ ++ return rootNode; + } +-} +\ No newline at end of file ++} +``` + +
+ + > If not working -> checkout `step3` branch + +## 4. Step +Add a action bar for all devices + + - use routing to check if on a device + - use content projection to avoid root element + +
+ Diff + +``` + diff --git a/bookmarks/add-bookmark.component.ts b/bookmarks/add-bookmark.component.ts +new file mode 100644 +index 0000000..8adaff1 +--- /dev/null ++++ b/bookmarks/add-bookmark.component.ts +@@ -0,0 +1,24 @@ ++import { Component, ViewChild, ViewContainerRef } from "@angular/core"; ++ ++@Component({ ++ selector: "[c8y-add-bookmark]", ++ template: ` ++ ++
  • ++ ++
  • ++
    ++ `, ++}) ++export class AddBookmarkComponent { ++ @ViewChild("template", { static: true }) template; ++ ++ constructor(private viewContainerRef: ViewContainerRef) {} ++ ++ ngOnInit() { ++ this.viewContainerRef.createEmbeddedView(this.template); ++ } ++} +diff --git a/bookmarks/add-bookmark.service.ts b/bookmarks/add-bookmark.service.ts +new file mode 100644 +index 0000000..242e9e5 +--- /dev/null ++++ b/bookmarks/add-bookmark.service.ts +@@ -0,0 +1,22 @@ ++import { Injectable } from "@angular/core"; ++import { Router } from "@angular/router"; ++import { ActionBarItem } from "@c8y/ngx-components"; ++import { AddBookmarkComponent } from "./add-bookmark.component"; ++ ++@Injectable({ ++ providedIn: "root", ++}) ++export class AddBookmarkService { ++ constructor(private router: Router) {} ++ ++ get() { ++ if (/device\/\w+/.test(this.router.url)) { ++ return { ++ template: AddBookmarkComponent, ++ priority: 0, ++ placement: "more", ++ } as ActionBarItem; ++ } ++ return []; ++ } ++} +diff --git a/bookmarks/bookmarks.module.ts b/bookmarks/bookmarks.module.ts +index 9907e4a..c370178 100644 +--- a/bookmarks/bookmarks.module.ts ++++ b/bookmarks/bookmarks.module.ts +@@ -1,15 +1,18 @@ + import { NgModule } from "@angular/core"; +-import { CommonModule } from "@angular/common"; ++import { CommonModule } from "@c8y/ngx-components"; + import { BookmarksService } from "./bookmarks.service"; +-import { HOOK_NAVIGATOR_NODES } from "@c8y/ngx-components"; ++import { AddBookmarkService } from "./add-bookmark.service"; ++import { HOOK_ACTION_BAR, HOOK_NAVIGATOR_NODES } from "@c8y/ngx-components"; ++import { AddBookmarkComponent } from "./add-bookmark.component"; + + @NgModule({ +- declarations: [], ++ declarations: [AddBookmarkComponent], + imports: [CommonModule], + exports: [], + providers: [ + BookmarksService, + { provide: HOOK_NAVIGATOR_NODES, useClass: BookmarksService, multi: true }, ++ { provide: HOOK_ACTION_BAR, useClass: AddBookmarkService, multi: true } + ], + }) + export class BookmarksModule {} + +``` + +
    + +## 5. Step +Add the logic to bookmark a device + + - get the context + - update the device + +
    + Diff + +``` +diff --git a/bookmarks/add-bookmark.component.ts b/bookmarks/add-bookmark.component.ts +index d16c009..bc69b7d 100644 +--- a/bookmarks/add-bookmark.component.ts ++++ b/bookmarks/add-bookmark.component.ts +@@ -1,4 +1,11 @@ + import { Component, ViewChild, ViewContainerRef } from "@angular/core"; ++import { Router } from "@angular/router"; ++import { InventoryService } from "@c8y/client"; ++import { ++ ContextRouteService, ++ getActivatedRoute, ++ NavigatorService, ++} from "@c8y/ngx-components"; + + @Component({ + selector: "[c8y-add-bookmark]", +@@ -8,8 +15,9 @@ import { Component, ViewChild, ViewContainerRef } from "@angular/core"; + + +@@ -17,11 +25,35 @@ import { Component, ViewChild, ViewContainerRef } from "@angular/core"; + `, + }) + export class AddBookmarkComponent { ++ isBookmarked = true; + @ViewChild("template", { static: true }) template; + +- constructor(private viewContainerRef: ViewContainerRef) {} ++ constructor( ++ private viewContainerRef: ViewContainerRef, ++ private router: Router, ++ private inventory: InventoryService, ++ private contextRoute: ContextRouteService, ++ private navigator: NavigatorService ++ ) {} + + ngOnInit() { + this.viewContainerRef.createEmbeddedView(this.template); ++ this.isBookmarked = !!this.contextRoute.getContextData( ++ getActivatedRoute(this.router) ++ )?.contextData?.c8y_IsBookmarked; ++ } ++ ++ async bookmark() { ++ const currentDevice = this.contextRoute.getContextData( ++ getActivatedRoute(this.router) ++ )?.contextData; ++ if (currentDevice) { ++ await this.inventory.update({ ++ id: currentDevice.id as string, ++ c8y_IsBookmarked: this.isBookmarked ? null : {}, ++ }); ++ this.isBookmarked = !this.isBookmarked; ++ } ++ this.navigator.refresh(); + } + } + +``` + +
    diff --git a/app.module.ts b/app.module.ts new file mode 100644 index 0000000..ba322a8 --- /dev/null +++ b/app.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterModule as ngRouterModule } from '@angular/router'; +import { BootstrapComponent, CoreModule, RouterModule } from '@c8y/ngx-components'; +import { BsModalRef } from 'ngx-bootstrap/modal'; +import { CockpitDashboardModule } from '@c8y/ngx-components/context-dashboard'; + +@NgModule({ + imports: [ + BrowserAnimationsModule, + ngRouterModule.forRoot([], { enableTracing: false, useHash: true }), + RouterModule.forRoot(), + CoreModule.forRoot(), + CockpitDashboardModule, + ], + providers: [BsModalRef], + bootstrap: [BootstrapComponent] +}) +export class AppModule {} diff --git a/bookmarks/add-bookmark.component.ts b/bookmarks/add-bookmark.component.ts new file mode 100644 index 0000000..bc69b7d --- /dev/null +++ b/bookmarks/add-bookmark.component.ts @@ -0,0 +1,59 @@ +import { Component, ViewChild, ViewContainerRef } from "@angular/core"; +import { Router } from "@angular/router"; +import { InventoryService } from "@c8y/client"; +import { + ContextRouteService, + getActivatedRoute, + NavigatorService, +} from "@c8y/ngx-components"; + +@Component({ + selector: "[c8y-add-bookmark]", + template: ` + +
  • + +
  • +
    + `, +}) +export class AddBookmarkComponent { + isBookmarked = true; + @ViewChild("template", { static: true }) template; + + constructor( + private viewContainerRef: ViewContainerRef, + private router: Router, + private inventory: InventoryService, + private contextRoute: ContextRouteService, + private navigator: NavigatorService + ) {} + + ngOnInit() { + this.viewContainerRef.createEmbeddedView(this.template); + this.isBookmarked = !!this.contextRoute.getContextData( + getActivatedRoute(this.router) + )?.contextData?.c8y_IsBookmarked; + } + + async bookmark() { + const currentDevice = this.contextRoute.getContextData( + getActivatedRoute(this.router) + )?.contextData; + if (currentDevice) { + await this.inventory.update({ + id: currentDevice.id as string, + c8y_IsBookmarked: this.isBookmarked ? null : {}, + }); + this.isBookmarked = !this.isBookmarked; + } + this.navigator.refresh(); + } +} diff --git a/bookmarks/add-bookmark.service.ts b/bookmarks/add-bookmark.service.ts new file mode 100644 index 0000000..242e9e5 --- /dev/null +++ b/bookmarks/add-bookmark.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from "@angular/core"; +import { Router } from "@angular/router"; +import { ActionBarItem } from "@c8y/ngx-components"; +import { AddBookmarkComponent } from "./add-bookmark.component"; + +@Injectable({ + providedIn: "root", +}) +export class AddBookmarkService { + constructor(private router: Router) {} + + get() { + if (/device\/\w+/.test(this.router.url)) { + return { + template: AddBookmarkComponent, + priority: 0, + placement: "more", + } as ActionBarItem; + } + return []; + } +} diff --git a/bookmarks/bookmarks.module.ts b/bookmarks/bookmarks.module.ts new file mode 100644 index 0000000..c370178 --- /dev/null +++ b/bookmarks/bookmarks.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@c8y/ngx-components"; +import { BookmarksService } from "./bookmarks.service"; +import { AddBookmarkService } from "./add-bookmark.service"; +import { HOOK_ACTION_BAR, HOOK_NAVIGATOR_NODES } from "@c8y/ngx-components"; +import { AddBookmarkComponent } from "./add-bookmark.component"; + +@NgModule({ + declarations: [AddBookmarkComponent], + imports: [CommonModule], + exports: [], + providers: [ + BookmarksService, + { provide: HOOK_NAVIGATOR_NODES, useClass: BookmarksService, multi: true }, + { provide: HOOK_ACTION_BAR, useClass: AddBookmarkService, multi: true } + ], +}) +export class BookmarksModule {} diff --git a/bookmarks/bookmarks.service.ts b/bookmarks/bookmarks.service.ts new file mode 100644 index 0000000..00c23fc --- /dev/null +++ b/bookmarks/bookmarks.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from "@angular/core"; +import { InventoryService } from "@c8y/client"; +import { NavigatorNode } from "@c8y/ngx-components"; +import { AssetNode, AssetNodeService } from "@c8y/ngx-components/assets-navigator"; + +@Injectable({ + providedIn: "root", +}) +export class BookmarksService { + constructor(private inventory: InventoryService, private assetService: AssetNodeService) {} + + async get() { + const { data: bookmarkedDevices } = await this.inventory.list({ + fragmentType: "c8y_IsBookmarked", + pageSize: 2000 + }); + + const children = bookmarkedDevices.map((mo) => new AssetNode(this.assetService, { mo })); + + const rootNode = new NavigatorNode({ + label: "Bookmarks", + icon: "bookmark", + priority: -1000, + }); + + rootNode.children = children; + + return rootNode; + } +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..8aa7bce --- /dev/null +++ b/index.ts @@ -0,0 +1,16 @@ +import './polyfills'; + +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './app.module'; + +declare const __MODE__: string; +if (__MODE__ === 'production') { + enableProdMode(); +} + +export function bootstrap() { + platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch(err => console.log(err)); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bd81906 --- /dev/null +++ b/package.json @@ -0,0 +1,83 @@ +{ + "name": "c8y-summit-ui-demo", + "version": "1.0.0", + "description": "This is the Cumulocity module federation plugin. Plugins can be developed like any Cumulocity application, but can be used at runtime by other applications. Therefore, they export an Angular module which can then be imported by any other application. The exports are defined in `package.json`:", + "scripts": { + "start": "c8ycli server -u http://iot-summit-berlin.dev.c8y.io --shell cockpit", + "build": "c8ycli build", + "deploy": "c8ycli deploy", + "postinstall": "ngcc" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@angular/animations": "14.0.6", + "@angular/cdk": "14.1.2", + "@angular/common": "14.0.6", + "@angular/compiler": "14.0.6", + "@angular/core": "14.0.6", + "@angular/forms": "14.0.6", + "@angular/platform-browser": "14.0.6", + "@angular/platform-browser-dynamic": "14.0.6", + "@angular/router": "14.0.6", + "@angular/upgrade": "14.0.6", + "@c8y/client": "1015.164.0", + "@c8y/ngx-components": "1015.164.0", + "@ngx-translate/core": "14.0.0", + "rxjs": "~6.6.3", + "zone.js": "~0.11.7", + "@c8y/style": "1015.164.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "14.0.6", + "@angular/compiler-cli": "14.0.6", + "@angular/language-service": "14.0.6", + "@angular/service-worker": "14.0.6", + "@angular/localize": "14.0.6", + "@types/jest": "^28.1.6", + "@types/webpack": "^5.28.0", + "file-loader": "^6.2.0", + "jest": "^28.1.3", + "jest-preset-angular": "^12.2.0", + "typescript": "4.7.4", + "style-loader": "3.3.1", + "html-loader": "4.1.0", + "@c8y/cli": "1015.164.0" + }, + "c8y": { + "application": { + "name": "summit-ui-demo", + "description": "Custom widget with module federation", + "contextPath": "summit-ui-demo", + "key": "summit-ui-demo-application-key", + "globalTitle": "Custom widget with Module Federation", + "tabsHorizontal": true, + "isPackage": true, + "noAppSwitcher": true, + "package": "plugin", + "exports": [ + { + "name": "Example widget plugin", + "module": "WidgetPluginModule", + "path": "./widget/widget-plugin.module.ts", + "description": "Adds a custom widget to the shell application" + }, + { + "name": "Device bookmarks", + "module": "BookmarksModule", + "path": "./bookmarks/bookmarks.module.ts", + "description": "Allows you to bookmark your favorite device" + } + ], + "remotes": { + "summit-ui-demo": [ + "WidgetPluginModule", "BookmarksModule" + ] + } + } + }, + "browserslist": [ + "last 2 major versions" + ] +} diff --git a/polyfills.ts b/polyfills.ts new file mode 100644 index 0000000..5da3c97 --- /dev/null +++ b/polyfills.ts @@ -0,0 +1,33 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + */ + +(window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame +// (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick +(window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove', 'message']; + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. diff --git a/setup-jest.js b/setup-jest.js new file mode 100644 index 0000000..5553a4a --- /dev/null +++ b/setup-jest.js @@ -0,0 +1 @@ +import 'jest-preset-angular/setup-jest'; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..788b459 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "experimentalDecorators": true, + "target": "es6", + "module": "es2020", + "lib": ["dom", "es2015", "es2016"] + }, + "angularCompilerOptions": { + "enableIvy": true + } +} diff --git a/tsconfig.spec.json b/tsconfig.spec.json new file mode 100644 index 0000000..fda7443 --- /dev/null +++ b/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": ["jest"], + "esModuleInterop": true + }, + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] +} \ No newline at end of file