From da870bd3032407f352a567ba1693de092f407da2 Mon Sep 17 00:00:00 2001 From: Deniz Saner Date: Thu, 13 Jun 2024 12:13:14 +0200 Subject: [PATCH] initial commit --- .gitconfig | 2 + .github/workflows/release.yml | 27 +++ .gitignore | 2 + .vscode/launch.json | 14 ++ .vscode/settings.json | 5 + .vscode/tasks.json | 22 ++ ApiClient.pqm | 70 +++++++ CONTRIBUTING.md | 37 ++++ LICENSE | 18 ++ README.md | 32 +++ docs/images/connector-select.png | Bin 0 -> 30994 bytes docs/images/dataset-select.png | Bin 0 -> 40537 bytes docs/images/menu-bar.png | Bin 0 -> 33403 bytes downtimes/Downtimes.TableSchema.pqm | 16 ++ downtimes/Downtimes.Transform.pqm | 40 ++++ enlyze.pq | 136 ++++++++++++ enlyze.pq.proj | 35 ++++ enlyze.query.pq | 17 ++ helpers/Table.ChangeType.pqm | 195 ++++++++++++++++++ helpers/Table.ToNavigationTable.pqm | 21 ++ machines/Machines.TableSchema.pqm | 6 + machines/Machines.Transform.pqm | 40 ++++ productionRuns/ProductionRuns.TableSchema.pqm | 33 +++ productionRuns/ProductionRuns.Transform.pqm | 73 +++++++ productivityMetrics/DateTimeRanges.pqm | 136 ++++++++++++ ...chineProductivityMetrics.FunctionTypes.pqm | 28 +++ ...MachineProductivityMetrics.TableSchema.pqm | 20 ++ .../MachineProductivityMetrics.pqm | 53 +++++ .../ProductivityMetrics.Transform.pqm | 65 ++++++ products/Products.TableSchema.pqm | 1 + products/Products.Transform.pqm | 25 +++ push-extension.ps1 | 68 ++++++ resources.resx | 129 ++++++++++++ resources/ENLYZE16.png | Bin 0 -> 740 bytes resources/ENLYZE20.png | Bin 0 -> 900 bytes resources/ENLYZE24.png | Bin 0 -> 1160 bytes resources/ENLYZE32.png | Bin 0 -> 1522 bytes resources/ENLYZE40.png | Bin 0 -> 1892 bytes resources/ENLYZE48.png | Bin 0 -> 2367 bytes resources/ENLYZE64.png | Bin 0 -> 3237 bytes resources/ENLYZE80.png | Bin 0 -> 4162 bytes shared/Quantities.Transform.pqm | 12 ++ sites/Sites.TableSchema.pqm | 1 + sites/Sites.Transform.pqm | 25 +++ .../TimeseriesData.FunctionTypes.pqm | 43 ++++ timeseriesData/TimeseriesData.Transform.pqm | 23 +++ timeseriesData/TimeseriesData.pqm | 76 +++++++ variables/Variables.TableSchema.pqm | 13 ++ variables/Variables.Transform.pqm | 53 +++++ 49 files changed, 1612 insertions(+) create mode 100644 .gitconfig create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 ApiClient.pqm create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/images/connector-select.png create mode 100644 docs/images/dataset-select.png create mode 100644 docs/images/menu-bar.png create mode 100644 downtimes/Downtimes.TableSchema.pqm create mode 100644 downtimes/Downtimes.Transform.pqm create mode 100644 enlyze.pq create mode 100644 enlyze.pq.proj create mode 100644 enlyze.query.pq create mode 100644 helpers/Table.ChangeType.pqm create mode 100644 helpers/Table.ToNavigationTable.pqm create mode 100644 machines/Machines.TableSchema.pqm create mode 100644 machines/Machines.Transform.pqm create mode 100644 productionRuns/ProductionRuns.TableSchema.pqm create mode 100644 productionRuns/ProductionRuns.Transform.pqm create mode 100644 productivityMetrics/DateTimeRanges.pqm create mode 100644 productivityMetrics/MachineProductivityMetrics.FunctionTypes.pqm create mode 100644 productivityMetrics/MachineProductivityMetrics.TableSchema.pqm create mode 100644 productivityMetrics/MachineProductivityMetrics.pqm create mode 100644 productivityMetrics/ProductivityMetrics.Transform.pqm create mode 100644 products/Products.TableSchema.pqm create mode 100644 products/Products.Transform.pqm create mode 100644 push-extension.ps1 create mode 100644 resources.resx create mode 100644 resources/ENLYZE16.png create mode 100644 resources/ENLYZE20.png create mode 100644 resources/ENLYZE24.png create mode 100644 resources/ENLYZE32.png create mode 100644 resources/ENLYZE40.png create mode 100644 resources/ENLYZE48.png create mode 100644 resources/ENLYZE64.png create mode 100644 resources/ENLYZE80.png create mode 100644 shared/Quantities.Transform.pqm create mode 100644 sites/Sites.TableSchema.pqm create mode 100644 sites/Sites.Transform.pqm create mode 100644 timeseriesData/TimeseriesData.FunctionTypes.pqm create mode 100644 timeseriesData/TimeseriesData.Transform.pqm create mode 100644 timeseriesData/TimeseriesData.pqm create mode 100644 variables/Variables.TableSchema.pqm create mode 100644 variables/Variables.Transform.pqm diff --git a/.gitconfig b/.gitconfig new file mode 100644 index 0000000..1e25f91 --- /dev/null +++ b/.gitconfig @@ -0,0 +1,2 @@ +[core] + autocrlf = false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c9c8df9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,27 @@ +name: Attach Custom Connector as release asset + +on: + release: + types: [published] + +jobs: + build-and-attach: + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up MSBuild + uses: microsoft/setup-msbuild@v1 + + - name: Build .mez file + run: | + msbuild enlyze.pq.proj /p:Configuration=Release + + - name: Upload `.mez` as release asset + uses: softprops/action-gh-release@v1 + with: + files: bin/AnyCPU/Release/enlyze-powerbi.mez + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db4f53f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.DS_Store +bin/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a7e1185 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "powerquery", + "request": "launch", + "name": "Evaluate power query file.", + "program": "${workspaceFolder}/${command:AskForPowerQueryFileName}" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b305bf5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "powerquery.sdk.defaultQueryFile": "${workspaceFolder}\\enlyze.query.pq", + "powerquery.sdk.defaultExtension": "${workspaceFolder}\\bin\\AnyCPU\\Debug\\enlyze-powerbi.mez", + "powerquery.general.mode": "SDK" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..8ba8f8c --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,22 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Build and deploy", + "type": "shell", + "command": "powershell.exe", + "args": [ + "-ExecutionPolicy", + "Bypass", + "-File", + "${workspaceFolder}/push-extension.ps1" + ], + "presentation": { + "reveal": "always", + "panel": "new" + }, + "group": "build", + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/ApiClient.pqm b/ApiClient.pqm new file mode 100644 index 0000000..ddffbbd --- /dev/null +++ b/ApiClient.pqm @@ -0,0 +1,70 @@ +let + BaseUrl = "https://app.enlyze.com/api/v2", + CommonHeaders = [ + #"Content-Type" = "application/json", + #"user-agent" = "enlyze-powerbi/1.0.0" + ], + CreateHeaders = (apiToken as text) as record => + Record.Combine({CommonHeaders, [#"Authorization" = "Bearer " & apiToken]}), + FetchPage = (apiPath as text, cursor as nullable text, optional queryParams as nullable record) => + let + apiUrl = BaseUrl & apiPath, + apiToken = Extension.CurrentCredential()[Key], + headers = CreateHeaders(apiToken), + combinedQueryParams = + if queryParams <> null then + Record.Combine({queryParams, [cursor = cursor]}) + else + [cursor = cursor], + fieldNames = Record.FieldNames(combinedQueryParams), + nullValueFields = List.Select(fieldNames, each Record.Field(combinedQueryParams, _) = null), + queryParamsNonNull = Record.RemoveFields(combinedQueryParams, nullValueFields), + queryString = Uri.BuildQueryString(queryParamsNonNull), + apiUrlWithQueryParams = if Text.Length(queryString) > 0 then apiUrl & "?" & queryString else apiUrl, + parsedResponse = Json.Document(Web.Contents(apiUrlWithQueryParams, [Headers = headers])) + in + parsedResponse, + FetchPaginated = (apiPath as text, cursor as nullable text, optional queryParams as nullable record) as list => + let + currentPage = FetchPage(apiPath, cursor, queryParams), + nextCursor = currentPage[metadata][next_cursor], + data = currentPage[data], + remainingData = if nextCursor = null then {} else @FetchPaginated(apiPath, nextCursor, queryParams) + in + List.Combine({data, remainingData}), + PostRequestPage = (apiPath as text, body as record, cursor as nullable text) as record => + let + bodyWithCursor = if cursor <> null then Record.Combine({body, [cursor = cursor]}) else body, + url = BaseUrl & apiPath, + apiToken = Extension.CurrentCredential()[Key], + headers = CreateHeaders(apiToken), + response = Web.Contents( + url, + [ + Headers = headers, + Content = Json.FromValue(bodyWithCursor), + ManualStatusHandling = {400, 401, 403, 404, 422, 500} + ] + ), + statusCode = Value.Metadata(response)[Response.Status], + parsedResponse = + if statusCode = 200 then + Json.Document(response) + else + error "HTTP Error: " & Text.From(statusCode) & ". Response body: " & Text.FromBinary(response) + in + parsedResponse, + PaginatedPostRequest = (apiPath as text, body as record, optional cursor as nullable text) as list => + let + currentPage = PostRequestPage(apiPath, body, cursor), + dataMaybeRecord = currentPage[data], + data = if Type.Is(Value.Type(dataMaybeRecord), List.Type) then dataMaybeRecord else {dataMaybeRecord}, + nextCursor = currentPage[metadata][next_cursor], + remainingData = if nextCursor = null then {} else @PaginatedPostRequest(apiPath, body, nextCursor) + in + List.Combine({data, remainingData}) +in + [ + FetchPaginated = FetchPaginated, + PaginatedPostRequest = PaginatedPostRequest + ] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9c9541e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Contributing + +We are very excited you are interested in helping with our ENLYZE Power BI Integration! Let's get you started, even if you don't have any previous open-source experience! + +## New to Open Source? + +Take a look at [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) + +## Where to ask Questions? + +Check our [Github Issues](https://github.com/enlyze/enlyze-powerbi/issues) to see if someone has already answered your question. + +## Primer + +In case you are new to Power BI Custom Connector development, [this guide](https://learn.microsoft.com/en-us/power-query/install-sdk) may help you get started quickly. + +### Prerequisites + +In order to get your development setup up and running, you will need + +1. A Windows (Virtual) Machine +2. [Microsoft Visual Studio](https://visualstudio.microsoft.com/) or [Visual Studio Code](https://code.visualstudio.com/) +3. The latest version of the [Microsoft Power Query SDK](https://learn.microsoft.com/en-us/power-query/install-sdk) + +### Project Setup + +1. Fork the [enlyze-powerbi](https://github.com/enlyze/enlyze-powerbi) repository. +2. Clone the forked repository. + +## Modifying Code + +- Open the `enlyze-power` project in your IDE and start modifying the code. +- The [Microsoft Power Query SDK](https://learn.microsoft.com/en-us/power-query/install-sdk) will auto-format your changes. + +## Pull Request + +Reference the relevant issue or pull request and give a clear description of changes/features added when submitting a pull request. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e346fa7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +Copyright 2024 ENLYZE GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be included in all copies +or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e42adc --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# ENLYZE Power BI Integration + +This Power BI Integration enables users of [ENLYZE](https://enlyze.com) to use their production data in Power BI. The project is based on the [Power Query Connector Development SDK](https://github.com/microsoft/vscode-powerquery-sdk). + +## Features + +The ENLYZE Power BI Integration currently supports querying the following resources: + +- Sites +- Machines +- Production Runs +- Products +- Downtimes +- Productivity Metrics for machines + +## Installation + +If you are looking for a more detailed guide, head over to [our docs](https://docs.enlyze.com/platform/power-bi/installation) (German only). + +In order to get started with the ENLYZE Power BI Integration, + +1. Download the `enlyze-powerbi.mez` file from the [Latest Release](https://github.com/enlyze/enlyze-powerbi/releases/latest/download/enlyze-powerbi.mez) +2. Follow the steps [described in the official documentation](https://learn.microsoft.com/en-us/power-bi/connect-data/desktop-connector-extensibility#custom-connectors). +3. Restart Power BI + +## Usage Guide + +If you want use the ENLYZE Power BI Integration, [our docs](https://docs.enlyze.com/platform/power-bi) are a great place to start (German only). + +## Contributing + +Check out [CONTRIBUTING.md](/CONTRIBUTING.md) diff --git a/docs/images/connector-select.png b/docs/images/connector-select.png new file mode 100644 index 0000000000000000000000000000000000000000..7036ad055da9e652341f1fe189bd9a46230a4466 GIT binary patch literal 30994 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 F{{t?*xds3L literal 0 HcmV?d00001 diff --git a/docs/images/dataset-select.png b/docs/images/dataset-select.png new file mode 100644 index 0000000000000000000000000000000000000000..bed98be12e54b43b091217540da99ef1fea61350 GIT binary patch literal 40537 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 j<6lk#|9Dp(5Pu1a9Lih`F>kuQe14?F + let + binary = Extension.Contents(fileName), asText = Text.FromBinary(binary) + in + try + Expression.Evaluate(asText, #shared) catch (e) => + error + [ + Reason = "Extension.LoadModule Failure", + Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'", + Message.Parameters = {fileName, e[Reason], e[Message]}, + Detail = [File = fileName, Error = e] + ], + TableSchema = loadModule("Downtimes.TableSchema.pqm"), + Table.ChangeType = loadModule("Table.ChangeType.pqm"), + TimeseriesData = loadModule("TimeseriesData.pqm"), + TransformDowntimes = (downtimes as list) as table => + let + downtimesTable = Table.FromList(downtimes, Splitter.SplitByNothing(), null, null, ExtraValues.Error), + namedTable = Value.ReplaceMetadata(downtimesTable, Value.Metadata(downtimesTable) & [Name = "Downtimes"]), + expandedTable = Table.ExpandRecordColumn( + namedTable, "Column1", {"uuid", "machine", "comment", "type", "updated", "reason", "start", "end"} + ), + expandedUpdated = Table.ExpandRecordColumn( + expandedTable, + "updated", + {"first_name", "last_name", "timestamp"}, + {"updated_first_name", "updated_last_name", "updated_timestamp"} + ), + expandedReason = Table.ExpandRecordColumn( + expandedUpdated, + "reason", + {"uuid", "name", "category"}, + {"reason_uuid", "reason_name", "reason_category"} + ) + in + Table.ChangeType(expandedReason, TableSchema) +in + [TransformDowntimes = TransformDowntimes] diff --git a/enlyze.pq b/enlyze.pq new file mode 100644 index 0000000..9e55b96 --- /dev/null +++ b/enlyze.pq @@ -0,0 +1,136 @@ +[Version = "1.0.0"] +section enlyze; + +loadModule = (fileName as text) => + let + binary = Extension.Contents(fileName), asText = Text.FromBinary(binary) + in + try + Expression.Evaluate(asText, #shared) catch (e) => + error + [ + Reason = "loadModule Failure", + Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'", + Message.Parameters = {fileName, e[Reason], e[Message]}, + Detail = [File = fileName, Error = e] + ]; + +Table.ToNavigationTable = loadModule("Table.ToNavigationTable.pqm"); + +FetchPaginated = loadModule("ApiClient.pqm")[FetchPaginated]; +PaginatedPostRequest = loadModule("ApiClient.pqm")[PaginatedPostRequest]; + +TransformProductivityMetrics = loadModule("ProductivityMetrics.Transform.pqm")[TransformProductivityMetrics]; +TransformProductionRuns = loadModule("ProductionRuns.Transform.pqm")[TransformProductionRuns]; +TransformSites = loadModule("Sites.Transform.pqm")[TransformSites]; +TransformMachines = loadModule("Machines.Transform.pqm")[TransformMachines]; +TransformProducts = loadModule("Products.Transform.pqm")[TransformProducts]; +TransformDowntimes = loadModule("Downtimes.Transform.pqm")[TransformDowntimes]; +TransformTimeseriesData = loadModule("TimeseriesData.Transform.pqm")[TransformTimeseriesData]; +TransformVariables = loadModule("Variables.Transform.pqm")[TransformVariables]; + +MachineProductivityMetrics = loadModule("MachineProductivityMetrics.pqm"); +TimeseriesData = loadModule("TimeseriesData.pqm"); + +[DataSource.Kind = "enlyze", Publish = "enlyze.Publish"] +shared enlyze.Contents = () => + let + NavTable = Table.ToNavigationTable( + #table( + {"Name", "Key", "Data", "ItemKind", "ItemName", "IsLeaf"}, + { + { + "Downtimes", + "downtimes", + TransformDowntimes(FetchPaginated("/downtimes", null)), + "Table", + "Table", + true + }, + { + "Production Runs", + "productionRuns", + TransformProductionRuns(FetchPaginated("/production-runs", null)), + "Table", + "Table", + true + }, + { + "Machines", + "machines", + TransformMachines(FetchPaginated("/machines", null)), + "Table", + "Table", + true + }, + {"Sites", "sites", TransformSites(FetchPaginated("/sites", null)), "Table", "Table", true}, + { + "Products", + "products", + TransformProducts(FetchPaginated("/products", null)), + "Table", + "Table", + true + }, + { + "Machine Productivity Metrics", + "machineProductivityMetrics", + MachineProductivityMetrics, + "Function", + "Function", + true + }, + { + "Variables", + "variables", + TransformVariables(FetchPaginated("/variables", null)), + "Table", + "Table", + true + }, + {"Timeseries", "Timeseries", TimeseriesData, "Function", "Function", true} + } + ), + {"Key"}, + "Name", + "Data", + "ItemKind", + "ItemName", + "IsLeaf" + ) + in + NavTable; + +enlyze = [ + Authentication = [ + Key = [ + Label = "ENLYZE API Key", + KeyLabel = "ENLYZE API Key" + ] + ], + Label = "ENLYZE" +]; + +enlyze.Publish = [ + Beta = true, + Category = "Other", + ButtonText = {Extension.LoadString("ButtonTitle"), Extension.LoadString("ButtonHelp")}, + LearnMoreUrl = "https://docs.enlyze.com/platform/power-bi", + SourceImage = enlyze.Icons, + SourceTypeImage = enlyze.Icons +]; + +enlyze.Icons = [ + Icon16 = { + Extension.Contents("ENLYZE16.png"), + Extension.Contents("ENLYZE20.png"), + Extension.Contents("ENLYZE24.png"), + Extension.Contents("ENLYZE32.png") + }, + Icon32 = { + Extension.Contents("ENLYZE32.png"), + Extension.Contents("ENLYZE40.png"), + Extension.Contents("ENLYZE48.png"), + Extension.Contents("ENLYZE64.png") + } +]; diff --git a/enlyze.pq.proj b/enlyze.pq.proj new file mode 100644 index 0000000..9560ef7 --- /dev/null +++ b/enlyze.pq.proj @@ -0,0 +1,35 @@ + + + + Debug + + + $(MSBuildProjectDirectory)\bin\AnyCPU\Debug\ + $(MSBuildProjectDirectory)\obj\ + $(IntermediateOutputPath)MEZ\ + $(OutputPath)enlyze-powerbi.mez + + + $(MSBuildProjectDirectory)\bin\AnyCPU\Release\ + $(MSBuildProjectDirectory)\obj\Release\ + $(IntermediateOutputPath)MEZ\ + $(OutputPath)enlyze-powerbi.mez + + + + + + + + + + + + + + + + + + + diff --git a/enlyze.query.pq b/enlyze.query.pq new file mode 100644 index 0000000..f4d4336 --- /dev/null +++ b/enlyze.query.pq @@ -0,0 +1,17 @@ +let + navTable = enlyze.Contents(), + machines = navTable{[Key="machines"]}[Data], + downtimes = navTable{[Key="downtimes"]}[Data], + + start = #datetimezone(2024, 1, 1, 0, 0, 0, 0, 0), + end = #datetimezone(2024, 1, 7, 0, 0, 0, 0, 0), + resolution = "daily", + + productivityMetrics = navTable{[Key="machineProductivityMetrics"]}[Data]( + machines, + start, + end, + resolution + ) +in + downtimes diff --git a/helpers/Table.ChangeType.pqm b/helpers/Table.ChangeType.pqm new file mode 100644 index 0000000..a4cca1c --- /dev/null +++ b/helpers/Table.ChangeType.pqm @@ -0,0 +1,195 @@ +let + // table should be an actual Table.Type, or a List.Type of Records + Table.ChangeType = (table, tableType as type) as nullable table => + // we only operate on table types + if (not Type.Is(tableType, type table)) then + error "type argument should be a table type" + else + // if we have a null value, just return it + if (table = null) then + table + else + let + columnsForType = Type.RecordFields(Type.TableRow(tableType)), + columnsAsTable = Record.ToTable(columnsForType), + schema = Table.ExpandRecordColumn(columnsAsTable, "Value", {"Type"}, {"Type"}), + previousMeta = Value.Metadata(tableType), + // make sure we have a table + parameterType = Value.Type(table), + _table = + if (Type.Is(parameterType, type table)) then + table + else if (Type.Is(parameterType, type list)) then + let + asTable = Table.FromList(table, Splitter.SplitByNothing(), {"Column1"}), + firstValueType = Value.Type(Table.FirstValue(asTable, null)), + result = + // if the member is a record (as expected), then expand it. + if (Type.Is(firstValueType, type record)) then + Table.ExpandRecordColumn(asTable, "Column1", schema[Name]) + else + error + Error.Record( + "Error.Parameter", + "table argument is a list, but not a list of records", + [ + ValueType = firstValueType + ] + ) + in + if (List.IsEmpty(table)) then + #table({"a"}, {}) + else + result + else + error + Error.Record( + "Error.Parameter", + "table argument should be a table or list of records", + [ + ValueType = parameterType + ] + ), + reordered = Table.SelectColumns(_table, schema[Name], MissingField.UseNull), + // process primitive values - this will call Table.TransformColumnTypes + map = (t) => + if Type.Is(t, type table) or Type.Is(t, type list) or Type.Is(t, type record) or t = type any + then + null + else + t, + mapped = Table.TransformColumns(schema, {"Type", map}), + omitted = Table.SelectRows(mapped, each [Type] <> null), + existingColumns = Table.ColumnNames(reordered), + removeMissing = Table.SelectRows(omitted, each List.Contains(existingColumns, [Name])), + primitiveTransforms = Table.ToRows(removeMissing), + changedPrimitives = Table.TransformColumnTypes(reordered, primitiveTransforms), + // Get the list of transforms we'll use for Record types + recordColumns = Table.SelectRows(schema, each Type.Is([Type], type record)), + recordTypeTransformations = Table.AddColumn( + recordColumns, "RecordTransformations", each (r) => Record.ChangeType(r, [Type]), type function + ), + recordChanges = Table.ToRows( + Table.SelectColumns(recordTypeTransformations, {"Name", "RecordTransformations"}) + ), + // Get the list of transforms we'll use for List types + listColumns = Table.SelectRows(schema, each Type.Is([Type], type list)), + listTransforms = Table.AddColumn( + listColumns, "ListTransformations", each (t) => List.ChangeType(t, [Type]), Function.Type + ), + listChanges = Table.ToRows(Table.SelectColumns(listTransforms, {"Name", "ListTransformations"})), + // Get the list of transforms we'll use for Table types + tableColumns = Table.SelectRows(schema, each Type.Is([Type], type table)), + tableTransforms = Table.AddColumn( + tableColumns, "TableTransformations", each (t) => @Table.ChangeType(t, [Type]), Function.Type + ), + tableChanges = Table.ToRows(Table.SelectColumns(tableTransforms, {"Name", "TableTransformations"})), + // Perform all of our transformations + allColumnTransforms = recordChanges & listChanges & tableChanges, + changedRecordTypes = + if (List.IsEmpty(allColumnTransforms)) then + changedPrimitives + else + Table.TransformColumns(changedPrimitives, allColumnTransforms, null, MissingField.Ignore), + // set final type + withType = Value.ReplaceType(changedRecordTypes, tableType) + in + if (List.IsEmpty(Record.FieldNames(columnsForType))) then + table + else + withType meta previousMeta, + // If given a generic record type (no predefined fields), the original record is returned + Record.ChangeType = (record as record, recordType as type) => + let + // record field format is [ fieldName = [ Type = type, Optional = logical], ... ] + fields = + try + Type.RecordFields(recordType) + otherwise + error "Record.ChangeType: failed to get record fields. Is this a record type?", + fieldNames = Record.FieldNames(fields), + fieldTable = Record.ToTable(fields), + optionalFields = Table.SelectRows(fieldTable, each[Value][Optional])[Name], + requiredFields = List.Difference(fieldNames, optionalFields), + // make sure all required fields exist + withRequired = Record.SelectFields(record, requiredFields, MissingField.UseNull), + // append optional fields + withOptional = withRequired & Record.SelectFields(record, optionalFields, MissingField.Ignore), + // set types + transforms = GetTransformsForType(recordType), + withTypes = Record.TransformFields(withOptional, transforms, MissingField.Ignore), + // order the same as the record type + reorder = Record.ReorderFields(withTypes, fieldNames, MissingField.Ignore) + in + if (List.IsEmpty(fieldNames)) then + record + else + reorder, + List.ChangeType = (list as list, listType as type) => + if (not Type.Is(listType, type list)) then + error "type argument should be a list type" + else + let + listItemType = Type.ListItem(listType), + transform = GetTransformByType(listItemType), + modifiedValues = List.Transform(list, transform), + typed = Value.ReplaceType(modifiedValues, listType) + in + typed, + // Returns a table type for the provided schema table + Schema.ToTableType = (schema as table) as type => + let + toList = List.Transform(schema[Type], (t) => [Type = t, Optional = false]), + toRecord = Record.FromList(toList, schema[Name]), + toType = Type.ForRecord(toRecord, false), + previousMeta = Value.Metadata(schema) + in + type table (toType) meta previousMeta, + // Returns a list of transformations that can be passed to Table.TransformColumns, or Record.TransformFields + // Format: {"Column", (f) => ...) .... ex: {"A", Number.From} + GetTransformsForType = (_type as type) as list => + let + fieldsOrColumns = + if (Type.Is(_type, type record)) then + Type.RecordFields(_type) + else if (Type.Is(_type, type table)) then + Type.RecordFields(Type.TableRow(_type)) + else + error "GetTransformsForType: record or table type expected", + toTable = Record.ToTable(fieldsOrColumns), + transformColumn = Table.AddColumn( + toTable, "Transform", each GetTransformByType([Value][Type]), Function.Type + ), + transformMap = Table.ToRows(Table.SelectColumns(transformColumn, {"Name", "Transform"})) + in + transformMap, + GetTransformByType = (type_in as type) as function => + let + _type = Type.NonNullable(type_in) + in + if (Type.Is(_type, type number)) then + Number.From + else if (Type.Is(_type, type text)) then + Text.From + else if (Type.Is(_type, type date)) then + Date.From + else if (Type.Is(_type, type datetime)) then + DateTime.From + else if (Type.Is(_type, type duration)) then + Duration.From + else if (Type.Is(_type, type datetimezone)) then + DateTimeZone.From + else if (Type.Is(_type, type logical)) then + Logical.From + else if (Type.Is(_type, type time)) then + Time.From + else if (Type.Is(_type, type record)) then + (t) => if (t <> null) then @Record.ChangeType(t, _type) else t + else if (Type.Is(_type, type table)) then + (t) => if (t <> null) then @Table.ChangeType(t, _type) else t + else if (Type.Is(_type, type list)) then + (t) => if (t <> null) then @List.ChangeType(t, _type) else t + else + (t) => t +in + Table.ChangeType diff --git a/helpers/Table.ToNavigationTable.pqm b/helpers/Table.ToNavigationTable.pqm new file mode 100644 index 0000000..fa78f5e --- /dev/null +++ b/helpers/Table.ToNavigationTable.pqm @@ -0,0 +1,21 @@ +( + 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 diff --git a/machines/Machines.TableSchema.pqm b/machines/Machines.TableSchema.pqm new file mode 100644 index 0000000..f1f16e9 --- /dev/null +++ b/machines/Machines.TableSchema.pqm @@ -0,0 +1,6 @@ +let + MachinesTableSchema = type table [ + name = text, uuid = text, site = text, genesis_date = datetimezone, #"productivity_metrics" = any + ] +in + MachinesTableSchema diff --git a/machines/Machines.Transform.pqm b/machines/Machines.Transform.pqm new file mode 100644 index 0000000..fc2235f --- /dev/null +++ b/machines/Machines.Transform.pqm @@ -0,0 +1,40 @@ +let + loadModule = (fileName as text) => + let + binary = Extension.Contents(fileName), asText = Text.FromBinary(binary) + in + try + Expression.Evaluate(asText, #shared) catch (e) => + error + [ + Reason = "Extension.LoadModule Failure", + Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'", + Message.Parameters = {fileName, e[Reason], e[Message]}, + Detail = [File = fileName, Error = e] + ], + MachineProductivityMetrics = loadModule("MachineProductivityMetrics.pqm"), + FunctionTypeSingleMachine = loadModule( + "MachineProductivityMetrics.FunctionTypes.pqm" + )[MachineProductivityMetricsSingleMachineType], + TableSchema = loadModule("Machines.TableSchema.pqm"), + Table.ChangeType = loadModule("Table.ChangeType.pqm"), + TransformMachines = (machines as list) as table => + let + machinesTable = Table.FromList(machines, Splitter.SplitByNothing(), null, null, ExtraValues.Error), + namedTable = Value.ReplaceMetadata(machinesTable, Value.Metadata(machinesTable) & [Name = "Machines"]), + expandedTable = Table.ExpandRecordColumn(namedTable, "Column1", {"name", "uuid", "site", "genesis_date"}), + columnNames = Table.ColumnNames(expandedTable), + machinesWithMetrics = Table.AddColumn( + expandedTable, + "productivity_metrics", + (row) => + let + func = (start as datetimezone, end as datetimezone, resolution as text) => + MachineProductivityMetrics(Table.FromRecords({row}), start, end, resolution) + in + Value.ReplaceType(func, FunctionTypeSingleMachine) + ) + in + Table.ChangeType(machinesWithMetrics, TableSchema) +in + [TransformMachines = TransformMachines] diff --git a/productionRuns/ProductionRuns.TableSchema.pqm b/productionRuns/ProductionRuns.TableSchema.pqm new file mode 100644 index 0000000..c884d45 --- /dev/null +++ b/productionRuns/ProductionRuns.TableSchema.pqm @@ -0,0 +1,33 @@ +let + ProductionRunsTableSchema = type table [ + uuid = text, + machine = text, + production_order = text, + product = text, + start = datetimezone, + end = nullable datetimezone, + average_throughput = nullable number, + availability_score = nullable number, + availability_time_loss = nullable number, + performance_score = nullable number, + performance_time_loss = nullable number, + quality_score = nullable number, + quality_time_loss = nullable number, + productivity_score = nullable number, + productivity_time_loss = nullable number, + quantity_scrap_value = nullable number, + quantity_scrap_unit = nullable text, + quantity_yield_value = nullable number, + quantity_yield_unit = nullable text, + quantity_total_value = nullable number, + quantity_total_unit = nullable text, + data_coverage = nullable number, + overlap_percentage = nullable number, + overlap_time = nullable number, + max_run_speed_value = nullable number, + max_run_speed_unit = nullable text, + max_run_speed_start = nullable datetimezone, + max_run_speed_end = nullable datetimezone + ] +in + ProductionRunsTableSchema diff --git a/productionRuns/ProductionRuns.Transform.pqm b/productionRuns/ProductionRuns.Transform.pqm new file mode 100644 index 0000000..279de03 --- /dev/null +++ b/productionRuns/ProductionRuns.Transform.pqm @@ -0,0 +1,73 @@ +let + loadModule = (fileName as text) => + let + binary = Extension.Contents(fileName), asText = Text.FromBinary(binary) + in + try + Expression.Evaluate(asText, #shared) catch (e) => + error + [ + Reason = "Extension.LoadModule Failure", + Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'", + Message.Parameters = {fileName, e[Reason], e[Message]}, + Detail = [File = fileName, Error = e] + ], + TableSchema = loadModule("ProductionRuns.TableSchema.pqm"), + Table.ChangeType = loadModule("Table.ChangeType.pqm"), + TransformProductivity = loadModule("ProductivityMetrics.Transform.pqm")[TransformProductivity], + TransformAvailability = loadModule("ProductivityMetrics.Transform.pqm")[TransformAvailability], + TransformPerformance = loadModule("ProductivityMetrics.Transform.pqm")[TransformPerformance], + TransformQuality = loadModule("ProductivityMetrics.Transform.pqm")[TransformQuality], + TransformQuantityScrap = loadModule("Quantities.Transform.pqm")[TransformQuantityScrap], + TransformQuantityYield = loadModule("Quantities.Transform.pqm")[TransformQuantityYield], + TransformQuantityTotal = loadModule("Quantities.Transform.pqm")[TransformQuantityTotal], + TransformProductionRuns = (productionRuns as list) as table => + let + productionRunsTable = Table.FromList( + productionRuns, Splitter.SplitByNothing(), null, null, ExtraValues.Error + ), + namedTable = Value.ReplaceMetadata( + productionRunsTable, Value.Metadata(productionRunsTable) & [Name = "Production Runs"] + ), + expandedTable = Table.ExpandRecordColumn( + namedTable, + "Column1", + { + "uuid", + "machine", + "production_order", + "product", + "start", + "end", + "average_throughput", + "quantity_total", + "quantity_scrap", + "quantity_yield", + "availability", + "performance", + "quality", + "productivity", + "maximum_run_speed", + "data_quality" + } + ), + expandedAvailability = TransformAvailability(expandedTable), + expandedPerformance = TransformPerformance(expandedAvailability), + expandedQuality = TransformQuality(expandedPerformance), + expandedProductivity = TransformProductivity(expandedQuality), + expandedQuantityScrap = TransformQuantityScrap(expandedProductivity), + expandedQuantityYield = TransformQuantityYield(expandedQuantityScrap), + expandedQuantityTotal = TransformQuantityTotal(expandedQuantityYield), + expandedDataQuality = Table.ExpandRecordColumn( + expandedQuantityTotal, "data_quality", {"data_coverage", "overlap_percentage", "overlap_time"} + ), + expandedMaxRunSpeed = Table.ExpandRecordColumn( + expandedDataQuality, + "maximum_run_speed", + {"value", "observation_period_start", "observation_period_end", "unit"}, + {"max_run_speed_value", "max_run_speed_start", "max_run_speed_end", "max_run_speed_unit"} + ) + in + Table.ChangeType(expandedMaxRunSpeed, TableSchema) +in + [TransformProductionRuns = TransformProductionRuns] diff --git a/productivityMetrics/DateTimeRanges.pqm b/productivityMetrics/DateTimeRanges.pqm new file mode 100644 index 0000000..f1b768a --- /dev/null +++ b/productivityMetrics/DateTimeRanges.pqm @@ -0,0 +1,136 @@ +let + ToIso8601 = (dt as datetimezone) as text => DateTimeZone.ToText(dt, [Format = "O", Culture = "en-US"]), + ToHourlyDateTimeRanges = (start as datetimezone, end as datetimezone, resolution as text) as list => + let + roundToHour = (dt as datetimezone) => + #datetimezone( + Date.Year(dt), + Date.Month(dt), + Date.Day(dt), + Time.Hour(dt), + 0, + 0, + DateTimeZone.ZoneHours(dt), + DateTimeZone.ZoneMinutes(dt) + ), + durationOneHour = #duration(0, 1, 0, 0), + addOneHour = (dt as datetimezone) => DateTimeZone.From(dt + durationOneHour), + roundedStart = roundToHour(start), + hourStarts = List.Generate(() => roundedStart, each _ <= end, each addOneHour(_)), + ranges = List.Transform( + hourStarts, (hourStart) => [ + start = ToIso8601(hourStart), + end = ToIso8601(addOneHour(hourStart)) + ] + ) + in + ranges, + ToDailyDateTimeRanges = (start as datetimezone, end as datetimezone) as list => + let + roundToDay = (dt as datetimezone) => + #datetimezone( + Date.Year(dt), + Date.Month(dt), + Date.Day(dt), + 0, + 0, + 0, + DateTimeZone.ZoneHours(dt), + DateTimeZone.ZoneMinutes(dt) + ), + addDay = (dt as datetimezone) => DateTimeZone.From(Date.AddDays(dt, 1)), + dayStarts = List.Generate(() => roundToDay(start), each Date.From(_) <= Date.From(end), each addDay(_)), + ranges = List.Transform( + dayStarts, (dayStart) => [ + start = ToIso8601(dayStart), + end = ToIso8601(addDay(dayStart)) + ] + ) + in + ranges, + ToWeeklyDateTimeRanges = (start as datetimezone, end as datetimezone) as list => + let + roundToWeekStart = (dt as datetimezone) => + let + dayOfWeek = Date.DayOfWeek(Date.From(dt), Day.Monday) + 1, + mondayOfWeek = Date.AddDays(Date.From(dt), - (dayOfWeek - 1)) + in + #datetimezone( + Date.Year(mondayOfWeek), + Date.Month(mondayOfWeek), + Date.Day(mondayOfWeek), + 0, + 0, + 0, + DateTimeZone.ZoneHours(dt), + DateTimeZone.ZoneMinutes(dt) + ), + endOfWeek = (dt as datetimezone) => + let + nextMonday = roundToWeekStart(dt) + #duration(7, 0, 0, 0) + in + #datetimezone( + Date.Year(nextMonday), + Date.Month(nextMonday), + Date.Day(nextMonday), + 0, + 0, + 0, + DateTimeZone.ZoneHours(dt), + DateTimeZone.ZoneMinutes(dt) + ), + weekStarts = List.Generate( + () => roundToWeekStart(start), each _ <= end, each DateTimeZone.From(Date.AddDays(_, 7)) + ), + ranges = List.Transform( + weekStarts, (weekStart) => [ + start = ToIso8601(weekStart), + end = ToIso8601(endOfWeek(weekStart)) + ] + ) + in + ranges, + ToMonthlyDateTimeRanges = (start as datetimezone, end as datetimezone) as list => + let + roundToMonthStart = (dt as datetimezone) => + #datetimezone( + Date.Year(dt), Date.Month(dt), 1, 0, 0, 0, DateTimeZone.ZoneHours(dt), DateTimeZone.ZoneMinutes( + dt + ) + ), + startOfNextMonth = (dt as datetimezone) => + let + nextMonth = Date.AddMonths(Date.From(dt), 1) + in + #datetimezone( + Date.Year(nextMonth), + Date.Month(nextMonth), + 1, + 0, + 0, + 0, + DateTimeZone.ZoneHours(dt), + DateTimeZone.ZoneMinutes(dt) + ), + monthStarts = List.Generate( + () => roundToMonthStart(start), each _ <= end, each DateTimeZone.From(Date.AddMonths(_, 1)) + ), + ranges = List.Transform( + monthStarts, + (monthStart) => + [ + start = ToIso8601(monthStart), + end = ToIso8601( + if end <= startOfNextMonth(monthStart) then end else startOfNextMonth(monthStart) + ) + ] + ) + in + ranges +in + [ + ToHourlyDateTimeRanges = ToHourlyDateTimeRanges, + ToDailyDateTimeRanges = ToDailyDateTimeRanges, + ToWeeklyDateTimeRanges = ToWeeklyDateTimeRanges, + ToMonthlyDateTimeRanges = ToMonthlyDateTimeRanges + ] diff --git a/productivityMetrics/MachineProductivityMetrics.FunctionTypes.pqm b/productivityMetrics/MachineProductivityMetrics.FunctionTypes.pqm new file mode 100644 index 0000000..37a244f --- /dev/null +++ b/productivityMetrics/MachineProductivityMetrics.FunctionTypes.pqm @@ -0,0 +1,28 @@ +let + MachineProductivityMetricsSingleMachineType = type function ( + start as datetimezone, + end as datetimezone, + resolution as ( + type text meta [ + Documentation.Label = "Resolution", + Documentation.Description = "Select a resolution.", + Documentation.AllowedValues = {"hourly", "daily", "weekly", "monthly"} + ] + ) + ) as table, + MachineProductivityMetricsType = type function ( + machines as table, + start as datetimezone, + end as datetimezone, + resolution as ( + type text meta [ + Documentation.Description = "Select a resolution.", + Documentation.AllowedValues = {"hourly", "daily", "weekly", "monthly"} + ] + ) + ) as table +in + [ + MachineProductivityMetricsSingleMachineType = MachineProductivityMetricsSingleMachineType, + MachineProductivityMetricsType = MachineProductivityMetricsType + ] diff --git a/productivityMetrics/MachineProductivityMetrics.TableSchema.pqm b/productivityMetrics/MachineProductivityMetrics.TableSchema.pqm new file mode 100644 index 0000000..6adc543 --- /dev/null +++ b/productivityMetrics/MachineProductivityMetrics.TableSchema.pqm @@ -0,0 +1,20 @@ +let + productivityMetricsTableSchema = type table [ + start = datetimezone, + end = datetimezone, + machine = text, + availability_score = nullable number, + availability_time_loss = nullable number, + performance_score = nullable number, + performance_time_loss = nullable number, + quality_score = nullable number, + quality_time_loss = nullable number, + productivity_score = nullable number, + productivity_time_loss = nullable number, + quantity_scrap_value = nullable number, + quantity_scrap_unit = nullable text, + quantity_yield_value = nullable number, + quantity_yield_unit = nullable text + ] +in + productivityMetricsTableSchema diff --git a/productivityMetrics/MachineProductivityMetrics.pqm b/productivityMetrics/MachineProductivityMetrics.pqm new file mode 100644 index 0000000..a618bc1 --- /dev/null +++ b/productivityMetrics/MachineProductivityMetrics.pqm @@ -0,0 +1,53 @@ +let + loadModule = (fileName as text) => + let + binary = Extension.Contents(fileName), asText = Text.FromBinary(binary) + in + try + Expression.Evaluate(asText, #shared) catch (e) => + error + [ + Reason = "Extension.LoadModule Failure", + Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'", + Message.Parameters = {fileName, e[Reason], e[Message]}, + Detail = [File = fileName, Error = e] + ], + TableSchema = loadModule("MachineProductivityMetrics.TableSchema.pqm"), + DateTimeRanges = loadModule("DateTimeRanges.pqm"), + PaginatedPostRequest = loadModule("ApiClient.pqm")[PaginatedPostRequest], + TransformProductivityMetrics = loadModule("ProductivityMetrics.Transform.pqm")[TransformProductivityMetrics], + FunctionType = loadModule("MachineProductivityMetrics.FunctionTypes.pqm")[MachineProductivityMetricsType], + MachineProductivityMetrics = (machines as table, start as datetimezone, end as datetimezone, resolution as text) as table => + let + machineUuids = Table.Column(machines, "uuid"), + dateTimeRanges = + if resolution = "hourly" then + DateTimeRanges[ToHourlyDateTimeRanges](start, end, resolution) + else if resolution = "daily" then + DateTimeRanges[ToDailyDateTimeRanges](start, end) + else if resolution = "weekly" then + DateTimeRanges[ToWeeklyDateTimeRanges](start, end) + else if resolution = "monthly" then + DateTimeRanges[ToMonthlyDateTimeRanges](start, end) + else + error "Invalid resolution. Please choose 'hourly', 'daily', 'weekly', or 'monthly'.", + _ = Diagnostics.Trace(TraceLevel.Error, "Generated Date Ranges:", Text.From(dateTimeRanges)), + accumulated = List.Accumulate( + machineUuids, + #table(TableSchema, {}), + (state, machineUuid) => + let + responseData = PaginatedPostRequest( + "/machines/" & machineUuid & "/productivity-metrics", [datetime_ranges = dateTimeRanges] + ), + responseDataWithMachine = List.Transform( + responseData, (r) => Record.AddField(r, "machine", machineUuid) + ) + in + Table.Combine({state, TransformProductivityMetrics(responseDataWithMachine)}) + ) + in + accumulated, + MachineProductivityMetricsCorrectType = Value.ReplaceType(MachineProductivityMetrics, FunctionType) +in + MachineProductivityMetricsCorrectType diff --git a/productivityMetrics/ProductivityMetrics.Transform.pqm b/productivityMetrics/ProductivityMetrics.Transform.pqm new file mode 100644 index 0000000..31759bc --- /dev/null +++ b/productivityMetrics/ProductivityMetrics.Transform.pqm @@ -0,0 +1,65 @@ +let + loadModule = (fileName as text) => + let + binary = Extension.Contents(fileName), asText = Text.FromBinary(binary) + in + try + Expression.Evaluate(asText, #shared) catch (e) => + error + [ + Reason = "Extension.LoadModule Failure", + Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'", + Message.Parameters = {fileName, e[Reason], e[Message]}, + Detail = [File = fileName, Error = e] + ], + TableSchema = loadModule("MachineProductivityMetrics.TableSchema.pqm"), + Table.ChangeType = loadModule("Table.ChangeType.pqm"), + TransformQuantityScrap = loadModule("Quantities.Transform.pqm")[TransformQuantityScrap], + TransformQuantityYield = loadModule("Quantities.Transform.pqm")[TransformQuantityYield], + TransformMetric = (tbl as table, fieldName as text) as table => + Table.ExpandRecordColumn( + tbl, fieldName, {"score", "time_loss"}, {fieldName & "_score", fieldName & "_time_loss"} + ), + TransformAvailability = (tbl as table) as table => TransformMetric(tbl, "availability"), + TransformPerformance = (tbl as table) as table => TransformMetric(tbl, "performance"), + TransformQuality = (tbl as table) as table => TransformMetric(tbl, "quality"), + TransformProductivity = (tbl as table) as table => TransformMetric(tbl, "productivity"), + TransformProductivityMetrics = (productivityMetrics as list) as table => + let + productivityMetricsTable = Table.FromList( + productivityMetrics, Splitter.SplitByNothing(), null, null, ExtraValues.Error + ), + namedTable = Value.ReplaceMetadata( + productivityMetricsTable, Value.Metadata(productivityMetricsTable) & [Name = "Productivity Metrics"] + ), + expandedTable = Table.ExpandRecordColumn( + namedTable, + "Column1", + { + "start", + "end", + "machine", + "availability", + "performance", + "quality", + "productivity", + "quantity_scrap", + "quantity_yield" + } + ), + expandedAvailability = TransformAvailability(expandedTable), + expandedPerformance = TransformPerformance(expandedAvailability), + expandedQuality = TransformQuality(expandedPerformance), + expandedProductivity = TransformProductivity(expandedQuality), + expandedQuantityScrap = TransformQuantityScrap(expandedProductivity), + expandedQuantityYield = TransformQuantityYield(expandedQuantityScrap) + in + Table.ChangeType(expandedQuantityYield, TableSchema) +in + [ + TransformProductivityMetrics = TransformProductivityMetrics, + TransformAvailability = TransformAvailability, + TransformPerformance = TransformPerformance, + TransformProductivity = TransformProductivity, + TransformQuality = TransformQuality + ] diff --git a/products/Products.TableSchema.pqm b/products/Products.TableSchema.pqm new file mode 100644 index 0000000..1c8259d --- /dev/null +++ b/products/Products.TableSchema.pqm @@ -0,0 +1 @@ +let ProductsTableSchema = type table [uuid = text, external_id = text, name = text] in ProductsTableSchema diff --git a/products/Products.Transform.pqm b/products/Products.Transform.pqm new file mode 100644 index 0000000..5d06770 --- /dev/null +++ b/products/Products.Transform.pqm @@ -0,0 +1,25 @@ +let + loadModule = (fileName as text) => + let + binary = Extension.Contents(fileName), asText = Text.FromBinary(binary) + in + try + Expression.Evaluate(asText, #shared) catch (e) => + error + [ + Reason = "Extension.LoadModule Failure", + Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'", + Message.Parameters = {fileName, e[Reason], e[Message]}, + Detail = [File = fileName, Error = e] + ], + TableSchema = loadModule("Products.TableSchema.pqm"), + Table.ChangeType = loadModule("Table.ChangeType.pqm"), + TransformProducts = (products as list) as table => + let + productsTable = Table.FromList(products, Splitter.SplitByNothing(), null, null, ExtraValues.Error), + namedTable = Value.ReplaceMetadata(productsTable, Value.Metadata(productsTable) & [Name = "Products"]), + expandedTable = Table.ExpandRecordColumn(namedTable, "Column1", {"uuid", "external_id", "name"}) + in + Table.ChangeType(expandedTable, TableSchema) +in + [TransformProducts = TransformProducts] diff --git a/push-extension.ps1 b/push-extension.ps1 new file mode 100644 index 0000000..7db0860 --- /dev/null +++ b/push-extension.ps1 @@ -0,0 +1,68 @@ +# PowerShell script to deploy custom connector using VS Code's Power Query SDK + +# Configuration +$projectPath = $PSScriptRoot # Assumes the script is in the project directory +$mezFileName = "enlyze-powerbi.mez" # Replace with your .mez file name +$customConnectorsPath = "C:\Mac\Home\Documents\Microsoft Power BI Desktop\Custom Connectors" # Default Power BI custom connectors path +$projectXmlPath = "enlyze.pq.proj" + +function Find-PowerBIDesktop { + # If not found in predefined paths, search in common directories + $commonDirs = @("C:\Program Files", "C:\Program Files\WindowsApps", "C:\Program Files (x86)", "$env:ProgramFiles", "${env:ProgramFiles(x86)}", "$env:LocalAppData") + foreach ($dir in $commonDirs) { + $found = Get-ChildItem -Path $dir -Recurse -ErrorAction SilentlyContinue | + Where-Object { $_.Name -eq "PBIDesktop.exe" } | + Select-Object -First 1 -ExpandProperty FullName + if ($found) { + return $found + } + } + + return $null +} + + +msbuild $projectXmlPath -t:Clean +msbuild $projectXmlPath + +# Find the .mez file +Write-Host "Searching for the compiled .mez file..." +$mezFile = Get-ChildItem -Path $projectPath -Recurse -Filter $mezFileName | Select-Object -First 1 + +# Check if compilation was successful +if ($null -eq $mezFile) { + Write-Host "Compilation failed or .mez file not found. Please check for errors in VS Code and try again." + exit 1 +} + +Write-Host ".mez file found at: $($mezFile.FullName)" + +# Copy .mez file to custom connectors directory +Write-Host "Copying .mez file to custom connectors directory..." +Copy-Item $mezFile.FullName -Destination $customConnectorsPath -Force + +# Check if copy was successful +if ($?) { + Write-Host ".mez file successfully copied to custom connectors directory." +} else { + Write-Host "Failed to copy .mez file. Please check permissions and try again." + exit 1 +} + + +Write-Host "Closing Power BI..." +Get-Process "PBIDesktop" -ErrorAction SilentlyContinue | Stop-Process -Force + +Write-Host "Starting Power BI..." +$pbiPath = Find-PowerBIDesktop +if ($null -eq $pbiPath) { + Write-Host "Power BI Desktop executable not found. Please start Power BI Desktop manually." +} else { + Write-Host "Closing Power BI..." + Get-Process "PBIDesktop" -ErrorAction SilentlyContinue | Stop-Process -Force + + Write-Host "Starting Power BI..." + Start-Process $pbiPath +} + +Write-Host "Deployment complete!" \ No newline at end of file diff --git a/resources.resx b/resources.resx new file mode 100644 index 0000000..254ea9c --- /dev/null +++ b/resources.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Connect to ENLYZE + + + ENLYZE + + + ENLYZE + + diff --git a/resources/ENLYZE16.png b/resources/ENLYZE16.png new file mode 100644 index 0000000000000000000000000000000000000000..e971cc334161a444ec516265c7291da34ddd756c GIT binary patch literal 740 zcmV&8hU(av6&c4$xamL33xJ3a!$&c-(9vrkCEn%1B=v5;T97Uea@!gLx!xASyphr(6_a~U8QF9i3^a@3xIv1?}@nSBFv~; zFVj7f{0lOJDzri588Ew2-0PlvioC>Rb0#i;I`4MhKbJ@a54eRbzTERyiMUJzS$S4h zsC&UQUx>m0xm3tmGw;2bxPYN>BNlYHsy=(I$fKakC_9vMZVT$3vC^O2!{Oh7gDB6= z)}aGsA7b6vlL`)P&Q9$3D$Pt|?AOOg9jR1xjg$=HocJkbE*78_`Jg_0XF(%YZ@zzO zxA^G|5a?_gQ;v+0ig@&fuz@=?k`E4ng~wY4Yee(Vhype^uw5TceSKjjz3uM5-(rzP zM4_*n#nd>i$?iZ8oAc)l9*ZYAL{d?XsWV?5T{-4$s~*5dxe(F7F|!X5B1DJ~ef$Kf W$p-ZQt`mL$0000Khyhg{{2wGp3@t)x*vb?Kf!eKMDIGvfMg$UJ z04gzHCI$uuhHk_XNJV0x0=2ar`yS6YPTY{B?hguszmj#y<>kGvKkx2rfJBKBB}(*P zi@`4IYHms8wLNdpR#aHyfObk>c-mX&6rX}!76&Dd$N(i2z@rIQzU>wto8VY(Ql;Q| z`lvSl%$5PLGFzN3#L1yeE~&PqRGFjbFK3U$`VWVgs+$uu8sX*7BY^IkkCVgkcpfB2 zxtEtAred|YZa^G8cN+SxTquwC4;$e;8LiS?<{-E_O4FBAXN6@LgYv85VH0917C{Qi z`{VJU{)&s359jBK(=&~0~FWWP_)6$woM`_2eNha#&fr|8@b6WY{%eOkig#v zja_$lVA!yL8Eb7Q(v@k=F`|F*TdPW?l^#wYG;?gpAQdiAl52F%m7?=5LcylBsC{fw zB-Zz-=pfych{iJDdDt|iP5Rgf;NU=Own5k{*IS1+FOs`x|odx<}snY zUKIZKVo@h^=&wbMOKS#@S6|#Y*4SVXdZjq@wr#L-X~+PNnVt=K?O9dT=Bw(r6PGL2 z{*%idJ1__>lp^$Y_F3FbSYrinVcxafPh^3)2hYMPKM$$ZA8_XT<}kl7`zG(tyzk%p z-g^KE2?+@a2?=)(gndLo+sd@tkb^e;(@sO^BW})r~(HguJGP`(ebT+&yg{B^wsBE{~tV%eJeZF^4s6g<}Xr_ zkK=`zw>N*K@R{Nwoo(px6^P{l622(Q@dxj(*#H^tICH^v61?X1i{you-rLC2pef+O z7)E$WWCh6jDiwKptn$g%OAeyB!-W>?v*Ipblpe`@{cj29_`b|M9N7X4Nn`~S&#Giy zcd~2DSzw4q&;I#yDcBD|RK5P%t-e>NY^apErYZKnZ|KJ?7?MZ|NNZAfl<3v+)8sqH zpEA1u&5lq);c^fT>ENbRENWUMGw7Q(BZF{&R0OvU;{4$#lAo=dfU7NpptV;7?lFc`qDI6>;x@RtGBJop~^r}XMev` z^y`9N=s>3w>|a>iP)W>^;IQ8KaH1A$JGEWn`>wVB!lw$)kA5W3D`m>oRN0D z;8G0M+l-e5qlr+%D6Az%^}xGD2+ z&-#9vj>^a`G=-mx0LOzb8(gr$d{#P&xKcaI8$5B*+@QTD0D> z5QEWpK(fuNbeeB5fxTXEExJAS$SQ@8DmeeVj}W0vG7clV(A1}=TC?B%c8)G4<=L

9RJ^X|!9I&{e7{PHjHc4!85GKTAq&G4}NgIrbIdFtQrl&sz zr3?O>co9T91!MMl^UDvOwnAgb`tt5b9qY>xQ)iHoU+N31msQ6`4ej(%*rCtcWTS0_ z!&}W<=GqTAxOs!h%O82CSlJH>4C18rZY~Xcm0s0wi+g%qSk@PHNJvOXNJvOXNJvPy alUxVAA*K%vDDnva0000o_r+Kq? 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 literal 0 HcmV?d00001 diff --git a/resources/ENLYZE64.png b/resources/ENLYZE64.png new file mode 100644 index 0000000000000000000000000000000000000000..7660e0ff844dc946a2629a8fd3e83e7de88e7cd5 GIT binary patch literal 3237 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 aPV+C%9x2Q@^}nwIxMyf?P=j>&^M3)tGYL5W literal 0 HcmV?d00001 diff --git a/resources/ENLYZE80.png b/resources/ENLYZE80.png new file mode 100644 index 0000000000000000000000000000000000000000..fb85c3374c4af0a1d1ae53279f15568510ea4fbc GIT binary patch literal 4162 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 JQQaZ#{{U8}#hL&B literal 0 HcmV?d00001 diff --git a/shared/Quantities.Transform.pqm b/shared/Quantities.Transform.pqm new file mode 100644 index 0000000..e6cd36b --- /dev/null +++ b/shared/Quantities.Transform.pqm @@ -0,0 +1,12 @@ +let + TransformQuantity = (tbl as table, fieldName as text) as table => + Table.ExpandRecordColumn(tbl, fieldName, {"unit", "value"}, {fieldName & "_unit", fieldName & "_value"}), + TransformQuantityScrap = (tbl as table) as table => TransformQuantity(tbl, "quantity_scrap"), + TransformQuantityYield = (tbl as table) as table => TransformQuantity(tbl, "quantity_yield"), + TransformQuantityTotal = (tbl as table) as table => TransformQuantity(tbl, "quantity_total") +in + [ + TransformQuantityScrap = TransformQuantityScrap, + TransformQuantityYield = TransformQuantityYield, + TransformQuantityTotal = TransformQuantityTotal + ] diff --git a/sites/Sites.TableSchema.pqm b/sites/Sites.TableSchema.pqm new file mode 100644 index 0000000..5b6f18e --- /dev/null +++ b/sites/Sites.TableSchema.pqm @@ -0,0 +1 @@ +let SitesTableSchema = type table [uuid = text, name = text, address = text] in SitesTableSchema diff --git a/sites/Sites.Transform.pqm b/sites/Sites.Transform.pqm new file mode 100644 index 0000000..3b31da7 --- /dev/null +++ b/sites/Sites.Transform.pqm @@ -0,0 +1,25 @@ +let + loadModule = (fileName as text) => + let + binary = Extension.Contents(fileName), asText = Text.FromBinary(binary) + in + try + Expression.Evaluate(asText, #shared) catch (e) => + error + [ + Reason = "Extension.LoadModule Failure", + Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'", + Message.Parameters = {fileName, e[Reason], e[Message]}, + Detail = [File = fileName, Error = e] + ], + TableSchema = loadModule("Sites.TableSchema.pqm"), + Table.ChangeType = loadModule("Table.ChangeType.pqm"), + TransformSites = (sites as list) as table => + let + sitesTable = Table.FromList(sites, Splitter.SplitByNothing(), null, null, ExtraValues.Error), + namedTable = Value.ReplaceMetadata(sitesTable, Value.Metadata(sitesTable) & [Name = "Sites"]), + expandedTable = Table.ExpandRecordColumn(namedTable, "Column1", {"uuid", "name", "address"}) + in + Table.ChangeType(expandedTable, TableSchema) +in + [TransformSites = TransformSites] diff --git a/timeseriesData/TimeseriesData.FunctionTypes.pqm b/timeseriesData/TimeseriesData.FunctionTypes.pqm new file mode 100644 index 0000000..516edde --- /dev/null +++ b/timeseriesData/TimeseriesData.FunctionTypes.pqm @@ -0,0 +1,43 @@ +let + TimeseriesDataType = type function ( + variables as (type table meta [Documentation.Label = "Variables"]), + start as datetimezone, + end as datetimezone, + resolution as ( + type text meta [ + Documentation.Label = "Resolution", + Documentation.Description = "Select a resolution.", + Documentation.AllowedValues = {"1m", "10m", "30m", "1h", "1d", "1w"} + ] + ), + resampling_method as ( + type text meta [ + Documentation.Label = "Resampling Method", + Documentation.Description = "Select a resampling method.", + Documentation.AllowedValues = {"first", "last", "max", "min", "count", "sum", "avg", "median"} + ] + ) + ) as table, + TimeseriesSingleVariableType = type function ( + start as datetimezone, + end as datetimezone, + resolution as ( + type text meta [ + Documentation.Label = "Resolution", + Documentation.Description = "Select a resolution.", + Documentation.AllowedValues = {"1m", "10m", "30m", "1h", "1d", "1w"} + ] + ), + resampling_method as ( + type text meta [ + Documentation.Label = "Resampling Method", + Documentation.Description = "Select a resampling method.", + Documentation.AllowedValues = {"first", "last", "max", "min", "count", "sum", "avg", "median"} + ] + ) + ) as table +in + [ + TimeseriesDataType = TimeseriesDataType, + TimeseriesSingleVariableType = TimeseriesSingleVariableType + ] diff --git a/timeseriesData/TimeseriesData.Transform.pqm b/timeseriesData/TimeseriesData.Transform.pqm new file mode 100644 index 0000000..65edb6d --- /dev/null +++ b/timeseriesData/TimeseriesData.Transform.pqm @@ -0,0 +1,23 @@ +let + TransformTimeseriesData = (timeseriesDataPages as list) as table => + let + transformedPages = List.Transform( + timeseriesDataPages, + (page) => + let + columns = page[columns], + records = page[records], + transformedRecords = List.Transform(records, each Record.FromList(_, columns)), + pageTable = Table.FromRecords(transformedRecords) + in + Table.TransformColumns( + pageTable, {{"time", each DateTimeZone.FromText(_), type datetimezone}} + ) + ), + combinedTable = Table.Combine(transformedPages) + in + combinedTable +in + [ + TransformTimeseriesData = TransformTimeseriesData + ] diff --git a/timeseriesData/TimeseriesData.pqm b/timeseriesData/TimeseriesData.pqm new file mode 100644 index 0000000..c1c0925 --- /dev/null +++ b/timeseriesData/TimeseriesData.pqm @@ -0,0 +1,76 @@ +let + loadModule = (fileName as text) => + let + binary = Extension.Contents(fileName), asText = Text.FromBinary(binary) + in + try + Expression.Evaluate(asText, #shared) catch (e) => + error + [ + Reason = "Extension.LoadModule Failure", + Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'", + Message.Parameters = {fileName, e[Reason], e[Message]}, + Detail = [File = fileName, Error = e] + ], + PaginatedPostRequest = loadModule("ApiClient.pqm")[PaginatedPostRequest], + TransformTimeseriesData = loadModule("TimeseriesData.Transform.pqm")[TransformTimeseriesData], + TimeseriesDataType = loadModule("TimeseriesData.FunctionTypes.pqm")[TimeseriesDataType], + TimeseriesData = ( + variables as table, start as datetimezone, end as datetimezone, resolution as text, resampling_method as text + ) as table => + let + resampling_interval_seconds = + if resolution = "1m" then + 60 + else if resolution = "10m" then + 600 + else if resolution = "30m " then + 1800 + else if resolution = "1h" then + 3600 + else if resolution = "1d" then + 86400 + else + 604800, + variablesByMachine = Table.Group( + variables, {"machine"}, {{"GroupedVariables", each _, type table [uuid = text, machine = text]}} + ), + bodyBase = [start = start, end = end, resampling_interval = resampling_interval_seconds], + fetchTimeseriesForMachine = (variablesByMachine as table, machineUuid as text) as table => + let + variableCount = Table.RowCount(variablesByMachine), + _ = + if variableCount > 100 then + error + "Error: The number of variables for machine " + & machineUuid + & " exceeds the limit of 100." + else + null, + requestBody = Record.Combine( + { + bodyBase, + [ + machine = machineUuid, + variables = List.Transform( + Table.ToRecords(variablesByMachine), + each [uuid = _[uuid], resampling_method = resampling_method] + ) + ] + } + ), + timeseriesDataPages = PaginatedPostRequest("/timeseries", requestBody, null), + transformedData = TransformTimeseriesData(timeseriesDataPages) + in + transformedData, + resultRecord = Record.FromList( + Table.TransformRows( + variablesByMachine, each fetchTimeseriesForMachine([GroupedVariables], [machine]) + ), + variablesByMachine[machine] + ) + in + Table.FromRecords({resultRecord}), + TimeseriesDataCorrectType = Value.ReplaceType(TimeseriesData, TimeseriesDataType) +in + TimeseriesDataCorrectType diff --git a/variables/Variables.TableSchema.pqm b/variables/Variables.TableSchema.pqm new file mode 100644 index 0000000..0c1d280 --- /dev/null +++ b/variables/Variables.TableSchema.pqm @@ -0,0 +1,13 @@ +let + VariablesTableSchema = type table [ + display_name = nullable text, + #"type" = text, + machine = text, + uuid = text, + unit = nullable text, + data_type = text, + scaling_factor = nullable number, + #"timeseries_data" = any + ] +in + VariablesTableSchema diff --git a/variables/Variables.Transform.pqm b/variables/Variables.Transform.pqm new file mode 100644 index 0000000..02469f5 --- /dev/null +++ b/variables/Variables.Transform.pqm @@ -0,0 +1,53 @@ +let + loadModule = (fileName as text) => + let + binary = Extension.Contents(fileName), asText = Text.FromBinary(binary) + in + try + Expression.Evaluate(asText, #shared) catch (e) => + error + [ + Reason = "Extension.LoadModule Failure", + Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'", + Message.Parameters = {fileName, e[Reason], e[Message]}, + Detail = [File = fileName, Error = e] + ], + TableSchema = loadModule("Variables.TableSchema.pqm"), + Table.ChangeType = loadModule("Table.ChangeType.pqm"), + TimeseriesData = loadModule("TimeseriesData.pqm"), + TimeseriesSingleVariableType = loadModule("TimeseriesData.FunctionTypes.pqm")[TimeseriesSingleVariableType], + TransformVariables = (variables as list) as table => + let + variablesTable = Table.FromList(variables, Splitter.SplitByNothing(), null, null, ExtraValues.Error), + expandedTable = Table.ExpandRecordColumn( + variablesTable, + "Column1", + {"uuid", "display_name", "machine", "unit", "type", "data_type", "scaling_factor"} + ), + expandedDisplayName = Table.TransformColumns( + expandedTable, {{"display_name", each if _ = null then "" else _, type text}} + ), + expandedUnit = Table.TransformColumns( + expandedDisplayName, {{"unit", each if _ = null then "" else _, type text}} + ), + expandedScalingFactor = Table.TransformColumns( + expandedUnit, {{"scaling_factor", each if _ = null then null else _, type number}} + ), + variablesWithTimeseries = Table.AddColumn( + expandedScalingFactor, + "timeseries_data", + (row) => + let + func = ( + start as datetimezone, end as datetimezone, resolution as text, resampling_method as text + ) => + TimeseriesData(Table.FromRecords({row}), start, end, resolution, resampling_method) + in + Value.ReplaceType(func, TimeseriesSingleVariableType) + ) + in + Table.ChangeType(variablesWithTimeseries, TableSchema) +in + [ + TransformVariables = TransformVariables + ]