Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f4f2248bf | |||
| 19f847dacd | |||
| b027474b53 | |||
| d3e11bcc2e | |||
| 34f96bfaf2 | |||
| d26b8545c6 | |||
| 379cb192a6 | |||
| 11eed4bebc | |||
| 53df28be54 | |||
| 45b2ea3962 | |||
| 156dd8bbc5 | |||
| 6d9c605741 | |||
| 52f4325619 | |||
| 6e92c857a7 | |||
| 8edc01b7d6 | |||
| 6d5f638895 | |||
| ccecf7cb2b | |||
| 156c3a5c3a | |||
| 887dbb2344 | |||
| 556196a45f | |||
| 1bf6e76252 | |||
| b26801ef8a | |||
| 74ab91be3b |
@@ -46,10 +46,6 @@ jobs:
|
||||
name: "npm run build"
|
||||
command: "npm run build"
|
||||
working_directory: src/powerbi-visual
|
||||
- run:
|
||||
name: "npm run pack"
|
||||
command: "npm run pack"
|
||||
working_directory: src/powerbi-visual
|
||||
- store_artifacts:
|
||||
path: dist/*.pbiviz
|
||||
- persist_to_workspace:
|
||||
|
||||
@@ -341,3 +341,6 @@ ASALocalRun/
|
||||
**/webpack.statistics.html
|
||||
**/Thumbs.db
|
||||
installer/
|
||||
|
||||
localhost.pem
|
||||
localhost-key.pem
|
||||
@@ -1,14 +1,74 @@
|
||||
<h1 align="center">
|
||||
<<h1 align="center">
|
||||
<img src="https://user-images.githubusercontent.com/2679513/131189167-18ea5fe1-c578-47f6-9785-3748178e4312.png" width="150px"/><br/>
|
||||
Speckle | PowerBI
|
||||
Speckle | Power BI
|
||||
</h1>
|
||||
<h3 align="center">
|
||||
Data Connector and 3D Viewer Visual for PowerBI platform
|
||||
</h3>
|
||||
<p align="center"><b>Speckle</b> is the data infrastructure for the AEC industry.</p><br/>
|
||||
|
||||
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&style=flat-square&logo=discourse&logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&logo=read-the-docs&logoColor=white" alt="docs"></a></p>
|
||||
<p align="center"></p>
|
||||
|
||||
> Speckle is the first AEC data hub that connects with your favorite AEC tools. Speckle exists to overcome the challenges of working in a fragmented industry where communication, creative workflows, and the exchange of data are often hindered by siloed software and processes. It is here to make the industry better.
|
||||
|
||||
<h3 align="center">
|
||||
Speckle Connector and 3D Viewer Visual for Power BI
|
||||
</h3>
|
||||
|
||||
# Features
|
||||
|
||||
Speckle Power BI Data Connector lets you easily get data from Speckle into Power BI reports and visualizations. You can access and analyze data from various AEC apps (like Revit, Archicad, Grasshopper, and more) and open-source files (IFC, STL, OBJ, etc.) into Power BI with ease.
|
||||
|
||||
Speckle’s connection to Power BI consists of two parts:
|
||||
|
||||
- **Data Connector** fetches the data you uploaded from AEC apps to Speckle.
|
||||
- **3D Visual** allows you to see those models in 3D within Power BI.
|
||||
|
||||

|
||||
|
||||
# Repo Structure
|
||||
|
||||
This repo is home to our Power BI connector. The Speckle Server provides all the web-facing functionality and can be found [here](https://github.com/specklesystems/Server).
|
||||
|
||||
`src/powerbi-data-connector` contains all the code for the Data connector.
|
||||
|
||||
`src/powerbi-visual` contains all the code for 3D Visual.
|
||||
|
||||
# Installation
|
||||
|
||||
Speckle connector can be installed directly from [Manager for Speckle](https://speckle.systems/download/). Full instructions for [installation](https://speckle.guide/user/powerbi/installation.html) and [configuration](https://speckle.guide/user/powerbi/configuration.html) can be found on our docs.
|
||||
|
||||
# Using 3D Visual
|
||||
|
||||
3D Visual can be imported as any other Power BI custom visual.
|
||||
|
||||
1. Navigate to the Visualization Pane.
|
||||
2. Click the three dots (…) and select “Import a visual from a file”.
|
||||
3. Go to `Documents/Power BI Desktop/Custom Visuals` and import `Speckle 3D Visual.pbiviz` file.
|
||||
4. Speckle cube will appear in the Visualization pane.
|
||||
|
||||
For more on how to use the visual, [check our docs](https://speckle.guide/user/powerbi-visual/introduction.html).
|
||||
|
||||
# Usage
|
||||
|
||||
To get started with Power BI connectors, please take a look at the [documentation](https://speckle.guide/user/powerbi/introduction.html) and extensive [tutorials](https://www.youtube.com/playlist?list=PLlI5Dyt2HaEsZHG2WJ75WIM0Brx6VHT2S) published.
|
||||
|
||||
# **Developing & Debugging**
|
||||
|
||||
We encourage everyone interested to debug/hack/contribute/give feedback to this project.
|
||||
|
||||
## **Setup**
|
||||
|
||||
### **Install PowerQuery SDK**
|
||||
|
||||
Follow the instructions from the [official docs](https://docs.microsoft.com/en-us/power-query/installingsdk)
|
||||
|
||||
### **Build with Visual Studio**
|
||||
|
||||
Every time you build the connector, VisualStudio will copy the latest `.mez` connector file to the appropriate location. Just restart PowerBI to see the latest changes.
|
||||
|
||||
### **Debug**
|
||||
|
||||
You can start the PowerQuery connector in VisualStudio, this will open a standalone connector you can use for testing purposes.
|
||||
|
||||
We don't know of a way to debug the connector live in PowerBI, but we'd be happy to hear about it.
|
||||
|
||||
|
||||
# About Speckle
|
||||
|
||||
@@ -31,8 +91,7 @@ What is Speckle? Check our ](https://speckle.xyz) ⇒ creating an account at our public server
|
||||
- [](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
|
||||
- [](https://app.speckle.systems) ⇒ creating an account at our public server
|
||||
|
||||
### Resources
|
||||
|
||||
@@ -41,56 +100,3 @@ Give Speckle a try in no time by:
|
||||
- [](https://speckle.guide/dev/) reference on almost any end-user and developer functionality
|
||||
|
||||

|
||||
|
||||
# Repo structure
|
||||
|
||||
This repo is the home to our Speckle 2.0 PowerBI project. The [Speckle Server](https://github.com/specklesystems/Server) is providing all the web-facing functionality and can be found [here](https://github.com/specklesystems/Server).
|
||||
|
||||
## Install
|
||||
|
||||
Go to the [Releases](https://github.com/specklesystems/speckle-powerbi/releases) page, downlad the `.mez` file of the latest release and copy it into the following folder in your computer:
|
||||
|
||||
```
|
||||
YOUR_USER_FOLDER\Documents\Power BI Desktop\Custom Connectors\
|
||||
```
|
||||
If the folder doesn't exist, create it.
|
||||
|
||||
### Allow custom extensions to run
|
||||
|
||||
Go to `Settings -> Security -> Data Extensions` and activate the following option:
|
||||
|
||||

|
||||
|
||||
### Checking the connector is loaded
|
||||
|
||||
Now open PowerBI and you should see `Speckle (beta)` appear in the data source.
|
||||
|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
> More detailed instructions on how to use the connector will be added shortly!
|
||||
|
||||
### Current limitations
|
||||
|
||||
Chunked data currently is not automatically de-chunked when received, we are aware of this limitation and are working to resolve it!
|
||||
|
||||
## Developing & Debugging
|
||||
|
||||
We encourage everyone interested to debug / hack / contribute / give feedback to this project.
|
||||
|
||||
### Setup
|
||||
|
||||
#### Install PowerQuery SDK
|
||||
|
||||
Follow the instructions from the [official docs](https://docs.microsoft.com/en-us/power-query/installingsdk)
|
||||
|
||||
#### Build with Visual Studio
|
||||
|
||||
Every time you build the connector, VisualStudio will copy the latest `.mez` connector file to the appropriate location. Just restart PowerBI to see the latest changes.
|
||||
|
||||
#### Debug
|
||||
|
||||
You can start the PowerQuery connector in VisualStudio, this will open a standalone connector you can use for testing purposes.
|
||||
|
||||
We don't know of a way to debug the connector live in PowerBI, but we'd be happy to hear about it.
|
||||
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "speckle-powerbi",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -1,46 +1,52 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"name": "root",
|
||||
"path": "."
|
||||
"name": "👀 powerbi-visual",
|
||||
"path": "src/powerbi-visual"
|
||||
},
|
||||
{
|
||||
"name": "DataConnector",
|
||||
"name": "➡️ powerbi-data-connector",
|
||||
"path": "src/powerbi-data-connector"
|
||||
},
|
||||
{
|
||||
"name": "Visual",
|
||||
"path": "src/powerbi-visual"
|
||||
}
|
||||
"name": "🏠 root",
|
||||
"path": "."
|
||||
},
|
||||
],
|
||||
"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,
|
||||
"**/node_modules/**": true,
|
||||
".tmp": true
|
||||
},
|
||||
"files.exclude": {
|
||||
".tmp": true
|
||||
},
|
||||
"search.exclude": {
|
||||
".tmp": true,
|
||||
"typings": true
|
||||
"typings": true,
|
||||
"dist": true,
|
||||
"wepbpack.statistics.dev.html": true,
|
||||
"wepbpack.statistics.html": true,
|
||||
},
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/pbiviz.json"],
|
||||
"fileMatch": [
|
||||
"/pbiviz.json"
|
||||
],
|
||||
"url": "./src/powerbi-visual/node_modules/powerbi-visuals-api/schema.pbiviz.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": ["/capabilities.json"],
|
||||
"fileMatch": [
|
||||
"/capabilities.json"
|
||||
],
|
||||
"url": "./src/powerbi-visual/node_modules/powerbi-visuals-api/schema.capabilities.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": ["/dependencies.json"],
|
||||
"fileMatch": [
|
||||
"/dependencies.json"
|
||||
],
|
||||
"url": "./src/powerbi-visual/node_modules/powerbi-visuals-api/schema.dependencies.json"
|
||||
}
|
||||
]
|
||||
@@ -51,4 +57,4 @@
|
||||
"powerquery.vscode-powerquery-sdk"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,100 @@
|
||||
[Version = "2.0.0"]
|
||||
[Version = "3.0.0"]
|
||||
section Speckle;
|
||||
|
||||
AuthAppId = "spklpwerbi";
|
||||
AuthAppSecret = "spklpwerbi";
|
||||
|
||||
// The data source definition, used when connecting to any speckle server
|
||||
// Function to load `pqm` files - this is essential and must be kept
|
||||
shared Speckle.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName),
|
||||
asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared)
|
||||
catch (e) =>
|
||||
error
|
||||
[
|
||||
Reason = "Speckle.LoadFunction Failure",
|
||||
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
];
|
||||
|
||||
// here we register the functions to expose them globally
|
||||
[DataSource.Kind = "Speckle"]
|
||||
shared Speckle.Parser = Value.ReplaceType(
|
||||
Speckle.LoadFunction("Parser.pqm"),
|
||||
type function (url as Uri.Type) as record
|
||||
);
|
||||
|
||||
[DataSource.Kind = "Speckle"]
|
||||
shared Speckle.GetUser = Value.ReplaceType(
|
||||
Speckle.LoadFunction("GetUser.pqm"),
|
||||
type function (url as Uri.Type) as record
|
||||
);
|
||||
|
||||
[DataSource.Kind = "Speckle"]
|
||||
shared Speckle.GetModel = Value.ReplaceType(
|
||||
Speckle.LoadFunction("GetModel.pqm"),
|
||||
type function (url as Uri.Type) as record
|
||||
);
|
||||
|
||||
[DataSource.Kind = "Speckle"]
|
||||
shared Speckle.GetRawData = Value.ReplaceType(
|
||||
Speckle.LoadFunction("GetRawData.pqm"),
|
||||
type function (url as Uri.Type) as table
|
||||
);
|
||||
|
||||
[DataSource.Kind = "Speckle"]
|
||||
shared Speckle.GetStructuredData = Value.ReplaceType(
|
||||
Speckle.LoadFunction("GetStructuredData.pqm"),
|
||||
type function (url as Uri.Type) as table
|
||||
);
|
||||
|
||||
[DataSource.Kind = "Speckle", Publish="GetByUrl.Publish"]
|
||||
shared Speckle.GetByUrl = Value.ReplaceType(
|
||||
Speckle.LoadFunction("GetByUrl.pqm"),
|
||||
type function (
|
||||
url as (
|
||||
Uri.Type meta [
|
||||
Documentation.FieldCaption = "Speckle Model URL",
|
||||
Documentation.FieldDescription = "The URL of a model in a Speckle server project. You can copy it directly from your browser.",
|
||||
Documentation.SampleValues = {"https://app.speckle.systems/projects/7902de1f57/models/7f890a65df"}
|
||||
]
|
||||
)
|
||||
) as table meta [
|
||||
Documentation.Name = "Speckle - Get Data by URL",
|
||||
Documentation.DisplayName = "Speckle - Get Data by URL",
|
||||
Documentation.LongDescription = "Returns structured data from a Speckle model URL.#(lf)
|
||||
Supports the following URL formats:#(lf)
|
||||
- Model URL: Gets the latest version of the specified model#(lf)
|
||||
(e.g., 'https://app.speckle.systems/projects/PROJECT_ID/models/MODEL_ID')#(lf)
|
||||
- Version URL: Gets a specific version from the project#(lf)
|
||||
(e.g., 'https://app.speckle.systems/projects/PROJECT_ID/models/MODEL_ID@VERSION_ID')"
|
||||
]
|
||||
);
|
||||
|
||||
GetByUrl.Publish = [
|
||||
Beta = true,
|
||||
Cateogry = "Other",
|
||||
ButtonText = {"Connect to Speckle"},
|
||||
LearnMoreUrl = "https://speckle.guide/user/powerbi/introduction.html",
|
||||
SourceImage = GetByUrl.Icons,
|
||||
SourceTypeImage = GetByUrl.Icons
|
||||
];
|
||||
|
||||
GetByUrl.Icons = [
|
||||
Icon16 = { Extension.Contents("SpeckleLogo16.png"), Extension.Contents("SpeckleLogo20.png"), Extension.Contents("SpeckleLogo24.png"), Extension.Contents("SpeckleLogo32.png") },
|
||||
Icon32 = { Extension.Contents("SpeckleLogo32.png"), Extension.Contents("SpeckleLogo40.png"), Extension.Contents("SpeckleLogo48.png"), Extension.Contents("SpeckleLogo64.png") }
|
||||
];
|
||||
|
||||
|
||||
// The data source definition
|
||||
Speckle = [
|
||||
// This is used when running the connector on an on-premises data gateway
|
||||
TestConnection = (path) => {"Speckle.Api.GetUser", path},
|
||||
// This is the custom authentication strategy for our Connector
|
||||
TestConnection = (path) => {"Speckle.GetUser", path},
|
||||
// Authentication strategy
|
||||
Authentication = [
|
||||
OAuth = [
|
||||
Label = "Speckle Account",
|
||||
@@ -94,124 +180,3 @@ Speckle = [
|
||||
],
|
||||
Label = "Speckle"
|
||||
];
|
||||
|
||||
// Gets the object referenced by a specific speckle URL
|
||||
[DataSource.Kind = "Speckle", Publish = "Get.ByUrl.Publish"]
|
||||
shared Speckle.GetByUrl.Structured = Value.ReplaceType(
|
||||
Speckle.LoadFunction("Get.ByUrl.pqm"),
|
||||
type function (
|
||||
url as (
|
||||
Uri.Type meta [
|
||||
Documentation.FieldCaption = "Gets a Speckle Object preserving it's structure",
|
||||
Documentation.FieldDescription = "The url of a model in a Speckle server project. You can copy it directly from your browser.",
|
||||
Documentation.SampleValues = {"https://app.speckle.systems/projects/23401adf/models/1234568"}
|
||||
]
|
||||
)
|
||||
) as record meta [
|
||||
Documentation.Name = "Speckle - Get Structured Object by URL",
|
||||
Documentation.LongDescription = "Returns the Speckle object the URL points to, while also preserving it's structure.
|
||||
Supports all types of model url:#(lf)
|
||||
- Model: will get the latest version of the specified model (i.e. 'https://app.speckle.systems/projects/PROJECT_ID/models/MODEL_ID')#(lf)
|
||||
- Version: will get a specific version from the project (i.e. 'https://app.speckle.systems/projects/PROJECT_ID/models/MODEL_ID@VERSION_ID')
|
||||
"
|
||||
]
|
||||
);
|
||||
|
||||
// [DataSource.Kind = "Speckle", Publish = "NavTable.Publish"]
|
||||
// shared Speckle.GetObjectAsNavTable = Value.ReplaceType(
|
||||
// NavigationTable.Simple, type function (url as Uri.Type) as table
|
||||
// );
|
||||
// Get's a flat list of speckle objects from a URL
|
||||
[DataSource.Kind = "Speckle", Publish = "GetByUrl.Publish"]
|
||||
shared Speckle.GetByUrl = Value.ReplaceType(
|
||||
Speckle.LoadFunction("GetByUrl.pqm"),
|
||||
type function (
|
||||
url as (
|
||||
Uri.Type meta [
|
||||
Documentation.FieldCaption = "Model URL",
|
||||
Documentation.FieldDescription = "The url of a model in a Speckle server. You can copy it directly from your browser.",
|
||||
Documentation.SampleValues = {"https://app.speckle.systems/projects/23401adf/models/1234568"}
|
||||
]
|
||||
)
|
||||
) as table meta [
|
||||
Documentation.Name = "Speckle - Get Model by URL",
|
||||
Documentation.LongDescription = "Returns a flat list of all objects contained in a Speckle model/version of a specific a project.
|
||||
Supports all types of model url:#(lf)
|
||||
- Model: will get the latest version of the specified model (i.e. 'https://app.speckle.systems/projects/PROJECT_ID/models/MODEL_ID')#(lf)
|
||||
- Version: will get a specific version from the project (i.e. 'https://app.speckle.systems/projects/PROJECT_ID/models/MODEL_ID@VERSION_ID')
|
||||
"
|
||||
]
|
||||
);
|
||||
|
||||
// Gets the current authenticated user, if any
|
||||
[DataSource.Kind = "Speckle"]
|
||||
shared Speckle.Api.GetUser = Value.ReplaceType(
|
||||
Speckle.LoadFunction("Api.GetUser.pqm"), type function (url as Uri.Type) as record
|
||||
);
|
||||
|
||||
// Generic fetch function to our GraphQL endpoint
|
||||
[DataSource.Kind = "Speckle"]
|
||||
shared Speckle.Api.Fetch = Value.ReplaceType(
|
||||
Speckle.LoadFunction("Api.Fetch.pqm"),
|
||||
type function (url as Uri.Type, optional query as text, optional variables as record) as record
|
||||
);
|
||||
|
||||
// Parses a stream url and returns a record with the type and values
|
||||
[DataSource.Kind = "Speckle"]
|
||||
shared Speckle.ParseUrl = Value.ReplaceType(
|
||||
Speckle.LoadFunction("ParseStreamUrl.pqm"), type function (url as Uri.Type) as record
|
||||
);
|
||||
|
||||
// [DataSource.Kind = "Speckle"]
|
||||
// shared Speckle.Api.REST.GetObject = Value.ReplaceType(
|
||||
// Speckle.LoadFunction("Api.REST.GetObject.pqm"),
|
||||
// type function (url as Uri.Type, optional streamId as text, optional objectId as text) as list
|
||||
// );
|
||||
Get.ByUrl.Publish = GetPublish("GetStream");
|
||||
|
||||
NavTable.Publish = GetPublish("GetObjectAsNavTable");
|
||||
|
||||
GetByUrl.Publish = GetPublish("GetByUrl");
|
||||
|
||||
GetPublish = Speckle.LoadFunction("GetPublish.pqm");
|
||||
|
||||
// Navigation table utility function
|
||||
Table.ToNavigationTable = Speckle.LoadFunction("Table.ToNavigationTable.pqm");
|
||||
|
||||
// Function to load `pqm` files
|
||||
shared Speckle.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName), asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared) catch (e) =>
|
||||
error
|
||||
[
|
||||
Reason = "Speckle.LoadFunction Failure",
|
||||
Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
|
||||
Message.Parameters = {fileName, e[Reason], e[Message]},
|
||||
Detail = [File = fileName, Error = e]
|
||||
];
|
||||
|
||||
shared Speckle.Revit.Parameters.ToNameValueRecord = (r as record, optional exclude as list) as record =>
|
||||
let
|
||||
defaultExclude = {"id", "speckle_type", "applicationId", "totalChildrenCount"},
|
||||
fullExclusion = if exclude = null then defaultExclude else List.Union(defaultExclude, exclude),
|
||||
clean = Record.RemoveFields(r, fullExclusion, MissingField.Ignore),
|
||||
recTable = Record.ToTable(clean),
|
||||
cleanTable = Table.RemoveColumns(recTable, "Name"),
|
||||
expanded = Table.ExpandRecordColumn(
|
||||
cleanTable, "Value", {"name", "value", "applicationInternalName"}, {"Name", "Value", "UID"}
|
||||
),
|
||||
joined = Table.AddColumn(expanded, "Combo", each [Name] & " [" & [UID] & "]"),
|
||||
renamed = Table.RenameColumns(joined, {{"Name", "x"}, {"Combo", "Name"}}),
|
||||
result = Record.FromTable(renamed)
|
||||
in
|
||||
result;
|
||||
|
||||
shared Speckle.Utils.DynamicColumnExpand = (tbl as table, col as text) as table =>
|
||||
let
|
||||
uniqueFields = List.Distinct(List.Combine(List.Transform(Table.Column(tbl, col), Record.FieldNames))),
|
||||
expanded = Table.ExpandRecordColumn(tbl, col, uniqueFields)
|
||||
in
|
||||
expanded;
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<MezContent Include="Speckle.pq" />
|
||||
<MezContent Include="utilities\**\*.pqm" />
|
||||
<MezContent Include="speckle\**\*.pqm" />
|
||||
<MezContent Include="assets\SpeckleLogo16.png" />
|
||||
<MezContent Include="assets\SpeckleLogo20.png" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 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"
|
||||
"https://app.speckle.systems/projects/e9141d302e/models/482749356d"
|
||||
)
|
||||
in
|
||||
result
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
let
|
||||
Fetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
CommitReceived = Extension.LoadFunction("Api.CommitReceived.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]
|
||||
]
|
||||
in
|
||||
(server as text, streamId as text, branchName as text, limit as number) as list =>
|
||||
let
|
||||
decodedBranchName = Record.Field(
|
||||
Record.Field(Uri.Parts("http://www.dummy.com?" & Uri.BuildQueryString([A = branchName])), "Query"),
|
||||
"A"
|
||||
),
|
||||
// Hacky way to decode base64 strings: Put them in a url query param and parse the URL
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
|
||||
query = "query($streamId: String!, $branchName: String!, $limit: Int!) {
|
||||
stream( id: $streamId ) {
|
||||
branch (name: $branchName ){
|
||||
commits (limit: $limit) {
|
||||
items {
|
||||
id
|
||||
referencedObject
|
||||
sourceApplication
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}",
|
||||
res = Fetch(server, query, [streamId = streamId, branchName = decodedBranchName, limit = limit]),
|
||||
branch = res[stream][branch],
|
||||
commits = branch[commits][items]
|
||||
in
|
||||
if branch = null then
|
||||
error Text.Format("The branch '#{0}' does not exist in stream '#{1}'", {decodedBranchName, streamId})
|
||||
else if List.Count(branch[commits][items]) = 0 then
|
||||
error Text.Format("The branch '#{0}' in stream #{1} has no commits", {decodedBranchName, streamId})
|
||||
else
|
||||
commits
|
||||
@@ -1,47 +0,0 @@
|
||||
let
|
||||
Fetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
Traverse = Extension.LoadFunction("Traverse.pqm"),
|
||||
GetObject = Extension.LoadFunction("Api.GetObject.pqm"),
|
||||
GetStreamCommit = Extension.LoadFunction("Get.StreamCommit.pqm"),
|
||||
GetBranchCommits = Extension.LoadFunction("Get.BranchCommits.pqm"),
|
||||
CommitReceived = Extension.LoadFunction("Api.CommitReceived.pqm"),
|
||||
ParseStreamUrl = Extension.LoadFunction("ParseStreamUrl.pqm"),
|
||||
CleanUpObject = Extension.LoadFunction("CleanUpObject.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]
|
||||
]
|
||||
in
|
||||
(url as text) as record =>
|
||||
let
|
||||
// Get server and streamId, and branchName / commitId / objectid from the input url
|
||||
stream = ParseStreamUrl(url),
|
||||
id = stream[id],
|
||||
server = stream[server],
|
||||
commit =
|
||||
if (stream[urlType] = "Stream") then
|
||||
GetBranchCommits(server, id, "main", 1){0}
|
||||
else if (stream[urlType] = "Branch") then
|
||||
GetBranchCommits(server, id, stream[branch], 1){0}
|
||||
else if (stream[urlType] = "Commit") then
|
||||
GetStreamCommit(server, id, stream[commit])
|
||||
else
|
||||
//We deal with object URLs directly
|
||||
[referencedObject = stream[object]],
|
||||
object = GetObject(server, id, commit[referencedObject]),
|
||||
rr = CommitReceived(server, id, commit),
|
||||
result = Traverse(CleanUpObject(object) meta [server = server, stream = id, commit = commit])
|
||||
in
|
||||
if rr then
|
||||
result
|
||||
else
|
||||
result
|
||||
@@ -1,36 +0,0 @@
|
||||
let
|
||||
Fetch = Extension.LoadFunction("Api.Fetch.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]
|
||||
]
|
||||
in
|
||||
(server as text, streamId as text, commitId as text) as record =>
|
||||
let
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
|
||||
query = "query($streamId: String!, $commitId: String!) {
|
||||
stream( id: $streamId ) {
|
||||
commit (id: $commitId) {
|
||||
id
|
||||
sourceApplication
|
||||
referencedObject
|
||||
}
|
||||
}
|
||||
}",
|
||||
variables = [streamId = streamId, commitId = commitId],
|
||||
#"JSON" = Fetch(server, query, variables),
|
||||
commit = #"JSON"[stream][commit]
|
||||
in
|
||||
if commit = null then
|
||||
error "The commit did not exist on this stream"
|
||||
else
|
||||
commit
|
||||
@@ -1,64 +0,0 @@
|
||||
(appName as text) =>
|
||||
let
|
||||
replaced = Text.Replace(appName, " ", ""), name = Text.Lower(replaced)
|
||||
in
|
||||
if Text.Contains(name, "dynamo") then
|
||||
"dynamo"
|
||||
else if Text.Contains(name, "revit") then
|
||||
"revit"
|
||||
else if Text.Contains(name, "autocad") then
|
||||
"autocad"
|
||||
else if Text.Contains(name, "civil") then
|
||||
"civil"
|
||||
else if Text.Contains(name, "rhino") then
|
||||
"rhino"
|
||||
else if Text.Contains(name, "grasshopper") then
|
||||
"grasshopper"
|
||||
else if Text.Contains(name, "unity") then
|
||||
"unity"
|
||||
else if Text.Contains(name, "gsa") then
|
||||
"gsa"
|
||||
else if Text.Contains(name, "microstation") then
|
||||
"microstation"
|
||||
else if Text.Contains(name, "openroads") then
|
||||
"openroads"
|
||||
else if Text.Contains(name, "openrail") then
|
||||
"openrail"
|
||||
else if Text.Contains(name, "openbuildings") then
|
||||
"openbuildings"
|
||||
else if Text.Contains(name, "etabs") then
|
||||
"etabs"
|
||||
else if Text.Contains(name, "sap") then
|
||||
"sap"
|
||||
else if Text.Contains(name, "csibridge") then
|
||||
"csibridge"
|
||||
else if Text.Contains(name, "safe") then
|
||||
"safe"
|
||||
else if Text.Contains(name, "teklastructures") then
|
||||
"teklastructures"
|
||||
else if Text.Contains(name, "dxf") then
|
||||
"dxf"
|
||||
else if Text.Contains(name, "excel") then
|
||||
"excel"
|
||||
else if Text.Contains(name, "unreal") then
|
||||
"unreal"
|
||||
else if Text.Contains(name, "powerbi") then
|
||||
"powerbi"
|
||||
else if Text.Contains(name, "blender") then
|
||||
"blender"
|
||||
else if Text.Contains(name, "qgis") then
|
||||
"qgis"
|
||||
else if Text.Contains(name, "arcgis") then
|
||||
"arcgis"
|
||||
else if Text.Contains(name, "sketchup") then
|
||||
"sketchup"
|
||||
else if Text.Contains(name, "archicad") then
|
||||
"archicad"
|
||||
else if Text.Contains(name, "topsolid") then
|
||||
"topsolid"
|
||||
else if Text.Contains(name, "python") then
|
||||
"python"
|
||||
else if Text.Contains(name, "net") then
|
||||
"net"
|
||||
else
|
||||
"other"
|
||||
@@ -1,58 +1,45 @@
|
||||
let
|
||||
Fetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
GetObject = Extension.LoadFunction("Api.GetObject.pqm"),
|
||||
GetAllObjectChildren = Extension.LoadFunction("Api.GetAllObjectChildren.pqm"),
|
||||
GetObjectFromCommit = Extension.LoadFunction("GetObjectFromCommit.pqm"),
|
||||
GetObjectFromBranch = Extension.LoadFunction("GetObjectFromBranch.pqm"),
|
||||
CommitReceived = Extension.LoadFunction("Api.CommitReceived.pqm"),
|
||||
ParseStreamUrl = Extension.LoadFunction("ParseStreamUrl.pqm"),
|
||||
Extension.LoadFunction = (fileName as text) =>
|
||||
let
|
||||
binary = Extension.Contents(fileName), asText = Text.FromBinary(binary)
|
||||
in
|
||||
try
|
||||
Expression.Evaluate(asText, #shared) catch (e) =>
|
||||
// Function for getting data by URL with navigation
|
||||
(url as text) as table =>
|
||||
let
|
||||
// Import required functions
|
||||
GetStructuredData = Extension.LoadFunction("GetStructuredData.pqm"),
|
||||
GetRawData = Extension.LoadFunction("GetRawData.pqm"),
|
||||
|
||||
// the logic for importing functions from other files
|
||||
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]
|
||||
]
|
||||
in
|
||||
(url as text) as table =>
|
||||
let
|
||||
// Get server and streamId, and branchName / commitId / objectid from the input url
|
||||
stream = ParseStreamUrl(url),
|
||||
id = stream[id],
|
||||
server = stream[server],
|
||||
commitObjectsTable =
|
||||
if (stream[urlType] = "Commit") then
|
||||
GetObjectFromCommit(server, id, stream[commit])
|
||||
else if (stream[urlType] = "Object") then
|
||||
GetAllObjectChildren(server, id, stream[object])
|
||||
else if (stream[urlType] = "Branch") then
|
||||
GetObjectFromBranch(server, id, stream[branch])
|
||||
else
|
||||
GetObjectFromBranch(server, id, "main"),
|
||||
addStreamUrl = Table.AddColumn(commitObjectsTable, "Model URL", each server & "/streams/" & id),
|
||||
addParentObjectId = Table.AddColumn(
|
||||
addStreamUrl, "Version Object ID", each Value.Metadata(commitObjectsTable)[objectId]
|
||||
),
|
||||
addUrlType = Table.AddColumn(addParentObjectId, "URL Type", each stream[urlType]),
|
||||
addObjectIdCol = Table.AddColumn(addUrlType, "Object ID", each try[data][id] otherwise null),
|
||||
addSpeckleTypeCol = Table.AddColumn(
|
||||
addObjectIdCol, "speckle_type", each try[data][speckle_type] otherwise null
|
||||
),
|
||||
final = Table.ReorderColumns(
|
||||
addSpeckleTypeCol, {
|
||||
"Model URL",
|
||||
"URL Type",
|
||||
"Version Object ID",
|
||||
"Object ID",
|
||||
"speckle_type",
|
||||
"data"
|
||||
}
|
||||
)
|
||||
in
|
||||
final
|
||||
],
|
||||
|
||||
// Create navigation table
|
||||
source = #table(
|
||||
{"Name", "Data"},
|
||||
{
|
||||
{ "Structured Data", GetStructuredData(url)},
|
||||
{ "Viewer Data", GetRawData(url)}
|
||||
}
|
||||
),
|
||||
|
||||
// Add navigation table metadata directly
|
||||
tableType = Value.Type(source),
|
||||
newTableType = Type.AddTableKey(tableType, {"Name"}, true) meta [
|
||||
NavigationTable.NameColumn = "Name",
|
||||
NavigationTable.DataColumn = "Data",
|
||||
Documentation.Name = "Speckle Model"
|
||||
],
|
||||
|
||||
// Convert to navigation table
|
||||
navTable = Value.ReplaceType(source, newTableType)
|
||||
in
|
||||
navTable
|
||||
@@ -1,55 +0,0 @@
|
||||
let
|
||||
Fetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
GetAllObjectChildren = Extension.LoadFunction("Api.GetAllObjectChildren.pqm"),
|
||||
CommitReceived = Extension.LoadFunction("Api.CommitReceived.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]
|
||||
]
|
||||
in
|
||||
(server as text, streamId as text, branchName as text) as table =>
|
||||
let
|
||||
decodedBranchName = Record.Field(
|
||||
Record.Field(Uri.Parts("http://www.dummy.com?" & Uri.BuildQueryString([A = branchName])), "Query"),
|
||||
"A"
|
||||
),
|
||||
// Hacky way to decode base64 strings: Put them in a url query param and parse the URL
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
|
||||
query = "query($streamId: String!, $branchName: String!) {
|
||||
stream( id: $streamId ) {
|
||||
branch (name: $branchName ){
|
||||
commits (limit: 1) {
|
||||
items {
|
||||
id
|
||||
referencedObject
|
||||
sourceApplication
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}",
|
||||
res = Fetch(server, query, [streamId = streamId, branchName = decodedBranchName]),
|
||||
branch = res[stream][branch],
|
||||
commit = branch[commits][items]{0},
|
||||
objectsTable = GetAllObjectChildren(server, streamId, commit[referencedObject]),
|
||||
rr = CommitReceived(server, streamId, commit)
|
||||
in
|
||||
if branch = null then
|
||||
error Text.Format("The branch '#{0}' does not exist in stream '#{1}'", {decodedBranchName, streamId})
|
||||
else if List.Count(branch[commits][items]) = 0 then
|
||||
error Text.Format("The branch '#{0}' in stream #{1} has no commits", {decodedBranchName, streamId})
|
||||
else
|
||||
// Force evaluation of read receipt (ideally it should happen after fetching, but can't find a way)
|
||||
if rr then
|
||||
objectsTable
|
||||
else
|
||||
objectsTable
|
||||
@@ -1,43 +0,0 @@
|
||||
let
|
||||
Fetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
GetAllObjectChildren = Extension.LoadFunction("Api.GetAllObjectChildren.pqm"),
|
||||
CommitReceived = Extension.LoadFunction("Api.CommitReceived.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]
|
||||
]
|
||||
in
|
||||
(server as text, streamId as text, commitId as text) as table =>
|
||||
let
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
|
||||
query = "query($streamId: String!, $commitId: String!) {
|
||||
stream( id: $streamId ) {
|
||||
commit (id: $commitId) {
|
||||
id
|
||||
sourceApplication
|
||||
referencedObject
|
||||
authorId
|
||||
}
|
||||
}
|
||||
}",
|
||||
variables = [streamId = streamId, commitId = commitId],
|
||||
#"JSON" = Fetch(server, query, variables),
|
||||
commit = #"JSON"[stream][commit],
|
||||
objectsTable = GetAllObjectChildren(server, streamId, commit[referencedObject]),
|
||||
rr = CommitReceived(server, streamId, commit)
|
||||
in
|
||||
if commit = null then
|
||||
error "The commit did not exist on this stream"
|
||||
else if rr then
|
||||
objectsTable
|
||||
else
|
||||
objectsTable
|
||||
@@ -1,33 +0,0 @@
|
||||
let
|
||||
Speckle.Api.Fetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
Speckle.LogEvent = Extension.LoadFunction("LogEvent.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]
|
||||
]
|
||||
in
|
||||
(server, streamId, commit) =>
|
||||
let
|
||||
query = "mutation($input: CommitReceivedInput!) {
|
||||
commitReceive(input: $input)
|
||||
}",
|
||||
variables = [
|
||||
input = [
|
||||
streamId = streamId,
|
||||
commitId = commit[id],
|
||||
sourceApplication = "PowerBI"
|
||||
]
|
||||
],
|
||||
s = Speckle.LogEvent(server, commit)
|
||||
in
|
||||
// Read receipts should fail gracefully no matter what
|
||||
try Speckle.Api.Fetch(s, query, variables)[commitReceive] otherwise false
|
||||
@@ -1,33 +0,0 @@
|
||||
(server as text, optional query as text, optional variables as record) as record =>
|
||||
let
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
|
||||
defaultQuery = "query {
|
||||
activeUser {
|
||||
email
|
||||
name
|
||||
}
|
||||
serverInfo {
|
||||
name
|
||||
company
|
||||
version
|
||||
}
|
||||
}",
|
||||
Source = Web.Contents(
|
||||
Text.Combine({server, "graphql"}, "/"),
|
||||
[
|
||||
Headers = [
|
||||
#"Method" = "POST",
|
||||
#"Content-Type" = "application/json",
|
||||
#"Authorization" = if apiKey = null then "" else Text.Format("Bearer #{0}", {apiKey})
|
||||
],
|
||||
ManualStatusHandling = {400},
|
||||
Content = Json.FromValue([query = Text.From(query ?? defaultQuery), variables = variables])
|
||||
]
|
||||
),
|
||||
#"JSON" = Json.Document(Source)
|
||||
in
|
||||
// Check if response contains errors, if so, return first error.
|
||||
if Record.HasFields(#"JSON", {"errors"}) then
|
||||
error #"JSON"[errors]{0}[message]
|
||||
else
|
||||
#"JSON"[data]
|
||||
@@ -1,46 +0,0 @@
|
||||
let
|
||||
Table.GenerateByPage = Extension.LoadFunction("Table.GenerateByPage.pqm"),
|
||||
Speckle.Api.GetObjectChildren = Extension.LoadFunction("Api.GetObjectChildren.pqm"),
|
||||
Speckle.Api.GetObject = Extension.LoadFunction("Api.GetObject.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]
|
||||
]
|
||||
in
|
||||
// Read all pages of data.
|
||||
// After every page, we check the "nextCursor" record on the metadata of the previous request.
|
||||
// Table.GenerateByPage will keep asking for more pages until we return null.
|
||||
(server as text, streamId as text, objectId as text, optional cursor as text) as table =>
|
||||
let
|
||||
parentObject = Speckle.Api.GetObject(server, streamId, objectId),
|
||||
childrenTable = Table.GenerateByPage(
|
||||
(previous) =>
|
||||
let
|
||||
// if previous is null, then this is our first page of data
|
||||
nextCursor = if (previous = null) then cursor else Value.Metadata(previous)[Cursor]?,
|
||||
// if the cursor is null but the prevous page is not, we've reached the end
|
||||
page =
|
||||
if (previous <> null and nextCursor = null) then
|
||||
null
|
||||
else
|
||||
Speckle.Api.GetObjectChildren(server, streamId, objectId, 1000, nextCursor)
|
||||
in
|
||||
page
|
||||
),
|
||||
parentTable = Table.FromRecords({[data = parentObject]}),
|
||||
resultTable =
|
||||
if (Table.ColumnCount(childrenTable) = 0) then
|
||||
parentTable
|
||||
else
|
||||
Table.Combine({parentTable, childrenTable})
|
||||
in
|
||||
resultTable meta [server = server, streamId = streamId, objectId = objectId]
|
||||
@@ -1,32 +0,0 @@
|
||||
let
|
||||
Speckle.Api.Fetch = Extension.LoadFunction("Api.Fetch.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]
|
||||
]
|
||||
in
|
||||
(server as text, projectId as text, modelId as text) =>
|
||||
let
|
||||
query = "query Project($projectId: String!, $modelId: String!) {
|
||||
project(id: $projectId) {
|
||||
model(id: $modelId) {
|
||||
name
|
||||
}
|
||||
}
|
||||
}",
|
||||
variables = [
|
||||
projectId = projectId,
|
||||
modelId = modelId
|
||||
]
|
||||
in
|
||||
// Read receipts should fail gracefully no matter what
|
||||
try Speckle.Api.Fetch(server, query, variables)[project][model] otherwise null
|
||||
@@ -1,28 +0,0 @@
|
||||
let
|
||||
Speckle.Api.Fetch = Extension.LoadFunction("Api.Fetch.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]
|
||||
]
|
||||
in
|
||||
(server as text, streamId as text, objectId as text) =>
|
||||
let
|
||||
query = "query($streamId: String!, $objectId: String!) {
|
||||
stream( id: $streamId ) {
|
||||
object (id: $objectId) {
|
||||
data
|
||||
}
|
||||
}
|
||||
}",
|
||||
#"JSON" = Speckle.Api.Fetch(server, query, [streamId = streamId, objectId = objectId])
|
||||
in
|
||||
#"JSON"[stream][object][data]
|
||||
@@ -1,54 +0,0 @@
|
||||
let
|
||||
Speckle.Api.Fetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
Speckle.CleanUpObjects = Extension.LoadFunction("CleanUpObjects.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]
|
||||
]
|
||||
in
|
||||
(
|
||||
server as text,
|
||||
streamId as text,
|
||||
objectId as text,
|
||||
optional limit as number,
|
||||
optional cursor as text,
|
||||
optional select as list
|
||||
) =>
|
||||
let
|
||||
query = "query($streamId: String!, $objectId: String!, $limit: Int, $cursor: String, $select: [String]) {
|
||||
stream( id: $streamId ) {
|
||||
object (id: $objectId) {
|
||||
children(select: $select, limit: $limit, cursor: $cursor) {
|
||||
cursor
|
||||
objects {
|
||||
data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}",
|
||||
#"JSON" = Speckle.Api.Fetch(
|
||||
server,
|
||||
query,
|
||||
[
|
||||
streamId = streamId,
|
||||
objectId = objectId,
|
||||
limit = limit,
|
||||
cursor = cursor,
|
||||
select = select
|
||||
]
|
||||
),
|
||||
children = #"JSON"[stream][object][children],
|
||||
nextCursor = children[cursor],
|
||||
clean = Speckle.CleanUpObjects(children[objects])
|
||||
in
|
||||
Table.FromRecords(clean) meta [Cursor = nextCursor]
|
||||
@@ -1,27 +0,0 @@
|
||||
(url as text) =>
|
||||
let
|
||||
userType = type [name = text, email = text, id = text],
|
||||
query = "query {
|
||||
activeUser { name email id }
|
||||
}",
|
||||
// Imports
|
||||
Speckle.Api.Fetch = Extension.LoadFunction("Api.Fetch.pqm"),
|
||||
ParseUrl = Extension.LoadFunction("ParseStreamUrl.pqm"),
|
||||
urlObject = ParseUrl(url),
|
||||
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]
|
||||
],
|
||||
user = Speckle.Api.Fetch(urlObject[server], query)[activeUser]
|
||||
in
|
||||
// Read receipts should fail gracefully no matter what
|
||||
Value.ReplaceType(user, userType)
|
||||
@@ -1,38 +0,0 @@
|
||||
(server as text, optional streamId as text, optional objectId as text) as table =>
|
||||
let
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
|
||||
Source = Web.Contents(
|
||||
Text.Combine({server, "objects", streamId, objectId}, "/"),
|
||||
[
|
||||
Headers = [
|
||||
#"Method" = "GET",
|
||||
#"Content-Type" = "application/json",
|
||||
#"Authorization" = if apiKey = null then "" else Text.Format("Bearer #{0}", {apiKey})
|
||||
],
|
||||
ManualStatusHandling = {400}
|
||||
]
|
||||
),
|
||||
json = Json.Document(Source),
|
||||
clean = List.Select(json, each _[speckle_type] <> "Speckle.Core.Models.DataChunk"),
|
||||
t = Table.FromColumns({clean}, {"data"}),
|
||||
addStreamUrl = Table.AddColumn(t, "Stream URL", each server & "/streams/" & streamId),
|
||||
addObjectIdCol = Table.AddColumn(addStreamUrl, "Object ID", each try _[data][id] otherwise null),
|
||||
addSpeckleTypeCol = Table.AddColumn(
|
||||
addObjectIdCol, "speckle_type", each try _[data][speckle_type] otherwise null
|
||||
),
|
||||
Speckle.CleanUpObjects = Extension.LoadFunction("CleanUpObjects.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]
|
||||
]
|
||||
in
|
||||
addSpeckleTypeCol
|
||||
@@ -0,0 +1,87 @@
|
||||
// function for getting model information through graphql query
|
||||
(url as text) as record =>
|
||||
let
|
||||
// Import the parser function
|
||||
Parser = Extension.LoadFunction("Parser.pqm"),
|
||||
|
||||
// the logic for importing functions from other files
|
||||
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
|
||||
parsedUrl = Parser(url),
|
||||
server = parsedUrl[baseUrl],
|
||||
projectId = parsedUrl[projectId],
|
||||
modelId = parsedUrl[modelId],
|
||||
|
||||
// Get API key if available
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
|
||||
|
||||
// GraphQL query to get model info including root object id
|
||||
query = "query ($projectId: String!, $modelId: String!) {
|
||||
project(id: $projectId) {
|
||||
model(id: $modelId) {
|
||||
id
|
||||
name
|
||||
versions {
|
||||
items {
|
||||
id
|
||||
referencedObject
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}",
|
||||
|
||||
variables = [
|
||||
projectId = projectId,
|
||||
modelId = modelId
|
||||
],
|
||||
|
||||
// Make the API request
|
||||
Source = Web.Contents(
|
||||
Text.Combine({server, "graphql"}, "/"),
|
||||
[
|
||||
Headers = [
|
||||
#"Method" = "POST",
|
||||
#"Content-Type" = "application/json",
|
||||
#"Authorization" = if apiKey = null then "" else Text.Format("Bearer #{0}", {apiKey})
|
||||
],
|
||||
ManualStatusHandling = {400, 401, 403},
|
||||
Content = Json.FromValue([
|
||||
query = query,
|
||||
variables = variables
|
||||
])
|
||||
]
|
||||
),
|
||||
|
||||
// Parse the response
|
||||
JsonResponse = Json.Document(Source),
|
||||
|
||||
// Extract needed information
|
||||
result = if Record.HasFields(JsonResponse, {"errors"}) then
|
||||
error JsonResponse[errors]{0}[message]
|
||||
else if JsonResponse[data]?[project]?[model] = null then
|
||||
error "Model not found or access denied. Please check your authentication and model ID."
|
||||
else
|
||||
[
|
||||
modelId = JsonResponse[data][project][model][id],
|
||||
modelName = JsonResponse[data][project][model][name],
|
||||
versionId = JsonResponse[data][project][model][versions][items]{0}[id],
|
||||
rootObjectId = JsonResponse[data][project][model][versions][items]{0}[referencedObject]
|
||||
]
|
||||
in
|
||||
result
|
||||
@@ -0,0 +1,65 @@
|
||||
// function for getting the user info with graphql query
|
||||
let
|
||||
// import the parser function from Parser.pqm file
|
||||
Parser = Extension.LoadFunction("Parser.pqm"),
|
||||
|
||||
// the logic for importing functions from other files
|
||||
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]
|
||||
]
|
||||
in
|
||||
(url as text) as record =>
|
||||
let
|
||||
// get base server URL using the imported function
|
||||
parsedUrl = Parser(url),
|
||||
server = parsedUrl[baseUrl],
|
||||
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
|
||||
|
||||
query = "query {
|
||||
activeUser {
|
||||
email
|
||||
name
|
||||
}
|
||||
serverInfo {
|
||||
name
|
||||
company
|
||||
version
|
||||
}
|
||||
}",
|
||||
Source = Web.Contents(
|
||||
Text.Combine({server, "graphql"}, "/"),
|
||||
[
|
||||
Headers = [
|
||||
#"Method" = "POST",
|
||||
#"Content-Type" = "application/json",
|
||||
#"Authorization" = if apiKey = null then "" else Text.Format("Bearer #{0}", {apiKey})
|
||||
],
|
||||
ManualStatusHandling = {400},
|
||||
Content = Json.FromValue([query = query])
|
||||
]
|
||||
),
|
||||
JsonResponse = Json.Document(Source)
|
||||
in
|
||||
if Record.HasFields(JsonResponse, {"errors"}) then
|
||||
error JsonResponse[errors]{0}[message]
|
||||
else
|
||||
[
|
||||
UserEmail = JsonResponse[data][activeUser][email],
|
||||
UserName = JsonResponse[data][activeUser][name],
|
||||
ServerName = JsonResponse[data][serverInfo][name],
|
||||
ServerCompany = JsonResponse[data][serverInfo][company],
|
||||
ServerVersion = JsonResponse[data][serverInfo][version]
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
// function for parsing the url into base url, project id and model id
|
||||
(url as text) as record =>
|
||||
let
|
||||
urlParts = Uri.Parts(url),
|
||||
baseUrl = Text.Combine({urlParts[Scheme], "://", urlParts[Host]}),
|
||||
|
||||
pathSegments = List.Select(Text.Split(urlParts[Path], "/"), each _ <> ""),
|
||||
|
||||
// extract project and model IDs if they exist
|
||||
projectId = if List.Count(pathSegments) >= 2 and pathSegments{0} = "projects"
|
||||
then pathSegments{1} else null,
|
||||
|
||||
modelId = if List.Count(pathSegments) >= 4 and pathSegments{2} = "models"
|
||||
then pathSegments{3} else null
|
||||
in
|
||||
[
|
||||
baseUrl = baseUrl,
|
||||
projectId = projectId,
|
||||
modelId = modelId
|
||||
]
|
||||
@@ -1,7 +0,0 @@
|
||||
(object as record) as record =>
|
||||
let
|
||||
hiddenFields = {"__closure", "totalChildrenCount"},
|
||||
// remove closures from records
|
||||
clean = Record.RemoveFields(object, hiddenFields, MissingField.Ignore)
|
||||
in
|
||||
clean
|
||||
@@ -1,17 +0,0 @@
|
||||
(objects as list) as list =>
|
||||
let
|
||||
// remove closures from records, and remove DataChunk records
|
||||
removeClosureField = List.Transform(
|
||||
objects, each [data = Record.RemoveFields(_[data], "__closure", MissingField.Ignore)]
|
||||
),
|
||||
removeTotals = List.Transform(
|
||||
removeClosureField,
|
||||
each
|
||||
[
|
||||
data = try
|
||||
Record.RemoveFields(_[data], "totalChildrenCount", MissingField.Ignore) otherwise _[data]
|
||||
]
|
||||
),
|
||||
removed = List.Select(removeTotals, each _[data][speckle_type] <> "Speckle.Core.Models.DataChunk")
|
||||
in
|
||||
try removed otherwise objects
|
||||
@@ -1,30 +0,0 @@
|
||||
let
|
||||
beta = true,
|
||||
category = "Other",
|
||||
icons = [
|
||||
Icon16 = {
|
||||
Extension.Contents("SpeckleLogo16.png"),
|
||||
Extension.Contents("SpeckleLogo20.png"),
|
||||
Extension.Contents("SpeckleLogo24.png"),
|
||||
Extension.Contents("SpeckleLogo32.png")
|
||||
},
|
||||
Icon32 = {
|
||||
Extension.Contents("SpeckleLogo32.png"),
|
||||
Extension.Contents("SpeckleLogo40.png"),
|
||||
Extension.Contents("SpeckleLogo48.png"),
|
||||
Extension.Contents("SpeckleLogo64.png")
|
||||
}
|
||||
]
|
||||
in
|
||||
(key as text) as record =>
|
||||
[
|
||||
Beta = beta,
|
||||
Category = category,
|
||||
ButtonText = {
|
||||
Extension.LoadString(Text.Format("#{0}.Title", {key})),
|
||||
Extension.LoadString(Text.Format("#{0}.Label", {key}))
|
||||
},
|
||||
LearnMoreUrl = "https://speckle.guide",
|
||||
SourceImage = icons,
|
||||
SourceTypeImage = icons
|
||||
]
|
||||
@@ -1,50 +0,0 @@
|
||||
let
|
||||
GetApplicationSlug = Extension.LoadFunction("GetApplicationSlug.pqm"),
|
||||
GetUser = Extension.LoadFunction("Api.GetUser.pqm"),
|
||||
Hash = Extension.LoadFunction("Hash.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]
|
||||
]
|
||||
in
|
||||
(server as text, commit as any) =>
|
||||
let
|
||||
trackUrl = "https://analytics.speckle.systems/track?ip=1",
|
||||
user = GetUser(server),
|
||||
isMultiplayer = user[id] <> commit[authorId],
|
||||
body = [
|
||||
event = "Receive",
|
||||
properties = [
|
||||
server_id = Hash(server),
|
||||
token = "acd87c5a50b56df91a795e999812a3a4",
|
||||
hostApp = "powerbi",
|
||||
sourceHostApp = GetApplicationSlug(commit[sourceApplication]),
|
||||
sourceHostAppVersion = commit[sourceApplication],
|
||||
isMultiplayer = user[id] <> commit[authorId]
|
||||
]
|
||||
],
|
||||
Result = Web.Contents(
|
||||
trackUrl,
|
||||
[
|
||||
Headers = [
|
||||
#"Method" = "POST",
|
||||
#"Accept" = "text/plain",
|
||||
#"Content-Type" = "application/json"
|
||||
],
|
||||
Content = Text.ToBinary(Text.Combine({"data=", Text.FromBinary(Json.FromValue(body))}))
|
||||
]
|
||||
),
|
||||
// Hack to force execution
|
||||
Join = Text.Combine({server, Text.From(Json.Document(Result))}, "_____"),
|
||||
Disjoin = Text.Split(Join, "_____"){0}
|
||||
in
|
||||
Disjoin
|
||||
@@ -1,81 +0,0 @@
|
||||
let
|
||||
GetModel = Extension.LoadFunction("Api.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]
|
||||
],
|
||||
IsFe2Url = (segments as list) as logical => List.Count(segments) = 4 and segments{2} = "models",
|
||||
GetUrlType = (branchName as nullable text, commitId as nullable text, objectId as nullable text) as text =>
|
||||
if (commitId <> null) then
|
||||
"Commit"
|
||||
else if (objectId <> null) then
|
||||
"Object"
|
||||
else if (branchName <> null) then
|
||||
"Branch"
|
||||
else
|
||||
"Stream",
|
||||
ParseFe1Url = (server as text, segments as list) as record =>
|
||||
let
|
||||
streamId = segments{1},
|
||||
branchName = if (List.Count(segments) = 4 and segments{2} = "branches") then segments{3} else null,
|
||||
commitId = if (List.Count(segments) = 4 and segments{2} = "commits") then segments{3} else null,
|
||||
objectId = if (List.Count(segments) = 4 and segments{2} = "objects") then segments{3} else null,
|
||||
urlType = GetUrlType(branchName, commitId, objectId)
|
||||
in
|
||||
[
|
||||
urlType = urlType,
|
||||
server = server as text,
|
||||
id = streamId as nullable text,
|
||||
branch = branchName as nullable text,
|
||||
commit = commitId as nullable text,
|
||||
object = objectId as nullable text
|
||||
],
|
||||
ParseFe2Url = (server as text, segments as list) as record =>
|
||||
let
|
||||
streamId = segments{1},
|
||||
modelList = segments{3},
|
||||
isMultimodel = Text.Contains(modelList, ","),
|
||||
firstModel = Text.Split(modelList, ","){0},
|
||||
modelAndVersion = Text.Split(firstModel, "@"),
|
||||
modelId = modelAndVersion{0},
|
||||
versionId = if (List.Count(modelAndVersion) > 1) then modelAndVersion{1} else null,
|
||||
model = if (modelId <> null) then GetModel(server, streamId, modelId) else null,
|
||||
urlType = GetUrlType(model[name], versionId, null)
|
||||
in
|
||||
if isMultimodel then
|
||||
error
|
||||
Error.Record(
|
||||
"NotSupported",
|
||||
"Multi-model URLs are not supported.",
|
||||
"Try to select just one single model in the web app and paste that in."
|
||||
)
|
||||
else
|
||||
[
|
||||
urlType = urlType,
|
||||
server = server,
|
||||
id = streamId,
|
||||
branch = model[name],
|
||||
commit = versionId,
|
||||
object = null
|
||||
]
|
||||
in
|
||||
(url as text) as record =>
|
||||
let
|
||||
// Get server and streamId, and branchName / commitId / objectid from the input url
|
||||
server = Text.Combine({Uri.Parts(url)[Scheme], "://", Uri.Parts(url)[Host]}),
|
||||
segments = Text.Split(Text.AfterDelimiter(Uri.Parts(url)[Path], "/", 0), "/"),
|
||||
isFe2 = IsFe2Url(segments)
|
||||
in
|
||||
if (isFe2) then
|
||||
ParseFe2Url(server, segments)
|
||||
else
|
||||
ParseFe1Url(server, segments)
|
||||
@@ -1,67 +0,0 @@
|
||||
let
|
||||
GetObject = Extension.LoadFunction("Api.GetObject.pqm"),
|
||||
Diagnostics.Log = Extension.LoadFunction("Diagnostics.pqm")[LogValue],
|
||||
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]
|
||||
],
|
||||
//TODO: Not implemented yet
|
||||
TraverseTable = (item as table) as table => item,
|
||||
// Will traverse an undetermined value (list, table, record).
|
||||
TraverseValue = (i as any) as any =>
|
||||
let
|
||||
item = Diagnostics.Log("Traverse value", i) meta Value.Metadata(i)
|
||||
in
|
||||
if Value.Is(item, type list) then
|
||||
// Return a transformed list by traversing all items
|
||||
Diagnostics.Log(
|
||||
"List travered",
|
||||
List.Transform(item, (a) => @TraverseValue(Value.ReplaceMetadata(a, Value.Metadata(i))))
|
||||
)
|
||||
else if Value.Is(item, type record) then
|
||||
// Traverse this record individually
|
||||
TraverseRecord(item)
|
||||
else if Value.Is(item, type table) then
|
||||
// Traverse this table
|
||||
TraverseTable(item)
|
||||
else
|
||||
// If none of the above, assume it's just a primitive type and return it as-is.
|
||||
item,
|
||||
// Traverses a generic record
|
||||
TraverseRecord = (object as record) as any =>
|
||||
let
|
||||
isSpeckle = Diagnostics.Log("Is Speckle", Record.HasFields(object, {"speckle_type"})),
|
||||
isReference = Diagnostics.Log("Is Reference", object[speckle_type] = "reference"),
|
||||
// Get the names of all fields
|
||||
fields = Record.FieldNames(object),
|
||||
// Remove all known fields that don't need traversing
|
||||
cleanFields = List.RemoveItems(fields, {"id", "speckle_type", "applicationId"}),
|
||||
// Transform the list of field names into a set of transform operations
|
||||
transformOps = List.Transform(
|
||||
cleanFields, each {_, (a) => TraverseValue(Value.ReplaceMetadata(a, Value.Metadata(object)))}
|
||||
),
|
||||
// Get the object's metadata (server and stream will be saved in here)
|
||||
info = Value.Metadata(object)
|
||||
in
|
||||
// Transform all fields and return the modified object
|
||||
if (isReference) then
|
||||
// Swap reference for call to GetObject
|
||||
() =>
|
||||
TraverseValue(
|
||||
Value.ReplaceMetadata(
|
||||
GetObject(info[server], info[stream], object[referencedId]), Value.Metadata(object)
|
||||
)
|
||||
)
|
||||
else
|
||||
try Record.TransformFields(object, transformOps, MissingField.Error) otherwise error "oopsies"
|
||||
in
|
||||
TraverseValue
|
||||
@@ -0,0 +1,61 @@
|
||||
// function for getting object data
|
||||
(url as text) as table =>
|
||||
let
|
||||
// Import the parser function and getModel
|
||||
Parser = Extension.LoadFunction("Parser.pqm"),
|
||||
GetModel = Extension.LoadFunction("GetModel.pqm"),
|
||||
// the logic for importing functions from other files
|
||||
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}
|
||||
]
|
||||
),
|
||||
|
||||
// Parse the response and return the raw JSON
|
||||
JsonResponse = Json.Document(Source),
|
||||
|
||||
ConvertedToTable = Table.FromList(
|
||||
JsonResponse,
|
||||
Splitter.SplitByNothing(),
|
||||
{"viewer_data"},
|
||||
null,
|
||||
ExtraValues.Error
|
||||
),
|
||||
|
||||
ExpandedTable = Table.AddColumn(
|
||||
ConvertedToTable,
|
||||
"speckle_id",
|
||||
each Record.Field([viewer_data], "id"),
|
||||
type text
|
||||
)
|
||||
in
|
||||
ExpandedTable
|
||||
@@ -0,0 +1,98 @@
|
||||
// Function for getting structured object data
|
||||
(url as text) as table =>
|
||||
let
|
||||
// Import the parser function and getModel
|
||||
Parser = Extension.LoadFunction("Parser.pqm"),
|
||||
GetModel = Extension.LoadFunction("GetModel.pqm"),
|
||||
// the logic for importing functions from other files
|
||||
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}
|
||||
]
|
||||
),
|
||||
|
||||
// Parse the JSON response
|
||||
JsonResponse = Json.Document(Source),
|
||||
|
||||
// Convert list to table
|
||||
ConvertedToTable = Table.FromList(
|
||||
JsonResponse,
|
||||
Splitter.SplitByNothing(),
|
||||
null,
|
||||
null,
|
||||
ExtraValues.Error
|
||||
),
|
||||
|
||||
// Expand initial record
|
||||
ExpandedTable = Table.ExpandRecordColumn(
|
||||
ConvertedToTable,
|
||||
"Column1",
|
||||
{
|
||||
"id",
|
||||
"name",
|
||||
"type",
|
||||
"units",
|
||||
"version",
|
||||
"elements",
|
||||
"profile",
|
||||
"material",
|
||||
"__closure",
|
||||
"properties",
|
||||
"displayValue",
|
||||
"data",
|
||||
"speckle_type",
|
||||
"applicationId",
|
||||
"collectionType",
|
||||
"renderMaterialProxies",
|
||||
"totalChildrenCount"
|
||||
}
|
||||
),
|
||||
|
||||
// Remove rows where applicationId is null
|
||||
FilteredTable = Table.SelectRows(ExpandedTable, each [applicationId] <> null),
|
||||
|
||||
// Create data column by combining all other columns except applicationId and speckle_type
|
||||
FinalTable = Table.FromRecords(
|
||||
List.Transform(
|
||||
Table.ToRecords(FilteredTable),
|
||||
each [
|
||||
speckle_id = [id],
|
||||
speckle_type = [speckle_type],
|
||||
data = Record.RemoveFields(
|
||||
_,
|
||||
{"id", "speckle_type", "collectionType", "displayValue", "totalChildrenCount", "renderMaterialProxies", "data", "__closure"}
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
in
|
||||
FinalTable
|
||||
@@ -1,2 +0,0 @@
|
||||
// Use this file to write queries to test your data connector
|
||||
let result = Speckle.Api.Fetch("https://latest.speckle.dev") in Record.ToTable(result)
|
||||
@@ -1,7 +0,0 @@
|
||||
// Use this file to write queries to test your data connector
|
||||
let
|
||||
result = Speckle.Api.REST.GetObject(
|
||||
"https://latest.speckle.dev", "5f284e5c70", "85e5f250fe591ea74d8d5dc1137a9341"
|
||||
)
|
||||
in
|
||||
result
|
||||
@@ -1,30 +0,0 @@
|
||||
section UnitTestingUnitTests;
|
||||
|
||||
UT = Speckle.LoadFunction("Facts.pqm");
|
||||
Fact = UT[Fact];
|
||||
|
||||
Facts.Summarize = UT[SummarizeFacts];
|
||||
|
||||
shared Speckle.UnitTest = [
|
||||
// Put any common variables here if you only want them to be evaluated once
|
||||
// Fact(<Name of the Test>, <Expected Value>, <Actual Value>)
|
||||
// <Expected Value> and <Actual Value> can be a literal or let statement
|
||||
facts = {
|
||||
Fact(
|
||||
"Check that this function returns 'ABC'",
|
||||
// name of the test
|
||||
"ABC",
|
||||
// expected value
|
||||
UnitTesting.ReturnsABC()
|
||||
// expression to evaluate (let or single statement)
|
||||
),
|
||||
Fact("Check that this function returns '123'", "123", UnitTesting.Returns123()),
|
||||
Fact("Result should contain 5 rows", 5, Table.RowCount(UnitTesting.ReturnsTableWithFiveRows())),
|
||||
Fact("Values should be equal (using a let statement)", "Hello World", let a = "Hello World" in a)
|
||||
},
|
||||
report = Facts.Summarize(facts)
|
||||
][report];
|
||||
|
||||
shared UnitTesting.ReturnsABC = () => "ABC";
|
||||
shared UnitTesting.Returns123 = () => "123";
|
||||
shared UnitTesting.ReturnsTableWithFiveRows = () => Table.Repeat(#table({"a"}, {{1}}), 5);
|
||||
@@ -1,2 +0,0 @@
|
||||
// Use this file to write queries to test your data connector
|
||||
let result = Speckle.Get.ByUrl("https://latest.speckle.dev/streams/3d25474a18") in Record.ToTable(result)
|
||||
@@ -1,2 +0,0 @@
|
||||
// Use this file to write queries to test your data connector
|
||||
let result = Speckle.GetByUrl("https://latest.speckle.dev/streams/5f284e5c70/objects/85e5f250fe591ea74d8d5dc1137a9341") in result
|
||||
@@ -1,376 +0,0 @@
|
||||
let
|
||||
Diagnostics.LogValue = (prefix, value) =>
|
||||
Diagnostics.Trace(
|
||||
TraceLevel.Information,
|
||||
prefix & ": " & (try Diagnostics.ValueToText(value) otherwise "<error getting value>"),
|
||||
value
|
||||
),
|
||||
Diagnostics.LogValue2 = (prefix, value, result, optional delayed) =>
|
||||
Diagnostics.Trace(TraceLevel.Information, prefix & ": " & Diagnostics.ValueToText(value), result, delayed),
|
||||
Diagnostics.LogFailure = (text, function) =>
|
||||
let
|
||||
result = try function()
|
||||
in
|
||||
if result[HasError] then
|
||||
Diagnostics.LogValue2(text, result[Error], () => error result[Error], true)
|
||||
else
|
||||
result[Value],
|
||||
Diagnostics.WrapFunctionResult = (innerFunction as function, outerFunction as function) as function =>
|
||||
Function.From(Value.Type(innerFunction), (list) => outerFunction(() => Function.Invoke(innerFunction, list))),
|
||||
Diagnostics.WrapHandlers = (handlers as record) as record =>
|
||||
Record.FromList(
|
||||
List.Transform(
|
||||
Record.FieldNames(handlers),
|
||||
(h) =>
|
||||
Diagnostics.WrapFunctionResult(Record.Field(handlers, h), (fn) => Diagnostics.LogFailure(h, fn))
|
||||
),
|
||||
Record.FieldNames(handlers)
|
||||
),
|
||||
Diagnostics.ValueToText = (value) =>
|
||||
let
|
||||
_canBeIdentifier = (x) =>
|
||||
let
|
||||
keywords = {
|
||||
"and",
|
||||
"as",
|
||||
"each",
|
||||
"else",
|
||||
"error",
|
||||
"false",
|
||||
"if",
|
||||
"in",
|
||||
"is",
|
||||
"let",
|
||||
"meta",
|
||||
"not",
|
||||
"otherwise",
|
||||
"or",
|
||||
"section",
|
||||
"shared",
|
||||
"then",
|
||||
"true",
|
||||
"try",
|
||||
"type"
|
||||
},
|
||||
charAlpha = (c as number) => (c >= 65 and c <= 90) or (c >= 97 and c <= 122) or c = 95,
|
||||
charDigit = (c as number) => c >= 48 and c <= 57
|
||||
in
|
||||
try
|
||||
charAlpha(Character.ToNumber(Text.At(x, 0)))
|
||||
and List.MatchesAll(
|
||||
Text.ToList(x), (c) => let num = Character.ToNumber(c) in charAlpha(num)
|
||||
or charDigit(num)
|
||||
)
|
||||
and not List.MatchesAny(keywords, (li) => li = x) otherwise false,
|
||||
Serialize.Binary = (x) => "#binary(" & Serialize(Binary.ToList(x)) & ") ",
|
||||
Serialize.Date = (x) =>
|
||||
"#date(" & Text.From(Date.Year(x)) & ", " & Text.From(Date.Month(x)) & ", " & Text.From(Date.Day(x))
|
||||
& ") ",
|
||||
Serialize.Datetime = (x) =>
|
||||
"#datetime("
|
||||
& Text.From(Date.Year(DateTime.Date(x)))
|
||||
& ", "
|
||||
& Text.From(Date.Month(DateTime.Date(x)))
|
||||
& ", "
|
||||
& Text.From(Date.Day(DateTime.Date(x)))
|
||||
& ", "
|
||||
& Text.From(Time.Hour(DateTime.Time(x)))
|
||||
& ", "
|
||||
& Text.From(Time.Minute(DateTime.Time(x)))
|
||||
& ", "
|
||||
& Text.From(Time.Second(DateTime.Time(x)))
|
||||
& ") ",
|
||||
Serialize.Datetimezone = (x) =>
|
||||
let
|
||||
dtz = DateTimeZone.ToRecord(x)
|
||||
in
|
||||
"#datetimezone("
|
||||
& Text.From(dtz[Year])
|
||||
& ", "
|
||||
& Text.From(dtz[Month])
|
||||
& ", "
|
||||
& Text.From(dtz[Day])
|
||||
& ", "
|
||||
& Text.From(dtz[Hour])
|
||||
& ", "
|
||||
& Text.From(dtz[Minute])
|
||||
& ", "
|
||||
& Text.From(dtz[Second])
|
||||
& ", "
|
||||
& Text.From(dtz[ZoneHours])
|
||||
& ", "
|
||||
& Text.From(dtz[ZoneMinutes])
|
||||
& ") ",
|
||||
Serialize.Duration = (x) =>
|
||||
let
|
||||
dur = Duration.ToRecord(x)
|
||||
in
|
||||
"#duration("
|
||||
& Text.From(dur[Days])
|
||||
& ", "
|
||||
& Text.From(dur[Hours])
|
||||
& ", "
|
||||
& Text.From(dur[Minutes])
|
||||
& ", "
|
||||
& Text.From(dur[Seconds])
|
||||
& ") ",
|
||||
Serialize.Function = (x) =>
|
||||
_serialize_function_param_type(
|
||||
Type.FunctionParameters(Value.Type(x)), Type.FunctionRequiredParameters(Value.Type(x))
|
||||
)
|
||||
& " as "
|
||||
& _serialize_function_return_type(Value.Type(x))
|
||||
& " => (...) ",
|
||||
Serialize.List = (x) =>
|
||||
"{"
|
||||
& List.Accumulate(
|
||||
x, "", (seed, item) => if seed = "" then Serialize(item) else seed & ", " & Serialize(item)
|
||||
)
|
||||
& "} ",
|
||||
Serialize.Logical = (x) => Text.From(x),
|
||||
Serialize.Null = (x) => "null",
|
||||
Serialize.Number = (x) =>
|
||||
let
|
||||
Text.From = (i as number) as text =>
|
||||
if Number.IsNaN(i) then
|
||||
"#nan"
|
||||
else if i = Number.PositiveInfinity then
|
||||
"#infinity"
|
||||
else if i = Number.NegativeInfinity then
|
||||
"-#infinity"
|
||||
else
|
||||
Text.From(i)
|
||||
in
|
||||
Text.From(x),
|
||||
Serialize.Record = (x) =>
|
||||
"[ "
|
||||
& List.Accumulate(
|
||||
Record.FieldNames(x),
|
||||
"",
|
||||
(seed, item) =>
|
||||
(if seed = "" then Serialize.Identifier(item) else seed & ", " & Serialize.Identifier(
|
||||
item
|
||||
))
|
||||
& " = "
|
||||
& Serialize(Record.Field(x, item))
|
||||
)
|
||||
& " ] ",
|
||||
Serialize.Table = (x) =>
|
||||
"#table( type " & _serialize_table_type(Value.Type(x)) & ", " & Serialize(Table.ToRows(x)) & ") ",
|
||||
Serialize.Text = (x) => """" & _serialize_text_content(x) & """",
|
||||
_serialize_text_content = (x) =>
|
||||
let
|
||||
escapeText = (n as number) as text =>
|
||||
"#(#)(" & Text.PadStart(Number.ToText(n, "X", "en-US"), 4, "0") & ")"
|
||||
in
|
||||
List.Accumulate(
|
||||
List.Transform(
|
||||
Text.ToList(x),
|
||||
(c) =>
|
||||
let
|
||||
n = Character.ToNumber(c)
|
||||
in
|
||||
if n = 9 then
|
||||
"#(#)(tab)"
|
||||
else if n = 10 then
|
||||
"#(#)(lf)"
|
||||
else if n = 13 then
|
||||
"#(#)(cr)"
|
||||
else if n = 34 then
|
||||
""""""
|
||||
else if n = 35 then
|
||||
"#(#)(#)"
|
||||
else if n < 32 then
|
||||
escapeText(n)
|
||||
else if n < 127 then
|
||||
Character.FromNumber(n)
|
||||
else
|
||||
escapeText(n)
|
||||
),
|
||||
"",
|
||||
(s, i) => s & i
|
||||
),
|
||||
Serialize.Identifier = (x) => if _canBeIdentifier(x) then x else "#""" & _serialize_text_content(x) & """",
|
||||
Serialize.Time = (x) =>
|
||||
"#time("
|
||||
& Text.From(Time.Hour(x))
|
||||
& ", "
|
||||
& Text.From(Time.Minute(x))
|
||||
& ", "
|
||||
& Text.From(Time.Second(x))
|
||||
& ") ",
|
||||
Serialize.Type = (x) => "type " & _serialize_typename(x),
|
||||
_serialize_typename = (x, optional funtype as logical) =>
|
||||
/* Optional parameter: Is this being used as part of a function signature? */ let
|
||||
isFunctionType = (x as type) =>
|
||||
try if Type.FunctionReturn(x) is type then true else false otherwise false,
|
||||
isTableType = (x as type) =>
|
||||
try if Type.TableSchema(x) is table then true else false otherwise false,
|
||||
isRecordType = (x as type) =>
|
||||
try if Type.ClosedRecord(x) is type then true else false otherwise false,
|
||||
isListType = (x as type) => try if Type.ListItem(x) is type then true else false otherwise false
|
||||
in
|
||||
if funtype = null and isTableType(x) then
|
||||
_serialize_table_type(x)
|
||||
else if funtype = null and isListType(x) then
|
||||
"{ " & @_serialize_typename(Type.ListItem(x)) & " }"
|
||||
else if funtype = null and isFunctionType(x) then
|
||||
"function " & _serialize_function_type(x)
|
||||
else if funtype = null and isRecordType(x) then
|
||||
_serialize_record_type(x)
|
||||
else if x = type any then
|
||||
"any"
|
||||
else
|
||||
let
|
||||
base = Type.NonNullable(x)
|
||||
in
|
||||
(if Type.IsNullable(x) then "nullable " else "")
|
||||
& (
|
||||
if base = type anynonnull then
|
||||
"anynonnull"
|
||||
else if base = type binary then
|
||||
"binary"
|
||||
else if base = type date then
|
||||
"date"
|
||||
else if base = type datetime then
|
||||
"datetime"
|
||||
else if base = type datetimezone then
|
||||
"datetimezone"
|
||||
else if base = type duration then
|
||||
"duration"
|
||||
else if base = type logical then
|
||||
"logical"
|
||||
else if base = type none then
|
||||
"none"
|
||||
else if base = type null then
|
||||
"null"
|
||||
else if base = type number then
|
||||
"number"
|
||||
else if base = type text then
|
||||
"text"
|
||||
else if base = type time then
|
||||
"time"
|
||||
else if base = type type then
|
||||
"type"
|
||||
else /* Abstract types: */ if base = type function then
|
||||
"function"
|
||||
else if base = type table then
|
||||
"table"
|
||||
else if base = type record then
|
||||
"record"
|
||||
else if base = type list then
|
||||
"list"
|
||||
else
|
||||
"any /*Actually unknown type*/"
|
||||
),
|
||||
_serialize_table_type = (x) =>
|
||||
let
|
||||
schema = Type.TableSchema(x)
|
||||
in
|
||||
"table "
|
||||
& (
|
||||
if Table.IsEmpty(schema) then
|
||||
""
|
||||
else
|
||||
"["
|
||||
& List.Accumulate(
|
||||
List.Transform(
|
||||
Table.ToRecords(Table.Sort(schema, "Position")),
|
||||
each Serialize.Identifier(_[Name]) & " = " & _[Kind]
|
||||
),
|
||||
"",
|
||||
(seed, item) => (if seed = "" then item else seed & ", " & item)
|
||||
)
|
||||
& "] "
|
||||
),
|
||||
_serialize_record_type = (x) =>
|
||||
let
|
||||
flds = Type.RecordFields(x)
|
||||
in
|
||||
if Record.FieldCount(flds) = 0 then
|
||||
"record"
|
||||
else
|
||||
"["
|
||||
& List.Accumulate(
|
||||
Record.FieldNames(flds),
|
||||
"",
|
||||
(seed, item) =>
|
||||
seed
|
||||
& (if seed <> "" then ", " else "")
|
||||
& (
|
||||
Serialize.Identifier(item)
|
||||
& "="
|
||||
& _serialize_typename(Record.Field(flds, item)[Type])
|
||||
)
|
||||
)
|
||||
& (if Type.IsOpenRecord(x) then ",..." else "")
|
||||
& "]",
|
||||
_serialize_function_type = (x) =>
|
||||
_serialize_function_param_type(Type.FunctionParameters(x), Type.FunctionRequiredParameters(x))
|
||||
& " as "
|
||||
& _serialize_function_return_type(x),
|
||||
_serialize_function_param_type = (t, n) =>
|
||||
let
|
||||
funsig = Table.ToRecords(
|
||||
Table.TransformColumns(
|
||||
Table.AddIndexColumn(Record.ToTable(t), "isOptional", 1), {"isOptional", (x) => x > n}
|
||||
)
|
||||
)
|
||||
in
|
||||
"("
|
||||
& List.Accumulate(
|
||||
funsig,
|
||||
"",
|
||||
(seed, item) =>
|
||||
(if seed = "" then "" else seed & ", ")
|
||||
& (if item[isOptional] then "optional " else "")
|
||||
& Serialize.Identifier(item[Name])
|
||||
& " as "
|
||||
& _serialize_typename(item[Value], true)
|
||||
)
|
||||
& ")",
|
||||
_serialize_function_return_type = (x) => _serialize_typename(Type.FunctionReturn(x), true),
|
||||
Serialize = (x) as text =>
|
||||
if x is binary then
|
||||
try Serialize.Binary(x) otherwise "null /*serialize failed*/"
|
||||
else if x is date then
|
||||
try Serialize.Date(x) otherwise "null /*serialize failed*/"
|
||||
else if x is datetime then
|
||||
try Serialize.Datetime(x) otherwise "null /*serialize failed*/"
|
||||
else if x is datetimezone then
|
||||
try Serialize.Datetimezone(x) otherwise "null /*serialize failed*/"
|
||||
else if x is duration then
|
||||
try Serialize.Duration(x) otherwise "null /*serialize failed*/"
|
||||
else if x is function then
|
||||
try Serialize.Function(x) otherwise "null /*serialize failed*/"
|
||||
else if x is list then
|
||||
try Serialize.List(x) otherwise "null /*serialize failed*/"
|
||||
else if x is logical then
|
||||
try Serialize.Logical(x) otherwise "null /*serialize failed*/"
|
||||
else if x is null then
|
||||
try Serialize.Null(x) otherwise "null /*serialize failed*/"
|
||||
else if x is number then
|
||||
try Serialize.Number(x) otherwise "null /*serialize failed*/"
|
||||
else if x is record then
|
||||
try Serialize.Record(x) otherwise "null /*serialize failed*/"
|
||||
else if x is table then
|
||||
try Serialize.Table(x) otherwise "null /*serialize failed*/"
|
||||
else if x is text then
|
||||
try Serialize.Text(x) otherwise "null /*serialize failed*/"
|
||||
else if x is time then
|
||||
try Serialize.Time(x) otherwise "null /*serialize failed*/"
|
||||
else if x is type then
|
||||
try Serialize.Type(x) otherwise "null /*serialize failed*/"
|
||||
else
|
||||
"[#_unable_to_serialize_#]"
|
||||
in
|
||||
try Serialize(value) otherwise "<serialization failed>"
|
||||
in
|
||||
[
|
||||
LogValue = Diagnostics.LogValue,
|
||||
LogValue2 = Diagnostics.LogValue2,
|
||||
LogFailure = Diagnostics.LogFailure,
|
||||
WrapFunctionResult = Diagnostics.WrapFunctionResult,
|
||||
WrapHandlers = Diagnostics.WrapHandlers,
|
||||
ValueToText = Diagnostics.ValueToText
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
// This is here as reference for copy/pasting wherever there is need for importing pqm files.
|
||||
let
|
||||
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]
|
||||
]
|
||||
in
|
||||
Extension.LoadFunction
|
||||
@@ -1,231 +0,0 @@
|
||||
let
|
||||
/// COMMON UNIT TESTING CODE
|
||||
Fact = (_subject as text, _expected, _actual) as record =>
|
||||
[
|
||||
expected = try _expected,
|
||||
safeExpected = if expected[HasError] then "Expected : " & @ValueToText(expected[Error]) else expected[
|
||||
Value
|
||||
],
|
||||
actual = try _actual,
|
||||
safeActual = if actual[HasError] then "Actual : " & @ValueToText(actual[Error]) else actual[Value],
|
||||
attempt = try safeExpected = safeActual,
|
||||
result = if attempt[HasError] or not attempt[Value] then "Failure" else "Success",
|
||||
resultOp = if result = "Success" then " = " else " <> ",
|
||||
addendumEvalAttempt = if attempt[HasError] then @ValueToText(attempt[Error]) else "",
|
||||
addendumEvalExpected = try @ValueToText(safeExpected) otherwise "...",
|
||||
addendumEvalActual = try @ValueToText(safeActual) otherwise "...",
|
||||
fact = [
|
||||
Result = result & " " & addendumEvalAttempt,
|
||||
Notes = _subject,
|
||||
Details = " (" & addendumEvalExpected & resultOp & addendumEvalActual & ")"
|
||||
]
|
||||
][fact],
|
||||
Facts = (_subject as text, _predicates as list) => List.Transform(_predicates, each Fact(_subject, _{0}, _{1})),
|
||||
Facts.Summarize = (_facts as list) as table =>
|
||||
[
|
||||
Fact.CountSuccesses = (count, i) =>
|
||||
[
|
||||
result = try i[Result],
|
||||
sum = if result[HasError] or not Text.StartsWith(result[Value], "Success") then count else count + 1
|
||||
][sum],
|
||||
passed = List.Accumulate(_facts, 0, Fact.CountSuccesses),
|
||||
total = List.Count(_facts),
|
||||
format = if passed = total then "All #{0} Passed !!!" else "#{0} Passed - #{1} Failed",
|
||||
result = if passed = total then "Success" else "Failed",
|
||||
rate = Number.IntegerDivide(100 * passed, total),
|
||||
header = [
|
||||
Result = result,
|
||||
Notes = Text.Format(format, {passed, total - passed}),
|
||||
Details = Text.Format("#{0}% success rate", {rate})
|
||||
],
|
||||
report = Table.FromRecords(List.Combine({{header}, _facts}))
|
||||
][report],
|
||||
ValueToText = (value, optional depth) =>
|
||||
let
|
||||
List.TransformAndCombine = (list, transform, separator) =>
|
||||
Text.Combine(List.Transform(list, transform), separator),
|
||||
Serialize.Binary = (x) => "#binary(" & Serialize(Binary.ToList(x)) & ") ",
|
||||
Serialize.Function = (x) =>
|
||||
_serialize_function_param_type(
|
||||
Type.FunctionParameters(Value.Type(x)), Type.FunctionRequiredParameters(Value.Type(x))
|
||||
)
|
||||
& " as "
|
||||
& _serialize_function_return_type(Value.Type(x))
|
||||
& " => (...) ",
|
||||
Serialize.List = (x) => "{" & List.TransformAndCombine(x, Serialize, ", ") & "} ",
|
||||
Serialize.Record = (x) =>
|
||||
"[ "
|
||||
& List.TransformAndCombine(
|
||||
Record.FieldNames(x),
|
||||
(item) => Serialize.Identifier(item) & " = " & Serialize(Record.Field(x, item)),
|
||||
", "
|
||||
)
|
||||
& " ] ",
|
||||
Serialize.Table = (x) =>
|
||||
"#table( type " & _serialize_table_type(Value.Type(x)) & ", " & Serialize(Table.ToRows(x)) & ") ",
|
||||
Serialize.Identifier = Expression.Identifier,
|
||||
Serialize.Type = (x) => "type " & _serialize_typename(x),
|
||||
_serialize_typename = (x, optional funtype as logical) =>
|
||||
/* Optional parameter: Is this being used as part of a function signature? */ let
|
||||
isFunctionType = (x as type) =>
|
||||
try if Type.FunctionReturn(x) is type then true else false otherwise false,
|
||||
isTableType = (x as type) =>
|
||||
try if Type.TableSchema(x) is table then true else false otherwise false,
|
||||
isRecordType = (x as type) =>
|
||||
try if Type.ClosedRecord(x) is type then true else false otherwise false,
|
||||
isListType = (x as type) => try if Type.ListItem(x) is type then true else false otherwise false
|
||||
in
|
||||
if funtype = null and isTableType(x) then
|
||||
_serialize_table_type(x)
|
||||
else if funtype = null and isListType(x) then
|
||||
"{ " & @_serialize_typename(Type.ListItem(x)) & " }"
|
||||
else if funtype = null and isFunctionType(x) then
|
||||
"function " & _serialize_function_type(x)
|
||||
else if funtype = null and isRecordType(x) then
|
||||
_serialize_record_type(x)
|
||||
else if x = type any then
|
||||
"any"
|
||||
else
|
||||
let
|
||||
base = Type.NonNullable(x)
|
||||
in
|
||||
(if Type.IsNullable(x) then "nullable " else "")
|
||||
& (
|
||||
if base = type anynonnull then
|
||||
"anynonnull"
|
||||
else if base = type binary then
|
||||
"binary"
|
||||
else if base = type date then
|
||||
"date"
|
||||
else if base = type datetime then
|
||||
"datetime"
|
||||
else if base = type datetimezone then
|
||||
"datetimezone"
|
||||
else if base = type duration then
|
||||
"duration"
|
||||
else if base = type logical then
|
||||
"logical"
|
||||
else if base = type none then
|
||||
"none"
|
||||
else if base = type null then
|
||||
"null"
|
||||
else if base = type number then
|
||||
"number"
|
||||
else if base = type text then
|
||||
"text"
|
||||
else if base = type time then
|
||||
"time"
|
||||
else if base = type type then
|
||||
"type"
|
||||
else /* Abstract types: */ if base = type function then
|
||||
"function"
|
||||
else if base = type table then
|
||||
"table"
|
||||
else if base = type record then
|
||||
"record"
|
||||
else if base = type list then
|
||||
"list"
|
||||
else
|
||||
"any /*Actually unknown type*/"
|
||||
),
|
||||
_serialize_table_type = (x) =>
|
||||
let
|
||||
schema = Type.TableSchema(x)
|
||||
in
|
||||
"table "
|
||||
& (
|
||||
if Table.IsEmpty(schema) then
|
||||
""
|
||||
else
|
||||
"["
|
||||
& List.TransformAndCombine(
|
||||
Table.ToRecords(Table.Sort(schema, "Position")),
|
||||
each Serialize.Identifier(_[Name]) & " = " & _[Kind],
|
||||
", "
|
||||
)
|
||||
& "] "
|
||||
),
|
||||
_serialize_record_type = (x) =>
|
||||
let
|
||||
flds = Type.RecordFields(x)
|
||||
in
|
||||
if Record.FieldCount(flds) = 0 then
|
||||
"record"
|
||||
else
|
||||
"["
|
||||
& List.TransformAndCombine(
|
||||
Record.FieldNames(flds),
|
||||
(item) =>
|
||||
Serialize.Identifier(item)
|
||||
& "="
|
||||
& _serialize_typename(Record.Field(flds, item)[Type]),
|
||||
", "
|
||||
)
|
||||
& (if Type.IsOpenRecord(x) then ", ..." else "")
|
||||
& "]",
|
||||
_serialize_function_type = (x) =>
|
||||
_serialize_function_param_type(Type.FunctionParameters(x), Type.FunctionRequiredParameters(x))
|
||||
& " as "
|
||||
& _serialize_function_return_type(x),
|
||||
_serialize_function_param_type = (t, n) =>
|
||||
let
|
||||
funsig = Table.ToRecords(
|
||||
Table.TransformColumns(
|
||||
Table.AddIndexColumn(Record.ToTable(t), "isOptional", 1), {"isOptional", (x) => x > n}
|
||||
)
|
||||
)
|
||||
in
|
||||
"("
|
||||
& List.TransformAndCombine(
|
||||
funsig,
|
||||
(item) =>
|
||||
(if item[isOptional] then "optional " else "")
|
||||
& Serialize.Identifier(item[Name])
|
||||
& " as "
|
||||
& _serialize_typename(item[Value], true),
|
||||
", "
|
||||
)
|
||||
& ")",
|
||||
_serialize_function_return_type = (x) => _serialize_typename(Type.FunctionReturn(x), true),
|
||||
Serialize = (x) as text =>
|
||||
if x is binary then
|
||||
try Serialize.Binary(x) otherwise "null /*serialize failed*/"
|
||||
else if x is date then
|
||||
try Expression.Constant(x) otherwise "null /*serialize failed*/"
|
||||
else if x is datetime then
|
||||
try Expression.Constant(x) otherwise "null /*serialize failed*/"
|
||||
else if x is datetimezone then
|
||||
try Expression.Constant(x) otherwise "null /*serialize failed*/"
|
||||
else if x is duration then
|
||||
try Expression.Constant(x) otherwise "null /*serialize failed*/"
|
||||
else if x is function then
|
||||
try Serialize.Function(x) otherwise "null /*serialize failed*/"
|
||||
else if x is list then
|
||||
try Serialize.List(x) otherwise "null /*serialize failed*/"
|
||||
else if x is logical then
|
||||
try Expression.Constant(x) otherwise "null /*serialize failed*/"
|
||||
else if x is null then
|
||||
try Expression.Constant(x) otherwise "null /*serialize failed*/"
|
||||
else if x is number then
|
||||
try Expression.Constant(x) otherwise "null /*serialize failed*/"
|
||||
else if x is record then
|
||||
try Serialize.Record(x) otherwise "null /*serialize failed*/"
|
||||
else if x is table then
|
||||
try Serialize.Table(x) otherwise "null /*serialize failed*/"
|
||||
else if x is text then
|
||||
try Expression.Constant(x) otherwise "null /*serialize failed*/"
|
||||
else if x is time then
|
||||
try Expression.Constant(x) otherwise "null /*serialize failed*/"
|
||||
else if x is type then
|
||||
try Serialize.Type(x) otherwise "null /*serialize failed*/"
|
||||
else
|
||||
"[#_unable_to_serialize_#]"
|
||||
in
|
||||
try Serialize(value) otherwise "<serialization failed>"
|
||||
in
|
||||
[
|
||||
Fact = Fact,
|
||||
Facts = Facts,
|
||||
SummarizeFacts = Facts.Summarize,
|
||||
ValueToText = ValueToText
|
||||
]
|
||||
@@ -1,12 +0,0 @@
|
||||
(Value as text) =>
|
||||
let
|
||||
Solution = Binary.ToText(
|
||||
Binary.FromList(
|
||||
Binary.ToList(Binary.Compress(Text.ToBinary(Value, BinaryEncoding.Base64), Compression.GZip))
|
||||
)
|
||||
)
|
||||
in
|
||||
if Value = null then
|
||||
null
|
||||
else
|
||||
Solution
|
||||
@@ -1,23 +0,0 @@
|
||||
(getNextPage as function) as table =>
|
||||
let
|
||||
listOfPages = List.Generate(
|
||||
() => getNextPage(null),
|
||||
// get the first page of data
|
||||
(lastPage) => lastPage <> null,
|
||||
// stop when the function returns null
|
||||
(lastPage) => getNextPage(lastPage)
|
||||
// pass the previous page to the next function call
|
||||
),
|
||||
// concatenate the pages together
|
||||
tableOfPages = Table.FromList(listOfPages, Splitter.SplitByNothing(), {"Column1"}),
|
||||
firstRow = tableOfPages{0} ?
|
||||
in
|
||||
// if we didn't get back any pages of data, return an empty table
|
||||
// otherwise set the table type based on the columns of the first page
|
||||
if (firstRow = null) then
|
||||
Table.FromRows({})
|
||||
else
|
||||
Value.ReplaceType(
|
||||
Table.ExpandTableColumn(tableOfPages, "Column1", Table.ColumnNames(firstRow[Column1])),
|
||||
Value.Type(firstRow[Column1])
|
||||
)
|
||||
@@ -1,21 +0,0 @@
|
||||
(
|
||||
table as table,
|
||||
keyColumns as list,
|
||||
nameColumn as text,
|
||||
dataColumn as text,
|
||||
itemKindColumn as text,
|
||||
itemNameColumn as text,
|
||||
isLeafColumn as text
|
||||
) as table =>
|
||||
let
|
||||
tableType = Value.Type(table),
|
||||
newTableType = Type.AddTableKey(tableType, keyColumns, true) meta [
|
||||
NavigationTable.NameColumn = nameColumn,
|
||||
NavigationTable.DataColumn = dataColumn,
|
||||
NavigationTable.ItemKindColumn = itemKindColumn,
|
||||
Preview.DelayColumn = itemNameColumn,
|
||||
NavigationTable.IsLeafColumn = isLeafColumn
|
||||
],
|
||||
navigationTable = Value.ReplaceType(table, newTableType)
|
||||
in
|
||||
navigationTable
|
||||
@@ -1,14 +0,0 @@
|
||||
(producer as function, interval as function, optional count as number) as any =>
|
||||
let
|
||||
list = List.Generate(
|
||||
() => {0, null},
|
||||
(state) => state{0} <> null and (count = null or state{0} < count),
|
||||
(state) =>
|
||||
if state{1} <> null then
|
||||
{null, state{1}}
|
||||
else
|
||||
{1 + state{0}, Function.InvokeAfter(() => producer(state{0}), interval(state{0}))},
|
||||
(state) => state{1}
|
||||
)
|
||||
in
|
||||
List.Last(list)
|
||||
+2
-4
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"editor.tabSize": 4,
|
||||
"editor.tabSize": 2,
|
||||
"editor.insertSpaces": true,
|
||||
"files.eol": "\n",
|
||||
"files.watcherExclude": {
|
||||
@@ -7,12 +7,10 @@
|
||||
"**/node_modules/**": true,
|
||||
".tmp": true
|
||||
},
|
||||
"files.exclude": {
|
||||
".tmp": true
|
||||
},
|
||||
"files.associations": {
|
||||
"*.resjson": "json"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"search.exclude": {
|
||||
".tmp": true,
|
||||
"typings": true
|
||||
|
||||
@@ -76,6 +76,26 @@ You'll need to properly set up the certificate in order to be able to use the ho
|
||||
|
||||
> Hot Reload will only work on PowerBI Web (**not** on Desktop).
|
||||
|
||||
### Local dev guide (for powerbi-visual)
|
||||
|
||||
1. Cd into `./src/powerbi-visual`
|
||||
1. Run `npm install`
|
||||
1. To ensure proper SSL cert usage
|
||||
1. Ensure [mkcert](https://github.com/FiloSottile/mkcert) is installed
|
||||
1. Run `npm run generate-certs`
|
||||
1. If you're on WSL2, you'll need to copy over the root CA to the Windows side and install it there as a trusted root CA. Typically its in `~/.local/share/mkcert/rootCA.pem` on WSL2. From bash, `cd` to that folder and then do `explorer.exe .` to open it in Windows Explorer and then copy the pem file to someplace better accessible. Then open `crtmgr` and install it into **Trusted Root Certification Authorities**. "Certificates - Current User" > "Trusted Root Certification Authorities" > "Certificates" > Right Click "All Tasks" > "Import" > "Local Machine" > "Place all certificates in the following store" > "Trusted Root Certification Authorities". You may have to set the cert filter to "All Files" to see the `.pem` file.
|
||||
1. After the cert is installed you may have to restart your browser & dev server
|
||||
1. Run `npm run dev`
|
||||
1. PowerBI -> Home > New Report > Paste Or manually enter date > Auto-create > Create
|
||||
1. In the report, click on 'Edit' to open edit mode, and add a "Developer Visual" visual
|
||||
|
||||
#### Source map issues
|
||||
|
||||
Make sure you're running the dev build (`npm run dev`) and in your browser's dev tools trigger "Clear source maps cache" and "Enable JavaScript source maps". When everything's working, you should be able to click on the "App mounted" console message's file reference link which will take you to the source-mapped source code in dev tools.
|
||||
|
||||
Its still a bit janky in that it maye show multiple files with the same name in the file tree,
|
||||
but one of those is gonna be the real fully source mapped one.
|
||||
|
||||
### Contributing
|
||||
|
||||
Please make sure you read the [contribution guidelines](.github/CONTRIBUTING.md) for an overview of the best practices we try to follow.
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
{
|
||||
"dataRoles": [
|
||||
{
|
||||
"displayName": "Model URL",
|
||||
"displayName": "Viewer Data",
|
||||
"kind": "Grouping",
|
||||
"name": "stream"
|
||||
},
|
||||
{
|
||||
"displayName": "Version Object ID",
|
||||
"kind": "Grouping",
|
||||
"name": "parentObject"
|
||||
"name": "rootObject"
|
||||
},
|
||||
{
|
||||
"displayName": "Object ID",
|
||||
@@ -38,12 +33,7 @@
|
||||
"select": [
|
||||
{
|
||||
"bind": {
|
||||
"to": "stream"
|
||||
}
|
||||
},
|
||||
{
|
||||
"bind": {
|
||||
"to": "parentObject"
|
||||
"to": "rootObject"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -206,8 +196,10 @@
|
||||
"essential": true,
|
||||
"name": "WebAccess",
|
||||
"parameters": [
|
||||
"https://app.speckle.systems",
|
||||
"https://speckle.xyz",
|
||||
"https://*.speckle.xyz",
|
||||
"https://latest.speckle.systems",
|
||||
"https://latest.speckle.dev",
|
||||
"https://*.speckle.dev",
|
||||
"https://analytics.speckle.systems",
|
||||
|
||||
Generated
+1771
-435
File diff suppressed because it is too large
Load Diff
@@ -7,10 +7,10 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"pbiviz": "pbiviz",
|
||||
"pack": "webpack --config webpack.config.ts",
|
||||
"build": "webpack --config webpack.config.dev.ts",
|
||||
"serve": "webpack-dev-server --config webpack.config.dev.ts"
|
||||
"generate-certs": "mkcert localhost",
|
||||
"build": "webpack --config webpack.config.ts",
|
||||
"build:dev": "webpack --config webpack.config.dev.ts",
|
||||
"dev": "webpack-dev-server --config webpack.config.dev.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.5",
|
||||
@@ -19,7 +19,7 @@
|
||||
"@heroicons/vue": "^2.0.12",
|
||||
"@speckle/tailwind-theme": "2.14.7",
|
||||
"@speckle/ui-components": "2.14.7",
|
||||
"@speckle/viewer": "^2.18.14",
|
||||
"@speckle/viewer": "^2.21.0",
|
||||
"color-interpolate": "^1.0.5",
|
||||
"core-js": "^3.30.2",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -58,8 +58,8 @@
|
||||
"mini-css-extract-plugin": "^2.7.5",
|
||||
"postcss": "^8.4.23",
|
||||
"postcss-import": "^15.1.0",
|
||||
"powerbi-visuals-tools": "^5.4.3",
|
||||
"powerbi-visuals-webpack-plugin": "^4.0.0",
|
||||
"powerbi-visuals-tools": "^5.6.0",
|
||||
"powerbi-visuals-webpack-plugin": "^4.1.0",
|
||||
"prettier": "^2.8.8",
|
||||
"style-loader": "^3.3.2",
|
||||
"tailwindcss": "^3.3.2",
|
||||
@@ -77,4 +77,4 @@
|
||||
"webpack-dev-server": "^4.15.0"
|
||||
},
|
||||
"version": "2.0.0"
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"displayName": "Speckle PowerBI Viewer",
|
||||
"guid": "specklePowerBiVisual",
|
||||
"visualClassName": "Visual",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.0.0",
|
||||
"description": "An interactive 3D viewer for Speckle Data",
|
||||
"supportUrl": "https://speckle.community",
|
||||
"gitHubUrl": "https://github.com/specklesystems/speckle-powerbi-visuals"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import HomeView from './views/HomeView.vue'
|
||||
import ViewerView from './views/ViewerView.vue'
|
||||
import { computed } from 'vue'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { storeKey } from 'src/injectionKeys'
|
||||
|
||||
@@ -9,11 +9,15 @@ let store = useStore(storeKey)
|
||||
let status = computed(() => {
|
||||
return store.state.status
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
console.log("App mounted")
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ViewerView v-if="status == 'valid'" />
|
||||
<HomeView v-else />
|
||||
<ViewerView />
|
||||
<!-- <HomeView v-else /> -->
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { SpeckleDataInput } from 'src/types'
|
||||
import { debounce, throttle } from 'lodash'
|
||||
import { ContextOption } from 'src/settings/colorSettings'
|
||||
import { obj } from '@src/handlers/obj'
|
||||
|
||||
const selectionHandler = inject(selectionHandlerKey)
|
||||
const tooltipHandler = inject(tooltipHandlerKey)
|
||||
@@ -52,16 +53,19 @@ const onCameraMoved = throttle((_) => {
|
||||
tooltipHandler.move(screenPos)
|
||||
}, 50)
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
console.log('Viewer Wrapper mounted');
|
||||
|
||||
viewerHandler = new ViewerHandler(container.value)
|
||||
console.log('Viewer Handler created', viewerHandler);
|
||||
provide<ViewerHandler>(viewerHandlerKey, viewerHandler)
|
||||
setupTask = viewerHandler
|
||||
.init()
|
||||
.then(() => viewerHandler.addCameraUpdateEventListener(onCameraMoved))
|
||||
.finally(async () => {
|
||||
if (input.value) await cancelAndHandleDataUpdate()
|
||||
viewerHandler.updateSettings(settings.value)
|
||||
})
|
||||
// setupTask = viewerHandler
|
||||
// .init()
|
||||
// .then(() => viewerHandler.addCameraUpdateEventListener(onCameraMoved))
|
||||
// .finally(async () => {
|
||||
// await viewerHandler.loadObjects(obj, console.log, console.error)
|
||||
// viewerHandler.updateSettings(settings.value)
|
||||
// })
|
||||
})
|
||||
|
||||
onBeforeUnmount(async () => {
|
||||
@@ -78,52 +82,65 @@ watchEffect(() => {
|
||||
})
|
||||
|
||||
function handleDataUpdate(input: Ref<SpeckleDataInput>, signal: AbortSignal) {
|
||||
updateTask.value = setupTask
|
||||
.then(async () => {
|
||||
signal.throwIfAborted()
|
||||
// Clear previous selection
|
||||
await viewerHandler.selectObjects(null)
|
||||
console.log("in handleDataUpdate");
|
||||
if (input.value.objects){
|
||||
viewerHandler.selectObjects(null)
|
||||
|
||||
// Load
|
||||
await viewerHandler.loadObjectsWithAutoUnload(
|
||||
input.value.objectsToLoad,
|
||||
console.log,
|
||||
console.error,
|
||||
signal
|
||||
)
|
||||
viewerHandler.loadObjectsWithAutoUnload(
|
||||
input.value.objects,
|
||||
console.log,
|
||||
console.error,
|
||||
signal
|
||||
)
|
||||
}
|
||||
|
||||
// updateTask.value = setupTask
|
||||
// .then(async () => {
|
||||
// signal.throwIfAborted()
|
||||
// // Clear previous selection
|
||||
// await viewerHandler.selectObjects(null)
|
||||
|
||||
// Color
|
||||
await viewerHandler.colorObjectsByGroup(input.value.colorByIds)
|
||||
// // Load
|
||||
// await viewerHandler.loadObjectsWithAutoUnload(
|
||||
// input.value.rootObject,
|
||||
// console.log,
|
||||
// console.error,
|
||||
// signal
|
||||
// )
|
||||
|
||||
await viewerHandler.unIsolateObjects()
|
||||
const objectsToIsolate =
|
||||
input.value.selectedIds.length == 0 ? input.value.objectIds : input.value.selectedIds
|
||||
if (settings.value.color.context.value != ContextOption.show)
|
||||
await viewerHandler.isolateObjects(
|
||||
objectsToIsolate,
|
||||
settings.value.color.context.value === ContextOption.ghosted
|
||||
)
|
||||
if (settings.value.camera.zoomOnDataChange.value) viewerHandler.zoom(objectsToIsolate)
|
||||
// // Color
|
||||
// await viewerHandler.colorObjectsByGroup(input.value.colorByIds)
|
||||
|
||||
// Update available views
|
||||
views.value = viewerHandler.getViews()
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
console.log('Loading operation was aborted', e)
|
||||
})
|
||||
.finally(() => {
|
||||
updateTask.value = null
|
||||
})
|
||||
// await viewerHandler.unIsolateObjects()
|
||||
// const objectsToIsolate =
|
||||
// input.value.selectedIds.length == 0 ? input.value.objectIds : input.value.selectedIds
|
||||
// if (settings.value.color.context.value != ContextOption.show)
|
||||
// await viewerHandler.isolateObjects(
|
||||
// objectsToIsolate,
|
||||
// settings.value.color.context.value === ContextOption.ghosted
|
||||
// )
|
||||
// if (settings.value.camera.zoomOnDataChange.value) viewerHandler.zoom(objectsToIsolate)
|
||||
|
||||
// // Update available views
|
||||
// views.value = viewerHandler.getViews()
|
||||
// })
|
||||
// .catch((e: Error) => {
|
||||
// console.log('Loading operation was aborted', e)
|
||||
// })
|
||||
// .finally(() => {
|
||||
// updateTask.value = null
|
||||
// })
|
||||
}
|
||||
|
||||
async function cancelAndHandleDataUpdate() {
|
||||
console.log('Input has changed', input.value)
|
||||
if (updateTask.value) {
|
||||
ac.abort('New input is available')
|
||||
console.log('Cancelling previous load job')
|
||||
await updateTask.value
|
||||
ac = new AbortController()
|
||||
}
|
||||
// if (updateTask.value) {
|
||||
// ac.abort('New input is available')
|
||||
// console.log('Cancelling previous load job')
|
||||
// await updateTask.value
|
||||
// ac = new AbortController()
|
||||
// }
|
||||
const ac = new AbortController()
|
||||
const signal = ac.signal
|
||||
handleDataUpdate(input, signal)
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -4,13 +4,16 @@ import {
|
||||
LegacyViewer,
|
||||
IntersectionQuery,
|
||||
DefaultViewerParams,
|
||||
Box3,
|
||||
SpeckleView,
|
||||
CameraController
|
||||
CameraController,
|
||||
CameraEvent,
|
||||
SpeckleOfflineLoader
|
||||
} from '@speckle/viewer'
|
||||
import { pickViewableHit, projectToScreen } from '../utils/viewerUtils'
|
||||
import _ from 'lodash'
|
||||
import { SpeckleVisualSettingsModel } from 'src/settings/visualSettingsModel'
|
||||
import { PerspectiveCamera, OrthographicCamera, Box3 } from 'three'
|
||||
import { obj } from './obj'
|
||||
export default class ViewerHandler {
|
||||
private viewer: LegacyViewer
|
||||
private readonly parent: HTMLElement
|
||||
@@ -38,10 +41,9 @@ export default class ViewerHandler {
|
||||
break
|
||||
}
|
||||
|
||||
this.viewer.getExtension(CameraController).controls.maxPolarAngle = settings.camera
|
||||
.allowCameraUnder.value
|
||||
? Math.PI
|
||||
: Math.PI / 2
|
||||
var camController = this.viewer.getExtension(CameraController)
|
||||
var angle = settings.camera.allowCameraUnder.value ? Math.PI : Math.PI / 2
|
||||
camController.options = { maximumPolarAngle: angle }
|
||||
|
||||
// Lighting settings
|
||||
const newConfig = settings.lighting.getViewerConfiguration()
|
||||
@@ -59,10 +61,10 @@ export default class ViewerHandler {
|
||||
if (this.currentSectionBox === null) {
|
||||
const bbox = this.viewer.getSectionBoxFromObjects(objectIds)
|
||||
this.viewer.setSectionBox(bbox)
|
||||
this.currentSectionBox = bbox
|
||||
this.currentSectionBox = bbox as unknown as Box3
|
||||
} else {
|
||||
const bbox = this.viewer.getCurrentSectionBox()
|
||||
if (bbox) this.currentSectionBox = bbox
|
||||
if (bbox) this.currentSectionBox = bbox as unknown as Box3
|
||||
}
|
||||
this.viewer.sectionBoxOn()
|
||||
} else {
|
||||
@@ -72,7 +74,7 @@ export default class ViewerHandler {
|
||||
}
|
||||
|
||||
public addCameraUpdateEventListener(listener: (ev) => void) {
|
||||
this.viewer.getExtension(CameraController).controls.addEventListener('update', listener)
|
||||
this.viewer.getExtension(CameraController).on(CameraEvent.LateFrameUpdate, listener)
|
||||
}
|
||||
|
||||
public constructor(parent: HTMLElement) {
|
||||
@@ -107,52 +109,40 @@ export default class ViewerHandler {
|
||||
}
|
||||
|
||||
public async loadObjectsWithAutoUnload(
|
||||
objectUrls: string[],
|
||||
objects: string[],
|
||||
onLoad: (url: string, index: number) => void,
|
||||
onError: (url: string, error: Error) => void,
|
||||
signal: AbortSignal
|
||||
) {
|
||||
var objectsToUnload = _.difference([...this.loadedObjectsCache], objectUrls)
|
||||
await this.unloadObjects(objectsToUnload, signal)
|
||||
await this.loadObjects(objectUrls, onLoad, onError, signal)
|
||||
// console.log("rootObject in loadObjectsWithAutoUnload", rootObject);
|
||||
|
||||
// var objectsToUnload = _.difference([...this.loadedObjectsCache], rootObject)
|
||||
// await this.unloadObjects(objectsToUnload, signal)
|
||||
// await this.loadObjects(obj, onLoad, onError) // TODO: pass root object
|
||||
|
||||
|
||||
await this.loadObjects(objects, onLoad, onError)
|
||||
}
|
||||
|
||||
public async loadObjects(
|
||||
objectUrls: string[],
|
||||
objects: string[],
|
||||
onLoad: (url: string, index: number) => void,
|
||||
onError: (url: string, error: Error) => void,
|
||||
signal: AbortSignal
|
||||
onError: (url: string, error: Error) => void
|
||||
) {
|
||||
try {
|
||||
let index = 0
|
||||
let promises = []
|
||||
for (const url of objectUrls) {
|
||||
signal.throwIfAborted()
|
||||
console.log('Attempting to load', url)
|
||||
if (!this.loadedObjectsCache.has(url)) {
|
||||
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)
|
||||
if (promises.length == this.config.batchSize) {
|
||||
//this.promises.push(Promise.resolve(this.later(1000)))
|
||||
await Promise.all(promises)
|
||||
promises = []
|
||||
}
|
||||
} else {
|
||||
console.log('Object was already in cache')
|
||||
}
|
||||
}
|
||||
await Promise.all(promises)
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') return
|
||||
throw new Error(`Load objects failed: ${error}`)
|
||||
}
|
||||
//const stringifiedObject = objects.join()
|
||||
//console.log(stringifiedObject);
|
||||
|
||||
// let stringifiedObject = "["
|
||||
// objects.forEach((obj, index) => {
|
||||
// stringifiedObject += `{${obj}}`
|
||||
// if (index < objects.length - 1){
|
||||
// stringifiedObject += ","
|
||||
// }
|
||||
// });
|
||||
// stringifiedObject += "]"
|
||||
|
||||
const loader = new SpeckleOfflineLoader(this.viewer.getWorldTree(), ``)
|
||||
void this.viewer.loadObject(loader, true)
|
||||
}
|
||||
|
||||
public async intersect(coords: { x: number; y: number }) {
|
||||
@@ -213,13 +203,13 @@ export default class ViewerHandler {
|
||||
|
||||
public getScreenPosition(worldPosition): { x: number; y: number } {
|
||||
return projectToScreen(
|
||||
this.viewer.getExtension(CameraController).controls.camera,
|
||||
this.viewer.getExtension(CameraController).renderingCamera as unknown as PerspectiveCamera | OrthographicCamera,
|
||||
worldPosition
|
||||
)
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.viewer.getExtension(CameraController).controls.removeAllEventListeners()
|
||||
this.viewer.getExtension(CameraController).dispose()
|
||||
this.viewer.dispose()
|
||||
this.viewer = null
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface IViewerTooltip {
|
||||
}
|
||||
|
||||
export interface SpeckleDataInput {
|
||||
objectsToLoad: string[]
|
||||
objects: string[]
|
||||
objectIds: string[]
|
||||
selectedIds: string[]
|
||||
colorByIds: { objectIds: string[]; slice: fs.ColorPicker; color: string }[]
|
||||
|
||||
@@ -13,25 +13,25 @@ export function validateMatrixView(options: VisualUpdateOptions): {
|
||||
view: powerbi.DataViewMatrix
|
||||
} {
|
||||
const matrixVew = options.dataViews[0].matrix
|
||||
console.log(matrixVew);
|
||||
|
||||
if (!matrixVew) throw new Error('Data does not contain a matrix data view')
|
||||
|
||||
let hasStream = false,
|
||||
hasParentObject = false,
|
||||
let
|
||||
hasRootObject = false,
|
||||
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 (!hasRootObject) hasRootObject = source.roles['rootObject'] != 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')
|
||||
//if (!hasRootObject) throw new Error('Missing Root Object for Viewer')
|
||||
//if (!hasObject) throw new Error('Missing Object Id input')
|
||||
return {
|
||||
hasColorFilter,
|
||||
view: matrixVew
|
||||
@@ -126,94 +126,105 @@ export function processMatrixView(
|
||||
settings: SpeckleVisualSettingsModel,
|
||||
onSelectionPair: (objId: string, selectionId: powerbi.extensibility.ISelectionId) => void
|
||||
): SpeckleDataInput {
|
||||
const objectUrlsToLoad = [],
|
||||
const
|
||||
objectIds = [],
|
||||
selectedIds = [],
|
||||
colorByIds = [],
|
||||
objectTooltipData = new Map<string, IViewerTooltip>()
|
||||
|
||||
matrixView.rows.root.children.forEach((streamUrlChild) => {
|
||||
const url = streamUrlChild.value
|
||||
console.log('🪜 Processing Matrix View', matrixView);
|
||||
const rootObject = matrixView.rows.root.children[0].value as unknown as string
|
||||
console.log("rootObject", rootObject);
|
||||
|
||||
streamUrlChild.children?.forEach((parentObjectIdChild) => {
|
||||
const parentId = parentObjectIdChild.value
|
||||
objectUrlsToLoad.push(`${url}/objects/${parentId}`)
|
||||
|
||||
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()
|
||||
|
||||
const color = host.colorPalette.getColor(colorByChild.value as string)
|
||||
if (colorByChild.objects) {
|
||||
console.log(
|
||||
'⚠️COLOR NODE HAS objects',
|
||||
colorByChild.objects,
|
||||
colorByChild.objects.color?.fill
|
||||
)
|
||||
}
|
||||
|
||||
const colorSlice = new fs.ColorPicker({
|
||||
name: 'selectorFill',
|
||||
displayName: colorByChild.value.toString(),
|
||||
value: {
|
||||
value: color.value
|
||||
},
|
||||
selector: colorSelectionId.getSelector()
|
||||
})
|
||||
|
||||
const colorGroup = {
|
||||
color: color.value,
|
||||
slice: colorSlice,
|
||||
objectIds: []
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
})
|
||||
// // eslint-disable-next-line no-debugger
|
||||
// debugger
|
||||
|
||||
const objects = []
|
||||
matrixView.rows.root.children.forEach((obj) => {
|
||||
objects.push(obj.value)
|
||||
})
|
||||
|
||||
// matrixView.rows.root.children.forEach((streamUrlChild) => {
|
||||
// const url = streamUrlChild.value
|
||||
|
||||
// streamUrlChild.children?.forEach((parentObjectIdChild) => {
|
||||
// const parentId = parentObjectIdChild.value
|
||||
|
||||
// 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()
|
||||
|
||||
// const color = host.colorPalette.getColor(colorByChild.value as string)
|
||||
// if (colorByChild.objects) {
|
||||
// console.log(
|
||||
// '⚠️COLOR NODE HAS objects',
|
||||
// colorByChild.objects,
|
||||
// colorByChild.objects.color?.fill
|
||||
// )
|
||||
// }
|
||||
|
||||
// const colorSlice = new fs.ColorPicker({
|
||||
// name: 'selectorFill',
|
||||
// displayName: colorByChild.value.toString(),
|
||||
// value: {
|
||||
// value: color.value
|
||||
// },
|
||||
// selector: colorSelectionId.getSelector()
|
||||
// })
|
||||
|
||||
// const colorGroup = {
|
||||
// color: color.value,
|
||||
// slice: colorSlice,
|
||||
// objectIds: []
|
||||
// }
|
||||
|
||||
// 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)
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
|
||||
previousPalette = host.colorPalette['colorPalette']
|
||||
|
||||
return {
|
||||
objectsToLoad: objectUrlsToLoad,
|
||||
objects,
|
||||
objectIds,
|
||||
selectedIds,
|
||||
colorByIds: colorByIds.length > 0 ? colorByIds : null,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FilteringState } from '@speckle/viewer'
|
||||
import { OrthographicCamera, PerspectiveCamera } from 'three'
|
||||
|
||||
export function projectToScreen(cam, loc) {
|
||||
export function projectToScreen(cam: OrthographicCamera | PerspectiveCamera, loc) {
|
||||
cam.updateProjectionMatrix()
|
||||
const copy = loc.clone()
|
||||
copy.project(cam)
|
||||
|
||||
@@ -12,13 +12,12 @@ function goToForum() {
|
||||
function goToGuide() {
|
||||
host.launchUrl('https://speckle.guide/user/powerbi')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
id="speckle-home-view"
|
||||
class="flex flex-col justify-center items-center h-full w-full bg-primary text-center text-foundation"
|
||||
>
|
||||
<div id="speckle-home-view"
|
||||
class="flex flex-col justify-center items-center h-full w-full bg-primary text-center text-foundation">
|
||||
<div class="flex justify-center items-center">
|
||||
<img src="@assets/logo-white.png" alt="Logo" class="w-1/3" />
|
||||
</div>
|
||||
|
||||
@@ -68,22 +68,28 @@ export class Visual implements IVisual {
|
||||
|
||||
console.log('Selector colors', this.formattingSettings.colorSelector)
|
||||
let validationResult: { hasColorFilter: boolean; view: powerbi.DataViewMatrix } = null
|
||||
try {
|
||||
console.log('🔍 Validating input...', options)
|
||||
validationResult = validateMatrixView(options)
|
||||
console.log('✅Input valid', validationResult)
|
||||
} catch (e) {
|
||||
console.log('❌Input not valid:', (e as Error).message)
|
||||
this.host.displayWarningIcon(
|
||||
`Incomplete data input.`,
|
||||
`"Model URL", "Version Object ID" and "Object ID" data inputs are mandatory. If your data connector does not output all these columns, please update it.`
|
||||
)
|
||||
console.warn(
|
||||
`Incomplete data input. "Model URL", "Version Object ID" and "Object ID" data inputs are mandatory. If your data connector does not output all these columns, please update it.`
|
||||
)
|
||||
store.commit('setStatus', 'incomplete')
|
||||
return
|
||||
}
|
||||
validationResult = validateMatrixView(options)
|
||||
console.log(validationResult);
|
||||
|
||||
// try {
|
||||
// console.log('🔍 Validating input...', options)
|
||||
// validationResult = validateMatrixView(options)
|
||||
// console.log('✅Input valid', validationResult)
|
||||
// } catch (e) {
|
||||
// console.log('❌Input not valid:', (e as Error).message)
|
||||
// this.host.displayWarningIcon(
|
||||
// `Incomplete data input.`,
|
||||
// `"Model URL", "Version Object ID" and "Object ID" data inputs are mandatory. If your data connector does not output all these columns, please update it.`
|
||||
// )
|
||||
// console.warn(
|
||||
// `Incomplete data input. "Model URL", "Version Object ID" and "Object ID" data inputs are mandatory. If your data connector does not output all these columns, please update it.`
|
||||
// )
|
||||
// store.commit('setStatus', 'incomplete')
|
||||
// // return
|
||||
// }
|
||||
|
||||
console.log(options.type);
|
||||
|
||||
|
||||
switch (options.type) {
|
||||
case powerbi.VisualUpdateType.Resize:
|
||||
@@ -106,6 +112,8 @@ export class Visual implements IVisual {
|
||||
console.error('Data update error', error ?? 'Unknown')
|
||||
}
|
||||
}
|
||||
console.log("after switch");
|
||||
|
||||
}
|
||||
public getFormattingModel(): powerbi.visuals.FormattingModel {
|
||||
console.log('Showing Formatting settings', this.formattingSettings)
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import path from 'path'
|
||||
|
||||
// api configuration
|
||||
import powerbi from 'powerbi-visuals-api'
|
||||
import ExtraWatchWebpackPlugin from 'extra-watch-webpack-plugin'
|
||||
import { BundleAnalyzerPlugin as Visualizer } from 'webpack-bundle-analyzer'
|
||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
|
||||
import { PowerBICustomVisualsWebpackPlugin } from 'powerbi-visuals-webpack-plugin'
|
||||
import webpack from 'webpack'
|
||||
import fs from 'fs'
|
||||
import { WebpackConfiguration } from 'webpack-cli'
|
||||
import { VueLoaderPlugin } from 'vue-loader'
|
||||
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'
|
||||
|
||||
/**
|
||||
* MAIN CONSTS
|
||||
*/
|
||||
const devServerPort = 8080
|
||||
const pbivizPath = './pbiviz.json'
|
||||
const capabilitiesPath = './capabilities.json'
|
||||
const pluginLocation = './.tmp/precompile/visualPlugin.ts' // path to visual plugin file, the file generates by the plugin
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const powerbiApi: any = powerbi // Types for PowerBI seem to be off, so I'm instead forcing it to `any`
|
||||
|
||||
// visual configuration json path
|
||||
const pbivizFile = require(path.join(__dirname, pbivizPath))
|
||||
|
||||
const packageJsonFile = require(path.join(__dirname, 'package.json'))
|
||||
pbivizFile.visual.version = packageJsonFile.version
|
||||
|
||||
// the visual capabilities content
|
||||
const capabilitiesFile = require(path.join(__dirname, capabilitiesPath))
|
||||
|
||||
// string resources
|
||||
const resourcesFolder = path.join('.', 'stringResources')
|
||||
const localizationFolders = fs.existsSync(resourcesFolder) && fs.readdirSync(resourcesFolder)
|
||||
const statsLocation = '../../webpack.statistics.html'
|
||||
|
||||
// babel options to support IE11
|
||||
const babelOptions = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
ie: '11'
|
||||
},
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3,
|
||||
modules: false
|
||||
}
|
||||
]
|
||||
],
|
||||
plugins: [],
|
||||
sourceType: 'unambiguous', // tell to babel that the project can contain different module types, not only es2015 modules
|
||||
cacheDirectory: path.join('.tmp', 'babelCache') // path for cache files
|
||||
}
|
||||
|
||||
export const buildConfig = (params: { mode: 'dev' | 'prod' }) => {
|
||||
const isProd = params.mode === 'prod'
|
||||
|
||||
const loadCert = () => {
|
||||
const keyPath = path.resolve(__dirname, 'localhost-key.pem')
|
||||
const certPath = path.resolve(__dirname, 'localhost.pem')
|
||||
if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) {
|
||||
console.log('Unable to locate localhost certs, skipping...')
|
||||
return undefined
|
||||
}
|
||||
|
||||
console.log(
|
||||
'Using locally generated localhost certs, make sure the CA cert is installed & trusted!'
|
||||
)
|
||||
return {
|
||||
key: fs.readFileSync(keyPath),
|
||||
cert: fs.readFileSync(certPath)
|
||||
}
|
||||
}
|
||||
const certInfo = isProd ? undefined : loadCert()
|
||||
|
||||
const config: WebpackConfiguration = {
|
||||
entry: {
|
||||
visual: pluginLocation
|
||||
},
|
||||
optimization: {
|
||||
concatenateModules: false,
|
||||
minimize: isProd // enable minimization for create *.pbiviz file less than 2 Mb, can be disabled for dev mode
|
||||
},
|
||||
devtool: isProd ? false : 'inline-source-map',
|
||||
mode: isProd ? 'production' : 'development',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
use: ['vue-loader']
|
||||
},
|
||||
{
|
||||
parser: {
|
||||
amd: false
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /(\.ts)x|\.ts$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
// '@babel/react',
|
||||
'@babel/env'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: false,
|
||||
experimentalWatchApi: false,
|
||||
appendTsSuffixTo: [/\.vue$/]
|
||||
}
|
||||
}
|
||||
],
|
||||
exclude: [/node_modules/],
|
||||
include: /.tmp|powerbi-visuals-|src|precompile\\visualPlugin.ts/
|
||||
},
|
||||
{
|
||||
test: /(\.js)x|\.js$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: babelOptions
|
||||
}
|
||||
],
|
||||
exclude: [/node_modules/]
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
loader: 'json-loader',
|
||||
type: 'javascript/auto'
|
||||
},
|
||||
{
|
||||
test: /\.(css|scss)?$/,
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
|
||||
},
|
||||
{
|
||||
test: /\.(woff|ttf|ico|woff2|jpg|jpeg|png|webp|svg)$/i,
|
||||
use: ['base64-inline-loader']
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.jsx', '.js', '.css'],
|
||||
alias: {
|
||||
src: path.resolve(__dirname, 'src/'),
|
||||
assets: path.resolve(__dirname, 'assets/')
|
||||
},
|
||||
plugins: [new TsconfigPathsPlugin()]
|
||||
},
|
||||
output: {
|
||||
publicPath: '/assets',
|
||||
path: path.join(__dirname, '/.tmp', 'drop'),
|
||||
library: +powerbiApi.version.replace(/\./g, '') >= 320 ? pbivizFile.visual.guid : undefined,
|
||||
libraryTarget: +powerbiApi.version.replace(/\./g, '') >= 320 ? 'var' : undefined
|
||||
},
|
||||
...(isProd
|
||||
? {}
|
||||
: {
|
||||
devServer: {
|
||||
static: {
|
||||
directory: path.join(__dirname, '.tmp', 'drop'), // path with assets for dev server, they are generated by webpack plugin
|
||||
publicPath: '/assets'
|
||||
},
|
||||
compress: true,
|
||||
port: devServerPort, // dev server port
|
||||
hot: false,
|
||||
...(certInfo
|
||||
? {
|
||||
server: {
|
||||
type: 'https',
|
||||
options: {
|
||||
...certInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
: {
|
||||
https: {}
|
||||
}),
|
||||
liveReload: false,
|
||||
webSocketServer: false,
|
||||
headers: {
|
||||
'access-control-allow-origin': '*',
|
||||
'cache-control': 'public, max-age=0'
|
||||
}
|
||||
}
|
||||
}),
|
||||
externals:
|
||||
powerbiApi.version.replace(/\./g, '') >= 320
|
||||
? {
|
||||
'powerbi-visuals-api': 'null',
|
||||
fakeDefine: 'false'
|
||||
}
|
||||
: {
|
||||
'powerbi-visuals-api': 'null',
|
||||
fakeDefine: 'false',
|
||||
corePowerbiObject: "Function('return this.powerbi')()",
|
||||
realWindow: "Function('return this')()"
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__VUE_OPTIONS_API__: JSON.stringify(true),
|
||||
__VUE_PROD_DEVTOOLS__: JSON.stringify(false)
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'visual.css',
|
||||
chunkFilename: '[id].css'
|
||||
}),
|
||||
new Visualizer({
|
||||
reportFilename: statsLocation,
|
||||
openAnalyzer: false,
|
||||
analyzerMode: `static`
|
||||
}),
|
||||
// visual plugin regenerates with the visual source, but it does not require relaunching dev server
|
||||
new webpack.WatchIgnorePlugin({
|
||||
paths: [path.join(__dirname, pluginLocation), './.tmp/**/*.*']
|
||||
}),
|
||||
// custom visuals plugin instance with options
|
||||
new PowerBICustomVisualsWebpackPlugin({
|
||||
...pbivizFile,
|
||||
compression: isProd ? 9 : 0,
|
||||
capabilities: capabilitiesFile,
|
||||
stringResources:
|
||||
localizationFolders &&
|
||||
localizationFolders.map((localization) =>
|
||||
path.join(resourcesFolder, localization, 'resources.resjson')
|
||||
),
|
||||
apiVersion: powerbiApi.version,
|
||||
capabilitiesSchema: powerbiApi.schemas.capabilities,
|
||||
pbivizSchema: powerbiApi.schemas.pbiviz,
|
||||
stringResourcesSchema: powerbiApi.schemas.stringResources,
|
||||
dependenciesSchema: powerbiApi.schemas.dependencies,
|
||||
devMode: false,
|
||||
generatePbiviz: isProd,
|
||||
generateResources: true,
|
||||
minifyJS: isProd,
|
||||
minify: isProd,
|
||||
modules: true,
|
||||
visualSourceLocation: '../../src/visual',
|
||||
pluginLocation: pluginLocation,
|
||||
packageOutPath: path.join(__dirname, 'dist')
|
||||
}),
|
||||
new ExtraWatchWebpackPlugin({
|
||||
files: [pbivizPath, capabilitiesPath]
|
||||
}),
|
||||
powerbiApi.version.replace(/\./g, '') >= 320
|
||||
? new webpack.ProvidePlugin({
|
||||
define: 'fakeDefine'
|
||||
})
|
||||
: new webpack.ProvidePlugin({
|
||||
window: 'realWindow',
|
||||
define: 'fakeDefine',
|
||||
powerbi: 'corePowerbiObject'
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -1,228 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import path from 'path'
|
||||
import { buildConfig } from './webpack.config.base'
|
||||
|
||||
// api configuration
|
||||
import powerbi from 'powerbi-visuals-api'
|
||||
import ExtraWatchWebpackPlugin from 'extra-watch-webpack-plugin'
|
||||
import { BundleAnalyzerPlugin as Visualizer } from 'webpack-bundle-analyzer'
|
||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
|
||||
import { PowerBICustomVisualsWebpackPlugin } from 'powerbi-visuals-webpack-plugin'
|
||||
import webpack from 'webpack'
|
||||
import fs from 'fs'
|
||||
import { WebpackConfiguration } from 'webpack-cli'
|
||||
import { VueLoaderPlugin } from 'vue-loader'
|
||||
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const powerbiApi: any = powerbi // Types for PowerBI seem to be off, so I'm instead forcing it to `any`
|
||||
|
||||
// visual configuration json path
|
||||
const pbivizPath = './pbiviz.json'
|
||||
const pbivizFile = require(path.join(__dirname, pbivizPath))
|
||||
|
||||
const packageJsonFile = require(path.join(__dirname, 'package.json'))
|
||||
pbivizFile.visual.version = packageJsonFile.version
|
||||
|
||||
// the visual capabilities content
|
||||
const capabilitiesPath = './capabilities.json'
|
||||
const capabilitiesFile = require(path.join(__dirname, capabilitiesPath))
|
||||
|
||||
const pluginLocation = './.tmp/precompile/visualPlugin.ts' // path to visual plugin file, the file generates by the plugin
|
||||
|
||||
// string resources
|
||||
const resourcesFolder = path.join('.', 'stringResources')
|
||||
const localizationFolders = fs.existsSync(resourcesFolder) && fs.readdirSync(resourcesFolder)
|
||||
const statsLocation = '../../webpack.statistics.html'
|
||||
|
||||
// babel options to support IE11
|
||||
const babelOptions = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
ie: '11'
|
||||
},
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3,
|
||||
modules: false
|
||||
}
|
||||
]
|
||||
],
|
||||
plugins: [],
|
||||
sourceType: 'unambiguous', // tell to babel that the project can contain different module types, not only es2015 modules
|
||||
cacheDirectory: path.join('.tmp', 'babelCache') // path for cache files
|
||||
}
|
||||
|
||||
const config: WebpackConfiguration = {
|
||||
entry: {
|
||||
visual: pluginLocation
|
||||
},
|
||||
optimization: {
|
||||
concatenateModules: false,
|
||||
minimize: false // enable minimization for create *.pbiviz file less than 2 Mb, can be disabled for dev mode
|
||||
},
|
||||
devtool: 'source-map',
|
||||
mode: 'development',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
use: ['vue-loader']
|
||||
},
|
||||
{
|
||||
parser: {
|
||||
amd: false
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /(\.ts)x|\.ts$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
// '@babel/react',
|
||||
'@babel/env'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: false,
|
||||
experimentalWatchApi: false,
|
||||
appendTsSuffixTo: [/\.vue$/]
|
||||
}
|
||||
}
|
||||
],
|
||||
exclude: [/node_modules/],
|
||||
include: /.tmp|powerbi-visuals-|src|precompile\\visualPlugin.ts/
|
||||
},
|
||||
{
|
||||
test: /(\.js)x|\.js$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: babelOptions
|
||||
}
|
||||
],
|
||||
exclude: [/node_modules/]
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
loader: 'json-loader',
|
||||
type: 'javascript/auto'
|
||||
},
|
||||
{
|
||||
test: /\.(css|scss)?$/,
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
|
||||
},
|
||||
{
|
||||
test: /\.(woff|ttf|ico|woff2|jpg|jpeg|png|webp|svg)$/i,
|
||||
use: ['base64-inline-loader']
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.jsx', '.js', '.css'],
|
||||
alias: {
|
||||
src: path.resolve(__dirname, 'src/'),
|
||||
assets: path.resolve(__dirname, 'assets/')
|
||||
},
|
||||
plugins: [new TsconfigPathsPlugin()]
|
||||
},
|
||||
output: {
|
||||
publicPath: '/assets',
|
||||
path: path.join(__dirname, '/.tmp', 'drop'),
|
||||
library: +powerbiApi.version.replace(/\./g, '') >= 320 ? pbivizFile.visual.guid : undefined,
|
||||
libraryTarget: +powerbiApi.version.replace(/\./g, '') >= 320 ? 'var' : undefined
|
||||
},
|
||||
devServer: {
|
||||
static: {
|
||||
directory: path.join(__dirname, '.tmp', 'drop'), // path with assets for dev server, they are generated by webpack plugin
|
||||
publicPath: '/assets'
|
||||
},
|
||||
compress: true,
|
||||
port: 8080, // dev server port
|
||||
hot: false,
|
||||
https: {},
|
||||
liveReload: false,
|
||||
webSocketServer: false,
|
||||
headers: {
|
||||
'access-control-allow-origin': '*',
|
||||
'cache-control': 'public, max-age=0'
|
||||
}
|
||||
},
|
||||
externals:
|
||||
powerbiApi.version.replace(/\./g, '') >= 320
|
||||
? {
|
||||
'powerbi-visuals-api': 'null',
|
||||
fakeDefine: 'false'
|
||||
}
|
||||
: {
|
||||
'powerbi-visuals-api': 'null',
|
||||
fakeDefine: 'false',
|
||||
corePowerbiObject: "Function('return this.powerbi')()",
|
||||
realWindow: "Function('return this')()"
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__VUE_OPTIONS_API__: JSON.stringify(true),
|
||||
__VUE_PROD_DEVTOOLS__: JSON.stringify(false)
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'visual.css',
|
||||
chunkFilename: '[id].css'
|
||||
}),
|
||||
new Visualizer({
|
||||
reportFilename: statsLocation,
|
||||
openAnalyzer: false,
|
||||
analyzerMode: `static`
|
||||
}),
|
||||
// visual plugin regenerates with the visual source, but it does not require relaunching dev server
|
||||
new webpack.WatchIgnorePlugin({
|
||||
paths: [path.join(__dirname, pluginLocation), './.tmp/**/*.*']
|
||||
}),
|
||||
// custom visuals plugin instance with options
|
||||
new PowerBICustomVisualsWebpackPlugin({
|
||||
...pbivizFile,
|
||||
compression: 0,
|
||||
capabilities: capabilitiesFile,
|
||||
stringResources:
|
||||
localizationFolders &&
|
||||
localizationFolders.map((localization) =>
|
||||
path.join(resourcesFolder, localization, 'resources.resjson')
|
||||
),
|
||||
apiVersion: powerbiApi.version,
|
||||
capabilitiesSchema: powerbiApi.schemas.capabilities,
|
||||
pbivizSchema: powerbiApi.schemas.pbiviz,
|
||||
stringResourcesSchema: powerbiApi.schemas.stringResources,
|
||||
dependenciesSchema: powerbiApi.schemas.dependencies,
|
||||
devMode: false,
|
||||
generatePbiviz: false,
|
||||
generateResources: true,
|
||||
minifyJS: false,
|
||||
minify: false,
|
||||
modules: true,
|
||||
visualSourceLocation: '../../src/visual',
|
||||
pluginLocation: pluginLocation,
|
||||
packageOutPath: path.join(__dirname, 'dist')
|
||||
}),
|
||||
new ExtraWatchWebpackPlugin({
|
||||
files: [pbivizPath, capabilitiesPath]
|
||||
}),
|
||||
powerbiApi.version.replace(/\./g, '') >= 320
|
||||
? new webpack.ProvidePlugin({
|
||||
define: 'fakeDefine'
|
||||
})
|
||||
: new webpack.ProvidePlugin({
|
||||
window: 'realWindow',
|
||||
define: 'fakeDefine',
|
||||
powerbi: 'corePowerbiObject'
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
export default config
|
||||
export default buildConfig({ mode: 'dev' })
|
||||
|
||||
@@ -1,212 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import path from 'path'
|
||||
import { buildConfig } from './webpack.config.base'
|
||||
|
||||
// api configuration
|
||||
import powerbi from 'powerbi-visuals-api'
|
||||
import ExtraWatchWebpackPlugin from 'extra-watch-webpack-plugin'
|
||||
import { BundleAnalyzerPlugin as Visualizer } from 'webpack-bundle-analyzer'
|
||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
|
||||
import { PowerBICustomVisualsWebpackPlugin } from 'powerbi-visuals-webpack-plugin'
|
||||
import webpack from 'webpack'
|
||||
import fs from 'fs'
|
||||
import { WebpackConfiguration } from 'webpack-cli'
|
||||
import { VueLoaderPlugin } from 'vue-loader'
|
||||
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const powerbiApi: any = powerbi // Types for PowerBI seem to be off, so I'm instead forcing it to `any`
|
||||
|
||||
// visual configuration json path
|
||||
const pbivizPath = './pbiviz.json'
|
||||
const pbivizFile = require(path.join(__dirname, pbivizPath))
|
||||
|
||||
const packageJsonFile = require(path.join(__dirname, 'package.json'))
|
||||
pbivizFile.visual.version = packageJsonFile.version
|
||||
|
||||
// the visual capabilities content
|
||||
const capabilitiesPath = './capabilities.json'
|
||||
const capabilitiesFile = require(path.join(__dirname, capabilitiesPath))
|
||||
|
||||
const pluginLocation = './.tmp/precompile/visualPlugin.ts' // path to visual plugin file, the file generates by the plugin
|
||||
|
||||
// string resources
|
||||
const resourcesFolder = path.join('.', 'stringResources')
|
||||
const localizationFolders = fs.existsSync(resourcesFolder) && fs.readdirSync(resourcesFolder)
|
||||
const statsLocation = '../../webpack.statistics.html'
|
||||
|
||||
// babel options to support IE11
|
||||
const babelOptions = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
ie: '11'
|
||||
},
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3,
|
||||
modules: false
|
||||
}
|
||||
]
|
||||
],
|
||||
plugins: [],
|
||||
sourceType: 'unambiguous', // tell to babel that the project can contain different module types, not only es2015 modules
|
||||
cacheDirectory: path.join('.tmp', 'babelCache') // path for cache files
|
||||
}
|
||||
|
||||
const config: WebpackConfiguration = {
|
||||
entry: {
|
||||
visual: pluginLocation
|
||||
},
|
||||
optimization: {
|
||||
concatenateModules: false,
|
||||
minimize: true // enable minimization for create *.pbiviz file less than 2 Mb, can be disabled for dev mode
|
||||
},
|
||||
devtool: 'source-map',
|
||||
mode: 'production',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
use: ['vue-loader']
|
||||
},
|
||||
{
|
||||
parser: {
|
||||
amd: false
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /(\.ts)x|\.ts$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
// '@babel/react',
|
||||
'@babel/env'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: false,
|
||||
experimentalWatchApi: false,
|
||||
appendTsSuffixTo: [/\.vue$/]
|
||||
}
|
||||
}
|
||||
],
|
||||
exclude: [/node_modules/],
|
||||
include: /.tmp|powerbi-visuals-|src|precompile\\visualPlugin.ts/
|
||||
},
|
||||
{
|
||||
test: /(\.js)x|\.js$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: babelOptions
|
||||
}
|
||||
],
|
||||
exclude: [/node_modules/]
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
loader: 'json-loader',
|
||||
type: 'javascript/auto'
|
||||
},
|
||||
{
|
||||
test: /\.(css|scss)?$/,
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
|
||||
},
|
||||
{
|
||||
test: /\.(woff|ttf|ico|woff2|jpg|jpeg|png|webp|svg)$/i,
|
||||
use: ['base64-inline-loader']
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.jsx', '.js', '.css'],
|
||||
alias: {
|
||||
src: path.resolve(__dirname, 'src/'),
|
||||
assets: path.resolve(__dirname, 'assets/')
|
||||
},
|
||||
plugins: [new TsconfigPathsPlugin()]
|
||||
},
|
||||
output: {
|
||||
publicPath: '/assets',
|
||||
path: path.join(__dirname, '/.tmp', 'drop'),
|
||||
library: +powerbiApi.version.replace(/\./g, '') >= 320 ? pbivizFile.visual.guid : undefined,
|
||||
libraryTarget: +powerbiApi.version.replace(/\./g, '') >= 320 ? 'var' : undefined
|
||||
},
|
||||
externals:
|
||||
powerbiApi.version.replace(/\./g, '') >= 320
|
||||
? {
|
||||
'powerbi-visuals-api': 'null',
|
||||
fakeDefine: 'false'
|
||||
}
|
||||
: {
|
||||
'powerbi-visuals-api': 'null',
|
||||
fakeDefine: 'false',
|
||||
corePowerbiObject: "Function('return this.powerbi')()",
|
||||
realWindow: "Function('return this')()"
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__VUE_OPTIONS_API__: JSON.stringify(true),
|
||||
__VUE_PROD_DEVTOOLS__: JSON.stringify(false)
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'visual.css',
|
||||
chunkFilename: '[id].css'
|
||||
}),
|
||||
new Visualizer({
|
||||
reportFilename: statsLocation,
|
||||
openAnalyzer: false,
|
||||
analyzerMode: `static`
|
||||
}),
|
||||
// visual plugin regenerates with the visual source, but it does not require relaunching dev server
|
||||
new webpack.WatchIgnorePlugin({
|
||||
paths: [path.join(__dirname, pluginLocation), './.tmp/**/*.*']
|
||||
}),
|
||||
// custom visuals plugin instance with options
|
||||
new PowerBICustomVisualsWebpackPlugin({
|
||||
...pbivizFile,
|
||||
compression: 9,
|
||||
capabilities: capabilitiesFile,
|
||||
stringResources:
|
||||
localizationFolders &&
|
||||
localizationFolders.map((localization) =>
|
||||
path.join(resourcesFolder, localization, 'resources.resjson')
|
||||
),
|
||||
apiVersion: powerbiApi.version,
|
||||
capabilitiesSchema: powerbiApi.schemas.capabilities,
|
||||
pbivizSchema: powerbiApi.schemas.pbiviz,
|
||||
stringResourcesSchema: powerbiApi.schemas.stringResources,
|
||||
dependenciesSchema: powerbiApi.schemas.dependencies,
|
||||
devMode: false,
|
||||
generatePbiviz: true,
|
||||
generateResources: true,
|
||||
minifyJS: true,
|
||||
minify: true,
|
||||
modules: true,
|
||||
visualSourceLocation: '../../src/visual',
|
||||
pluginLocation: pluginLocation,
|
||||
packageOutPath: path.join(__dirname, 'dist')
|
||||
}),
|
||||
new ExtraWatchWebpackPlugin({
|
||||
files: [pbivizPath, capabilitiesPath]
|
||||
}),
|
||||
powerbiApi.version.replace(/\./g, '') >= 320
|
||||
? new webpack.ProvidePlugin({
|
||||
define: 'fakeDefine'
|
||||
})
|
||||
: new webpack.ProvidePlugin({
|
||||
window: 'realWindow',
|
||||
define: 'fakeDefine',
|
||||
powerbi: 'corePowerbiObject'
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
export default config
|
||||
export default buildConfig({ mode: 'prod' })
|
||||
|
||||
Reference in New Issue
Block a user