Initial commit
This commit is contained in:
658
references/advanced-topics.md
Normal file
658
references/advanced-topics.md
Normal file
@@ -0,0 +1,658 @@
|
||||
# SAP SAC Custom Widget Advanced Topics
|
||||
|
||||
Advanced features including custom types, script data types, and administration.
|
||||
|
||||
**Source**: [SAP Custom Widget Developer Guide](https://help.sap.com/doc/c813a28922b54e50bd2a307b099787dc/release/en-US/CustomWidgetDevGuide_en.pdf)
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Custom Types](#custom-types)
|
||||
2. [Script API Data Types](#script-api-data-types)
|
||||
3. [Widget Installation](#widget-installation)
|
||||
4. [Third-Party Library Integration](#third-party-library-integration)
|
||||
5. [Advanced Data Binding](#advanced-data-binding)
|
||||
6. [Multi-Language Support](#multi-language-support)
|
||||
|
||||
---
|
||||
|
||||
## Custom Types
|
||||
|
||||
Custom types enable complex data structures in widget properties and script interactions.
|
||||
|
||||
### Custom Data Structures
|
||||
|
||||
Define reusable object types in JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "com.company.advancedwidget",
|
||||
"version": "1.0.0",
|
||||
"name": "Advanced Widget",
|
||||
"types": {
|
||||
"ChartConfig": {
|
||||
"description": "Chart configuration object",
|
||||
"properties": {
|
||||
"chartType": {
|
||||
"type": "string",
|
||||
"default": "bar",
|
||||
"description": "Type of chart"
|
||||
},
|
||||
"showLegend": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Show chart legend"
|
||||
},
|
||||
"colors": {
|
||||
"type": "string[]",
|
||||
"default": [],
|
||||
"description": "Color palette"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DataPoint": {
|
||||
"description": "Single data point",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Data point label"
|
||||
},
|
||||
"value": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Data point value"
|
||||
},
|
||||
"color": {
|
||||
"type": "string",
|
||||
"default": "#336699",
|
||||
"description": "Data point color"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"config": {
|
||||
"type": "ChartConfig",
|
||||
"default": {
|
||||
"chartType": "bar",
|
||||
"showLegend": true,
|
||||
"colors": ["#5470c6", "#91cc75", "#fac858"]
|
||||
},
|
||||
"description": "Chart configuration"
|
||||
},
|
||||
"dataPoints": {
|
||||
"type": "DataPoint[]",
|
||||
"default": [],
|
||||
"description": "Array of data points"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Enumerations
|
||||
|
||||
Define allowed values:
|
||||
|
||||
```json
|
||||
{
|
||||
"types": {
|
||||
"ChartTypeEnum": {
|
||||
"description": "Allowed chart types",
|
||||
"values": [
|
||||
{
|
||||
"id": "bar",
|
||||
"description": "Bar Chart"
|
||||
},
|
||||
{
|
||||
"id": "line",
|
||||
"description": "Line Chart"
|
||||
},
|
||||
{
|
||||
"id": "pie",
|
||||
"description": "Pie Chart"
|
||||
},
|
||||
{
|
||||
"id": "area",
|
||||
"description": "Area Chart"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AlignmentEnum": {
|
||||
"description": "Text alignment options",
|
||||
"values": [
|
||||
{ "id": "left", "description": "Left aligned" },
|
||||
{ "id": "center", "description": "Center aligned" },
|
||||
{ "id": "right", "description": "Right aligned" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"chartType": {
|
||||
"type": "ChartTypeEnum",
|
||||
"default": "bar",
|
||||
"description": "Type of chart to display"
|
||||
},
|
||||
"titleAlignment": {
|
||||
"type": "AlignmentEnum",
|
||||
"default": "center",
|
||||
"description": "Title text alignment"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Custom Types in Web Component
|
||||
|
||||
```javascript
|
||||
class AdvancedWidget extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._props = {
|
||||
config: {
|
||||
chartType: "bar",
|
||||
showLegend: true,
|
||||
colors: ["#5470c6", "#91cc75", "#fac858"]
|
||||
},
|
||||
dataPoints: []
|
||||
};
|
||||
}
|
||||
|
||||
// Getter returns the full object
|
||||
get config() {
|
||||
return this._props.config;
|
||||
}
|
||||
|
||||
// Setter accepts object and validates
|
||||
set config(value) {
|
||||
if (typeof value !== "object") {
|
||||
console.warn("config must be an object");
|
||||
return;
|
||||
}
|
||||
this._props.config = {
|
||||
...this._props.config,
|
||||
...value
|
||||
};
|
||||
this._render();
|
||||
}
|
||||
|
||||
get dataPoints() {
|
||||
return this._props.dataPoints;
|
||||
}
|
||||
|
||||
set dataPoints(value) {
|
||||
if (!Array.isArray(value)) {
|
||||
console.warn("dataPoints must be an array");
|
||||
return;
|
||||
}
|
||||
this._props.dataPoints = value;
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Type Name Qualification
|
||||
|
||||
Internally, custom type names are qualified with widget ID to avoid conflicts:
|
||||
|
||||
- Defined as: `ChartConfig`
|
||||
- Internal name: `com.company.advancedwidget.ChartConfig`
|
||||
|
||||
---
|
||||
|
||||
## Script API Data Types
|
||||
|
||||
Types available for properties and method parameters.
|
||||
|
||||
### Selection Type
|
||||
|
||||
Represents a data selection in SAC:
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"currentSelection": {
|
||||
"type": "Selection",
|
||||
"default": {},
|
||||
"description": "Current data selection"
|
||||
}
|
||||
},
|
||||
"methods": {
|
||||
"setSelection": {
|
||||
"description": "Set data selection",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "selection",
|
||||
"type": "Selection",
|
||||
"description": "Selection to apply"
|
||||
}
|
||||
],
|
||||
"body": "this._setSelection(selection);"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in Scripts**:
|
||||
```javascript
|
||||
// In SAC script
|
||||
var selection = {
|
||||
"Account": "Revenue",
|
||||
"Year": "2024"
|
||||
};
|
||||
Widget_1.setSelection(selection);
|
||||
```
|
||||
|
||||
### MemberInfo Type
|
||||
|
||||
Information about a dimension member:
|
||||
|
||||
```javascript
|
||||
// MemberInfo object structure
|
||||
{
|
||||
id: "MEMBER_ID", // Technical ID
|
||||
description: "Member Name", // Display name
|
||||
dimensionId: "DIM_ID", // Parent dimension
|
||||
modelId: "MODEL_ID", // Data model
|
||||
displayId: "DISPLAY_ID" // Display ID
|
||||
}
|
||||
```
|
||||
|
||||
**Using in Widget**:
|
||||
```javascript
|
||||
class MyWidget extends HTMLElement {
|
||||
setMemberInfo(memberInfo) {
|
||||
this._currentMember = memberInfo;
|
||||
this._shadowRoot.getElementById("memberLabel").textContent =
|
||||
memberInfo.description || memberInfo.id;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ResultMemberInfo Type
|
||||
|
||||
Extended member information from result set:
|
||||
|
||||
```javascript
|
||||
// ResultMemberInfo structure
|
||||
{
|
||||
id: "MEMBER_ID",
|
||||
description: "Member Name",
|
||||
parentId: "PARENT_ID", // For hierarchies
|
||||
properties: {
|
||||
"Property1": "Value1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### DataSource Methods
|
||||
|
||||
Access data source information:
|
||||
|
||||
```javascript
|
||||
// In SAC script with data binding
|
||||
var ds = Widget_1.getDataSource();
|
||||
|
||||
// Get members
|
||||
var members = ds.getMembers("Account", { limit: 100 });
|
||||
|
||||
// Get result member
|
||||
var selection = { "Account": "Revenue" };
|
||||
var memberInfo = ds.getResultMember("Account", selection);
|
||||
|
||||
// Get data cell value
|
||||
var value = ds.getData(selection);
|
||||
```
|
||||
|
||||
### Color Type
|
||||
|
||||
SAC Color type for color properties:
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"primaryColor": {
|
||||
"type": "Color",
|
||||
"default": "#336699",
|
||||
"description": "Primary widget color"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Widget Installation
|
||||
|
||||
### Administrator Steps
|
||||
|
||||
1. **Access Custom Widgets**:
|
||||
- Main Menu > **Analytic Applications**
|
||||
- Select **Custom Widgets** tab
|
||||
|
||||
2. **Upload Widget**:
|
||||
- Click **+** (Add) button
|
||||
- Select JSON file from local system
|
||||
- Widget appears in list after upload
|
||||
|
||||
3. **Manage Widgets**:
|
||||
- View installed widgets in list
|
||||
- Delete widgets no longer needed
|
||||
- Update by re-uploading JSON
|
||||
|
||||
### Requirements
|
||||
|
||||
- **Role**: Administrator or custom widget manager
|
||||
- **Files**: JSON metadata file (resource files hosted externally)
|
||||
- **Hosting**: Resource files accessible via HTTPS
|
||||
|
||||
### SAC-Hosted Widgets (QRC Q2 2023+)
|
||||
|
||||
Upload resource files directly to SAC:
|
||||
|
||||
1. **Prepare Files**:
|
||||
- Pack JSON and JS files into ZIP
|
||||
- Or upload individually to SAC Files
|
||||
|
||||
2. **Configure JSON for SAC Hosting**:
|
||||
```json
|
||||
{
|
||||
"webcomponents": [
|
||||
{
|
||||
"kind": "main",
|
||||
"tag": "my-widget",
|
||||
"url": "/my-widget.js",
|
||||
"integrity": "",
|
||||
"ignoreIntegrity": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Note: URL starts with `/` for SAC-hosted files
|
||||
|
||||
3. **Upload to SAC**:
|
||||
- Go to Files > Public Files
|
||||
- Create folder for widget
|
||||
- Upload JS files
|
||||
- Upload JSON to Custom Widgets
|
||||
|
||||
### Using Widgets in Stories
|
||||
|
||||
1. Open story in Edit mode
|
||||
2. Open widget panel (Insert > Widget)
|
||||
3. Find custom widget in Custom section
|
||||
4. Drag onto canvas
|
||||
5. Configure via Builder/Styling panels
|
||||
|
||||
---
|
||||
|
||||
## Third-Party Library Integration
|
||||
|
||||
### Supported Libraries
|
||||
|
||||
Common libraries used with SAC widgets:
|
||||
|
||||
| Library | Use Case | CDN |
|
||||
|---------|----------|-----|
|
||||
| ECharts | Charts | `[https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js`](https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js`) |
|
||||
| D3.js | Data viz | `[https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js`](https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js`) |
|
||||
| Chart.js | Charts | `[https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js`](https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js`) |
|
||||
| Leaflet | Maps | `[https://unpkg.com/leaflet@1.9/dist/leaflet.js`](https://unpkg.com/leaflet@1.9/dist/leaflet.js`) |
|
||||
| Moment.js | Dates | `[https://cdn.jsdelivr.net/npm/moment@2/moment.min.js`](https://cdn.jsdelivr.net/npm/moment@2/moment.min.js`) |
|
||||
|
||||
### Integration Pattern
|
||||
|
||||
```javascript
|
||||
(function() {
|
||||
// Library URLs
|
||||
const LIBS = {
|
||||
echarts: "[https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"](https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js")
|
||||
};
|
||||
|
||||
// Track loading state
|
||||
const libState = {
|
||||
echarts: { loaded: false, loading: false, callbacks: [] }
|
||||
};
|
||||
|
||||
// Load library once
|
||||
function loadLibrary(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const state = libState[name];
|
||||
|
||||
// Already loaded
|
||||
if (state.loaded) {
|
||||
resolve(window[name === "echarts" ? "echarts" : name]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Loading - queue callback
|
||||
if (state.loading) {
|
||||
state.callbacks.push({ resolve, reject });
|
||||
return;
|
||||
}
|
||||
|
||||
// Start loading
|
||||
state.loading = true;
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = LIBS[name];
|
||||
|
||||
script.onload = () => {
|
||||
state.loaded = true;
|
||||
state.loading = false;
|
||||
const lib = window[name === "echarts" ? "echarts" : name];
|
||||
resolve(lib);
|
||||
state.callbacks.forEach(cb => cb.resolve(lib));
|
||||
state.callbacks = [];
|
||||
};
|
||||
|
||||
script.onerror = (err) => {
|
||||
state.loading = false;
|
||||
reject(err);
|
||||
state.callbacks.forEach(cb => cb.reject(err));
|
||||
state.callbacks = [];
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
class ChartWidget extends HTMLElement {
|
||||
async connectedCallback() {
|
||||
try {
|
||||
const echarts = await loadLibrary("echarts");
|
||||
this._initChart(echarts);
|
||||
} catch (error) {
|
||||
this._showError("Failed to load chart library");
|
||||
}
|
||||
}
|
||||
|
||||
_initChart(echarts) {
|
||||
const container = this._shadowRoot.getElementById("chart");
|
||||
this._chart = echarts.init(container);
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("chart-widget", ChartWidget);
|
||||
})();
|
||||
```
|
||||
|
||||
### License Considerations
|
||||
|
||||
**Important**: Review third-party library licenses before deployment.
|
||||
|
||||
- MIT/Apache: Generally safe for commercial use
|
||||
- GPL: May have copyleft requirements
|
||||
- Commercial: May require license purchase
|
||||
|
||||
Check license compatibility with SAC deployment.
|
||||
|
||||
---
|
||||
|
||||
## Advanced Data Binding
|
||||
|
||||
### Multiple Data Bindings
|
||||
|
||||
```json
|
||||
{
|
||||
"dataBindings": {
|
||||
"primaryData": {
|
||||
"feeds": [
|
||||
{ "id": "xAxis", "description": "X-Axis", "type": "dimension" },
|
||||
{ "id": "yAxis", "description": "Y-Axis", "type": "mainStructureMember" }
|
||||
]
|
||||
},
|
||||
"secondaryData": {
|
||||
"feeds": [
|
||||
{ "id": "categories", "description": "Categories", "type": "dimension" },
|
||||
{ "id": "values", "description": "Values", "type": "mainStructureMember" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: Currently only the first dataBinding is used. Multiple bindings are defined but only one is active.
|
||||
|
||||
### Accessing Metadata
|
||||
|
||||
```javascript
|
||||
_processData() {
|
||||
const data = this.primaryData;
|
||||
if (!data || !data.data) return;
|
||||
|
||||
// Access metadata
|
||||
const metadata = data.metadata;
|
||||
|
||||
// Dimension info
|
||||
if (metadata.dimensions) {
|
||||
Object.entries(metadata.dimensions).forEach(([key, dim]) => {
|
||||
console.log(`Dimension: ${dim.description}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Measure info
|
||||
if (metadata.mainStructureMembers) {
|
||||
Object.entries(metadata.mainStructureMembers).forEach(([key, measure]) => {
|
||||
console.log(`Measure: ${measure.description}, Unit: ${measure.unitOfMeasure}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### DataBinding Object Methods
|
||||
|
||||
```javascript
|
||||
// Get DataBinding object
|
||||
const binding = this.dataBindings.getDataBinding("primaryData");
|
||||
|
||||
// Available methods (async)
|
||||
const resultSet = await binding.getResultSet();
|
||||
const members = await binding.getMembers("DimensionName");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Multi-Language Support
|
||||
|
||||
### Externalize Strings
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"titleKey": {
|
||||
"type": "string",
|
||||
"default": "WIDGET_TITLE",
|
||||
"description": "Translation key for title"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Translation Pattern
|
||||
|
||||
```javascript
|
||||
class MyWidget extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._translations = {
|
||||
en: {
|
||||
WIDGET_TITLE: "My Widget",
|
||||
NO_DATA: "No data available",
|
||||
LOADING: "Loading..."
|
||||
},
|
||||
de: {
|
||||
WIDGET_TITLE: "Mein Widget",
|
||||
NO_DATA: "Keine Daten verfügbar",
|
||||
LOADING: "Laden..."
|
||||
}
|
||||
};
|
||||
this._locale = "en";
|
||||
}
|
||||
|
||||
_t(key) {
|
||||
const translations = this._translations[this._locale] || this._translations.en;
|
||||
return translations[key] || key;
|
||||
}
|
||||
|
||||
_render() {
|
||||
this._shadowRoot.getElementById("title").textContent = this._t(this._props.titleKey);
|
||||
}
|
||||
|
||||
// Set locale from SAC context
|
||||
setLocale(locale) {
|
||||
this._locale = locale.substring(0, 2); // "en-US" -> "en"
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging Advanced Widgets
|
||||
|
||||
### Console Inspection
|
||||
|
||||
```javascript
|
||||
// Expose widget for debugging
|
||||
connectedCallback() {
|
||||
// Make accessible in console
|
||||
window.__myWidget = this;
|
||||
|
||||
// Log initialization
|
||||
console.log("[MyWidget] Initialized", {
|
||||
props: this._props,
|
||||
dataBinding: this.primaryData
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Profiling
|
||||
|
||||
```javascript
|
||||
_render() {
|
||||
const start = performance.now();
|
||||
|
||||
// Rendering logic
|
||||
this._doRender();
|
||||
|
||||
const duration = performance.now() - start;
|
||||
if (duration > 16) { // > 1 frame
|
||||
console.warn(`[MyWidget] Slow render: ${duration.toFixed(2)}ms`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [SAP Custom Widget Developer Guide (PDF)](https://help.sap.com/doc/c813a28922b54e50bd2a307b099787dc/release/en-US/CustomWidgetDevGuide_en.pdf)
|
||||
- [Analytics Designer API Reference](https://help.sap.com/doc/958d4c11261f42e992e8d01a4c0dde25/release/en-US/index.html)
|
||||
- [Hosting in SAC](https://community.sap.com/t5/technology-blogs-by-sap/hosting-and-uploading-custom-widgets-resource-files-into-sap-analytics/ba-p/13563064)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-11-22
|
||||
Reference in New Issue
Block a user