Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:55:27 +08:00
commit 3c265b6541
12 changed files with 6207 additions and 0 deletions

View 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