Skip to content

Commit

Permalink
Merge pull request #21 from SoftwareAG/feature-migration-1019
Browse files Browse the repository at this point in the history
Feature migration 1019
  • Loading branch information
ck-c8y authored Apr 4, 2024
2 parents 1dfc3e9 + f48f5a6 commit 157d67e
Show file tree
Hide file tree
Showing 16 changed files with 1,465 additions and 1,146 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
This solution extends the functionality of the standard Cumulocity Streaming Analytics application by incorporating a plugin to manage and add Analytics Builder extensions. The current standard user interface lacks the capability to upload custom blocks packaged as **.zip** files. This plugin enhances the Streaming-Analytics UI by introducing these capabilities.

The extension allows you to manage the complete lifecycle of an extension:
* List, upload, download, and delete Analytics Builder extensions.
* List, upload, download, update and delete Analytics Builder extensions.
* List installed blocks.
* Manage GitHub repositories, which act as a source for dynamically building new extensions.
* Build new extensions from selected blocks and deploy them in the Streaming Analytics engine.
Expand All @@ -41,6 +41,9 @@ For a deletion or upload to take effect you need to restart the analytics stream

An externally built extensions can be uploaded via the button **Add extension**.
Simply drop the **.zip** file to the modal dialog and the extension will be loaded to the repository, but not yet deployed. To use them, restart the Streaming Analytics engine by clicking on the button **Deploy extension (Restart)** and wait for the notification confirming the engine restart.
When you try to add an extension with a name that already exists, you have the choice to confirm that the version of the existing extension is replaces with the new one you are about to upload. You can as well cancel the process if this is not what you intend.

![Build custom extension](resources/images/manage-extension-modal.png)

After the deployment (restart of streaming analytics) the block will be available within the Streaming Analytics Application.

Expand All @@ -61,10 +64,12 @@ You can build and uploads a custom extension by following the screen flow below:

### Options for custom extension
For a custom extension you have the following options:
* Delete: deletes the custom extension permanently. To take effect you need to restart the streaming analytics engine
* Delete: deletes the custom extension permanently. To take effect you need to restart the streaming analytics engine.
* Update: updates the custom extension with a new version of the extension. To take effect you need to restart the streaming analytics engine.
* Details: lists the included blocks of the custom extension on a detail page.
* Download: downloads the custom extension as a zip file.


![Build custom extension](resources/images/manage-extension.png)

## Samples repositories and building custom extensions
Expand Down
2 changes: 1 addition & 1 deletion analytics-service/c8y_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def upload_extension(self, extension_name, ext_file, request_headers):
b = Binary(
c8y=self.c8yapp.get_tenant_instance(headers=request_headers),
type="application/zip",
name=f"{extension_name}.zip",
name=f"{extension_name}",
file=ext_file,
pas_extension=extension_name,
).create()
Expand Down
2,157 changes: 1,166 additions & 991 deletions analytics-ui/package-lock.json

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions analytics-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "analytics-extension",
"version": "2.3.3",
"version": "2.3.4",
"description": "Extends the standard Cumulocity administration web application with a dialog to add Analytics Builder extensions.",
"repository": {
"type": "git",
Expand Down Expand Up @@ -32,9 +32,9 @@
"@angular/platform-browser-dynamic": "^16.2.0",
"@angular/router": "^16.2.0",
"@angular/upgrade": "^16.2.0",
"@c8y/client": "1019.6.3",
"@c8y/ngx-components": "1019.6.3",
"@c8y/style": "1019.6.3",
"@c8y/client": "1019.13.5",
"@c8y/ngx-components": "1019.13.5",
"@c8y/style": "1019.13.5",
"@c8y/websdk": "^1019.0.7",
"@ngx-translate/core": "14.0.0",
"@types/ws": "<8.5.5",
Expand All @@ -52,7 +52,7 @@
"@angular/cli": "^16.2.12",
"@angular/compiler-cli": "^16.2.0",
"@angular/language-service": "^16.2.0",
"@c8y/widget-plugin": "1019.6.3",
"@c8y/widget-plugin": "1019.13.5",
"@semantic-release/changelog": "^6.0.2",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1",
Expand Down
2 changes: 1 addition & 1 deletion analytics-ui/src/block/block-grid.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
<div class="col-lg-8 col-lg-offset-2 text-center m-b-48">
<h2>
<span class="label label-primary text-center text-14">
Please stand by until the streaming analytics engine is started
Please stand by until the blocks metadata is loaded
successfully. This may take a short while ...
</span>
</h2>
Expand Down
6 changes: 6 additions & 0 deletions analytics-ui/src/manage/extension-card.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@
{{ 'Delete' | translate }}
</button>
</li>
<li>
<button (click)="update()" title="{{ 'Update' }}">
<i c8yIcon="change" class="m-r-4"></i>
{{ 'Update' | translate }}
</button>
</li>
</ul>
</div>
</div>
Expand Down
40 changes: 34 additions & 6 deletions analytics-ui/src/manage/extension-card.component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { IManagedObject } from '@c8y/client';
import { AlertService } from '@c8y/ngx-components';
import {
AlertService,
WizardConfig,
WizardModalService
} from '@c8y/ngx-components';
import { saveAs } from 'file-saver';
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import { BsModalRef, BsModalService, ModalOptions } from 'ngx-bootstrap/modal';
import { AnalyticsService, ConfirmationModalComponent } from '../shared';

@Component({
Expand All @@ -12,14 +16,15 @@ import { AnalyticsService, ConfirmationModalComponent } from '../shared';
})
export class ExtensionCardComponent {
@Input() extension: IManagedObject;
@Output() appDeleted: EventEmitter<void> = new EventEmitter();
@Output() extensionChanged: EventEmitter<void> = new EventEmitter();

constructor(
private analyticsService: AnalyticsService,
private alertService: AlertService,
private router: Router,
private activatedRoute: ActivatedRoute,
private bsModalService: BsModalService
private bsModalService: BsModalService,
private wizardModalService: WizardModalService
) {}

async detail() {
Expand Down Expand Up @@ -53,8 +58,8 @@ export class ExtensionCardComponent {
// console.log("Confirmation delete result:", result);
if (result) {
try {
await this.analyticsService.deleteExtension(this.extension);
this.appDeleted.emit();
await this.analyticsService.deleteExtension(this.extension, true);
this.extensionChanged.emit();
} catch (ex) {
if (ex) {
this.alertService.addServerFailure(ex);
Expand All @@ -79,4 +84,27 @@ export class ExtensionCardComponent {
}
}
}

async update() {
const wizardConfig: WizardConfig = {
headerIcon: 'upload'
};

const initialState: any = {
wizardConfig,
id: 'uploadAnalyticsExtension',
componentInitialState: {
mode: 'update',
extensionToReplace: this.extension,
headerText: 'Update extension',
},
};

const modalOptions: ModalOptions = { initialState };

const modalRef = this.wizardModalService.show(modalOptions);
modalRef.content.onClose.subscribe(() => {
this.extensionChanged.emit();
});
}
}
41 changes: 25 additions & 16 deletions analytics-ui/src/manage/extension-grid.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@
<a
href="https://cumulocity.com/guides/streaming-analytics/troubleshooting/#safe-mode-on-startup"
target="'_blank'"
><i c8yIcon="warning" class="animated fadeIn infinite" style="animation-duration: 1.5s"></i><span class="text-warning p-r-8 p-l-4">{{ 'Safe Mode' }}</span
></a
><i
c8yIcon="warning"
class="animated fadeIn infinite"
style="animation-duration: 1.5s"
></i
><span class="text-warning p-r-8 p-l-4">{{ 'Safe Mode' }}</span></a
>
</c8y-action-bar-item>

Expand Down Expand Up @@ -76,7 +80,7 @@
class="btn btn-link"
title="{{ 'Refresh' | translate }}"
>
<i [ngClass]="{ 'icon-spin': loading }" c8yIcon="refresh"></i>
<i [ngClass]="{ 'icon-spin': (cepEngineStatus$ | async) === 'loading' }" c8yIcon="refresh"></i>
{{ 'Refresh' | translate }}
</button>
</c8y-action-bar-item>
Expand All @@ -88,7 +92,7 @@
></c8y-list-display-switch>
</c8y-action-bar-item>

<div *ngIf="!loadingError && (extensions$ | async)?.length === 0" class="row">
<div *ngIf="(cepEngineStatus$ | async) === 'empty'" class="row">
<div class="col-lg-4 col-lg-offset-5 text-center">
<c8y-ui-empty-state
[icon]="'plugin'"
Expand All @@ -107,18 +111,10 @@
</div>
</div>

<p class="text-center">
<c8y-progress-bar
message="Streaming Analytics Engine is restarting ..."
*ngIf="(cepOperationObject$ | async)?.c8y_Status?.status === 'Down'"
>
</c8y-progress-bar>
</p>

<div
[ngClass]="listClass"
class="card-group"
*ngIf="!loading && (cepOperationObject$ | async)?.c8y_Status?.status === 'Up'"
*ngIf="(cepEngineStatus$ | async) === 'up' || (cepEngineStatus$ | async) === 'loaded'"
>
<div
class="page-sticky-header hidden-xs d-flex"
Expand All @@ -145,7 +141,7 @@
class="col-xs-12 col-sm-4 col-md-3"
>
<a17t-extension-card
(appDeleted)="loadExtensions()"
(extensionChanged)="loadExtensions()"
[extension]="extension"
class="d-contents"
></a17t-extension-card>
Expand All @@ -155,7 +151,7 @@
<div
class="col-lg-8 col-lg-offset-2 m-b-48 text-center"
style="padding-top: 48px"
*ngIf="loadingError"
*ngIf="(cepEngineStatus$ | async) === 'loadingError'"
>
<h2>
<span class="label label-primary text-14">
Expand All @@ -168,7 +164,7 @@ <h2>
<div
class="col-lg-8 col-lg-offset-2 m-b-48 text-center"
style="padding-top: 48px"
*ngIf="loading"
*ngIf="(cepEngineStatus$ | async) === 'loading'"
>
<h2 class="p-b-24">
<span class="label label-primary text-14">
Expand All @@ -177,3 +173,16 @@ <h2 class="p-b-24">
</h2>
<c8y-loading></c8y-loading>
</div>

<div
class="col-lg-8 col-lg-offset-2 m-b-48 text-center"
style="padding-top: 48px"
*ngIf="(cepEngineStatus$ | async) === 'down'"
>
<h2 class="p-b-24">
<span class="label label-primary text-14">
Streaming Analytics Engine is restarting ...
</span>
</h2>
<c8y-loading></c8y-loading>
</div>
75 changes: 54 additions & 21 deletions analytics-ui/src/manage/extension-grid.component.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,47 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { IManagedObject } from '@c8y/client';
import { AlertService, WizardConfig, WizardModalService, gettext } from '@c8y/ngx-components';
import {
AlertService,
WizardConfig,
WizardModalService,
gettext
} from '@c8y/ngx-components';
import { ModalOptions } from 'ngx-bootstrap/modal';
import { BehaviorSubject, Observable, Subject, of } from 'rxjs';
import { catchError, shareReplay, switchMap, tap } from 'rxjs/operators';
import { AnalyticsService } from '../shared';
import {
BehaviorSubject,
Observable,
Subject,
Subscription,
merge,
of
} from 'rxjs';
import { catchError, shareReplay, switchMap, take, tap } from 'rxjs/operators';
import { AnalyticsService, CEPEngineStatus, CEPStatusObject } from '../shared';

@Component({
selector: 'a17t-extension',
templateUrl: './extension-grid.component.html',
styleUrls: ['./extension-grid.component.css']
})
export class ExtensionGridComponent implements OnInit {
loading: boolean = false;
cepOperationObject$: Subject<any>;
cepCtrlStatus: any;
export class ExtensionGridComponent implements OnInit, OnDestroy {
cepOperationObject$: Subject<IManagedObject>;
cepCtrlStatus: CEPStatusObject;
cepEngineStatus$: BehaviorSubject<CEPEngineStatus> =
new BehaviorSubject<CEPEngineStatus>('unknown');
cepId: string;
loadingError: boolean = false;
reload$: BehaviorSubject<boolean> = new BehaviorSubject(false);
extensions$: Observable<IManagedObject[]>;
sub1: Subscription;
listClass: string;
rescue: boolean = false;

constructor(
private analyticsService: AnalyticsService,
private alertService: AlertService,
private wizardModalService: WizardModalService
) {}
ngOnDestroy(): void {
this.sub1.unsubscribe();
}

ngOnInit() {
this.init();
Expand All @@ -38,21 +53,36 @@ export class ExtensionGridComponent implements OnInit {
this.cepId = microservice_application_id as string;
this.cepCtrlStatus = await this.analyticsService.getCEP_CtrlStatus();
this.cepOperationObject$ = this.analyticsService.getCEP_OperationObject();
this.extensions$ = this.reload$.pipe(
this.sub1 = this.cepOperationObject$.subscribe((mo) => {
if (mo.c8y_Status?.status === 'Down') {
this.cepEngineStatus$.next('down');
} else if (mo.c8y_Status?.status === 'Up') {
this.cepEngineStatus$.next('up');
// this.alertService.clearAll();
}
});
this.extensions$ = merge(
this.analyticsService.getReloadThroughService(),
this.reload$
).pipe(
tap((clearCache) => {
if (clearCache) {
this.analyticsService.clearCaches();
}
this.loading = true;
this.loadingError = false;
this.cepEngineStatus$.next('loading');
}),
switchMap(() => this.analyticsService.getExtensionsMetadataEnriched()),
catchError(() => {
this.loadingError = true;
this.loading = false;
this.cepEngineStatus$.next('loadingError');
return of([]);
}),
tap(() => (this.loading = false)),
tap((res) => {
if (res && res.length > 0) {
this.cepEngineStatus$.next('loaded');
} else {
this.cepEngineStatus$.next('empty');
}
}),
shareReplay()
);

Expand All @@ -70,19 +100,22 @@ export class ExtensionGridComponent implements OnInit {

addExtension() {
const wizardConfig: WizardConfig = {
headerText: 'Add extension',
headerIcon: 'c8y-atom'
headerIcon: 'plus'
};

const initialState: any = {
wizardConfig,
id: 'uploadAnalyticsExtension'
id: 'uploadAnalyticsExtension',
componentInitialState: {
mode: 'add',
headerText: 'Add extension'
}
};

const modalOptions: ModalOptions = { initialState };

const modalRef = this.wizardModalService.show(modalOptions);
modalRef.content.onClose.subscribe(() => {
modalRef.content.onClose.pipe(take(1)).subscribe(() => {
this.loadExtensions();
});
}
Expand Down
Loading

0 comments on commit 157d67e

Please sign in to comment.