Compare commits

...

6 Commits

Author SHA1 Message Date
oguzhankoral 83f1e57b6e Align states 2025-01-23 13:12:43 +03:00
oguzhankoral b883379bca more WIP 2025-01-22 11:38:24 +03:00
oguzhankoral f65b7de227 WIP 2025-01-22 11:37:09 +03:00
oguzhankoral 539c9160b0 POC from local viewer data 2025-01-21 18:41:21 +03:00
oguzhankoral 1014c96543 Join strings 2025-01-21 00:06:55 +03:00
oguzhankoral 585883d94d Join strings 2025-01-20 17:02:14 +03:00
14 changed files with 311 additions and 147 deletions
@@ -0,0 +1,13 @@
let
headers = [#"Content-Type" = "application/json"],
postData = Json.FromValue([key = 235.7, value = 41.53]),
response = Web.Contents(
"http://localhost:8091/send-data",
[
Headers = headers,
Content = postData
]
),
jsonResponse = Json.Document(response)
in
jsonResponse
+12
View File
@@ -52,6 +52,18 @@ shared Speckle.GetRawJSON = Value.ReplaceType(
type function (url as Uri.Type) as table
);
[DataSource.Kind = "Speckle"]
shared Speckle.GetRawJSON2 = Value.ReplaceType(
Speckle.LoadFunction("GetRawJSON2.pqm"),
type function (url as Uri.Type) as table
);
[DataSource.Kind = "Speckle"]
shared Speckle.LocalSend = Value.ReplaceType(
Speckle.LoadFunction("LocalSend.pqm"),
type function (url as Uri.Type) as table
);
[DataSource.Kind = "Speckle"]
shared Speckle.GetStructuredData = Value.ReplaceType(
Speckle.LoadFunction("GetStructuredData.pqm"),
+1 -2
View File
@@ -1,7 +1,6 @@
// use this file to write queries to test your data connector
let
result = Speckle.GetByUrl(
result = Speckle.GetRawJSON(
"https://app.speckle.systems/projects/e9141d302e/models/482749356d"
)
in
@@ -19,7 +19,7 @@
Message.Parameters = {fileName, e[Reason], e[Message]},
Detail = [File = fileName, Error = e]
],
// get parsed URL components and model info
parsedUrl = Parser(url),
server = parsedUrl[baseUrl],
@@ -35,6 +35,7 @@
Headers = [
#"Authorization" = if apiKey = null then "" else Text.Format("Bearer #{0}", {apiKey})
],
Query = [timestamp = Text.From(DateTime.LocalNow())],
ManualStatusHandling = {400, 401, 403}
]
),
@@ -77,6 +78,16 @@
// convert to table
Chunks = Table.FromRecords(List.Transform(ChunkIndices, CreateChunk)),
Response = Web.Contents(
"http://localhost:8091/send-data",
[
Headers = [#"Content-Type" = "application/json"],
Query = [timestamp = Text.From(DateTime.LocalNow())],
Content = Json.FromValue([key = 235.7, value = 41.53])
]
),
JsonResponse = Json.Document(Response),
// set correct data types
FinalTable = Table.TransformColumnTypes(
@@ -84,6 +95,6 @@
{
{"viewer_data", type text}
}
)
)
in
FinalTable
@@ -0,0 +1,43 @@
// function for getting JSON data with prefixed chunks
(url as text) as table =>
let
// import the Parser and GetModel functions
Parser = Extension.LoadFunction("Parser.pqm"),
GetModel = Extension.LoadFunction("GetModel.pqm"),
Extension.LoadFunction = (fileName as text) =>
let
binary = Extension.Contents(fileName),
asText = Text.FromBinary(binary)
in
try
Expression.Evaluate(asText, #shared)
catch (e) =>
error
[
Reason = "Extension.LoadFunction Failure",
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
Message.Parameters = {fileName, e[Reason], e[Message]},
Detail = [File = fileName, Error = e]
],
// get parsed URL components and model info
parsedUrl = Parser(url),
server = parsedUrl[baseUrl],
modelInfo = GetModel(url),
// get API key if available
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
// make the API request to objects endpoint
Source = Web.Contents(
Text.Combine({server, "objects", parsedUrl[projectId], modelInfo[rootObjectId]}, "/"),
[
Headers = [
#"Authorization" = if apiKey = null then "" else Text.Format("Bearer #{0}", {apiKey})
],
ManualStatusHandling = {400, 401, 403}
]
),
Result = Json.FromValue(Json.Document(Source))
in
Result
@@ -0,0 +1,14 @@
(url as text) as table =>
let
headers = [#"Content-Type" = "application/json"],
postData = Json.FromValue([key = 235.7, value = 41.53]),
response = Web.Contents(
"http://localhost:8091/send-data",
[
Headers = headers,
Content = postData
]
),
jsonResponse = Json.Document(response)
in
jsonResponse
+1 -12
View File
@@ -1,10 +1,5 @@
{
"dataRoles": [
{
"displayName": "Viewer Data",
"kind": "Grouping",
"name": "viewerData"
},
{
"displayName": "Object IDs",
"kind": "Grouping",
@@ -31,11 +26,6 @@
}
},
"select": [
{
"bind": {
"to": "viewerData"
}
},
{
"bind": {
"to": "objectColorBy"
@@ -60,7 +50,6 @@
},
"conditions": [
{
"viewerData": { "max": 1 },
"objectIds": { "max": 1 }
}
]
@@ -208,7 +197,7 @@
{
"essential": true,
"name": "WebAccess",
"parameters": ["https://analytics.speckle.systems", "*"]
"parameters": ["https://analytics.speckle.systems", "http://localhost:8091", "*"]
},
{
"essential": false,
+2 -5
View File
@@ -1,18 +1,15 @@
<template>
<ViewerView v-if="status === 'valid'" />
<ViewerView v-if="visualStore.isViewerReadyToLoad" />
<HomeView v-else />
</template>
<script setup lang="ts">
import HomeView from './views/HomeView.vue'
import ViewerView from './views/ViewerView.vue'
import { computed, onMounted } from 'vue'
import { onMounted } from 'vue'
import { useVisualStore } from './store/visualStore'
const visualStore = useVisualStore()
let status = computed(() => {
return visualStore.dataInputStatus
})
onMounted(() => {
console.log('App mounted')
+9
View File
@@ -39,6 +39,7 @@ export interface IViewerEvents {
unIsolateObjects: () => void
zoomExtends: () => void
loadObjects: (dataInput: SpeckleDataInput) => void
loadObjectsFromJSON: (objects: object[]) => void
}
export class ViewerHandler {
@@ -58,6 +59,7 @@ export class ViewerHandler {
this.emitter.on('zoomExtends', this.zoomExtends)
this.emitter.on('zoomObjects', this.zoomObjects)
this.emitter.on('loadObjects', this.loadObjects)
this.emitter.on('loadObjectsFromJSON', this.loadObjectsFromJSON)
}
async init(parent: HTMLElement) {
@@ -143,6 +145,13 @@ export class ViewerHandler {
}
}
public loadObjectsFromJSON = async (objects: object[]) => {
await this.viewer.unloadAll()
const stringifiedObject = JSON.stringify(objects)
const loader = new SpeckleOfflineLoader(this.viewer.getWorldTree(), stringifiedObject)
await this.viewer.loadObject(loader, true)
}
public loadObjects = async (dataInput: SpeckleDataInput) => {
const stringifiedObject = JSON.stringify(dataInput.objects)
await this.viewer.unloadAll()
+34 -57
View File
@@ -6,7 +6,6 @@ import { ref, shallowRef } from 'vue'
export type InputState = 'valid' | 'incomplete' | 'invalid'
export type FieldInputState = {
viewerData: boolean
objectIds: boolean
colorBy: boolean
tooltipData: boolean
@@ -16,10 +15,10 @@ export const useVisualStore = defineStore('visualStore', () => {
const host = shallowRef<powerbi.extensibility.visual.IVisualHost>()
const objectsFromStore = ref<object[]>(undefined)
const isViewerInitialized = ref<boolean>(false)
const isViewerReadyToInitialize = ref<boolean>(false)
const isViewerReadyToLoad = ref<boolean>(false)
const isViewerObjectsLoaded = ref<boolean>(false)
const viewerReloadNeeded = ref<boolean>(false)
const fieldInputState = ref<FieldInputState>({
viewerData: false,
objectIds: false,
colorBy: false,
tooltipData: false
@@ -74,11 +73,28 @@ export const useVisualStore = defineStore('visualStore', () => {
id: string
}
const loadObjectsFromFile = async (objects: object[]) => {
await viewerEmit.value('loadObjectsFromJSON', objects)
host.value.persistProperties({
merge: [
{
objectName: 'storedData',
properties: {
fullData: JSON.stringify(objects)
},
selector: null
}
]
})
isViewerObjectsLoaded.value = true
}
const loadObjectsFromStore = async () => {
lastLoadedRootObjectId.value = (dataInput.value.objects[0] as SpeckleObject).id
console.log(`📦 Loading viewer from cached data with ${lastLoadedRootObjectId.value} id.`)
viewerReloadNeeded.value = false
await viewerEmit.value('loadObjects', dataInput.value)
isViewerObjectsLoaded.value = true
}
/**
@@ -91,85 +107,46 @@ export const useVisualStore = defineStore('visualStore', () => {
await loadObjectsFromStore()
return
}
// here we have to check upcoming data is require viewer to force update! like a new model or some explicit force..
if (viewerReloadNeeded.value || !lastLoadedRootObjectId.value) {
lastLoadedRootObjectId.value = (dataInput.value.objects[0] as SpeckleObject).id
console.log(
`🔄 Forcing viewer re-render for new root object with ${lastLoadedRootObjectId.value} id.`
)
viewerReloadNeeded.value = false
await viewerEmit.value('loadObjects', dataInput.value)
if (dataInput.value.selectedIds.length > 0) {
viewerEmit.value('isolateObjects', dataInput.value.selectedIds)
} else {
if (dataInput.value.selectedIds.length > 0) {
viewerEmit.value('isolateObjects', dataInput.value.selectedIds)
} else {
viewerEmit.value('isolateObjects', dataInput.value.objectIds)
}
viewerEmit.value('colorObjectsByGroup', dataInput.value.colorByIds)
viewerEmit.value('isolateObjects', dataInput.value.objectIds)
}
viewerEmit.value('colorObjectsByGroup', dataInput.value.colorByIds)
}
const setFieldInputState = (newFieldInputState: FieldInputState) => {
if (!newFieldInputState.viewerData || !newFieldInputState.objectIds) {
setInputStatus('incomplete')
} else {
setInputStatus('valid')
}
// Check for the changes on fields that viewer care, if user changes important fields, we have to ask for viewer reload
if (
fieldInputState.value.viewerData &&
fieldInputState.value.objectIds &&
(!newFieldInputState.viewerData || !newFieldInputState.objectIds)
) {
viewerReloadNeeded.value = true
}
// if (!isViewerInitialized.value) {
// if (
// fieldInputState.value.viewerData &&
// fieldInputState.value.objectIds &&
// !fieldInputState.value.tooltipData &&
// !fieldInputState.value.colorBy
// ) {
// viewerReloadNeeded.value = true
// }
// }
fieldInputState.value = newFieldInputState
}
/**
* Sets input status as flags `viewerReloadNeeded` if the new status is not 'valid'
*/
const setInputStatus = (newValue: InputState) => {
console.log('❓ Data input statues changed to:', newValue)
dataInputStatus.value = newValue
if (dataInputStatus.value !== 'valid') {
viewerReloadNeeded.value = true
}
}
const clearDataInput = () => {
dataInput.value = null
}
const setViewerReadyToLoad = () => {
isViewerReadyToLoad.value = true
}
return {
host,
objectsFromStore,
isViewerInitialized,
isViewerReadyToLoad,
isViewerObjectsLoaded,
viewerReloadNeeded,
dataInput,
dataInputStatus,
viewerEmit,
fieldInputState,
loadObjectsFromFile,
loadObjectsFromStore,
setHost,
setObjectsFromStore,
setViewerEmitter,
setDataInput,
setFieldInputState,
setInputStatus,
clearDataInput
clearDataInput,
setViewerReadyToLoad
}
})
+83 -49
View File
@@ -12,14 +12,12 @@ import { FieldInputState, useVisualStore } from '@src/store/visualStore'
export function validateMatrixView(options: VisualUpdateOptions): FieldInputState {
const matrixVew = options.dataViews[0].matrix
let hasViewerData = false,
hasObjectIds = false,
let hasObjectIds = false,
hasColorFilter = false,
hasTooltipData = false
matrixVew.rows.levels.forEach((level) => {
level.sources.forEach((source) => {
if (!hasViewerData) hasViewerData = source.roles['viewerData'] != undefined
if (!hasObjectIds) hasObjectIds = source.roles['objectIds'] != undefined
if (!hasColorFilter) hasColorFilter = source.roles['objectColorBy'] != undefined
})
@@ -32,7 +30,6 @@ export function validateMatrixView(options: VisualUpdateOptions): FieldInputStat
})
return {
viewerData: hasViewerData,
objectIds: hasObjectIds,
colorBy: hasColorFilter,
tooltipData: hasTooltipData
@@ -110,9 +107,7 @@ function processObjectIdLevel(
host: powerbi.extensibility.visual.IVisualHost,
matrixView: powerbi.DataViewMatrix
) {
return parentObjectIdChild.children?.map((objectIdChild) =>
processObjectNode(objectIdChild, host, matrixView)
)
return processObjectNode(parentObjectIdChild, host, matrixView)
}
export let previousPalette = null
@@ -138,35 +133,56 @@ export function processMatrixView(
const objects: Record<string, string[]> = {}
// let objectsString = ''
// NOTE: matrix view gave us already filtered out rows from tooltip data if it is assigned
matrixView.rows.root.children.forEach((obj) => {
// otherwise there is no point to collect objects
if (visualStore.viewerReloadNeeded) {
const id = obj.children[0].value as unknown as string
const value = (obj.value as unknown as string).slice(9)
const existingObjectId = Object.keys(objects).find((k) => id.includes(k))
if (!existingObjectId) {
objects[id] = [value]
} else {
objects[existingObjectId].push(value)
}
}
// const id = obj.children[0].value as unknown as string
// if (visualStore.viewerReloadNeeded) {
// const viewerDataValue = obj.value as unknown as string
// if (!viewerDataValue.startsWith('z_')) {
// objectsString += viewerDataValue.slice(9)
// }
// }
// before row optimization
// if (visualStore.viewerReloadNeeded) {
// const id = obj.children[0].value as unknown as string
// const value = (obj.value as unknown as string).slice(9)
// const existingObjectId = Object.keys(objects).find((k) => id.includes(k))
// if (!existingObjectId) {
// objects[id] = [value]
// } else {
// objects[existingObjectId].push(value)
// }
// }
const processedObjectIdLevels = processObjectIdLevel(obj, host, matrixView)
processedObjectIdLevels.forEach((objRes) => {
objectIds.push(objRes.id)
onSelectionPair(objRes.id, objRes.selectionId)
if (objRes.shouldSelect) {
selectedIds.push(objRes.id)
}
objectTooltipData.set(objRes.id, {
selectionId: objRes.selectionId,
data: objRes.data
})
objectIds.push(processedObjectIdLevels.id)
onSelectionPair(processedObjectIdLevels.id, processedObjectIdLevels.selectionId)
if (processedObjectIdLevels.shouldSelect) {
selectedIds.push(processedObjectIdLevels.id)
}
objectTooltipData.set(processedObjectIdLevels.id, {
selectionId: processedObjectIdLevels.selectionId,
data: processedObjectIdLevels.data
})
// processedObjectIdLevels.forEach((objRes) => {
// objectIds.push(objRes.id)
// onSelectionPair(objRes.id, objRes.selectionId)
// if (objRes.shouldSelect) {
// selectedIds.push(objRes.id)
// }
// objectTooltipData.set(objRes.id, {
// selectionId: objRes.selectionId,
// data: objRes.data
// })
// })
if (hasColorFilter) {
obj.children.forEach((child) => {
const colorSelectionId = host
@@ -191,33 +207,51 @@ export function processMatrixView(
objectIds: []
}
processObjectIdLevel(child, 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
})
const processedObjectIdLevels = processObjectIdLevel(child, host, matrixView)
objectIds.push(processedObjectIdLevels.id)
onSelectionPair(processedObjectIdLevels.id, processedObjectIdLevels.selectionId)
if (processedObjectIdLevels.shouldSelect) selectedIds.push(processedObjectIdLevels.id)
if (processedObjectIdLevels.shouldColor) {
colorGroup.objectIds.push(processedObjectIdLevels.id)
}
objectTooltipData.set(processedObjectIdLevels.id, {
selectionId: processedObjectIdLevels.selectionId,
data: processedObjectIdLevels.data
})
// processObjectIdLevel(child, 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)
})
}
})
const jsonObjects: object[] = []
try {
// otherwise there is no point to join collected objects
if (visualStore.viewerReloadNeeded) {
for (const objs of Object.values(objects)) {
jsonObjects.push(JSON.parse(objs.join('')))
}
}
} catch (error) {
console.error(error)
}
const jsonObjects = []
// if (visualStore.viewerReloadNeeded) {
// jsonObjects = JSON.parse(objectsString)
// }
// // const jsonObjects: object[] = JSON.parse(objectsString)
// try {
// // otherwise there is no point to join collected objects
// // if (visualStore.viewerReloadNeeded) {
// // for (const objs of Object.values(objects)) {
// // jsonObjects.push(JSON.parse(objs.join('')))
// // }
// // }
// jsonObjects = JSON.parse(objectsString)
// } catch (error) {
// console.error(error)
// }
previousPalette = host.colorPalette['colorPalette']
+42
View File
@@ -10,10 +10,12 @@
<div class="flex justify-center mt-2 gap-1">
<button :class="buttonClass" @click="goToForum">Help</button>
<button :class="buttonClass" @click="goToGuide">Getting started</button>
<button :class="buttonClass" @click="triggerFileInput">Upload File</button>
<!-- TODO: dependency issue need to be resolved to be able to use ui-components library-->
<!-- <FormButton color="subtle" @click="goToForum">Help</FormButton>
<FormButton color="subtle" @click="goToGuide">Getting started</FormButton> -->
</div>
<input ref="fileInput" type="file" style="display: none" @change="handleFileChange" />
<!-- <CommonLoadingBar :loading="true"/> -->
</div>
</template>
@@ -34,4 +36,44 @@ function goToForum() {
function goToGuide() {
visualStore.host.launchUrl('https://speckle.guide/user/powerbi')
}
// Method to programmatically trigger the file input
function triggerFileInput() {
const fileInput = document.querySelector<HTMLInputElement>('input[type="file"]')
fileInput?.click()
}
// Method to handle file selection
function handleFileChange(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
if (file) {
console.log('Selected file:', file.name)
console.log(file)
const reader = new FileReader()
reader.onload = (e) => {
const fileContent = e.target?.result
if (fileContent) {
const visualStore = useVisualStore()
visualStore.setViewerReadyToLoad()
setTimeout(() => {
const objects = JSON.parse(fileContent as string)
console.log('File content:', objects)
visualStore.loadObjectsFromFile(objects)
}, 250)
// Process the file content (e.g., parse JSON, display text, etc.)
}
}
// Handle errors if any occur
reader.onerror = (e) => {
console.error('Error reading file:', e)
}
// Read the file as text (you can also use readAsDataURL, readAsBinaryString, etc.)
reader.readAsText(file)
// Add logic to process the file as needed
}
}
</script>
@@ -2,14 +2,50 @@
<div class="absolute top-0 left-0 z-10" @click="goToSpeckleWebsite">
<img class="w-16 h-auto mt-1 mr-1 cursor-pointer" src="@assets/powered-by-speckle.png" />
</div>
<div
v-if="isInteractive"
class="absolute top-2 left-1/2 -translate-x-1/2 z-20 bg-white bg-opacity-70 text-black text-center text-sm px-4 py-2 rounded shadow"
>
<div v-if="bothFieldsMissing">
<strong>Object IDs</strong>
and
<strong>Tooltip Data</strong>
fields are needed for interactivity.
</div>
<div v-else-if="onlyObjectIdsMissing">
<strong>Object IDs</strong>
field is needed for interactivity.
</div>
<div v-else-if="onlyTooltipDataMissing">
<strong>Tooltip Data</strong>
field is needed for interactivity.
</div>
</div>
<viewer-wrapper id="speckle-3d-view" class="h-full w-full"></viewer-wrapper>
</template>
<script setup lang="ts">
import ViewerWrapper from 'src/components/ViewerWrapper.vue'
import { useVisualStore } from '../store/visualStore'
import { computed } from 'vue'
const visualStore = useVisualStore()
const onlyObjectIdsMissing = computed(
() => !visualStore.fieldInputState.objectIds && visualStore.fieldInputState.tooltipData
)
const onlyTooltipDataMissing = computed(
() => visualStore.fieldInputState.objectIds && !visualStore.fieldInputState.tooltipData
)
const bothFieldsMissing = computed(
() => !visualStore.fieldInputState.objectIds && !visualStore.fieldInputState.tooltipData
)
const isInteractive = computed(
() => !visualStore.fieldInputState.objectIds || !visualStore.fieldInputState.tooltipData
)
const goToSpeckleWebsite = () => visualStore.host.launchUrl('https://speckle.systems')
</script>
+8 -20
View File
@@ -79,11 +79,14 @@ export class Visual implements IVisual {
console.log('Selector colors', this.formattingSettings.colorSelector)
try {
const matrixVew = options.dataViews[0].matrix
if (!matrixVew) throw new Error('Data does not contain a matrix data view') // TODO: Could be toast notificiation too!
const matrixView = options.dataViews[0].matrix
if (!matrixView) throw new Error('Data does not contain a matrix data view') // TODO: Could be toast notificiation too!
console.log(matrixView)
// we first need to check which inputs user provided to decide our strategy
const validationResult = validateMatrixView(options)
console.log(validationResult)
visualStore.setFieldInputState(validationResult)
// read saved data from file if any
@@ -105,7 +108,7 @@ export class Visual implements IVisual {
case powerbi.VisualUpdateType.Data:
try {
const input = processMatrixView(
matrixVew,
matrixView,
this.host,
validationResult.colorBy,
this.formattingSettings,
@@ -128,8 +131,7 @@ export class Visual implements IVisual {
console.warn(
`Incomplete data input. "Viewer Data", "Object IDs" data inputs are mandatory. If your data connector does not output all these columns, please update it.`
)
visualStore.setInputStatus('incomplete')
visualStore.setFieldInputState({ objectIds: false, colorBy: false, tooltipData: false })
return
}
}
@@ -153,21 +155,7 @@ export class Visual implements IVisual {
input.objects = visualStore.objectsFromStore
input.isFromStore = true
}
// if (!visualStore.isViewerInitialized && visualStore.viewerReloadNeeded) {
// this.host.persistProperties({
// merge: [
// {
// objectName: 'storedData',
// properties: {
// fullData: JSON.stringify(input.objects)
// },
// selector: null
// }
// ]
// })
// }
// visualStore.setDataInput(input)
visualStore.setViewerReadyToLoad()
if (visualStore.isViewerInitialized && !visualStore.viewerReloadNeeded) {
visualStore.setDataInput(input)