@c8y/tutorial 1022.4.17 → 1022.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -213,6 +213,13 @@ export default {
213
213
  description: 'A sample for action bar hook.',
214
214
  scope: 'self'
215
215
  },
216
+ {
217
+ name: 'Preview feature',
218
+ module: 'PreviewFeatureModule',
219
+ path: './src/hooks/preview-feature/preview-feature.module.ts',
220
+ description: 'A sample for feature preview hook.',
221
+ scope: 'self'
222
+ },
216
223
  {
217
224
  name: 'Breadcrumbs hook',
218
225
  module: 'BreadcrumbsModule',
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@c8y/tutorial",
3
- "version": "1022.4.17",
3
+ "version": "1022.6.1",
4
4
  "description": "This package is used to scaffold a tutorial for Cumulocity IoT Web SDK which explains a lot of concepts.",
5
5
  "dependencies": {
6
- "@c8y/style": "1022.4.17",
7
- "@c8y/ngx-components": "1022.4.17",
8
- "@c8y/client": "1022.4.17",
9
- "@c8y/bootstrap": "1022.4.17",
6
+ "@c8y/style": "1022.6.1",
7
+ "@c8y/ngx-components": "1022.6.1",
8
+ "@c8y/client": "1022.6.1",
9
+ "@c8y/bootstrap": "1022.6.1",
10
10
  "@angular/cdk": "^19.2.18",
11
11
  "ngx-bootstrap": "19.0.2",
12
12
  "leaflet": "1.9.4",
13
13
  "rxjs": "7.8.1"
14
14
  },
15
15
  "devDependencies": {
16
- "@c8y/options": "1022.4.17",
17
- "@c8y/devkit": "1022.4.17"
16
+ "@c8y/options": "1022.6.1",
17
+ "@c8y/devkit": "1022.6.1"
18
18
  },
19
19
  "peerDependencies": {
20
20
  "@angular/common": ">=19 <20"
@@ -0,0 +1,55 @@
1
+ import { IFetchResponse } from '@c8y/client';
2
+ import { ApiCall, HttpHandler, HttpInterceptor } from '@c8y/ngx-components/api';
3
+ import { handleRequest } from '../utils/common';
4
+ import { Observable } from 'rxjs';
5
+
6
+ const LOCAL_STORAGE_KEY = 'previewFeatureState';
7
+
8
+ export class FeatureApiInterceptor implements HttpInterceptor {
9
+ features = [
10
+ {
11
+ active: localStorage.getItem(LOCAL_STORAGE_KEY),
12
+ phase: 'PUBLIC_PREVIEW',
13
+ key: 'preview-feature-key',
14
+ strategy: 'TENANT'
15
+ }
16
+ ];
17
+
18
+ intercept(req: ApiCall, next: HttpHandler): Observable<IFetchResponse> {
19
+ return handleRequest(req, next, '/features', {
20
+ POST: this.mockPOST.bind(this),
21
+ PUT: this.mockPUT.bind(this),
22
+ GET: this.mockGET.bind(this)
23
+ });
24
+ }
25
+
26
+ mockPOST(_requestDescriptor: string) {
27
+ return null;
28
+ }
29
+
30
+ mockPUT(requestDescriptor: string) {
31
+ const match = requestDescriptor.match(/\/features\/([^/]+)\/by-tenant/);
32
+ if (match) {
33
+ const key = match[1];
34
+ const bodyStartIndex = requestDescriptor.indexOf('{');
35
+ const body = bodyStartIndex !== -1 ? JSON.parse(requestDescriptor.slice(bodyStartIndex)) : {};
36
+ const feature = this.features.find(f => f.key === key);
37
+ if (feature) {
38
+ feature.active = body.active;
39
+ localStorage.setItem(LOCAL_STORAGE_KEY, String(body.active));
40
+ }
41
+ }
42
+
43
+ return {
44
+ status: 200,
45
+ json: async () => this.features
46
+ };
47
+ }
48
+
49
+ private async mockGET(_requestDescriptor: string) {
50
+ return {
51
+ status: 200,
52
+ json: async () => this.features
53
+ };
54
+ }
55
+ }
@@ -2,7 +2,12 @@ export * from './mock.service';
2
2
  export * from './mock.model';
3
3
  export * from './mock.realtime';
4
4
  export * from './mock.realtime-subject';
5
- import { AppStateService, OptionsService, RealtimeSubjectService } from '@c8y/ngx-components';
5
+ import {
6
+ AppStateService,
7
+ OptionsService,
8
+ Permissions,
9
+ RealtimeSubjectService
10
+ } from '@c8y/ngx-components';
6
11
  import { InventoryInterceptor } from './global-mocks/inventory.interceptor';
7
12
  import { MeasurementsInterceptor } from './global-mocks/measurements.interceptor';
8
13
  import { API_MOCK_CONFIG, ApiMockConfig } from './mock.model';
@@ -18,6 +23,7 @@ import { MeasurementsSeriesInterceptor } from './scoped-mocks/measurement-series
18
23
  import { EnvironmentProviders, Provider, inject, provideAppInitializer } from '@angular/core';
19
24
  import { MockService } from './mock.service';
20
25
  import { IUser } from '@c8y/client';
26
+ import { FeatureApiInterceptor } from './global-mocks/feature-api';
21
27
 
22
28
  export function provideAPIMock() {
23
29
  return [
@@ -146,6 +152,15 @@ export function provideAPIMock() {
146
152
  } as ApiMockConfig,
147
153
  multi: true
148
154
  },
155
+ {
156
+ provide: API_MOCK_CONFIG,
157
+ useValue: {
158
+ // The interceptors are sorted by their ID, so the scoped interceptors should be before the global ones.
159
+ id: 'z-global-feature-preview-interceptor-interceptor',
160
+ mockService: FeatureApiInterceptor
161
+ } as ApiMockConfig,
162
+ multi: true
163
+ },
149
164
  {
150
165
  provide: RealtimeSubjectService,
151
166
  useExisting: RealtimeSubjectServiceWithMocking
@@ -161,7 +176,16 @@ export function provideAPIMock() {
161
176
  appStateService.currentUser.next({
162
177
  id: 'NO_LOGIN',
163
178
  userName: 'noLogin',
164
- displayName: 'noLogin'
179
+ displayName: 'noLogin',
180
+ roles: {
181
+ references: [
182
+ {
183
+ role: {
184
+ id: Permissions.ROLE_TENANT_MANAGEMENT_ADMIN
185
+ }
186
+ }
187
+ ] as any
188
+ }
165
189
  } as IUser);
166
190
  }
167
191
  };
@@ -20,8 +20,7 @@ import { DeviceGridModule } from '@c8y/ngx-components/device-grid';
20
20
  import { ServerTreeGridExampleService } from './server-tree-grid-example.service';
21
21
 
22
22
  /**
23
- * This is an example of using DataGridComponent for displaying, filtering and sorting managed objects
24
- * using customized columns and dynamically built inventory queries.
23
+ * This is an example of using DataGridComponent as a tree grid for displaying hierarchical data.
25
24
  */
26
25
  @Component({
27
26
  selector: 'c8y-server-tree-grid-example',
@@ -73,7 +72,7 @@ export class ServerTreeGridExampleComponent implements GridConfigContextProvider
73
72
  * You can provide data here that can be used for grid configration storage,
74
73
  * action control matchers, etc.
75
74
  */
76
- key: 'server-grid-example'
75
+ key: 'server-tree-grid-example'
77
76
  };
78
77
  }
79
78
 
@@ -83,13 +82,14 @@ export class ServerTreeGridExampleComponent implements GridConfigContextProvider
83
82
  }
84
83
 
85
84
  /**
86
- * This method loads data when data grid requests it (e.g. on initial load or on column settings change).
87
- * It gets the object with current data grid setup and is supposed to return:
85
+ * This method loads data when data grid requests it (e.g. on initial load or when a row with child entries is expanded).
86
+ * It receives the `DataSourceModifier` context with current data grid setup and is supposed to return:
88
87
  * full response, list of items, paging object, the number of items in the filtered subset, the number of all items.
89
88
  */
90
89
  async onDataSourceModifier(
91
90
  dataSourceModifier: DataSourceModifier
92
91
  ): Promise<ServerSideDataResult> {
92
+ // If the `DataSourceModifier` context object has a `parentRow`, it means we are loading child nodes for a specific parent row.
93
93
  const { parentRow } = dataSourceModifier;
94
94
  if (parentRow) {
95
95
  const { res, data, paging } = await this.service.getChildDevices(
@@ -98,6 +98,8 @@ export class ServerTreeGridExampleComponent implements GridConfigContextProvider
98
98
  );
99
99
 
100
100
  data.forEach(row => {
101
+ // The `hasChildren` property should be set for each row to indicate if it has child entries.
102
+ // This is used by the grid to display expand/collapse icons.
101
103
  row.hasChildren = row.childDevices.count > 0;
102
104
  });
103
105
 
@@ -0,0 +1,11 @@
1
+ <c8y-title>Custom hook preview</c8y-title>
2
+ <div class="card">
3
+ <div class="card-block">
4
+ <p>
5
+ This is an example component of a feature which can be activated in "Manage preview features"
6
+ because it was registered with
7
+ <code>hookPreview</code>
8
+ with custom content.
9
+ </p>
10
+ </div>
11
+ </div>
@@ -0,0 +1,18 @@
1
+ import { Component } from '@angular/core';
2
+ import { CoreModule } from '@c8y/ngx-components';
3
+
4
+ /**
5
+ * This is a standard angular component.
6
+ * Obviously it does not do anything.
7
+ */
8
+ @Component({
9
+ selector: 'tut-custom-basic-preview-feature-view',
10
+ templateUrl: './basic-view-custom.component.html',
11
+ standalone: true,
12
+ imports: [CoreModule]
13
+ })
14
+ export class CustomBasicViewComponent {
15
+ /**
16
+ * Your content of the Basic View goes in here!
17
+ */
18
+ }
@@ -0,0 +1,31 @@
1
+ import { inject, Injectable } from '@angular/core';
2
+ import { NavigatorNode, NavigatorNodeFactory, PreviewService } from '@c8y/ngx-components';
3
+ import { distinctUntilChanged, map, Observable } from 'rxjs';
4
+
5
+ @Injectable()
6
+ export class ExampleCustomPreviewFeatureNavigationFactory implements NavigatorNodeFactory {
7
+ private readonly previewFeatureService = inject(PreviewService);
8
+
9
+ get(): Observable<NavigatorNode[]> {
10
+ // For custom features we provide the label as they have no key
11
+ const customFeatureLabel = 'Custom feature preview';
12
+ return this.previewFeatureService.getState$(customFeatureLabel).pipe(
13
+ distinctUntilChanged(),
14
+ map(state => {
15
+ if (state) {
16
+ return [
17
+ new NavigatorNode({
18
+ priority: 100,
19
+ path: 'hooks/preview-feature-custom',
20
+ icon: 'science',
21
+ label: 'Feature Preview Custom',
22
+ parent: 'Hooks',
23
+ preventDuplicates: true
24
+ })
25
+ ];
26
+ }
27
+ return [];
28
+ })
29
+ );
30
+ }
31
+ }
@@ -0,0 +1,45 @@
1
+ import { NgModule } from '@angular/core';
2
+ import { hookNavigator, hookPreview, hookRoute } from '@c8y/ngx-components';
3
+ import { BehaviorSubject } from 'rxjs';
4
+ import { ExampleCustomPreviewFeatureNavigationFactory } from './preview-feature-custom.factory';
5
+
6
+ // needed only for the example, we will store the state in local storage
7
+ const LOCAL_STORAGE_KEY = 'customPreviewFeatureState';
8
+ const savedState = localStorage.getItem(LOCAL_STORAGE_KEY) === 'true';
9
+ const customPreviewFeatureState$ = new BehaviorSubject<boolean>(savedState);
10
+
11
+ /**
12
+ * Use our predefined InjectionTokens and provide your own classes to extend behavior
13
+ * and functionality of existing ones. Implement your own NavigationNodes, Tabs, Actions and Breadcrumbs.
14
+ * Note: Hooks should always be implemented in the module where they are used, so that
15
+ * a module can act standalone and has no dependencies on other modules.
16
+ */
17
+ export const customHooks = [
18
+ hookRoute({
19
+ path: 'hooks/preview-feature-custom',
20
+ loadComponent: () =>
21
+ import('./basic-view-custom.component').then(m => m.CustomBasicViewComponent)
22
+ }),
23
+ hookNavigator(ExampleCustomPreviewFeatureNavigationFactory),
24
+ hookPreview({
25
+ active$: customPreviewFeatureState$.asObservable(),
26
+ onToggle: async (state: boolean) => {
27
+ localStorage.setItem(LOCAL_STORAGE_KEY, String(state));
28
+ customPreviewFeatureState$.next(state);
29
+ return true;
30
+ },
31
+ label: 'Custom feature preview',
32
+ description: () => Promise.resolve('This is a custom feature'),
33
+ settings: {
34
+ reload: true
35
+ }
36
+ })
37
+ ];
38
+
39
+ @NgModule({
40
+ /**
41
+ * Adding the hooks to the providers:
42
+ */
43
+ providers: [...customHooks]
44
+ })
45
+ export class PreviewFeatureCustomModule {}
@@ -0,0 +1,11 @@
1
+ <c8y-title>Hook Preview</c8y-title>
2
+ <div class="card">
3
+ <div class="card-block">
4
+ <p>
5
+ This is an example component of a feature which can be activated in "Manage preview features"
6
+ because it was registered with
7
+ <code>hookPreview</code>
8
+ .
9
+ </p>
10
+ </div>
11
+ </div>
@@ -0,0 +1,18 @@
1
+ import { Component } from '@angular/core';
2
+ import { CoreModule } from '@c8y/ngx-components';
3
+
4
+ /**
5
+ * This is a standard angular component.
6
+ * Obviously it does not do anything.
7
+ */
8
+ @Component({
9
+ selector: 'tut-basic-preview-feature-view',
10
+ templateUrl: './basic-view-default.component.html',
11
+ standalone: true,
12
+ imports: [CoreModule]
13
+ })
14
+ export class BasicViewComponent {
15
+ /**
16
+ * Your content of the Basic View goes in here!
17
+ */
18
+ }
@@ -0,0 +1,34 @@
1
+ import { NgModule } from '@angular/core';
2
+ import { hookNavigator, hookPreview, hookRoute } from '@c8y/ngx-components';
3
+ import { ExamplePreviewFeatureNavigationFactory } from './preview-feature.factory';
4
+
5
+ /**
6
+ * Use our predefined InjectionTokens and provide your own classes to extend behavior
7
+ * and functionality of existing ones. Implement your own NavigationNodes, Tabs, Actions and Breadcrumbs.
8
+ * Note: Hooks should always be implemented in the module where they are used, so that
9
+ * a module can act standalone and has no dependencies on other modules.
10
+ */
11
+ export const hooks = [
12
+ hookRoute({
13
+ path: 'hooks/preview-feature-default',
14
+ loadComponent: () => import('./basic-view-default.component').then(m => m.BasicViewComponent)
15
+ }),
16
+ hookNavigator(ExamplePreviewFeatureNavigationFactory),
17
+ hookPreview({
18
+ key: 'preview-feature-key',
19
+ label: 'Example preview feature relying on feature toggles API',
20
+ description: () =>
21
+ import('@c8y/style/markdown-files/codex-example-markdown.md').then(m => m.default),
22
+ settings: {
23
+ reload: true
24
+ }
25
+ })
26
+ ];
27
+
28
+ @NgModule({
29
+ /**
30
+ * Adding the hooks to the providers:
31
+ */
32
+ providers: [...hooks]
33
+ })
34
+ export class PreviewFeatureDefaultModule {}
@@ -0,0 +1,29 @@
1
+ import { inject, Injectable } from '@angular/core';
2
+ import { NavigatorNode, NavigatorNodeFactory, PreviewService } from '@c8y/ngx-components';
3
+ import { distinctUntilChanged, map, Observable } from 'rxjs';
4
+
5
+ @Injectable()
6
+ export class ExamplePreviewFeatureNavigationFactory implements NavigatorNodeFactory {
7
+ private readonly previewFeatureService = inject(PreviewService);
8
+
9
+ get(): Observable<NavigatorNode[]> {
10
+ return this.previewFeatureService.getState$('preview-feature-key').pipe(
11
+ distinctUntilChanged(),
12
+ map(state => {
13
+ if (state) {
14
+ return [
15
+ new NavigatorNode({
16
+ priority: 100,
17
+ path: 'hooks/preview-feature-default',
18
+ icon: 'science',
19
+ label: 'Preview Feature',
20
+ parent: 'Hooks',
21
+ preventDuplicates: true
22
+ })
23
+ ];
24
+ }
25
+ return [];
26
+ })
27
+ );
28
+ }
29
+ }
@@ -0,0 +1,3 @@
1
+ export * from './preview-feature.module';
2
+ export * from './basic-view-custom/preview-feature-custom.module';
3
+ export * from './basic-view-default/preview-feature-default.module';
@@ -0,0 +1,11 @@
1
+ import { NgModule } from '@angular/core';
2
+ import { PreviewFeatureCustomModule } from './basic-view-custom/preview-feature-custom.module';
3
+ import { PreviewFeatureDefaultModule } from './basic-view-default/preview-feature-default.module';
4
+
5
+ /**
6
+ * This module combines both PreviewFeatureDefaultModule and PreviewFeatureCustomModule.
7
+ */
8
+ @NgModule({
9
+ imports: [PreviewFeatureDefaultModule, PreviewFeatureCustomModule]
10
+ })
11
+ export class PreviewFeatureModule {}