@c8y/tutorial 1022.4.16 → 1022.6.0
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.
- package/cumulocity.config.ts +7 -0
- package/package.json +7 -7
- package/src/__mocks/global-mocks/feature-api.ts +55 -0
- package/src/__mocks/index.ts +26 -2
- package/src/grids/tree-grid-example/server-tree-grid-example.component.ts +7 -5
- package/src/hooks/preview-feature/basic-view-custom/basic-view-custom.component.html +11 -0
- package/src/hooks/preview-feature/basic-view-custom/basic-view-custom.component.ts +18 -0
- package/src/hooks/preview-feature/basic-view-custom/preview-feature-custom.factory.ts +31 -0
- package/src/hooks/preview-feature/basic-view-custom/preview-feature-custom.module.ts +45 -0
- package/src/hooks/preview-feature/basic-view-default/basic-view-default.component.html +11 -0
- package/src/hooks/preview-feature/basic-view-default/basic-view-default.component.ts +18 -0
- package/src/hooks/preview-feature/basic-view-default/preview-feature-default.module.ts +34 -0
- package/src/hooks/preview-feature/basic-view-default/preview-feature.factory.ts +29 -0
- package/src/hooks/preview-feature/index.ts +3 -0
- package/src/hooks/preview-feature/preview-feature.module.ts +11 -0
package/cumulocity.config.ts
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1022.6.0",
|
|
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.
|
|
7
|
-
"@c8y/ngx-components": "1022.
|
|
8
|
-
"@c8y/client": "1022.
|
|
9
|
-
"@c8y/bootstrap": "1022.
|
|
6
|
+
"@c8y/style": "1022.6.0",
|
|
7
|
+
"@c8y/ngx-components": "1022.6.0",
|
|
8
|
+
"@c8y/client": "1022.6.0",
|
|
9
|
+
"@c8y/bootstrap": "1022.6.0",
|
|
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.
|
|
17
|
-
"@c8y/devkit": "1022.
|
|
16
|
+
"@c8y/options": "1022.6.0",
|
|
17
|
+
"@c8y/devkit": "1022.6.0"
|
|
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
|
+
}
|
package/src/__mocks/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
87
|
-
* It
|
|
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,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 {}
|