Compare commits

...

2 Commits

Author SHA1 Message Date
Alan Rynne ff7db38b8d feat: Fix load/unload logic and cache to use objectIds 2024-07-12 12:51:56 +02:00
Alan Rynne 1702c95e50 feat: Initial working version 2024-07-12 11:03:16 +02:00
8 changed files with 123 additions and 142 deletions
+1 -1
View File
@@ -16,7 +16,7 @@
"settings": {
"powerquery.general.mode": "SDK",
"powerquery.sdk.defaultQueryFile": "${workspaceFolder}\\src\\powerbi-data-connector\\Speckle.query.pq",
"powerquery.sdk.defaultExtension": "${workspaceFolder}\\bin\\${workspaceFolderBasename}.mez",
"powerquery.sdk.defaultExtension": "${workspaceFolder}\\src\\powerbi-data-connector\\bin\\Speckle.mez",
"files.eol": "\n",
"files.watcherExclude": {
"**/.git/objects/**": true,
+1 -6
View File
@@ -1,7 +1,2 @@
// Use this file to write queries to test your data connector
let
result = Speckle.GetByUrl(
"https://app.speckle.systems/projects/e2988234fb/models/60b2300470@b1f31a351a,60b2300470"
)
in
result
let result = Speckle.GetByUrl("https://app.speckle.systems/projects/e2988234fb/models/60b2300470@b1f31a351a") in result
@@ -44,15 +44,13 @@ in
addSpeckleTypeCol = Table.AddColumn(
addObjectIdCol, "speckle_type", each try[data][speckle_type] otherwise null
),
// TODO: JSON Column must be added here so that any detached objects can be re-attached before doing the json thing. If we just pick the raw result from the API, it will only work in the simplest of objects.
addJsonCol = Table.AddColumn(
addSpeckleTypeCol, "json", each try Text.FromBinary(Json.FromValue([data])) otherwise null
),
final = Table.ReorderColumns(
addSpeckleTypeCol, {
"Model URL",
"URL Type",
"Version Object ID",
"Object ID",
"speckle_type",
"data"
}
addJsonCol,
{"Model URL", "URL Type", "Version Object ID", "Object ID", "speckle_type", "data", "json"}
)
in
final
+11 -10
View File
@@ -20,6 +20,11 @@
"kind": "Grouping",
"name": "objectColorBy"
},
{
"displayName": "JSON Data",
"kind": "Measure",
"name": "json"
},
{
"displayName": "Tooltip Data",
"kind": "Measure",
@@ -36,16 +41,6 @@
}
},
"select": [
{
"bind": {
"to": "stream"
}
},
{
"bind": {
"to": "parentObject"
}
},
{
"bind": {
"to": "objectColorBy"
@@ -60,6 +55,11 @@
},
"values": {
"select": [
{
"bind": {
"to": "json"
}
},
{
"bind": {
"to": "objectData"
@@ -206,6 +206,7 @@
"essential": true,
"name": "WebAccess",
"parameters": [
"https://*.speckle.systems",
"https://speckle.xyz",
"https://*.speckle.xyz",
"https://latest.speckle.dev",
+2 -2
View File
@@ -6,6 +6,7 @@
"packages": {
"": {
"name": "@specklesystems/powerbi-visual",
"version": "2.0.0",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.21.5",
@@ -70,8 +71,7 @@
"webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^5.1.1",
"webpack-dev-server": "^4.15.0"
},
"version": "2.0.0"
}
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
@@ -90,62 +90,74 @@ export default class ViewerHandler {
}
public async unloadObjects(
objects: string[],
objectIds: string[],
signal?: AbortSignal,
onObjectUnloaded?: (url: string) => void
) {
for (const url of objects) {
console.log('Unloading objects', objectIds)
for (const speckleObjectId of objectIds) {
if (signal?.aborted) return
await this.viewer
.cancelLoad(url, true)
.catch((e) => console.warn('Viewer Unload error', url, e))
.finally(() => {
if (this.loadedObjectsCache.has(url)) this.loadedObjectsCache.delete(url)
if (onObjectUnloaded) onObjectUnloaded(url)
})
// TODO: Here's where the viewer unloads any objects that have been removed. It first ensures any loading is cancelled.
// await this.viewer
// .cancelLoad(url, true)
// .catch((e) => console.warn('Viewer error while cancelling load', url, e))
if (this.loadedObjectsCache.has(speckleObjectId))
this.loadedObjectsCache.delete(speckleObjectId)
if (onObjectUnloaded) onObjectUnloaded(speckleObjectId)
}
}
public async loadObjectsWithAutoUnload(
objectUrls: string[],
objects: any[],
onLoad: (url: string, index: number) => void,
onError: (url: string, error: Error) => void,
signal: AbortSignal
) {
var objectsToUnload = _.difference([...this.loadedObjectsCache], objectUrls)
var objectsToUnload = _.difference(
[...this.loadedObjectsCache],
objects.map((o) => o.id as string)
)
await this.unloadObjects(objectsToUnload, signal)
await this.loadObjects(objectUrls, onLoad, onError, signal)
await this.loadObjects(objects, onLoad, onError, signal)
}
public async loadObjects(
objectUrls: string[],
objectsToLoad: any[],
onLoad: (url: string, index: number) => void,
onError: (url: string, error: Error) => void,
signal: AbortSignal
) {
console.log('Loading objects', objectsToLoad)
try {
let index = 0
//let index = 0
let promises = []
for (const url of objectUrls) {
for (const speckleObject of objectsToLoad) {
signal.throwIfAborted()
console.log('Attempting to load', url)
if (!this.loadedObjectsCache.has(url)) {
console.log('Attempting to load', speckleObject.id, speckleObject)
if (!this.loadedObjectsCache.has(speckleObject.id)) {
console.log('Object is not in cache')
const promise = this.viewer
.loadObjectAsync(url, this.config.authToken, false)
.then(() => onLoad(url, index++))
.catch((e: Error) => onError(url, e))
.finally(() => {
if (!this.loadedObjectsCache.has(url)) this.loadedObjectsCache.add(url)
})
promises.push(promise)
// TODO: Here's were the viewer loads each object, this used to be done via URL but is now changed to use JSON objects instead (already deserialized)
// const promise = this.viewer
// .loadObjectAsync(speckleObject, this.config.authToken, false)
// .then(() => onLoad(speckleObject.id, index++))
// .catch((e: Error) => onError(speckleObject, e))
// promises.push(promise)
// If batchSize has been reached, wait till all promises resolve before continuing
if (promises.length == this.config.batchSize) {
//this.promises.push(Promise.resolve(this.later(1000)))
await Promise.all(promises)
promises = []
}
if (!this.loadedObjectsCache.has(speckleObject.id))
this.loadedObjectsCache.add(speckleObject.id)
} else {
console.log('Object was already in cache')
console.log('Object was already in cache/already loaded')
}
}
await Promise.all(promises)
@@ -174,6 +186,7 @@ export default class ViewerHandler {
objects: res.objects
}
}
public zoom(objectIds?: string[]) {
this.viewer.zoom(objectIds)
}
@@ -181,6 +194,7 @@ export default class ViewerHandler {
public zoomExtents() {
this.viewer.zoom()
}
public async unIsolateObjects() {
if (this.state.isolatedObjects)
this.state = await this.viewer.unIsolateObjects(this.state.isolatedObjects, 'powerbi', true)
+61 -83
View File
@@ -1,10 +1,6 @@
import powerbi from 'powerbi-visuals-api'
import { IViewerTooltip, IViewerTooltipData, SpeckleDataInput } from '../types'
import { formattingSettings as fs } from 'powerbi-visuals-utils-formattingmodel'
import {
createDataViewWildcardSelector,
DataViewWildcardMatchingOption
} from 'powerbi-visuals-utils-dataviewutils/lib/dataViewWildcard'
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions
import { SpeckleVisualSettingsModel } from 'src/settings/visualSettingsModel'
@@ -15,23 +11,18 @@ export function validateMatrixView(options: VisualUpdateOptions): {
const matrixVew = options.dataViews[0].matrix
if (!matrixVew) throw new Error('Data does not contain a matrix data view')
let hasStream = false,
hasParentObject = false,
hasObject = false,
let hasObject = false,
hasColorFilter = false
matrixVew.rows.levels.forEach((level) => {
level.sources.forEach((source) => {
if (!hasStream) hasStream = source.roles['stream'] != undefined
if (!hasParentObject) hasParentObject = source.roles['parentObject'] != undefined
if (!hasObject) hasObject = source.roles['object'] != undefined
if (!hasColorFilter) hasColorFilter = source.roles['objectColorBy'] != undefined
})
})
if (!hasStream) throw new Error('Missing Stream ID input')
if (!hasParentObject) throw new Error('Missing Commit Object ID input')
if (!hasObject) throw new Error('Missing Object Id input')
return {
hasColorFilter,
view: matrixVew
@@ -65,7 +56,7 @@ function processObjectValues(
displayName: colInfo.displayName,
value: value.value.toString()
}
objectData.push(propData)
if (value.valueSourceIndex) objectData.push(propData)
})
return { data: objectData, shouldColor, shouldSelect }
}
@@ -126,94 +117,50 @@ export function processMatrixView(
settings: SpeckleVisualSettingsModel,
onSelectionPair: (objId: string, selectionId: powerbi.extensibility.ISelectionId) => void
): SpeckleDataInput {
const objectUrlsToLoad = [],
const objectJsonToLoad = [],
objectIds = [],
selectedIds = [],
colorByIds = [],
objectTooltipData = new Map<string, IViewerTooltip>()
matrixView.rows.root.children.forEach((streamUrlChild) => {
const url = streamUrlChild.value
// Assume this has color filter
matrixView.rows.root?.children?.forEach((colorByGroup) => {
const colorByValue = colorByGroup.value
console.log('Color by group', colorByValue, colorByGroup)
streamUrlChild.children?.forEach((parentObjectIdChild) => {
const parentId = parentObjectIdChild.value
objectUrlsToLoad.push(`${url}/objects/${parentId}`)
const colorGroup = createColorGroup(host, colorByGroup, matrixView)
if (!hasColorFilter) {
processObjectIdLevel(parentObjectIdChild, host, matrixView).forEach((objRes) => {
objectIds.push(objRes.id)
onSelectionPair(objRes.id, objRes.selectionId)
if (objRes.shouldSelect) selectedIds.push(objRes.id)
if (objRes.color) {
let group = colorByIds.find((g) => g.color === objRes.color)
if (!group) {
group = {
color: objRes.color,
objectIds: []
}
colorByIds.push(group)
}
group.objectIds.push(objRes.id)
}
objectTooltipData.set(objRes.id, {
selectionId: objRes.selectionId,
data: objRes.data
})
})
} else {
if (previousPalette) host.colorPalette['colorPalette'] = previousPalette
parentObjectIdChild.children?.forEach((colorByChild) => {
const colorSelectionId = host
.createSelectionIdBuilder()
.withMatrixNode(colorByChild, matrixView.rows.levels)
.createSelectionId()
colorByGroup.children.forEach((objectIdGroup) => {
const uniqueId = objectIdGroup.value
const jsonValue = objectIdGroup.values[0] // TODO: Json value is set as first value in capabilities.json
const color = host.colorPalette.getColor(colorByChild.value as string)
if (colorByChild.objects) {
console.log(
'⚠️COLOR NODE HAS objects',
colorByChild.objects,
colorByChild.objects.color?.fill
)
}
objectJsonToLoad.push(JSON.parse(jsonValue.value.toString()))
colorGroup.objectIds.push(uniqueId)
const colorSlice = new fs.ColorPicker({
name: 'selectorFill',
displayName: colorByChild.value.toString(),
value: {
value: color.value
},
selector: colorSelectionId.getSelector()
})
if (jsonValue.highlight) console.log(uniqueId, jsonValue)
var processedObject = processObjectNode(objectIdGroup, host, matrixView)
console.log(processedObject)
const colorGroup = {
color: color.value,
slice: colorSlice,
objectIds: []
}
onSelectionPair(uniqueId.toString(), processedObject.selectionId)
processObjectIdLevel(colorByChild, host, matrixView).forEach((objRes) => {
objectIds.push(objRes.id)
onSelectionPair(objRes.id, objRes.selectionId)
if (objRes.shouldSelect) selectedIds.push(objRes.id)
if (objRes.shouldColor) {
colorGroup.objectIds.push(objRes.id)
}
objectTooltipData.set(objRes.id, {
selectionId: objRes.selectionId,
data: objRes.data
})
})
if (colorGroup.objectIds.length > 0) colorByIds.push(colorGroup)
})
}
if (processedObject.shouldSelect) selectedIds.push(processedObject.id)
if (processedObject.shouldColor) colorGroup.objectIds.push(processedObject.id)
objectTooltipData.set(processedObject.id, {
selectionId: processedObject.selectionId,
data: processedObject.data
})
})
if (colorGroup.objectIds.length > 0) colorByIds.push(colorGroup)
})
// TODO: Code behavior without color filter
previousPalette = host.colorPalette['colorPalette']
return {
objectsToLoad: objectUrlsToLoad,
objectsToLoad: objectJsonToLoad,
objectIds,
selectedIds,
colorByIds: colorByIds.length > 0 ? colorByIds : null,
@@ -221,3 +168,34 @@ export function processMatrixView(
view: matrixView
}
}
function createColorGroup(
host: powerbi.extensibility.visual.IVisualHost,
colorByGroup: powerbi.DataViewMatrixNode,
matrixView: powerbi.DataViewMatrix
) {
const colorSelectionId = host
.createSelectionIdBuilder()
.withMatrixNode(colorByGroup, matrixView.rows.levels)
.createSelectionId()
const color = host.colorPalette.getColor(colorByGroup.value as string)
if (colorByGroup.objects) {
console.log('⚠️COLOR NODE HAS objects', colorByGroup.objects, colorByGroup.objects.color?.fill)
}
const colorSlice = new fs.ColorPicker({
name: 'selectorFill',
displayName: colorByGroup.value.toString(),
value: {
value: color.value
},
selector: colorSelectionId.getSelector()
})
const colorGroup = {
color: color.value,
slice: colorSlice,
objectIds: []
}
return colorGroup
}
-5
View File
@@ -20,11 +20,6 @@ import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructor
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions
import IVisual = powerbi.extensibility.visual.IVisual
import ITooltipService = powerbi.extensibility.ITooltipService
import {
createDataViewWildcardSelector,
DataViewWildcardMatchingOption
} from 'powerbi-visuals-utils-dataviewutils/lib/dataViewWildcard'
import { ColorSelectorSettings } from 'src/settings/colorSettings'
// noinspection JSUnusedGlobalSymbols
export class Visual implements IVisual {