In diesem Artikel erstellen wir ein einfaches OptiCloud Widget als Einführung in den OptiCloud Widget Editor.
Einrichtung
Um ein benutzerdefiniertes Widget zu erstellen, müssen wir zunächst eine neue Widget-Bibliothek erstellen. Navigieren Sie dazu in der Seitenleiste zu „Administration“ und wählen Sie „Widget-Bibliothek“.
Klicken Sie im Abschnitt „Widget-Bibliothek“ oben links auf „Erstellen“, um ein neues Bundle zu erstellen. Geben Sie ihm einen Namen (und eine Beschreibung).
Öffnen Sie dieses neue Bundle, indem Sie auf die Schaltfläche „Details anzeigen“ (Info-Symbol) in der Spalte neben seinem Namen klicken.
Klicken Sie auf „Widget erstellen“ und wählen Sie „Aktueller Wert“ im Popup-Fenster aus.
Der Bildschirm wird in zwei Bereiche unterteilt:
Der rechte Bereich ist die Widget-Vorschau – hier wird bereits das Standard-Widget angezeigt.
Im linken Bereich finden die Arbeiten statt. Hier finden Sie alle Teile eines Widgets, unterteilt in Kategorien:
- „Allgemein“ enthält allgemeine Einstellungen, die festlegen, wie das Widget in Menüs usw. angezeigt wird.
- „HTML“ enthält den HTML-Code des Widgets.
- „CSS“ enthält das CSS des Widgets.
- „Javascript“ enthält den Code, den das Widget ausführt.
- „Ressourcen“ enthält Optionen zum Hinzufügen und Verwalten externer Ressourcen, auf die das Widget möglicherweise angewiesen ist.
- „Einstellungsschema“ ist der Bereich, in dem die Einstellungen des Widgets definiert werden.
- „Datenschlüsseleinstellungen“ ist der Bereich, in dem die Konfiguration für die dem Widget bereitgestellten Datenschlüssel definiert werden kann.
Als ersten Schritt zur Erstellung unseres Widgets löschen wir den gesamten Standardcode und beginnen mit einer leeren Seite. Löschen Sie die Felder „Einstellungen für Selektor“ und „Datenschlüsseleinstellungen-Selektor“ unter „Allgemein“ und löschen Sie den Inhalt von HTML, CSS und Javascript.
HTML
Unser Widget wird in drei Hauptbereiche unterteilt: den Titel, den Inhalt und den Untertitel.
<div class="titlebar">
<p>Config</p>
</div>
<div class="details">
<div id="staticMod" class="detailContainer">
<span class="title">Static Changed</span>
<span class="data">---</span>
</div>
<div id="dynamicMod" class="detailContainer">
<span class="title">Dynamic Changed</span>
<span class="data">---</span>
</div>
<div id="version" class="detailContainer">
<span class="title">Version</span>
<span class="data">---</span>
</div>
</div>
<div class="metainfo">
<p>Timestamp of most recent data</p>
</div>
CSS
Wir definieren auch einen Stil für jedes unserer Elemente:
.titlebar {
display: flex;
justify-content: center;
border-bottom: lightgray 1px solid;
border-radius: 0 0 1em 1em;
padding: 0 0 0.5em 0;
margin-bottom: 0.5em;
}
.titlebar p {
font-size: large;
margin: unset;
}
.details {
display: flex;
flex-direction: column;
justify-content: center;
grid-row-gap: 1em;
padding: 0 1em;
}
.detailContainer {
display: flex;
justify-content: space-between;
align-items: center;
}
.details .title {
font-size: large;
margin-right: 0.5em;
}
.details .data {
background-color: lightgrey;
border-radius: 0.5em;
align-self: end;
padding: 0.5em 2.5em;
min-width: 50%;
text-align: center;
}
.metainfo {
margin-top: 1em;
display: flex;
justify-content: center;
position: relative;
}
JS (das Wesentliche)
onInit()
Die Methode
onInit()
wird aufgerufen, wenn unser Widget initialisiert wird. Sie kann verwendet werden, um dynamische HTML-Elemente zu erstellen, Einstellungen anzuwenden und vieles mehr. Für dieses einfache Widget müssen wir jedoch nichts davon tun, daher rufen wir einfach this.onResize()
auf.onResize()
Die Methode
onResize()
wird immer dann aufgerufen, wenn die Größe unseres Widgets in einem Dashboard geändert wird. Innerhalb dieser Methode können Sie Code ausführen, um die neue Größe anzupassen und sicherzustellen, dass alles weiterhin funktionsfähig und lesbar ist.onDataUpdated()
Schließlich sind wir beim Kern unseres Widgets angelangt – der Methode
onDataUpdated()
. Immer wenn neue Daten in der Cloud eintreffen, wird diese Funktion ausgeführt. Wir erfassen diese aktualisierten mit Hilfe des Objekts
self.ctx.data
. Es ist wie folgt strukturiert:data = [
{
datasource: {...}, // Informationen über die Quelle der Daten
dataKey: { // Metadaten zu diesem Datenkanal
name: 'name', // Name des jeweiligen Entitätsattributs/der Zeitreihe
type: 'timeseries', // Typ des dataKey. Kann „timeseries”, „attribute” oder „function” sein
label: 'Sin', // Bezeichnung des dataKey. Wird als Anzeigewert verwendet (z. B. im Legendenbereich des Widgets)
color: '#ffffff', // Farbe des Schlüssels. Kann vom Widget verwendet werden, um die Farbe der Schlüsseldaten festzulegen (z. B. Linien im Liniendiagramm oder Segmente im Kreisdiagramm).
funcBody: "", // gilt nur für Datenquellen vom Typ „function” und mit dem Schlüsseltyp „function”. Definiert den Hauptteil der Funktion zur Generierung simulierter Daten.
settings: {} // datenschlüsselspezifische Einstellungen mit einer Struktur gemäß dem definierten JSON-Schema für Datenschlüsseleinstellungen. Siehe Abschnitt „Schema für Einstellungen”.
},
data: [ // Array von Datenpunkten
[ // Datenpunkt
1498150092317, // Unix-Zeitstempel des Datenpunkts in Millisekunden
1, // Wert, kann entweder eine Zeichenfolge, eine Zahl oder ein Boolescher Wert sein
],
//...
]
},
//...
]
Da wir in diesem Fall nach bestimmten Datenschlüsseln suchen, durchlaufen wir alle empfangenen Schlüssel und prüfen, ob ihr Name mit unseren Erwartungen übereinstimmt.
Dazu teilen wir den Namen des Datenschlüssels am Zeichen
.
und überprüfen dann das letzte Schlüsselwort anhand einer Reihe statischer Zeichenfolgen. Während die Namen der Bereiche vom Benutzer definiert werden können, ist der Name des Datenschlüssels selbst konstant und bietet uns eine Möglichkeit, sicherzustellen, dass wir die richtigen Daten an der richtigen Stelle anzeigen.
for (const signal of self.ctx.data) {
const lastKey = signal.dataKey.name.split('.').pop();
...
}
Sobald wir einen Datenschlüssel gefunden haben, der mit dem erwarteten Namen übereinstimmt, verarbeiten wir die Daten in drei einfachen Schritten.
Zunächst speichern wir die Daten (
self.ctx.data[i].data[0][1]
) in einer Variablen, um die Übersichtlichkeit zu verbessern. Im Falle der beiden Zeitstempel übergeben wir die Daten dann an die integrierte Klasse
Date()
, um problemlos mit dem Zeitstempel arbeiten zu können. Im letzten Schritt führen wir zwei Dinge gleichzeitig aus: Wir verwenden JQuery, um das Element zu finden, in dem wir unsere Daten anzeigen möchten, und legen dessen Textinhalt fest.
Um den Textinhalt zu generieren, verwenden wir die Methode
toLocaleString()
unseres Date-Objekts, um den Zeitstempel basierend auf den Locale-Einstellungen des Benutzers in eine für Menschen lesbare Form zu konvertieren. Beispielsweise sieht ein Benutzer mit amerikanischer Englisch-Ländereinstellung „6/21/2024, 9:09:30 PM“, während ein Benutzer mit deutscher Ländereinstellung „21.6.2024, 21:09:30“ sieht.Im Fall des Versionsdatenschlüssels prüfen wir zunächst, ob die empfangenen Daten eine leere Zeichenfolge sind. Ist dies nicht der Fall, suchen wir das Element, das die Versionsdaten enthält, und setzen seinen Textinhalt auf die empfangenen Daten. Ist es leer, setzen wir den Inhalt auf einen Platzhalter, um anzuzeigen, dass keine Daten vorhanden sind.
if (lastKey == 'StaticModified') {
const staticTimestamp = signal.data[0][1];
const staticDate = new Date(staticTimestamp);
self.ctx.$container.find('.details #staticMod .data').text(staticDate.toLocaleString());
} else if (lastKey == 'DynamicModified') {
const dynamicTimestamp = signal.data[0][1];
const dynamicDate = new Date(dynamicTimestamp);
self.ctx.$container.find('.details #dynamicMod .data').text(dynamicDate.toLocaleString());
} else if (lastKey == 'Version') {
const versionString = signal.data[0][1];
if (versionString != '') {
self.ctx.$container.find('.details #version .data').text(versionString);
} else {
self.ctx.$container.find('.details #version .data').text("---");
}
}
Zu guter Letzt speichern und verarbeiten wir mit jedem empfangenen Datenschlüssel auch dessen Zeitstempel und zeigen diesen im Untertitel an. Auf diese Weise können Benutzer sicherstellen, dass die angezeigten Daten nicht veraltet sind und aktualisiert werden.
let timestamp = new Date(signal.data[0][0]);
const MetaInfoSection = self.ctx.$container.find('.metainfo');
MetaInfoSection.find('p').text(`Timestamp: ${timestamp.toLocaleString()}`);
typeParameters
Die Funktion „typeParameters“ teilt der Webanwendung mit, wie viele Datenquellen und Datenschlüssel das Widget akzeptieren soll. In unserem Fall möchten wir drei Datenschlüssel von einem Gerät verarbeiten, daher legen wir die Parameter entsprechend fest.
self.typeParameters = function() {
return {
maxDatasources: 1,
maxDataKeys: 3
};
};
Conclusion
Damit ist der Javascript-Teil unseres Widgets fertiggestellt:
self.onInit = function() {
self.onResize();
};
self.onDataUpdated = function() {
for (const signal of self.ctx.data) {
// get last part of key name, since that is unchangeable
const lastKey = signal.dataKey.name.split('.').pop();
if (lastKey == 'StaticModified') {
const staticTimestamp = signal.data[0][1];
const staticDate = new Date(staticTimestamp);
self.ctx.$container.find('.details #staticMod .data').text(staticDate.toLocaleString());
} else if (lastKey == 'DynamicModified') {
const dynamicTimestamp = signal.data[0][1];
const dynamicDate = new Date(dynamicTimestamp);
self.ctx.$container.find('.details #dynamicMod .data').text(dynamicDate.toLocaleString());
} else if (lastKey == 'Version') {
const versionString = signal.data[0][1];
if (versionString != '') {
self.ctx.$container.find('.details #version .data').text(versionString);
} else {
self.ctx.$container.find('.details #version .data').text("---");
}
}
// get timestamp of each data point, to show to the user, that data is not outdated
let timestamp = new Date(signal.data[0][0]);
const MetaInfoSection = self.ctx.$container.find('.metainfo');
MetaInfoSection.find('p').text(`Timestamp: ${timestamp.toLocaleString()}`);
}
};
self.onResize = function() {
//do nothing
};
self.typeParameters = function() {
return {
maxDatasources: 1,
maxDataKeys: 3
};
};