├── .gitignore
├── Jamf PowerBI Basic Installation Instructions for PowerBI Desktop.pdf
├── JamfPro.mez
├── JamfPro
├── .vscode
│ ├── settings.json
│ └── tasks.json
├── ComputerMappings.pqm
├── JamfPro.pq
├── JamfPro.proj
├── JamfPro.query.pq
├── JamfPro16.png
├── JamfPro20.png
├── JamfPro24.png
├── JamfPro32.png
├── JamfPro40.png
├── JamfPro48.png
├── JamfPro64.png
├── MobileDeviceMappings.pqm
└── resources.resx
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .DS_Store
3 | bin
--------------------------------------------------------------------------------
/Jamf PowerBI Basic Installation Instructions for PowerBI Desktop.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/powerbi/46bf1fd74bb3548e52f61454d5a079c7244f00c8/Jamf PowerBI Basic Installation Instructions for PowerBI Desktop.pdf
--------------------------------------------------------------------------------
/JamfPro.mez:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/powerbi/46bf1fd74bb3548e52f61454d5a079c7244f00c8/JamfPro.mez
--------------------------------------------------------------------------------
/JamfPro/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "powerquery.general.mode": "SDK",
3 | "powerquery.sdk.defaultExtension": "${workspaceFolder}\\bin\\AnyCPU\\Debug\\${workspaceFolderBasename}.mez",
4 | "powerquery.sdk.defaultQueryFile": "${workspaceFolder}\\${workspaceFolderBasename}.query.pq"
5 | }
--------------------------------------------------------------------------------
/JamfPro/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "powerquery",
6 | "operation": "compile",
7 | "additionalArgs": [],
8 | "group": "build",
9 | "problemMatcher": [],
10 | "label": "build"
11 | },
12 | {
13 | "type": "shell",
14 | "command": "cp bin\\AnyCPU\\Debug\\JamfPro.mez ..\\JamfPro.mez",
15 | "dependsOn": [
16 | "build"
17 | ],
18 | "label": "build and copy mez artifact to parent directory",
19 | "problemMatcher": [],
20 | "group": {
21 | "kind": "build",
22 | "isDefault": true
23 | }
24 | }
25 | ]
26 | }
--------------------------------------------------------------------------------
/JamfPro/ComputerMappings.pqm:
--------------------------------------------------------------------------------
1 | [
2 | ApiFields = (mappings as list) as list => List.Transform(mappings, each _[api]),
3 | TableFields = (mappings as list ) as list => List.Transform(mappings, each _[mapped]),
4 |
5 | GeneralFields = {
6 | [api = "name", mapped = "name"],
7 | [api = "lastIpAddress", mapped = "computerDetails.ip_address"],
8 | [api = "lastReportedIp", mapped = "computerDetails.last_reported_ip"],
9 | [api = "jamfBinaryVersion", mapped = "computerDetails.jamf_version"],
10 | [api = "platform", mapped = "computerDetails.platform"],
11 | [api = "barcode1", mapped = "computerDetails.barcode_1"],
12 | [api = "barcode2", mapped = "computerDetails.barcode_2"],
13 | [api = "assetTag", mapped = "computerDetails.asset_tag"],
14 | [api = "remoteManagement", mapped = "remoteManagement"],
15 | [api = "mdmCapable", mapped = "mdmCapable"],
16 | [api = "reportDate", mapped = "computerDetails.report_date_utc"],
17 | [api = "lastContactTime", mapped = "computerDetails.last_contact_time_utc"],
18 | [api = "lastCloudBackupDate", mapped = "computerDetails.last_cloud_backup_date_utc"],
19 | [api = "lastEnrolledDate", mapped = "computerDetails.last_enrolled_date_utc"],
20 | [api = "initialEntryDate", mapped = "computerDetails.initial_entry_date_utc"],
21 | [api = "distributionPoint", mapped = "computerDetails.distribution_point"],
22 | [api = "site", mapped = "computerDetails.site"],
23 | [api = "itunesStoreAccountActive", mapped = "computerDetails.itunes_store_account_is_active"]
24 | },
25 |
26 | RemoteManagementFields = {
27 | [api = "managed", mapped = "computerDetails.remote_management.managed"],
28 | [api = "managementUsername", mapped = "computerDetails.remote_management.management_username"]
29 | },
30 |
31 | MdmCapableFields = {
32 | [api = "capable", mapped = "computerDetails.mdm_capable"],
33 | [api = "capableUsers", mapped = "computerDetails.mdm_capable_users.mdm_capable_user"]
34 | },
35 |
36 | PurchasingFields = {
37 | [api = "purchased", mapped = "computerDetails.is_purchased"],
38 | [api = "leased", mapped = "computerDetails.is_leased"],
39 | [api = "poNumber", mapped = "computerDetails.po_number"],
40 | [api = "lifeExpectancy", mapped = "computerDetails.life_expectancy"],
41 | [api = "purchasePrice", mapped = "computerDetails.purchase_price"],
42 | [api = "purchasingAccount", mapped = "computerDetails.purchasing_account"],
43 | [api = "purchasingContact", mapped = "computerDetails.purchasing_contact"],
44 | [api = "appleCareId", mapped = "computerDetails.applecare_id"],
45 | [api = "vendor", mapped = "computerDetails.vendor"],
46 | [api = "leaseDate", mapped = "computerDetails.lease_expires_utc"],
47 | [api = "poDate", mapped = "computerDetails.po_date_utc"],
48 | [api = "warrantyDate", mapped = "computerDetails.warranty_expires_utc"]
49 | },
50 |
51 | UserAndLocationFields = {
52 | [api = "username", mapped = "computerDetails.username"],
53 | [api = "realname", mapped = "computerDetails.realname"],
54 | [api = "email", mapped = "computerDetails.email_address"],
55 | [api = "position", mapped = "computerDetails.position"],
56 | [api = "phone", mapped = "computerDetails.phone"],
57 | [api = "departmentId", mapped = "computerDetails.department"],
58 | [api = "buildingId", mapped = "computerDetails.building"],
59 | [api = "room", mapped = "computerDetails.room"]
60 | },
61 |
62 | HardwareFields = {
63 | [api = "make", mapped = "computerDetails.make"],
64 | [api = "model", mapped = "computerDetails.model"],
65 | [api = "modelIdentifier", mapped = "computerDetails.model_identifier"],
66 | [api = "serialNumber", mapped = "computerDetails.serial_number"],
67 | [api = "processorSpeedMhz", mapped = "computerDetails.processor_speed_mhz"],
68 | [api = "processorCount", mapped = "computerDetails.number_processors"],
69 | [api = "coreCount", mapped = "computerDetails.number_cores"],
70 | [api = "processorType", mapped = "computerDetails.processor_type"],
71 | [api = "processorArchitecture", mapped = "computerDetails.processor_architecture"],
72 | [api = "busSpeedMhz", mapped = "computerDetails.bus_speed_mhz"],
73 | [api = "cacheSizeKilobytes", mapped = "computerDetails.cache_size_kb"],
74 | [api = "macAddress", mapped = "computerDetails.mac_address"],
75 | [api = "altMacAddress", mapped = "computerDetails.alt_mac_address"],
76 | [api = "totalRamMegabytes", mapped = "computerDetails.total_ram_mb"],
77 | [api = "openRamSlots", mapped = "computerDetails.available_ram_slots"],
78 | [api = "batteryCapacityPercent", mapped = "computerDetails.battery_capacity"],
79 | [api = "smcVersion", mapped = "computerDetails.smc_version"],
80 | [api = "nicSpeed", mapped = "computerDetails.nic_speed"],
81 | [api = "opticalDrive", mapped = "computerDetails.optical_drive"],
82 | [api = "bootRom", mapped = "computerDetails.boot_rom"],
83 | [api = "bleCapable", mapped = "computerDetails.ble_capable"]
84 | },
85 |
86 | SecurityFields = {
87 | [api = "sipStatus", mapped = "computerDetails.sip_status"],
88 | [api = "gatekeeperStatus", mapped = "computerDetails.gatekeeper_status"],
89 | [api = "xprotectVersion", mapped = "computerDetails.xprotect_version"]
90 | },
91 |
92 | OperatingSystemFields = {
93 | [api = "name", mapped = "computerDetails.os_name"],
94 | [api = "version", mapped = "computerDetails.os_version"],
95 | [api = "build", mapped = "computerDetails.os_build"],
96 | [api = "activeDirectoryStatus", mapped = "computerDetails.active_directory_status"]
97 | },
98 |
99 | FieldsToDuplicate = {
100 | [original = "computerDetails.realname", duplicate = "computerDetails.real_name"],
101 | [original = "computerDetails.phone", duplicate = "computerDetails.phone_number"],
102 | [original = "computerDetails.warranty_expires_utc", duplicate = "computerDetails.warranty_expires"],
103 | [original = "computerDetails.lease_expires_utc", duplicate = "computerDetails.lease_expires"],
104 | [original = "computerDetails.po_date_utc", duplicate = "computerDetails.po_date"],
105 | [original = "computerDetails.report_date_utc", duplicate = "computerDetails.report_date"],
106 | [original = "computerDetails.last_contact_time_utc", duplicate = "computerDetails.last_contact_time"],
107 | [original = "computerDetails.cache_size_kb", duplicate = "computerDetails.cache_size"],
108 | [original = "computerDetails.total_ram_mb", duplicate = "computerDetails.total_ram"],
109 | [original = "computerDetails.processor_speed_mhz", duplicate = "computerDetails.processor_speed"],
110 | [original = "computerDetails.bus_speed_mhz", duplicate = "computerDetails.bus_speed"],
111 | [original = "computerDetails.initial_entry_date_utc", duplicate = "computerDetails.initial_entry_date"]
112 | },
113 |
114 | General = [api = ApiFields(GeneralFields), mapped = TableFields(GeneralFields)],
115 | RemoteManagement = [api = ApiFields(RemoteManagementFields), mapped = TableFields(RemoteManagementFields)],
116 | MdmCapable = [api = ApiFields(MdmCapableFields), mapped = TableFields(MdmCapableFields)],
117 | Purchasing = [api = ApiFields(PurchasingFields), mapped = TableFields(PurchasingFields)],
118 | UserAndLocation = [api = ApiFields(UserAndLocationFields), mapped = TableFields(UserAndLocationFields)],
119 | Hardware = [api = ApiFields(HardwareFields), mapped = TableFields(HardwareFields)],
120 | Security = [api = ApiFields(SecurityFields), mapped = TableFields(SecurityFields)],
121 | OperatingSystem = [api = ApiFields(OperatingSystemFields), mapped = TableFields(OperatingSystemFields)],
122 |
123 | /**
124 | * These date/time fields are both mapped directly and converted to an `_epoch` column
125 | * format for compatibility with the legacy model. See the `copyUtcColumnsToEpoch`
126 | * function for detail.
127 | */
128 | UtcColumnNames = {
129 | "computerDetails.report_date_utc",
130 | "computerDetails.last_contact_time_utc",
131 | "computerDetails.last_cloud_backup_date_utc",
132 | "computerDetails.last_enrolled_date_utc",
133 | "computerDetails.initial_entry_date_utc",
134 | "computerDetails.lease_expires_utc",
135 | "computerDetails.po_date_utc",
136 | "computerDetails.warranty_expires_utc"
137 | },
138 |
139 | /**
140 | * Fields that did not have a clear analog to the Jamf Pro API data model. To ensure
141 | * backward compatibility with existing datasets and visualizations, these fields are
142 | * added with `null` values for all rows. See the `addUnmappedColumns` function for
143 | * detail.
144 | */
145 | UnmappedFields = {
146 | "computerDetails.country_name",
147 | "computerDetails.remote_management.management_password_sha256",
148 | "computerDetails.management_status",
149 | "computerDetails.sus",
150 | "computerDetails.netboot_server",
151 | "computerDetails.os_applecare_id",
152 | "computerDetails.os_maintenance_expires",
153 | "computerDetails.attachments",
154 | "computerDetails.master_password_set",
155 | "computerDetails.service_pack",
156 | "computerDetails.institutional_recovery_key",
157 | "computerDetails.disk_encryption_configuration",
158 | "computerDetails.filevault2_users",
159 | "computerDetails.mapped_printers"
160 | }
161 | ]
--------------------------------------------------------------------------------
/JamfPro/JamfPro.pq:
--------------------------------------------------------------------------------
1 | [Version = "2.0.0"]
2 | section JamfPro;
3 |
4 | // Number of computer or mobile device records to request per page.
5 | PAGE_SIZE = 100;
6 |
7 | [DataSource.Kind = "JamfPro", Publish = "JamfPro.Publish"]
8 | // gets initial jamf URL from user; uses this to make requests
9 | shared JamfPro.Contents = Value.ReplaceType(
10 | JamfPro.JamfNavTable,
11 | type function (
12 | jamfUrl as (
13 | Uri.Type meta [
14 | Documentation.Name = Extension.LoadString("FunctionParamJamfUrlName"),
15 | Documentation.FieldCaption = Extension.LoadString("FunctionParamJamfUrlFieldCaption"),
16 | Documentation.FieldDescription = Extension.LoadString("FunctionParamJamfUrlFieldDescription"),
17 | Documentation.FieldSampleValues = {"https://acme.jamfcloud.com"}
18 | ]
19 | )
20 | ) as text meta [
21 | Documentation.Name = Extension.LoadString("FunctionName")
22 | ]
23 | );
24 |
25 | // sets initial navigation table that links to individual table queries
26 | shared JamfPro.JamfNavTable = (url as text) as table =>
27 | let
28 | url = validateUrlScheme(url),
29 | computerPages = getTotalNumberOfPages(url, "v1/computers-inventory"),
30 | mobileDevicesPages = getTotalNumberOfPages(url, "v2/mobile-devices"),
31 | source = #table(
32 | {"Name", "Data", "ItemKind", "ItemName", "IsLeaf"},
33 | {
34 | {"Computers", computers(url, computerPages), "Table", "Table", true},
35 | {"Computer Device Groups", computerGroups(url), "Table", "Table", true},
36 | {"Computers - Applications", computerApplications(url, computerPages), "Table", "Table", true},
37 | {"Computers - Extension attributes", computerExtensionAttributes(url, computerPages), "Table", "Table", true},
38 | {"Mobile Devices", mobileDevices(url, mobileDevicesPages), "Table", "Table", true},
39 | {"Mobile - Applications", mobileDeviceApplications(url, mobileDevicesPages), "Table", "Table", true},
40 | {"Mobile Device Groups", mobileDeviceGroups(url), "Table", "Table", true},
41 | {"Mobile Devices - ExtensionAttributes", mobileDeviceExtensionAtttributes(url, mobileDevicesPages), "Table", "Table", true}
42 | }
43 | ),
44 | navTable = Table.ToNavigationTable(source, {"Name"}, "Name", "Data", "ItemKind", "ItemName", "IsLeaf")
45 | in
46 | navTable;
47 |
48 | validateUrlScheme = (url as text) as text =>
49 | if (Uri.Parts(url)[Scheme] <> "https") then
50 | error "Url scheme must be HTTPS"
51 | else
52 | removeTrailingSlash(url);
53 |
54 | removeTrailingSlash = (url as text) as text =>
55 | if Text.EndsWith(url, "/") then
56 | Text.RemoveRange(url, Text.Length(url) - 1, 1)
57 | else
58 | url;
59 |
60 | getTotalNumberOfPages = (url as text, relativePath as text) as number =>
61 | let
62 | totalCount = UAPIResource(
63 | url, "api/" & relativePath, [#"page-size" = Text.From(PAGE_SIZE)]
64 | )[totalCount],
65 | pages = if totalCount = 0 then 0 else Number.RoundUp(totalCount / PAGE_SIZE)
66 | in
67 | pages;
68 |
69 | getPageComputers = (url as text, sections as list, page as number) as list =>
70 | getPage(url, sections, page, "api/v1/computers-inventory", "id");
71 |
72 | getPageMobileDevices = (url as text, sections as list, page as number) as list =>
73 | getPage(url, sections, page, "api/v2/mobile-devices/detail", "mobileDeviceId");
74 |
75 | getPage = (url as text, sections as list, page as number, path as text, sortKey as text) as list =>
76 | let
77 | result = UAPIResource(
78 | url,
79 | path,
80 | [
81 | #"page-size" = Text.From(PAGE_SIZE),
82 | page = Text.From(page),
83 | section = sections,
84 | sort = sortKey
85 | ]
86 | )[results]
87 | in
88 | result;
89 |
90 | mobileDeviceGroups = (url as text) as table =>
91 | let
92 | jsonDeviceGroups = JSSResource(url, "/mobiledevicegroups"),
93 | deviceGroups = Table.FromRecords(jsonDeviceGroups[mobile_device_groups]),
94 | selectedColumns = Table.SelectColumns(deviceGroups, {"id", "name", "is_smart"}),
95 | groupsPlusMembership = Table.AddColumn(
96 | selectedColumns, "temp_column", each getMobileDeviceMembership([id], url), type list
97 | ),
98 | tempTable = Table.ExpandListColumn(groupsPlusMembership, "temp_column"),
99 | mobileDeviceGroupExpanded = Table.ExpandRecordColumn(
100 | tempTable,
101 | "temp_column",
102 | {"id", "name", "mac_address", "udid", "wifi_mac_address", "serial_number"},
103 | {
104 | "device_id",
105 | "device_name",
106 | "device_mac_address",
107 | "device_udid",
108 | "device_wifi_mac_address",
109 | "device_serial_number"
110 | }
111 | )
112 | in
113 | mobileDeviceGroupExpanded;
114 |
115 | computerGroups = (url as text) as table =>
116 | let
117 | jsonComputerGroups = JSSResource(url, "/computergroups"),
118 | computerGroups = Table.FromRecords(jsonComputerGroups[computer_groups]),
119 | selectedColumns = Table.SelectColumns(computerGroups, {"id", "name", "is_smart"}),
120 | computerGroupsModified = Table.AddColumn(
121 | selectedColumns, "temp_column", each getComputerGroupMembershipIds([id], url), type list
122 | ),
123 | tempTable = Table.ExpandListColumn(computerGroupsModified, "temp_column"),
124 | computerGroupsModifiedExpanded = Table.ExpandRecordColumn(
125 | tempTable, "temp_column", {"id", "name"}, {"computer_id", "computer_name"}
126 | )
127 | in
128 | computerGroupsModifiedExpanded;
129 |
130 | getComputerGroupMembershipIds = (id as number, baseUrl as text) as list =>
131 | let
132 | jsonComputerGroupMembers = JSSResource(baseUrl, "/computergroups/id/" & Number.ToText(id)),
133 | computerGroupMembers = try jsonComputerGroupMembers[computer_group][computers] as list otherwise {}
134 | in
135 | computerGroupMembers;
136 |
137 | getMobileDeviceMembership = (id as number, baseUrl as text) as list =>
138 | let
139 | jsonMobileDeviceGroupMembers = JSSResource(baseUrl, "/mobiledevicegroups/id/" & Number.ToText(id)),
140 | mobileDeviceGroupMembers =
141 | try jsonMobileDeviceGroupMembers[mobile_device_group][mobile_devices] as list otherwise {}
142 | in
143 | mobileDeviceGroupMembers;
144 |
145 | // this function acts as our API call for queries
146 | UAPIResource = (
147 | baseUrl as text,
148 | relativepath as text,
149 | optional query as record,
150 | optional token as text,
151 | optional attempts as number
152 | ) =>
153 | let
154 | number_of_attempts = if attempts = null then 1 else (attempts + 1),
155 | response =
156 | if token = null then
157 | Web.Contents(
158 | baseUrl,
159 | [
160 | Headers = [
161 | #"Accept" = "application/json",
162 | #"Authorization" = TokenAuthorizationHeader(baseUrl)
163 | ],
164 | RelativePath = relativepath,
165 | ManualStatusHandling = {429, 401},
166 | IsRetry = attempts <> null,
167 | Query = query
168 | ]
169 | )
170 | else
171 | Web.Contents(
172 | baseUrl,
173 | [
174 | Headers = [
175 | #"Accept" = "application/json",
176 | #"Authorization" = token
177 | ],
178 | RelativePath = relativepath,
179 | Timeout = Duration.FromText("01:00:00.0"),
180 | ManualStatusHandling = {429, 401},
181 | IsRetry = attempts <> null,
182 | Query = query
183 | ]
184 | ),
185 | responseMetadata = Value.Metadata(response),
186 | responseCode = responseMetadata[Response.Status],
187 | responseHeaders = responseMetadata[Headers],
188 | json =
189 | if responseCode <> 200 and number_of_attempts < 3 then
190 | Function.InvokeAfter(
191 | () =>
192 | @UAPIResource(
193 | baseUrl,
194 | relativepath,
195 | query,
196 | TokenAuthorizationHeader(baseUrl, true),
197 | number_of_attempts
198 | ),
199 | #duration(0, 0, 0, 3)
200 | )
201 | else
202 | Json.Document(response)
203 | in
204 | json;
205 |
206 | // this function is the API call for classic Jamf Pro API data
207 | JSSResource = (
208 | baseUrl as text, relativepath as text, optional token as text, optional attempts as number
209 | ) =>
210 | let
211 | numberOfAttempts = if attempts = null then 1 else (attempts + 1),
212 | response =
213 | if token = null then
214 | Web.Contents(
215 | baseUrl & "/JSSResource",
216 | [
217 | Headers = [
218 | #"Accept" = "application/json",
219 | #"Authorization" = TokenAuthorizationHeader(baseUrl)
220 | ],
221 | RelativePath = relativepath
222 | ]
223 | )
224 | else
225 | Web.Contents(
226 | baseUrl,
227 | [
228 | Headers = [
229 | #"Accept" = "application/json",
230 | #"Authorization" = token
231 | ],
232 | RelativePath = relativepath,
233 | Timeout = Duration.FromText("01:00:00.0"),
234 | ManualStatusHandling = {429, 401},
235 | IsRetry = attempts <> null
236 | ]
237 | ),
238 | responseMetadata = Value.Metadata(response),
239 | responseCode = responseMetadata[Response.Status],
240 | responseHeaders = responseMetadata[Headers],
241 | json =
242 | if responseCode <> 200 and numberOfAttempts < 3 then
243 | Function.InvokeAfter(
244 | () =>
245 | @JSSResource(
246 | baseUrl, relativepath, TokenAuthorizationHeader(baseUrl, true),
247 | numberOfAttempts
248 | ),
249 | #duration(0, 0, 0, 3)
250 | )
251 | else
252 | Json.Document(response)
253 | in
254 | json;
255 |
256 | // gets token for auth header in API call
257 | TokenAuthorizationHeader = (baseUrl as text, optional isRetry as logical) =>
258 | let
259 | credentials = Extension.CurrentCredential(),
260 | token =
261 | if credentials[AuthenticationKind] = "Key" then
262 | let
263 | clientIdAndSecret = Text.Split(credentials[Key], ":"),
264 | accessToken = Json.Document(
265 | Web.Contents(
266 | baseUrl,
267 | [
268 | Headers = [#"Content-Type" = "application/x-www-form-urlencoded"],
269 | RelativePath = "/api/v1/oauth/token",
270 | Content = Text.ToBinary(
271 | "client_id="
272 | & clientIdAndSecret{0}
273 | & "&client_secret="
274 | & clientIdAndSecret{1}
275 | & "&grant_type=client_credentials"
276 | ),
277 | IsRetry = if isRetry <> null then isRetry else false
278 | ]
279 | )
280 | )[access_token]
281 | in
282 | accessToken
283 | else
284 | Json.Document(
285 | Web.Contents(
286 | baseUrl,
287 | [
288 | Headers = [],
289 | RelativePath = "/api/v1/auth/token",
290 | Content = Text.ToBinary(""),
291 | IsRetry = if isRetry <> null then isRetry else false
292 | ]
293 | )
294 | )[token],
295 |
296 | token_header = "Bearer " & token
297 | in
298 | token_header;
299 |
300 | computerApplications = (url as text, totalPagesCount as number) as table =>
301 | let
302 | fieldNamesFromApi = {"name", "path", "version"},
303 | fieldNamesForTable = {
304 | "computerDetails.computerApplications.name",
305 | "computerDetails.computerApplications.path",
306 | "computerDetails.computerApplications.version"
307 | },
308 | emptyTable = #table(List.Combine({{"id"}, fieldNamesForTable}), {}),
309 | result =
310 | if totalPagesCount = 0 then
311 | emptyTable
312 | else
313 | let
314 | pages = {0..totalPagesCount - 1},
315 | listOfPages = List.Transform(
316 | pages, each getPageComputers(url, {"GENERAL", "APPLICATIONS"}, _)
317 | ),
318 | rowPerComputer = List.Combine(listOfPages),
319 | data = Table.FromRecords(rowPerComputer),
320 | selectedColumns = Table.SelectColumns(data, {"id", "general", "applications"}),
321 | expandGeneral = Table.ExpandRecordColumn(selectedColumns, "general", {"name"}),
322 | expandApplications = Table.ExpandListColumn(expandGeneral, "applications"),
323 | expandedApplicationsWithFieldNames = Table.ExpandRecordColumn(
324 | expandApplications, "applications", fieldNamesFromApi, fieldNamesForTable
325 | )
326 | in
327 | expandedApplicationsWithFieldNames
328 | in
329 | result;
330 |
331 | // Computer lookup and mapping function. See notes on mobileDevices function for backward compatibility.
332 | computers = (url as text, totalPagesCount as number) as table =>
333 | let
334 | tableFieldsTopLevel = {"id", "computerDetails.udid"},
335 | emptyTable = #table(
336 | List.Combine(
337 | {
338 | tableFieldsTopLevel,
339 | ComputerMappings[General][mapped]
340 | }
341 | ),
342 | {}
343 | ),
344 | result =
345 | if totalPagesCount = 0 then
346 | emptyTable
347 | else
348 | let
349 | pages = {0..totalPagesCount - 1},
350 | listOfPages = List.Transform(
351 | pages,
352 | each
353 | getPageComputers(
354 | url, {"GENERAL", "DISK_ENCRYPTION", "PURCHASING", "USER_AND_LOCATION", "HARDWARE", "SECURITY", "OPERATING_SYSTEM"}, _
355 | )
356 | ),
357 | rowPerDevice = List.Combine(listOfPages),
358 | selectedColumns = Table.FromRecords(
359 | rowPerDevice,
360 | {"id", "udid", "general", "purchasing", "userAndLocation", "hardware", "security", "operatingSystem"},
361 | MissingField.UseNull
362 | ),
363 | renameUdid = Table.RenameColumns(selectedColumns, {{"udid", "computerDetails.udid"}}),
364 | expandGeneral = Table.ExpandRecordColumn(renameUdid, "general", ComputerMappings[General][api], ComputerMappings[General][mapped]),
365 | expandRemoteManagement = Table.ExpandRecordColumn(expandGeneral, "remoteManagement", ComputerMappings[RemoteManagement][api], ComputerMappings[RemoteManagement][mapped]),
366 | expandMdmCapable = Table.ExpandRecordColumn(expandRemoteManagement, "mdmCapable", ComputerMappings[MdmCapable][api], ComputerMappings[MdmCapable][mapped]),
367 | expandPurchasing = Table.ExpandRecordColumn(expandMdmCapable, "purchasing", ComputerMappings[Purchasing][api], ComputerMappings[Purchasing][mapped]),
368 | expandUserAndLocation = Table.ExpandRecordColumn(expandPurchasing, "userAndLocation", ComputerMappings[UserAndLocation][api], ComputerMappings[UserAndLocation][mapped]),
369 | expandHardware = Table.ExpandRecordColumn(expandUserAndLocation, "hardware", ComputerMappings[Hardware][api], ComputerMappings[Hardware][mapped]),
370 | expandSecurity = Table.ExpandRecordColumn(expandHardware, "security", ComputerMappings[Security][api], ComputerMappings[Security][mapped]),
371 | expandOperatingSystem = Table.ExpandRecordColumn(expandSecurity, "operatingSystem", ComputerMappings[OperatingSystem][api], ComputerMappings[OperatingSystem][mapped]),
372 |
373 | // add any unmapped columns to the final table for backward compatibility
374 | withUnmappedColumns = addUnmappedColumns(expandOperatingSystem, ComputerMappings[UnmappedFields]),
375 | withEpochColumns = copyUtcColumnsToEpoch(withUnmappedColumns, ComputerMappings[UtcColumnNames]),
376 | withDuplicateColumns = duplicateColumns(withEpochColumns, ComputerMappings[FieldsToDuplicate])
377 | in
378 | withDuplicateColumns
379 | in
380 | result;
381 |
382 | /**
383 | * Mobile device lookup and mapping function.
384 | *
385 | * Notes about backward compatibility with the legacy 1.0.0-beta version of the connector:
386 | * - "api" fields are those returned by the Jamf Pro API and "mapped" are the fields that will
387 | * be displayed in the Power BI table. The remapping was necessary to limit the number of
388 | * changes to the legacy data model, in an effort to preserve existing data sets and
389 | * visualizations where possible. See MobileDeviceFieldMappings.pqm for the list of mappings.
390 | */
391 | mobileDevices = (url as text, totalPagesCount as number) as table =>
392 | let
393 | apiFieldsTopLevel = {"mobileDeviceId", "deviceType"},
394 | tableFieldsTopLevel = {"id", "mobileDeviceDetails.os_type"},
395 |
396 | emptyTable = #table(
397 | List.Combine(
398 | {
399 | tableFieldsTopLevel,
400 | MobileDeviceMappings[General][mapped],
401 | MobileDeviceMappings[Hardware][mapped],
402 | MobileDeviceMappings[UserAndLocation][mapped],
403 | MobileDeviceMappings[Security][mapped],
404 | MobileDeviceMappings[LostModeLocation][mapped],
405 | MobileDeviceMappings[Purchasing][mapped],
406 | MobileDeviceMappings[Network][mapped]
407 | }
408 | ),
409 | {}
410 | ),
411 | result =
412 | if totalPagesCount = 0 then
413 | emptyTable
414 | else
415 | let
416 | pages = {0..totalPagesCount - 1},
417 | listOfPages = List.Transform(
418 | pages,
419 | each
420 | getPageMobileDevices(
421 | url, {"GENERAL", "HARDWARE", "USER_AND_LOCATION", "SECURITY", "NETWORK", "PURCHASING"}, _
422 | )
423 | ),
424 | rowPerDevice = List.Combine(listOfPages),
425 | selectedColumns = Table.FromRecords(
426 | rowPerDevice,
427 | {"mobileDeviceId", "deviceType", "general", "hardware", "userAndLocation", "security", "purchasing", "network"},
428 | MissingField.UseNull
429 | ),
430 | renameTopLevelFields = Table.RenameColumns(selectedColumns, List.Zip({apiFieldsTopLevel, tableFieldsTopLevel})),
431 | expandGeneral = Table.ExpandRecordColumn(renameTopLevelFields, "general", MobileDeviceMappings[General][api], MobileDeviceMappings[General][mapped]),
432 | expandHardware = Table.ExpandRecordColumn(expandGeneral, "hardware", MobileDeviceMappings[Hardware][api], MobileDeviceMappings[Hardware][mapped]),
433 | expandUserAndLocation = Table.ExpandRecordColumn(expandHardware, "userAndLocation", MobileDeviceMappings[UserAndLocation][api], MobileDeviceMappings[UserAndLocation][mapped]),
434 | expandSecurity = Table.ExpandRecordColumn(expandUserAndLocation, "security", MobileDeviceMappings[Security][api], MobileDeviceMappings[Security][mapped]),
435 | expandLostModeLocation = Table.ExpandRecordColumn(expandSecurity, "lostModeLocation", MobileDeviceMappings[LostModeLocation][api], MobileDeviceMappings[LostModeLocation][mapped]),
436 | expandPurchasing = Table.ExpandRecordColumn(expandLostModeLocation, "purchasing", MobileDeviceMappings[Purchasing][api], MobileDeviceMappings[Purchasing][mapped]),
437 | expandNetwork = Table.ExpandRecordColumn(expandPurchasing, "network", MobileDeviceMappings[Network][api], MobileDeviceMappings[Network][mapped])
438 | in
439 | expandNetwork,
440 |
441 | withUnmappedColumns = addUnmappedColumns(result, MobileDeviceMappings[UnmappedFields]),
442 | withEpochColumns = copyUtcColumnsToEpoch(withUnmappedColumns, MobileDeviceMappings[UtcColumnNames]),
443 | withDuplicateColumns = duplicateColumns(withEpochColumns, MobileDeviceMappings[FieldsToDuplicate])
444 | in
445 | withDuplicateColumns;
446 |
447 | mobileDeviceApplications = (url as text, totalPagesCount as number) as table =>
448 | let
449 | apiFieldsGeneral = {"displayName", "udid", "managed", "supervised"},
450 | tableFieldsGeneral = {"name", "udid", "managed", "supervised"},
451 | apiFieldsHardware = {"serialNumber", "wifiMacAddress", "model", "modelIdentifier"},
452 | tableFieldsHardware = {"serial_number", "wifi_mac_address", "model", "model_identifier"},
453 | apiFieldsApps = {"name", "version", "identifier"},
454 | tableFieldsApps = {
455 | "mobileDeviceDetails.mdApps.application_name",
456 | "mobileDeviceDetails.mdApps.application_version",
457 | "mobileDeviceDetails.mdApps.identifier"
458 | },
459 | apiFieldsUserAndLocation = {"phoneNumber", "username"},
460 | tableFieldsUserAndLocation = {"phone_number", "username"},
461 | emptyTable = #table(
462 | List.Combine(
463 | {{"id"}, tableFieldsGeneral, tableFieldsHardware, tableFieldsUserAndLocation, tableFieldsApps}
464 | ),
465 | {}
466 | ),
467 | result =
468 | if totalPagesCount = 0 then
469 | emptyTable
470 | else
471 | let
472 | pages = {0..totalPagesCount - 1},
473 | listOfPages = List.Transform(
474 | pages,
475 | each
476 | getPageMobileDevices(url, {"GENERAL", "APPLICATIONS", "HARDWARE", "USER_AND_LOCATION"}, _)
477 | ),
478 | rowPerDevice = List.Combine(listOfPages),
479 | selectedColumns = Table.FromRecords(
480 | rowPerDevice,
481 | {"mobileDeviceId", "general", "hardware", "userAndLocation", "applications"},
482 | MissingField.UseNull
483 | ),
484 | renameIdField = Table.RenameColumns(selectedColumns, {{"mobileDeviceId", "id"}}),
485 | expandGeneral = Table.ExpandRecordColumn(renameIdField, "general", apiFieldsGeneral, tableFieldsGeneral),
486 | expandHardware = Table.ExpandRecordColumn(expandGeneral, "hardware", apiFieldsHardware, tableFieldsHardware),
487 | expandUserAndLocation = Table.ExpandRecordColumn(expandHardware, "userAndLocation", apiFieldsUserAndLocation, tableFieldsUserAndLocation),
488 | normalizeApps = Table.ExpandListColumn(expandUserAndLocation, "applications"),
489 | expandApps = Table.ExpandRecordColumn(normalizeApps, "applications", apiFieldsApps, tableFieldsApps)
490 | in
491 | expandApps,
492 |
493 | withDuplicateColumns = duplicateColumns(result, MobileDeviceMappings[AppsAndEAsFieldsToDuplicate])
494 | in
495 | withDuplicateColumns;
496 |
497 | computerExtensionAttributes = (url as text, totalPagesCount as number) as table =>
498 | let
499 | fieldNamesForTable = {"computerDetails.extAttrs.id", "computerDetails.extAttrs.name", "computerDetails.extAttrs.type", "computerDetails.extAttrs.value"} ,
500 | emptyTable = #table(List.Combine({{"id", "name"}, fieldNamesForTable}), {}),
501 | result =
502 | if totalPagesCount = 0 then
503 | emptyTable
504 | else
505 | let
506 | pages = {0..totalPagesCount - 1},
507 | listOfPages = List.Transform(
508 | pages, each getPageComputers(
509 | url, {"GENERAL", "HARDWARE", "USER_AND_LOCATION", "PURCHASING", "OPERATING_SYSTEM", "EXTENSION_ATTRIBUTES"}, _
510 | )
511 | ),
512 | rowPerComputer = List.Combine(listOfPages),
513 | data = Table.FromRecords(rowPerComputer),
514 | selectedColumns = Table.SelectColumns(data, {"id", "general", "hardware", "userAndLocation", "purchasing", "operatingSystem", "extensionAttributes"}),
515 |
516 | expandGeneral = Table.ExpandRecordColumn(selectedColumns, "general", {"name", "extensionAttributes"}, {"name", "general.extensionAttributes"}),
517 | expandHardware = Table.ExpandRecordColumn(expandGeneral, "hardware", {"extensionAttributes"}, {"hardware.extensionAttributes"}),
518 | expandUserAndLocation = Table.ExpandRecordColumn(expandHardware, "userAndLocation", {"extensionAttributes"}, {"userAndLocation.extensionAttributes"}),
519 | expandPurchasing = Table.ExpandRecordColumn(expandUserAndLocation, "purchasing", {"extensionAttributes"}, {"purchasing.extensionAttributes"}),
520 | expandOperatingSystem = Table.ExpandRecordColumn(expandPurchasing, "operatingSystem", {"extensionAttributes"}, {"operatingSystem.extensionAttributes"}),
521 |
522 | eaApiFields = [id = "definitionId", name = "name", dataType = "dataType", value = "values"],
523 | eaTableFields = [id = "computerDetails.extAttrs.id", name = "computerDetails.extAttrs.name", dataType = "computerDetails.extAttrs.type", value = "computerDetails.extAttrs.value"],
524 | generalNormalized = normalizeEAs(expandOperatingSystem, "general.extensionAttributes", eaApiFields, eaTableFields),
525 | hardwareNormalized = normalizeEAs(expandOperatingSystem, "hardware.extensionAttributes", eaApiFields, eaTableFields),
526 | userAndLocationNormalized = normalizeEAs(expandOperatingSystem, "userAndLocation.extensionAttributes", eaApiFields, eaTableFields),
527 | purchasingNormalized = normalizeEAs(expandOperatingSystem, "purchasing.extensionAttributes", eaApiFields, eaTableFields),
528 | operatingSystemNormalized = normalizeEAs(expandOperatingSystem, "operatingSystem.extensionAttributes", eaApiFields, eaTableFields),
529 | extensionAttributesNormalized = normalizeEAs(expandOperatingSystem, "extensionAttributes", eaApiFields, eaTableFields),
530 | combinedEAs = Table.Combine({generalNormalized, hardwareNormalized, userAndLocationNormalized, purchasingNormalized, operatingSystemNormalized, extensionAttributesNormalized}),
531 |
532 | finalColumns = Table.SelectColumns(combinedEAs, List.Combine({{"id", "name"}, fieldNamesForTable}))
533 | in
534 | finalColumns
535 | in
536 | result;
537 |
538 | mobileDeviceExtensionAtttributes = (url as text, totalPagesCount as number) as table =>
539 | let
540 | apiFieldsGeneral = {"displayName", "udid", "managed", "supervised", "extensionAttributes"},
541 | tableFieldsGeneral = {"name", "udid", "managed", "supervised", "general.extensionAttributes"},
542 | apiFieldsHardware = {"serialNumber", "wifiMacAddress", "model", "modelIdentifier", "extensionAttributes"},
543 | tableFieldsHardware = {"serial_number", "wifi_mac_address", "model", "model_identifier", "hardware.extensionAttributes"},
544 | apiFieldsUserAndLocation = {"phoneNumber", "username", "extensionAttributes"},
545 | tableFieldsUserAndLocation = {"phone_number", "username", "userAndLocation.extensionAttributes"},
546 |
547 | // final column names, including tableFieldsGeneral, tableFieldsHardware, and tableFieldsUserAndLocation, minus anything with `extensionAttributes` in it
548 | eaFieldNames = {"ExtAttrId", "ExtAttrName", "ExtAttrType", "ExtAttrVal"},
549 | finalColumnNames = List.Combine(
550 | {
551 | {"id"},
552 | List.RemoveItems(tableFieldsGeneral, {"general.extensionAttributes"}),
553 | List.RemoveItems(tableFieldsHardware, {"hardware.extensionAttributes"}),
554 | List.RemoveItems(tableFieldsUserAndLocation, {"userAndLocation.extensionAttributes"}),
555 | eaFieldNames
556 | }
557 | ),
558 |
559 | emptyTable = #table(finalColumnNames, {}),
560 | result =
561 | if totalPagesCount = 0 then
562 | emptyTable
563 | else
564 | let
565 | pages = {0..totalPagesCount - 1},
566 | listOfPages = List.Transform(
567 | pages,
568 | each
569 | getPageMobileDevices(url, {"GENERAL", "HARDWARE", "USER_AND_LOCATION", "PURCHASING", "EXTENSION_ATTRIBUTES"}, _)
570 | ),
571 | rowPerDevice = List.Combine(listOfPages),
572 | selectedColumns = Table.FromRecords(
573 | rowPerDevice,
574 | {"mobileDeviceId", "general", "hardware", "userAndLocation", "purchasing", "extensionAttributes"},
575 | MissingField.UseNull
576 | ),
577 | renameIdField = Table.RenameColumns(selectedColumns, {{"mobileDeviceId", "id"}}),
578 | expandGeneral = Table.ExpandRecordColumn(renameIdField, "general", apiFieldsGeneral, tableFieldsGeneral),
579 | expandHardware = Table.ExpandRecordColumn(expandGeneral, "hardware", apiFieldsHardware, tableFieldsHardware),
580 | expandUserAndLocation = Table.ExpandRecordColumn(expandHardware, "userAndLocation", apiFieldsUserAndLocation, tableFieldsUserAndLocation),
581 | expandPurchasing = Table.ExpandRecordColumn(expandUserAndLocation, "purchasing", {"extensionAttributes"}, {"purchasing.extensionAttributes"}),
582 |
583 | eaApiFields = [id = "id", name = "name", dataType = "type", value = "value"],
584 | eaTableFields = [id = "ExtAttrId", name = "ExtAttrName", dataType = "ExtAttrType", value = "ExtAttrVal"],
585 | generalNormalized = normalizeEAs(expandPurchasing, "general.extensionAttributes", eaApiFields, eaTableFields),
586 | userAndLocationNormalized = normalizeEAs(expandPurchasing, "userAndLocation.extensionAttributes", eaApiFields, eaTableFields),
587 | hardwareNormalized = normalizeEAs(expandPurchasing, "hardware.extensionAttributes", eaApiFields, eaTableFields),
588 | purchasingNormalized = normalizeEAs(expandPurchasing, "purchasing.extensionAttributes", eaApiFields, eaTableFields),
589 | extensionAttributesNormalized = normalizeEAs(expandPurchasing, "extensionAttributes", eaApiFields, eaTableFields),
590 | combinedEAs = Table.Combine({generalNormalized, userAndLocationNormalized, hardwareNormalized, purchasingNormalized, extensionAttributesNormalized})
591 | in
592 | combinedEAs,
593 |
594 | finalUniqueColumns = Table.SelectColumns(result, finalColumnNames),
595 | withDuplicateColumns = duplicateColumns(finalUniqueColumns, MobileDeviceMappings[AppsAndEAsFieldsToDuplicate])
596 | in
597 | withDuplicateColumns;
598 |
599 | normalizeEAs = (t as table, columnName as text, apiFields as record, tableFields as record) as table =>
600 | let
601 | normalizeEAs = Table.ExpandListColumn(t, columnName),
602 | expandEAs = Table.ExpandRecordColumn(
603 | normalizeEAs,
604 | columnName,
605 | {apiFields[id], apiFields[name], apiFields[dataType], apiFields[value]},
606 | {tableFields[id], tableFields[name], tableFields[dataType], tableFields[value]}
607 | ),
608 | expandValues = Table.ExpandListColumn(expandEAs, tableFields[value]),
609 | filteredEAs = Table.SelectRows(expandValues, each Record.Field(_, tableFields[id]) <> null),
610 | result = Table.RemoveColumns(filteredEAs, {columnName}, MissingField.Ignore)
611 | in
612 | result;
613 |
614 | dateStringIso8601ToEpochMillis = (dateString as text) as number =>
615 | let
616 | dateObject = DateTime.FromText(dateString),
617 | epochMillis = Duration.TotalSeconds(Duration.From(dateObject - #datetime(1970, 1, 1, 0, 0, 0))) * 1000
618 | in
619 | epochMillis;
620 |
621 | copyUtcColumnsToEpoch = (t as table, utcColumnNames as list) as table =>
622 | let
623 | // Generate new column names by replacing "_utc" with "_epoch"
624 | epochColumnNames = List.Transform(utcColumnNames, each Text.Replace(_, "_utc", "_epoch")),
625 |
626 | // Convert each UTC column to epoch milliseconds and add as a new column
627 | addEpochColumns = List.Accumulate(
628 | List.Zip({utcColumnNames, epochColumnNames}),
629 | t,
630 | (state, current) =>
631 | let
632 | utcColumnName = current{0},
633 | epochColumnName = current{1},
634 | addColumn = Table.AddColumn(state, epochColumnName, each try dateStringIso8601ToEpochMillis(Record.Field(_, utcColumnName)) otherwise 0, type number)
635 | in
636 | addColumn
637 | )
638 | in
639 | addEpochColumns;
640 |
641 | addUnmappedColumns = (t as table, unmappedFields as list) as table =>
642 | let
643 | result = List.Accumulate(
644 | unmappedFields,
645 | t,
646 | (state, current) =>
647 | try
648 | Table.AddColumn(state, current, each null, type any)
649 | otherwise
650 | state
651 | )
652 | in
653 | result;
654 |
655 | duplicateColumns = (t as table, fieldsToDuplicate as list) as table =>
656 | let
657 | result = List.Accumulate(
658 | fieldsToDuplicate,
659 | t,
660 | (state, current) =>
661 | let
662 | addColumn = Table.AddColumn(state, current[duplicate], each Record.Field(_, current[original]), type any)
663 | in
664 | addColumn
665 | )
666 | in
667 | result;
668 |
669 | // Data Source definition
670 | JamfPro = [
671 | TestConnection = (dataSourcePath) => {"JamfPro.JamfNavTable", dataSourcePath},
672 | Authentication = [
673 | UsernamePassword = [
674 | Label = Extension.LoadString("AuthLabelUsernamePassword")
675 | ],
676 | Key = [
677 | Label = Extension.LoadString("AuthLabelApiClientKey"),
678 | KeyLabel = Extension.LoadString("AuthLabelApiClientKeyLabel")
679 | ]
680 | ],
681 | Label = Extension.LoadString("DataSourceLabel")
682 | ];
683 |
684 | // Data Source UI publishing description
685 | JamfPro.Publish = [
686 | Beta = false,
687 | Category = "Other",
688 | ButtonText = {Extension.LoadString("ButtonTitle"), Extension.LoadString("ButtonHelp")},
689 | LearnMoreUrl = "https://marketplace.jamf.com/details/power-bi",
690 | SourceImage = JamfPro__Get_Devices.Icons,
691 | SourceTypeImage = JamfPro__Get_Devices.Icons
692 | ];
693 |
694 | JamfPro__Get_Devices.Icons = [
695 | Icon16 = {
696 | Extension.Contents("JamfPro16.png"),
697 | Extension.Contents("JamfPro20.png"),
698 | Extension.Contents("JamfPro24.png"),
699 | Extension.Contents("JamfPro32.png")
700 | },
701 | Icon32 = {
702 | Extension.Contents("JamfPro32.png"),
703 | Extension.Contents("JamfPro40.png"),
704 | Extension.Contents("JamfPro48.png"),
705 | Extension.Contents("JamfPro64.png")
706 | }
707 | ];
708 |
709 | Table.ToNavigationTable = (
710 | table as table,
711 | keyColumns as list,
712 | nameColumn as text,
713 | dataColumn as text,
714 | itemKindColumn as text,
715 | itemNameColumn as text,
716 | isLeafColumn as text
717 | ) as table =>
718 | let
719 | tableType = Value.Type(table),
720 | newTableType = Type.AddTableKey(tableType, keyColumns, true) meta [
721 | NavigationTable.NameColumn = nameColumn,
722 | NavigationTable.DataColumn = dataColumn,
723 | NavigationTable.ItemKindColumn = itemKindColumn,
724 | Preview.DelayColumn = itemNameColumn,
725 | NavigationTable.IsLeafColumn = isLeafColumn
726 | ],
727 | navigationTable = Value.ReplaceType(table, newTableType)
728 | in
729 | navigationTable;
730 |
731 | Extension.LoadFunction = (fileName as text) =>
732 | let
733 | binary = Extension.Contents(fileName),
734 | asText = Text.FromBinary(binary)
735 | in
736 | try
737 | Expression.Evaluate(asText, #shared)
738 | catch (e) =>
739 | error [
740 | Reason = "Extension.LoadFunction Failure",
741 | Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
742 | Message.Parameters = {fileName, e[Reason], e[Message]},
743 | Detail = [File = fileName, Error = e]
744 | ];
745 |
746 | MobileDeviceMappings = Extension.LoadFunction("MobileDeviceMappings.pqm");
747 | ComputerMappings = Extension.LoadFunction("ComputerMappings.pqm");
748 |
--------------------------------------------------------------------------------
/JamfPro/JamfPro.proj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(MSBuildProjectDirectory)\bin\AnyCPU\Debug\
5 | $(MSBuildProjectDirectory)\obj\
6 | $(IntermediateOutputPath)MEZ\
7 | $(OutputPath)$(MsBuildProjectName).mez
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/JamfPro/JamfPro.query.pq:
--------------------------------------------------------------------------------
1 | // Use this file to write queries to test your data connector
2 | section Testing;
3 |
4 | // swap out the URL in Contents() for your own Jamf Pro instance URL
5 | // then add credentials in using the power query SDK credential helper for the instance's admin Username and Password
6 | result = JamfPro.Contents("https://yourjamfdomain.com");
7 |
8 | shared Testing.UnitTest = [
9 | // Put any common variables here if you only want them to be evaluated once
10 | // Fact(, , )
11 | // and can be a literal or let statement
12 | facts = {
13 | // this test checks that the global navigation table for the query returns the correct number of global tables / rows
14 | // swap out the URL in JamfNavTable() for your own Jamf Pro instance URL
15 | // then add credentials in using the power query SDK credential helper for the instance's admin Username and Password
16 | Fact("Nav table should return 6 rows", 6, Table.RowCount(result))
17 | },
18 | report = Facts.Summarize(facts)
19 | ][report];
20 |
21 | /// COMMON UNIT TESTING CODE
22 | Fact = (_subject as text, _expected, _actual) as record =>
23 | [
24 | expected = try _expected,
25 | safeExpected = if expected[HasError] then "Expected : " & @ValueToText(expected[Error]) else expected[Value],
26 | actual = try _actual,
27 | safeActual = if actual[HasError] then "Actual : " & @ValueToText(actual[Error]) else actual[Value],
28 | attempt = try safeExpected = safeActual,
29 | result = if attempt[HasError] or not attempt[Value] then "Failure ⛔" else "Success ✓",
30 | resultOp = if result = "Success ✓" then " = " else " <> ",
31 | addendumEvalAttempt = if attempt[HasError] then @ValueToText(attempt[Error]) else "",
32 | addendumEvalExpected = try @ValueToText(safeExpected) otherwise "...",
33 | addendumEvalActual = try @ValueToText(safeActual) otherwise "...",
34 | fact = [
35 | Result = result & " " & addendumEvalAttempt,
36 | Notes = _subject,
37 | Details = " (" & addendumEvalExpected & resultOp & addendumEvalActual & ")"
38 | ]
39 | ][fact];
40 |
41 | Facts = (_subject as text, _predicates as list) => List.Transform(_predicates, each Fact(_subject, _{0}, _{1}));
42 |
43 | Facts.Summarize = (_facts as list) as table =>
44 | [
45 | Fact.CountSuccesses = (count, i) =>
46 | [
47 | result = try i[Result],
48 | sum = if result[HasError] or not Text.StartsWith(result[Value], "Success") then count else count + 1
49 | ][sum],
50 | passed = List.Accumulate(_facts, 0, Fact.CountSuccesses),
51 | total = List.Count(_facts),
52 | format = if passed = total then "All #{0} Passed !!! ✓" else "#{0} Passed ☺ #{1} Failed ☹",
53 | result = if passed = total then "Success" else "⛔",
54 | rate = Number.IntegerDivide(100 * passed, total),
55 | header = [
56 | Result = result,
57 | Notes = Text.Format(format, {passed, total - passed}),
58 | Details = Text.Format("#{0}% success rate", {rate})
59 | ],
60 | report = Table.FromRecords(List.Combine({{header}, _facts}))
61 | ][report];
62 |
63 | ValueToText = (value, optional depth) =>
64 | let
65 | List.TransformAndCombine = (list, transform, separator) =>
66 | Text.Combine(List.Transform(list, transform), separator),
67 | Serialize.Binary = (x) => "#binary(" & Serialize(Binary.ToList(x)) & ") ",
68 | Serialize.Function = (x) =>
69 | _serialize_function_param_type(
70 | Type.FunctionParameters(Value.Type(x)), Type.FunctionRequiredParameters(Value.Type(x))
71 | )
72 | & " as "
73 | & _serialize_function_return_type(Value.Type(x))
74 | & " => (...) ",
75 | Serialize.List = (x) => "{" & List.TransformAndCombine(x, Serialize, ", ") & "} ",
76 | Serialize.Record = (x) =>
77 | "[ "
78 | & List.TransformAndCombine(
79 | Record.FieldNames(x),
80 | (item) => Serialize.Identifier(item) & " = " & Serialize(Record.Field(x, item)),
81 | ", "
82 | )
83 | & " ] ",
84 | Serialize.Table = (x) =>
85 | "#table( type " & _serialize_table_type(Value.Type(x)) & ", " & Serialize(Table.ToRows(x)) & ") ",
86 | Serialize.Identifier = Expression.Identifier,
87 | Serialize.Type = (x) => "type " & _serialize_typename(x),
88 | _serialize_typename = (x, optional funtype as logical) =>
89 | // Optional parameter: Is this being used as part of a function signature?
90 | let
91 | isFunctionType = (x as type) =>
92 | try if Type.FunctionReturn(x) is type then true else false otherwise false,
93 | isTableType = (x as type) => try if Type.TableSchema(x) is table then true else false otherwise false,
94 | isRecordType = (x as type) => try if Type.ClosedRecord(x) is type then true else false
95 | otherwise
96 | false,
97 | isListType = (x as type) => try if Type.ListItem(x) is type then true else false otherwise false
98 | in
99 | if funtype = null and isTableType(x) then
100 | _serialize_table_type(x)
101 | else if funtype = null and isListType(x) then
102 | "{ " & @_serialize_typename(Type.ListItem(x)) & " }"
103 | else if funtype = null and isFunctionType(x) then
104 | "function " & _serialize_function_type(x)
105 | else if funtype = null and isRecordType(x) then
106 | _serialize_record_type(x)
107 | else if x = type any then
108 | "any"
109 | else
110 | let
111 | base = Type.NonNullable(x)
112 | in
113 | (if Type.IsNullable(x) then "nullable " else "")
114 | & (
115 | if base = type anynonnull then
116 | "anynonnull"
117 | else if base = type binary then
118 | "binary"
119 | else if base = type date then
120 | "date"
121 | else if base = type datetime then
122 | "datetime"
123 | else if base = type datetimezone then
124 | "datetimezone"
125 | else if base = type duration then
126 | "duration"
127 | else if base = type logical then
128 | "logical"
129 | else if base = type none then
130 | "none"
131 | else if base = type null then
132 | "null"
133 | else if base = type number then
134 | "number"
135 | else if base = type text then
136 | "text"
137 | else if base = type time then
138 | "time"
139 | else if base = type type then
140 | "type"
141 | else
142 | // Abstract types
143 | if base = type function then
144 | "function"
145 | else if base = type table then
146 | "table"
147 | else if base = type record then
148 | "record"
149 | else if base = type list then
150 | "list"
151 | else
152 | "any /*Actually unknown type*/"
153 | ),
154 | _serialize_table_type = (x) =>
155 | let
156 | schema = Type.TableSchema(x)
157 | in
158 | "table "
159 | & (
160 | if Table.IsEmpty(schema) then
161 | ""
162 | else
163 | "["
164 | & List.TransformAndCombine(
165 | Table.ToRecords(Table.Sort(schema, "Position")),
166 | each Serialize.Identifier(_[Name]) & " = " & _[Kind],
167 | ", "
168 | )
169 | & "] "
170 | ),
171 | _serialize_record_type = (x) =>
172 | let
173 | flds = Type.RecordFields(x)
174 | in
175 | if Record.FieldCount(flds) = 0 then
176 | "record"
177 | else
178 | "["
179 | & List.TransformAndCombine(
180 | Record.FieldNames(flds),
181 | (item) =>
182 | Serialize.Identifier(item) & "=" & _serialize_typename(
183 | Record.Field(flds, item)[Type]
184 | ),
185 | ", "
186 | )
187 | & (if Type.IsOpenRecord(x) then ", ..." else "")
188 | & "]",
189 | _serialize_function_type = (x) =>
190 | _serialize_function_param_type(Type.FunctionParameters(x), Type.FunctionRequiredParameters(x))
191 | & " as "
192 | & _serialize_function_return_type(x),
193 | _serialize_function_param_type = (t, n) =>
194 | let
195 | funsig = Table.ToRecords(
196 | Table.TransformColumns(
197 | Table.AddIndexColumn(Record.ToTable(t), "isOptional", 1), {"isOptional", (x) => x > n}
198 | )
199 | )
200 | in
201 | "("
202 | & List.TransformAndCombine(
203 | funsig,
204 | (item) =>
205 | (if item[isOptional] then "optional " else "")
206 | & Serialize.Identifier(item[Name])
207 | & " as "
208 | & _serialize_typename(item[Value], true),
209 | ", "
210 | )
211 | & ")",
212 | _serialize_function_return_type = (x) => _serialize_typename(Type.FunctionReturn(x), true),
213 | Serialize = (x) as text =>
214 | if x is binary then
215 | try Serialize.Binary(x) otherwise "null /*serialize failed*/"
216 | else if x is date then
217 | try Expression.Constant(x) otherwise "null /*serialize failed*/"
218 | else if x is datetime then
219 | try Expression.Constant(x) otherwise "null /*serialize failed*/"
220 | else if x is datetimezone then
221 | try Expression.Constant(x) otherwise "null /*serialize failed*/"
222 | else if x is duration then
223 | try Expression.Constant(x) otherwise "null /*serialize failed*/"
224 | else if x is function then
225 | try Serialize.Function(x) otherwise "null /*serialize failed*/"
226 | else if x is list then
227 | try Serialize.List(x) otherwise "null /*serialize failed*/"
228 | else if x is logical then
229 | try Expression.Constant(x) otherwise "null /*serialize failed*/"
230 | else if x is null then
231 | try Expression.Constant(x) otherwise "null /*serialize failed*/"
232 | else if x is number then
233 | try Expression.Constant(x) otherwise "null /*serialize failed*/"
234 | else if x is record then
235 | try Serialize.Record(x) otherwise "null /*serialize failed*/"
236 | else if x is table then
237 | try Serialize.Table(x) otherwise "null /*serialize failed*/"
238 | else if x is text then
239 | try Expression.Constant(x) otherwise "null /*serialize failed*/"
240 | else if x is time then
241 | try Expression.Constant(x) otherwise "null /*serialize failed*/"
242 | else if x is type then
243 | try Serialize.Type(x) otherwise "null /*serialize failed*/"
244 | else
245 | "[#_unable_to_serialize_#]"
246 | in
247 | try Serialize(value) otherwise "";
--------------------------------------------------------------------------------
/JamfPro/JamfPro16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/powerbi/46bf1fd74bb3548e52f61454d5a079c7244f00c8/JamfPro/JamfPro16.png
--------------------------------------------------------------------------------
/JamfPro/JamfPro20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/powerbi/46bf1fd74bb3548e52f61454d5a079c7244f00c8/JamfPro/JamfPro20.png
--------------------------------------------------------------------------------
/JamfPro/JamfPro24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/powerbi/46bf1fd74bb3548e52f61454d5a079c7244f00c8/JamfPro/JamfPro24.png
--------------------------------------------------------------------------------
/JamfPro/JamfPro32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/powerbi/46bf1fd74bb3548e52f61454d5a079c7244f00c8/JamfPro/JamfPro32.png
--------------------------------------------------------------------------------
/JamfPro/JamfPro40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/powerbi/46bf1fd74bb3548e52f61454d5a079c7244f00c8/JamfPro/JamfPro40.png
--------------------------------------------------------------------------------
/JamfPro/JamfPro48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/powerbi/46bf1fd74bb3548e52f61454d5a079c7244f00c8/JamfPro/JamfPro48.png
--------------------------------------------------------------------------------
/JamfPro/JamfPro64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/powerbi/46bf1fd74bb3548e52f61454d5a079c7244f00c8/JamfPro/JamfPro64.png
--------------------------------------------------------------------------------
/JamfPro/MobileDeviceMappings.pqm:
--------------------------------------------------------------------------------
1 | [
2 | ApiFields = (mappings as list) as list => List.Transform(mappings, each _[api]),
3 | TableFields = (mappings as list ) as list => List.Transform(mappings, each _[mapped]),
4 |
5 | GeneralFields = {
6 | [api = "udid", mapped = "mobileDeviceDetails.udid"],
7 | [api = "displayName", mapped = "name"],
8 | [api = "assetTag", mapped = "mobileDeviceDetails.asset_tag"],
9 |
10 | // TODO: siteId is mapped to the site's ID, but the legacy connector maps it to a record with the site's ID and name
11 | [api = "siteId", mapped = "mobileDeviceDetails.site"],
12 | [api = "lastInventoryUpdateDate", mapped = "mobileDeviceDetails.last_inventory_update_utc"],
13 | [api = "osVersion", mapped = "mobileDeviceDetails.os_version"],
14 | [api = "osBuild", mapped = "mobileDeviceDetails.os_build"],
15 | [api = "ipAddress", mapped = "mobileDeviceDetails.ip_address"],
16 | [api = "managed", mapped = "mobileDeviceDetails.managed"],
17 | [api = "supervised", mapped = "mobileDeviceDetails.supervised"],
18 | [api = "deviceOwnershipType", mapped = "mobileDeviceDetails.device_ownership_level"],
19 | [api = "lastEnrolledDate", mapped = "mobileDeviceDetails.last_enrollment_utc"],
20 | [api = "sharedIpad", mapped = "mobileDeviceDetails.shared"],
21 | [api = "lastBackupDate", mapped = "mobileDeviceDetails.last_backup_time_utc"],
22 | [api = "deviceLocatorServiceEnabled", mapped = "mobileDeviceDetails.device_locator_service_enabled"],
23 | [api = "doNotDisturbEnabled", mapped = "mobileDeviceDetails.do_not_disturb_enabled"],
24 | [api = "cloudBackupEnabled", mapped = "mobileDeviceDetails.cloud_backup_enabled"],
25 | [api = "lastCloudBackupDate", mapped = "mobileDeviceDetails.last_cloud_backup_date_utc"],
26 | [api = "itunesStoreAccountActive", mapped = "mobileDeviceDetails.itunes_store_account_is_active"],
27 | [api = "exchangeDeviceId", mapped = "mobileDeviceDetails.exchange_activesync_device_identifier"],
28 | [api = "tethered", mapped = "mobileDeviceDetails.tethered"]
29 | },
30 |
31 | HardwareFields = {
32 | [api = "capacityMb", mapped = "mobileDeviceDetails.capacity_mb"],
33 | [api = "availableSpaceMb", mapped = "mobileDeviceDetails.available_mb"],
34 | [api = "usedSpacePercentage", mapped = "mobileDeviceDetails.percentage_used"],
35 | [api = "batteryLevel", mapped = "mobileDeviceDetails.battery_level"],
36 | [api = "serialNumber", mapped = "mobileDeviceDetails.serial_number"],
37 | [api = "wifiMacAddress", mapped = "mobileDeviceDetails.wifi_mac_address"],
38 | [api = "bluetoothMacAddress", mapped = "mobileDeviceDetails.bluetooth_mac_address"],
39 | [api = "modemFirmwareVersion", mapped = "mobileDeviceDetails.modem_firmware"],
40 | [api = "model", mapped = "mobileDeviceDetails.model"],
41 | [api = "modelIdentifier", mapped = "mobileDeviceDetails.model_identifier"],
42 | [api = "modelNumber", mapped = "mobileDeviceDetails.model_number"],
43 | [api = "bluetoothLowEnergyCapable", mapped = "mobileDeviceDetails.ble_capable"]
44 | },
45 |
46 | UserAndLocationFields = {
47 | [api = "username", mapped = "mobileDeviceDetails.username"],
48 | [api = "realName", mapped = "mobileDeviceDetails.real_name"],
49 | [api = "emailAddress", mapped = "mobileDeviceDetails.email_address"],
50 | [api = "position", mapped = "mobileDeviceDetails.position"],
51 | [api = "phoneNumber", mapped = "mobileDeviceDetails.phone_number"],
52 | [api = "room", mapped = "mobileDeviceDetails.room"],
53 | [api = "building", mapped = "mobileDeviceDetails.building"],
54 | [api = "department", mapped = "mobileDeviceDetails.department"]
55 | },
56 |
57 |
58 | NetworkFields = {
59 | [api = "cellularTechnology", mapped = "mobileDeviceDetails.cellular_technology"],
60 | [api = "voiceRoamingEnabled", mapped = "mobileDeviceDetails.voice_roaming_enabled"],
61 | [api = "imei", mapped = "mobileDeviceDetails.imei"],
62 | [api = "iccid", mapped = "mobileDeviceDetails.iccid"],
63 | [api = "meid", mapped = "mobileDeviceDetails.meid"],
64 | [api = "carrierSettingsVersion", mapped = "mobileDeviceDetails.carrier_settings_version"],
65 | [api = "currentCarrierNetwork", mapped = "mobileDeviceDetails.current_carrier_network"],
66 | [api = "currentMobileCountryCode", mapped = "mobileDeviceDetails.current_mobile_country_code"],
67 | [api = "currentMobileNetworkCode", mapped = "mobileDeviceDetails.current_mobile_network_code"],
68 | [api = "homeCarrierNetwork", mapped = "mobileDeviceDetails.home_carrier_network"],
69 | [api = "homeMobileCountryCode", mapped = "mobileDeviceDetails.home_mobile_country_code"],
70 | [api = "homeMobileNetworkCode", mapped = "mobileDeviceDetails.home_mobile_network_code"],
71 | [api = "dataRoamingEnabled", mapped = "mobileDeviceDetails.data_roaming_enabled"],
72 | [api = "roaming", mapped = "mobileDeviceDetails.roaming"],
73 | [api = "phoneNumber", mapped = "mobileDeviceDetails.phone"]
74 | },
75 |
76 | PurchasingFields = {
77 | [api = "purchased", mapped = "mobileDeviceDetails.is_purchased"],
78 | [api = "leased", mapped = "mobileDeviceDetails.is_leased"],
79 | [api = "poNumber", mapped = "mobileDeviceDetails.po_number"],
80 | [api = "vendor", mapped = "mobileDeviceDetails.vendor"],
81 | [api = "appleCareId", mapped = "mobileDeviceDetails.applecare_id"],
82 | [api = "purchasePrice", mapped = "mobileDeviceDetails.purchase_price"],
83 | [api = "purchasingAccount", mapped = "mobileDeviceDetails.purchasing_account"],
84 | [api = "poDate", mapped = "mobileDeviceDetails.po_date_utc"],
85 | [api = "warrantyExpiresDate", mapped = "mobileDeviceDetails.warranty_expires_utc"],
86 | [api = "leaseExpiresDate", mapped = "mobileDeviceDetails.lease_expires_utc"],
87 | [api = "lifeExpectancy", mapped = "mobileDeviceDetails.life_expectancy"],
88 | [api = "purchasingContact", mapped = "mobileDeviceDetails.purchasing_contact"]
89 | },
90 |
91 | SecurityFields = {
92 | [api = "dataProtected", mapped = "mobileDeviceDetails.data_protection"],
93 | [api = "blockLevelEncryptionCapable", mapped = "mobileDeviceDetails.block_level_encryption_capable"],
94 | [api = "fileLevelEncryptionCapable", mapped = "mobileDeviceDetails.file_level_encryption_capable"],
95 | [api = "passcodePresent", mapped = "mobileDeviceDetails.passcode_present"],
96 | [api = "passcodeCompliant", mapped = "mobileDeviceDetails.passcode_compliant"],
97 | [api = "passcodeCompliantWithProfile", mapped = "mobileDeviceDetails.passcode_compliant_with_profile"],
98 | [api = "hardwareEncryption", mapped = "mobileDeviceDetails.hardware_encryption"],
99 | [api = "activationLockEnabled", mapped = "mobileDeviceDetails.activation_lock_enabled"],
100 | [api = "jailBreakDetected", mapped = "mobileDeviceDetails.jailbreak_detected"],
101 | [api = "passcodeLockGracePeriodEnforcedSeconds", mapped = "mobileDeviceDetails.passcode_lock_grace_period_enforced"],
102 | [api = "lostModeEnabled", mapped = "mobileDeviceDetails.lost_mode_enabled"],
103 | [api = "lostModeMessage", mapped = "mobileDeviceDetails.lost_mode_message"],
104 | [api = "lostModePhoneNumber", mapped = "mobileDeviceDetails.lost_mode_phone"],
105 | [api = "lostModeFootnote", mapped = "mobileDeviceDetails.lost_mode_footnote"],
106 | [api = "lostModeLocation", mapped = "lostModeLocation"],
107 | [api = "lostModeEnabledDate", mapped = "mobileDeviceDetails.lost_mode_enable_issued_utc"]
108 | },
109 |
110 | LostModeLocationFields = {
111 | [api = "lastLocationUpdate", mapped = "mobileDeviceDetails.lost_location_utc"],
112 | [api = "lostModeLocationHorizontalAccuracyMeters", mapped = "mobileDeviceDetails.lost_location_horizontal_accuracy"],
113 | [api = "lostModeLocationVerticalAccuracyMeters", mapped = "mobileDeviceDetails.lost_location_vertical_accuracy"],
114 | [api = "lostModeLocationAltitudeMeters", mapped = "mobileDeviceDetails.lost_location_altitude"],
115 | [api = "lostModeLocationSpeedMetersPerSecond", mapped = "mobileDeviceDetails.lost_location_speed"],
116 | [api = "lostModeLocationCourseDegrees", mapped = "mobileDeviceDetails.lost_location_course"],
117 | [api = "lostModeLocationTimestamp", mapped = "mobileDeviceDetails.lost_mode_location_timestamp"]
118 | },
119 |
120 | AppsAndEAsFieldsToDuplicate = {
121 | [original = "name", duplicate = "device_name"],
122 | [original = "name", duplicate = "display_name"],
123 | [original = "model", duplicate = "model_display"],
124 | [original = "model", duplicate = "modelDisplay"]
125 | },
126 |
127 | FieldsToDuplicate = {
128 | [original = "mobileDeviceDetails.managed", duplicate = "managed"],
129 | [original = "mobileDeviceDetails.udid", duplicate = "udid"],
130 | [original = "mobileDeviceDetails.serial_number", duplicate = "serial_number"],
131 | [original = "mobileDeviceDetails.wifi_mac_address", duplicate = "wifi_mac_address"],
132 | [original = "mobileDeviceDetails.capacity_mb", duplicate = "mobileDeviceDetails.capacity"],
133 | [original = "mobileDeviceDetails.available_mb", duplicate = "mobileDeviceDetails.available"],
134 | [original = "name", duplicate = "device_name"],
135 | [original = "mobileDeviceDetails.model", duplicate = "model"],
136 | [original = "mobileDeviceDetails.model", duplicate = "modelDisplay"],
137 | [original = "mobileDeviceDetails.model", duplicate = "model_display"],
138 | [original = "mobileDeviceDetails.model", duplicate = "mobileDeviceDetails.modelDisplay"],
139 | [original = "mobileDeviceDetails.model", duplicate = "mobileDeviceDetails.model_display"],
140 | [original = "mobileDeviceDetails.model_identifier", duplicate = "model_identifier"],
141 | [original = "mobileDeviceDetails.supervised", duplicate = "supervised"],
142 | [original = "mobileDeviceDetails.username", duplicate = "username"],
143 | [original = "mobileDeviceDetails.real_name", duplicate = "mobileDeviceDetails.realname"],
144 | [original = "mobileDeviceDetails.phone_number", duplicate = "phone_number"],
145 | [original = "mobileDeviceDetails.po_date_utc", duplicate = "mobileDeviceDetails.po_date"],
146 | [original = "mobileDeviceDetails.last_inventory_update_utc", duplicate = "mobileDeviceDetails.last_inventory_update"],
147 | [original = "mobileDeviceDetails.lease_expires_utc", duplicate = "mobileDeviceDetails.lease_expires"],
148 | [original = "mobileDeviceDetails.warranty_expires_utc", duplicate = "mobileDeviceDetails.warranty_expires"]
149 | },
150 |
151 | General = [api = ApiFields(GeneralFields), mapped = TableFields(GeneralFields)],
152 | Hardware = [api = ApiFields(HardwareFields), mapped = TableFields(HardwareFields)],
153 | UserAndLocation = [api = ApiFields(UserAndLocationFields), mapped = TableFields(UserAndLocationFields)],
154 | Network = [api = ApiFields(NetworkFields), mapped = TableFields(NetworkFields)],
155 | Purchasing = [api = ApiFields(PurchasingFields), mapped = TableFields(PurchasingFields)],
156 | Security = [api = ApiFields(SecurityFields), mapped = TableFields(SecurityFields)],
157 | LostModeLocation = [api = ApiFields(LostModeLocationFields), mapped = TableFields(LostModeLocationFields)],
158 |
159 | /**
160 | * These date/time fields are both mapped directly and converted to an `_epoch` column
161 | * format for compatibility with the legacy model. See the `copyUtcColumnsToEpoch`
162 | * function for detail.
163 | */
164 | UtcColumnNames = {
165 | "mobileDeviceDetails.last_inventory_update_utc",
166 | "mobileDeviceDetails.last_enrollment_utc",
167 | "mobileDeviceDetails.last_backup_time_utc",
168 | "mobileDeviceDetails.last_cloud_backup_date_utc",
169 | "mobileDeviceDetails.initial_entry_date_utc",
170 | "mobileDeviceDetails.lease_expires_utc",
171 | "mobileDeviceDetails.po_date_utc",
172 | "mobileDeviceDetails.warranty_expires_utc",
173 | "mobileDeviceDetails.lost_mode_enable_issued_utc",
174 | "mobileDeviceDetails.lost_location_utc"
175 | },
176 |
177 | /**
178 | * Fields that did not have a clear analog to the Jamf Pro API data model. To ensure
179 | * backward compatibility with existing datasets and visualizations, these fields are
180 | * added with `null` values for all rows. See the `addUnmappedColumns` function for
181 | * detail.
182 | */
183 | UnmappedFields = {
184 | "mobileDeviceDetails.initial_entry_date_utc",
185 | "mobileDeviceDetails.location_services_enabled",
186 | "mobileDeviceDetails.attachments",
187 | "mobileDeviceDetails.lost_mode_enforced",
188 | "mobileDeviceDetails.lost_location_latitude",
189 | "mobileDeviceDetails.lost_location_longitude"
190 | }
191 | ]
192 |
--------------------------------------------------------------------------------
/JamfPro/resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 | Jamf Pro
122 |
123 |
124 | URL
125 |
126 |
127 | Jamf Pro URL
128 |
129 |
130 | The URL for your Jamf Pro instance
131 |
132 |
133 | Import data from Jamf Pro
134 |
135 |
136 | Jamf Pro
137 |
138 |
139 | Jamf Pro
140 |
141 |
142 | Username Password
143 |
144 |
145 | API Client
146 |
147 |
148 | Enter as client_id:client_secret
149 |
150 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # powerbi
2 | Jamf Power BI Integration - Custom Connector
3 |
4 | This custom connector workflow requires Jamf Pro 10.48 or higher.
5 |
6 | Refer to the [install guide pdf](./Jamf%20PowerBI%20Basic%20Installation%20Instructions%20for%20PowerBI%20Desktop.pdf)
7 | for installation and conenction settings.
8 |
9 | ### Developer set-up
10 | VS Code on Windows is the recommended development environment. Note that the Power Query SDK
11 | is required and is only available on Windows.
12 |
13 | 1. On Windows, launch Visual Studio Code.
14 | 2. Install the Power Query SDK extension from the extensions marketplace.
15 | 3. Open the folder `powerbi/JamfPro` in VS Code.
16 | 4. Select Setup workspace… from the Power Query SDK tools.
17 | 5. Go to Terminal → Run Build Task.
18 |
19 | The [`.vscode`](JamfPro/.vscode) directory includes the Power Query project environment settings and a
20 | default build task to assemble a `.mez` file and copy it to the repository's root directory. That resulting
21 | file can be placed in your 'Custom Connector' folder for use in Power BI Desktop.
22 |
23 | ### Branching strategy and contributions
24 | * `main` is protected and reflects the latest published version of the connector.
25 | * `develop` is the default branch for development. Any pull requests should be made against this branch.
26 |
27 | ### CI
28 | None yet, but a GitHub Action will be added to automate the build process. Until then, keep the `.mez`
29 | file in the repository root directory up-to-date with any code changes.
--------------------------------------------------------------------------------