Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a4ae9340a | |||
| 92bcf4b5c0 | |||
| 2a22bbf0af | |||
| 7b5e5397b6 | |||
| 24eeb44ff7 | |||
| b1f16c4005 | |||
| 2307d87735 | |||
| b80624396d | |||
| 098ef3d112 | |||
| 94fdc7a2c3 | |||
| 525857bd26 | |||
| 959bcaa671 |
@@ -26,7 +26,7 @@ jobs:
|
||||
run: |
|
||||
TAG=${{ github.ref_name }}
|
||||
if [[ "${{ github.ref }}" != refs/tags/* ]]; then
|
||||
TAG="v3.0.99.${{ github.run_number }}"
|
||||
TAG="v3.0.99"
|
||||
fi
|
||||
SEMVER="${TAG#v}"
|
||||
FILE_VERSION=$(echo "$TAG" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
run: |
|
||||
TAG=${{ github.ref_name }}
|
||||
if [[ "${{ github.ref }}" != refs/tags/* ]]; then
|
||||
TAG="v3.0.99.${{ github.run_number }}"
|
||||
TAG="v3.0.99"
|
||||
fi
|
||||
SEMVER="${TAG#v}"
|
||||
FILE_VERSION=$(echo "$TAG" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
|
||||
|
||||
@@ -142,11 +142,14 @@
|
||||
// get structured data
|
||||
structuredData = GetStructuredData(singleModelUrl),
|
||||
|
||||
// add the model name as context
|
||||
// add the model name as context - with version id if exists
|
||||
result = Table.AddColumn(
|
||||
structuredData,
|
||||
"Source Model",
|
||||
each modelName,
|
||||
structuredData,
|
||||
"Source Model",
|
||||
each if versionId <> null then
|
||||
Text.Combine({modelName, "-", versionId})
|
||||
else
|
||||
modelName,
|
||||
type text
|
||||
)
|
||||
in
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
(url as text) as list =>
|
||||
let
|
||||
// Import required functions
|
||||
GetModel = Extension.LoadFunction("GetModel.pqm"),
|
||||
Parser = Extension.LoadFunction("Parser.pqm"),
|
||||
GetUser = Extension.LoadFunction("GetUser.pqm"),
|
||||
GetVersion = Extension.LoadFunction("GetVersion.pqm"),
|
||||
GetWorkspace = Extension.LoadFunction("GetWorkspace.pqm"),
|
||||
|
||||
// the logic for importing functions from other files
|
||||
// helper function to load .pqm modules dynamically
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName),
|
||||
@@ -24,7 +23,6 @@
|
||||
Detail = [File = fileName, Error = e]
|
||||
],
|
||||
|
||||
// Get required information
|
||||
modelInfo = GetModel(url),
|
||||
parsedUrl = Parser(url),
|
||||
userInfo = GetUser(url),
|
||||
@@ -33,136 +31,144 @@
|
||||
connectorVersion = GetVersion(),
|
||||
workspaceInfo = GetWorkspace(url),
|
||||
|
||||
// Function to check if Desktop Service is available
|
||||
IsDesktopServiceAvailable = () =>
|
||||
// attempts to exchange powerful token for weak token via desktop service
|
||||
// returns [Success = true/false, Token = weak_token/null]
|
||||
TryTokenExchange = () =>
|
||||
try
|
||||
let
|
||||
PingResponse = Web.Contents(
|
||||
"http://127.0.0.1:29364/ping",
|
||||
tokenExchangeData = Json.FromValue([
|
||||
PowerfulToken = apiKey,
|
||||
Scopes = {"profile:read", "streams:read", "users:read"},
|
||||
ProjectId = parsedUrl[projectId],
|
||||
ServerUrl = parsedUrl[baseUrl]
|
||||
]),
|
||||
|
||||
tokenExchangeResponse = Web.Contents(
|
||||
"http://127.0.0.1:29364/auth/exchange-token",
|
||||
[
|
||||
Headers = [#"Method" = "GET"],
|
||||
Headers = [
|
||||
#"Content-Type" = "application/json",
|
||||
#"Method" = "POST"
|
||||
],
|
||||
Content = tokenExchangeData,
|
||||
ManualStatusHandling = {400, 401, 403, 404, 500, 502, 503, 504},
|
||||
Timeout = #duration(0, 0, 0, 2) // 2 second timeout for ping
|
||||
Timeout = #duration(0, 0, 0, 5)
|
||||
]
|
||||
),
|
||||
StatusCode = Value.Metadata(PingResponse)[Response.Status]
|
||||
|
||||
StatusCode = Value.Metadata(tokenExchangeResponse)[Response.Status],
|
||||
|
||||
Result = if StatusCode >= 200 and StatusCode < 300 then
|
||||
let
|
||||
tokenExchangeJson = Json.Document(tokenExchangeResponse),
|
||||
weakToken = tokenExchangeJson[token]
|
||||
in
|
||||
[Success = true, Token = weakToken]
|
||||
else
|
||||
[Success = false, Token = null]
|
||||
in
|
||||
StatusCode = 200
|
||||
Result
|
||||
otherwise
|
||||
false,
|
||||
[Success = false, Token = null],
|
||||
|
||||
// Function to use Desktop Service approach (only called if available)
|
||||
UseDesktopService = () =>
|
||||
// stores user info to desktop service for power bi visual consumption
|
||||
// returns status code (or 0 on failure)
|
||||
SendTelemetry = (token as text) =>
|
||||
try
|
||||
let
|
||||
userInfoData = Json.FromValue([
|
||||
Url = url,
|
||||
Server = parsedUrl[baseUrl],
|
||||
Email = userEmail,
|
||||
ProjectId = parsedUrl[projectId],
|
||||
RootObjectId = modelInfo[rootObjectId],
|
||||
SourceApplication = modelInfo[sourceApplication],
|
||||
Token = token,
|
||||
Version = connectorVersion,
|
||||
VersionId = parsedUrl[versionId],
|
||||
WorkspaceId = workspaceInfo[workspaceId],
|
||||
WorkspaceName = workspaceInfo[workspaceName],
|
||||
WorkspaceLogo = workspaceInfo[workspaceLogo],
|
||||
CanHideBranding = workspaceInfo[canHideBranding]
|
||||
]),
|
||||
|
||||
userInfoResponse = Web.Contents(
|
||||
"http://127.0.0.1:29364/store-user-info",
|
||||
[
|
||||
Headers = [
|
||||
#"Content-Type" = "application/json",
|
||||
#"Method" = "POST"
|
||||
],
|
||||
Content = userInfoData,
|
||||
ManualStatusHandling = {400, 401, 403, 404, 500, 502, 503, 504},
|
||||
Timeout = #duration(0, 0, 0, 3)
|
||||
]
|
||||
),
|
||||
|
||||
statusCode = Value.Metadata(userInfoResponse)[Response.Status]
|
||||
in
|
||||
statusCode
|
||||
otherwise
|
||||
0,
|
||||
|
||||
// downloads data directly from server without desktop service
|
||||
DirectDownload = (token as text) =>
|
||||
let
|
||||
// exchange powerful token for weak token via ds
|
||||
tokenExchangeData = Json.FromValue([
|
||||
PowerfulToken = apiKey,
|
||||
Scopes = {"profile:read", "streams:read", "users:read"},
|
||||
ProjectId = parsedUrl[projectId],
|
||||
ServerUrl = parsedUrl[baseUrl]
|
||||
]),
|
||||
|
||||
tokenExchangeResponse = Web.Contents(
|
||||
"http://127.0.0.1:29364/auth/exchange-token",
|
||||
[
|
||||
Headers = [
|
||||
#"Content-Type" = "application/json",
|
||||
#"Method" = "POST"
|
||||
],
|
||||
Content = tokenExchangeData,
|
||||
ManualStatusHandling = {400, 401, 403, 404, 500}
|
||||
]
|
||||
),
|
||||
|
||||
tokenExchangeJson = Json.Document(tokenExchangeResponse),
|
||||
weakToken = tokenExchangeJson[token],
|
||||
|
||||
// prepare request data with weak token
|
||||
requestData = Json.FromValue([
|
||||
Url = url,
|
||||
Server = parsedUrl[baseUrl],
|
||||
Email = userEmail,
|
||||
ProjectId = parsedUrl[projectId],
|
||||
RootObjectId = modelInfo[rootObjectId],
|
||||
SourceApplication = modelInfo[sourceApplication],
|
||||
Token = weakToken,
|
||||
Version = connectorVersion,
|
||||
VersionId = parsedUrl[versionId],
|
||||
WorkspaceId = workspaceInfo[workspaceId],
|
||||
WorkspaceName = workspaceInfo[workspaceName],
|
||||
WorkspaceLogo = workspaceInfo[workspaceLogo],
|
||||
CanHideBranding = workspaceInfo[canHideBranding]
|
||||
]),
|
||||
|
||||
// Send request to local server
|
||||
Response = Web.Contents(
|
||||
"http://127.0.0.1:29364/download",
|
||||
[
|
||||
Headers = [
|
||||
#"Content-Type" = "application/json",
|
||||
#"Method" = "POST"
|
||||
],
|
||||
Content = requestData,
|
||||
ManualStatusHandling = {400, 401, 403, 404, 500}
|
||||
]
|
||||
),
|
||||
|
||||
// Parse response
|
||||
JsonResponse = Json.Document(Response)
|
||||
in
|
||||
JsonResponse,
|
||||
|
||||
// Function to fallback to direct JSON download from Speckle server
|
||||
FallbackToDirectDownload = () =>
|
||||
let
|
||||
// Construct the direct object URL: {baseUrl}/objects/{projectId}/{rootObjectId}
|
||||
objectUrl = Text.Combine({
|
||||
parsedUrl[baseUrl],
|
||||
"/objects/",
|
||||
parsedUrl[projectId],
|
||||
"/",
|
||||
parsedUrl[baseUrl],
|
||||
"/objects/",
|
||||
parsedUrl[projectId],
|
||||
"/",
|
||||
modelInfo[rootObjectId]
|
||||
}),
|
||||
|
||||
// Download JSON directly from Speckle server
|
||||
|
||||
Response = Web.Contents(
|
||||
objectUrl,
|
||||
[
|
||||
Headers = [
|
||||
#"Authorization" = "Bearer " & apiKey,
|
||||
#"Authorization" = "Bearer " & token,
|
||||
#"Accept" = "application/json"
|
||||
],
|
||||
ManualStatusHandling = {400, 401, 403, 404, 500, 502, 503, 504}
|
||||
]
|
||||
),
|
||||
|
||||
// Check response status
|
||||
|
||||
StatusCode = Value.Metadata(Response)[Response.Status],
|
||||
|
||||
// Parse JSON response if successful
|
||||
|
||||
JsonResponse = if StatusCode >= 200 and StatusCode < 300 then
|
||||
Json.Document(Response)
|
||||
else
|
||||
error [
|
||||
Reason = "DirectDownloadFailed",
|
||||
Message = "Failed to download model data directly from Speckle server",
|
||||
Message.Format = "Failed to download model data from Speckle server (Status: #{0})",
|
||||
Message.Parameters = {Text.From(StatusCode)},
|
||||
Detail = [
|
||||
StatusCode = StatusCode,
|
||||
StatusCode = StatusCode,
|
||||
ObjectUrl = objectUrl,
|
||||
ProjectId = parsedUrl[projectId],
|
||||
RootObjectId = modelInfo[rootObjectId]
|
||||
RootObjectId = modelInfo[rootObjectId],
|
||||
UsedWeakToken = token <> apiKey
|
||||
]
|
||||
]
|
||||
in
|
||||
JsonResponse,
|
||||
|
||||
// Check Desktop Service availability and choose approach
|
||||
DesktopServiceAvailable = IsDesktopServiceAvailable(),
|
||||
|
||||
FinalResult = if DesktopServiceAvailable then
|
||||
UseDesktopService()
|
||||
// try token exchange, use weak token if successful, otherwise use powerful token
|
||||
// powerful token just for data connector, never stored in visual
|
||||
TokenExchangeResult = TryTokenExchange(),
|
||||
TokenToUse = if TokenExchangeResult[Success] then
|
||||
TokenExchangeResult[Token]
|
||||
else
|
||||
FallbackToDirectDownload()
|
||||
apiKey,
|
||||
|
||||
// send user info to desktop service
|
||||
TelemetryStatusCode = SendTelemetry(TokenToUse),
|
||||
|
||||
// download data
|
||||
FinalResult = if TelemetryStatusCode >= 0 then
|
||||
DirectDownload(TokenToUse)
|
||||
else
|
||||
DirectDownload(TokenToUse)
|
||||
|
||||
in
|
||||
FinalResult
|
||||
FinalResult
|
||||
|
||||
Generated
+14
-14
@@ -13,10 +13,10 @@
|
||||
"@babel/runtime-corejs3": "^7.21.5",
|
||||
"@headlessui/vue": "^1.7.13",
|
||||
"@heroicons/vue": "^2.0.12",
|
||||
"@speckle/objectloader2": "2.26.2",
|
||||
"@speckle/objectloader2": "2.26.7",
|
||||
"@speckle/tailwind-theme": "2.23.2",
|
||||
"@speckle/ui-components": "2.23.2",
|
||||
"@speckle/viewer": "2.26.1",
|
||||
"@speckle/viewer": "2.26.5",
|
||||
"color-interpolate": "^1.0.5",
|
||||
"core-js": "^3.30.2",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -3406,21 +3406,21 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@speckle/objectloader2": {
|
||||
"version": "2.26.2",
|
||||
"resolved": "https://registry.npmjs.org/@speckle/objectloader2/-/objectloader2-2.26.2.tgz",
|
||||
"integrity": "sha512-sX0Mpi9h54CoWAl58YVCef4JSxWNnB+pFfTjo1XNfBEuyfwL6JBO8j2ho5OYLIyag4VZ1yXu/3MfmSIY4lMq3w==",
|
||||
"version": "2.26.7",
|
||||
"resolved": "https://registry.npmjs.org/@speckle/objectloader2/-/objectloader2-2.26.7.tgz",
|
||||
"integrity": "sha512-bc3hp/83DBnyp2TOaVtKfqRaWz5UKcWouW281JcFcezTn/TZnB+FSF2csClBhOzrWSqmGVns0KfoJVfflETghQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@speckle/shared": "^2.26.2"
|
||||
"@speckle/shared": "^2.26.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@speckle/shared": {
|
||||
"version": "2.26.2",
|
||||
"resolved": "https://registry.npmjs.org/@speckle/shared/-/shared-2.26.2.tgz",
|
||||
"integrity": "sha512-fvsq8J0riSNEPL9WaExzStl2qyUZIzQPOrFPDe/Biylkgv89GTAJlZdYZ4q1AqfaL3o9wYYQ8tKudjl+cAgFrQ==",
|
||||
"version": "2.26.7",
|
||||
"resolved": "https://registry.npmjs.org/@speckle/shared/-/shared-2.26.7.tgz",
|
||||
"integrity": "sha512-3TJNmC1JFKvrAfnEDqX7A4P8V/P8M26T8+7sNRtsIMaINFAvKbBjTi7KGFzoAwQmzeXdbUsd2jXF4Kt94W3ZKQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"dayjs": "^1.11.13",
|
||||
@@ -3545,13 +3545,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@speckle/viewer": {
|
||||
"version": "2.26.1",
|
||||
"resolved": "https://registry.npmjs.org/@speckle/viewer/-/viewer-2.26.1.tgz",
|
||||
"integrity": "sha512-COqGbk+086GBRpctvvgvP/NhXbq1OeieRUIosmUnXswTq8b6G3PGd7FJXAPs2KI1uhQNda+igzql8sFurn7gNA==",
|
||||
"version": "2.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@speckle/viewer/-/viewer-2.26.5.tgz",
|
||||
"integrity": "sha512-kC/mKYVQZW9f+ek4fcYBEsSEo5fk/cvhBRPFpUzu974gNkaQJAJKWGCziB4U2WHai/twQXJS/yhyrCNOAVBnzQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@speckle/objectloader2": "^2.26.1",
|
||||
"@speckle/shared": "^2.26.1",
|
||||
"@speckle/objectloader2": "^2.26.5",
|
||||
"@speckle/shared": "^2.26.5",
|
||||
"@types/flat": "^5.0.2",
|
||||
"earcut": "3.0.1",
|
||||
"flat": "^5.0.2",
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
"@babel/runtime-corejs3": "^7.21.5",
|
||||
"@headlessui/vue": "^1.7.13",
|
||||
"@heroicons/vue": "^2.0.12",
|
||||
"@speckle/objectloader2": "2.26.2",
|
||||
"@speckle/objectloader2": "2.26.7",
|
||||
"@speckle/tailwind-theme": "2.23.2",
|
||||
"@speckle/ui-components": "2.23.2",
|
||||
"@speckle/viewer": "2.26.1",
|
||||
"@speckle/viewer": "2.26.5",
|
||||
"color-interpolate": "^1.0.5",
|
||||
"core-js": "^3.30.2",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<FormButton
|
||||
v-if="visualStore.latestAvailableVersion && !visualStore.isConnectorUpToDate"
|
||||
v-if="visualStore.latestAvailableVersion && !visualStore.isConnectorUpToDate && visualStore.isRunningInDesktop"
|
||||
v-tippy="{
|
||||
content: 'New connector version is available.<br>Click to download.',
|
||||
allowHTML: true
|
||||
@@ -64,7 +64,7 @@
|
||||
field is needed for interactivity with other visuals.
|
||||
</div>
|
||||
|
||||
<div v-if="visualStore.isNavbarHidden" class="fixed top-0 right-0 z-20">
|
||||
<div v-if="visualStore.isNavbarHidden" class="fixed top-4 right-2 z-20">
|
||||
<button
|
||||
class="transition opacity-50 hover:opacity-100"
|
||||
title="Show navbar"
|
||||
@@ -165,7 +165,7 @@ onMounted(async () => {
|
||||
|
||||
// Set up event listener for object clicks from the FilteredSelectionExtension
|
||||
viewerHandler.emitter.on('objectClicked', handleObjectClicked)
|
||||
|
||||
|
||||
visualStore.setViewerEmitter(viewerHandler.emit)
|
||||
})
|
||||
|
||||
|
||||
@@ -41,8 +41,12 @@ export function useUpdateConnector() {
|
||||
return new Date(b.Date).getTime() - new Date(a.Date).getTime()
|
||||
})
|
||||
versions.value = sortedVersions
|
||||
const sanitizedVersion = sanitizeVersion(sortedVersions[0].Number)
|
||||
latestAvailableVersion.value = { ...sortedVersions[0], Number: sanitizedVersion }
|
||||
|
||||
// Filter out prerelease versions
|
||||
const stableVersions = sortedVersions.filter((v) => !v.Prerelease)
|
||||
const latestVersion = stableVersions[0]
|
||||
const sanitizedVersion = sanitizeVersion(latestVersion.Number)
|
||||
latestAvailableVersion.value = { ...latestVersion, Number: sanitizedVersion }
|
||||
visualStore.setLatestAvailableVersion(latestAvailableVersion.value)
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,15 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
return false
|
||||
})
|
||||
|
||||
// detecting the env to control the visibility of update button
|
||||
// might use for different reasons in the future
|
||||
const isRunningInDesktop = computed(() => {
|
||||
// power bi hostEnv enum values:
|
||||
// web = 1, desktop = 4
|
||||
const hostEnv = host.value?.['hostEnv'] as number
|
||||
return hostEnv === 4
|
||||
})
|
||||
|
||||
/**
|
||||
* Ideally one time set when onMounted of `ViewerWrapper.vue` component
|
||||
* @param emit picky emit function to trigger events under `IViewerEvents` interface
|
||||
@@ -559,6 +568,7 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
isZoomOnFilterActive,
|
||||
latestAvailableVersion,
|
||||
isConnectorUpToDate,
|
||||
isRunningInDesktop,
|
||||
commonError,
|
||||
previousToggleState,
|
||||
setCommonError,
|
||||
|
||||
Reference in New Issue
Block a user