├── .gitignore ├── .pipelines ├── cdpx_run_ps.cmd ├── package.ps1 ├── pipeline.user.windows.yml ├── restore.ps1 └── version.ps1 ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Capture report views ├── css │ ├── content.css │ ├── header.css │ ├── modal.css │ ├── report.css │ └── share-bookmark.css ├── img │ ├── allviews.svg │ ├── captureview.svg │ ├── contoso.svg │ ├── cross.svg │ ├── spinner.svg │ └── tickicon.svg ├── index.html ├── js │ ├── error_listener.js │ ├── globals.js │ ├── index.js │ ├── session_utils.js │ └── share_bookmark.js └── share_bookmark.html ├── Customize report colors and mode ├── css │ ├── container.css │ ├── dropdown.css │ ├── header.css │ └── report.css ├── img │ ├── contoso.svg │ └── spinner.svg ├── index.html └── js │ ├── error_listener.js │ ├── globals.js │ ├── index.js │ ├── session_utils.js │ └── themes.js ├── Go from insights to quick action ├── css │ ├── dialog_campaign_list.css │ ├── dialog_send_coupon.css │ ├── dialog_success.css │ ├── embed_area_style.css │ └── header_style.css ├── img │ ├── contoso.svg │ ├── cross.svg │ ├── spinner.svg │ └── success.svg ├── index.html └── js │ ├── error_listener.js │ ├── globals.js │ ├── index.js │ └── session_utils.js ├── LICENSE ├── Personalize top insights ├── css │ ├── content.css │ ├── dropdown.css │ ├── header.css │ └── report.css ├── img │ ├── caret.svg │ ├── colspan.svg │ ├── column1.svg │ ├── column2.svg │ ├── column3.svg │ ├── contoso.svg │ ├── rowspan.svg │ └── spinner.svg ├── index.html └── js │ ├── error_listener.js │ ├── globals.js │ ├── index.js │ └── session_utils.js ├── Quickly create and personalize visuals ├── css │ ├── content.css │ ├── header.css │ ├── popup.css │ └── report.css ├── img │ ├── close.svg │ ├── contoso.svg │ ├── disabled-images │ │ ├── center-align.svg │ │ ├── chevron.svg │ │ ├── eraser.svg │ │ ├── left-align.svg │ │ └── right-align.svg │ ├── enabled-images │ │ ├── center-align.svg │ │ ├── chevron.svg │ │ ├── eraser.svg │ │ ├── left-align.svg │ │ └── right-align.svg │ ├── pencil.svg │ └── spinner.svg ├── index.html └── js │ ├── error_listener.js │ ├── globals.js │ ├── index.js │ ├── session_utils.js │ ├── themes.js │ └── visuals.js ├── README.md ├── SECURITY.md ├── SUPPORT.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /.pipelines/cdpx_run_ps.cmd: -------------------------------------------------------------------------------- 1 | setlocal enabledelayedexpansion 2 | pushd "%~dp0\.." 3 | powershell.exe -ExecutionPolicy Unrestricted -NoProfile -WindowStyle Hidden -NonInteractive -File "%~dp0%~1" 4 | endlocal 5 | popd 6 | exit /B %ERRORLEVEL% 7 | -------------------------------------------------------------------------------- /.pipelines/package.ps1: -------------------------------------------------------------------------------- 1 | $exitCode = 0; 2 | 3 | Write-Host "start: npm pack" 4 | & npm pack 5 | Write-Host "done: npm pack" 6 | 7 | $exitCode += $LASTEXITCODE; 8 | 9 | Write-Host "start: Get content of current folder" 10 | & dir 11 | Write-Host "done: Get content of current folder" 12 | 13 | exit $exitCode -------------------------------------------------------------------------------- /.pipelines/pipeline.user.windows.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | host: 3 | os: 'windows' 4 | flavor: 'server' 5 | version: '2016' 6 | runtime: 7 | provider: 'appcontainer' 8 | image: 'cdpxwinrs5.azurecr.io/global/vse2019/16.3.7:latest' 9 | source_mode: 'map' 10 | 11 | artifact_publish_options: 12 | publish_to_legacy_artifacts: false 13 | publish_to_pipeline_artifacts: true 14 | publish_to_cloudvault_artifacts: false 15 | 16 | # Enable signing on all declared artifacts. 17 | signing_options: 18 | profile: 'external_distribution' 19 | codesign_validation_glob_pattern: 'regex|.+(?:exe|dll)$;-|*.nd.dll;-|.gdn\\**' 20 | 21 | static_analysis_options: 22 | moderncop_options: 23 | files_to_scan: 24 | - from: '.\' 25 | exclude: 26 | - '**/*.svg' 27 | include: 28 | - '**/*' 29 | 30 | policheck_options: 31 | files_to_scan: 32 | - exclude: 33 | - '**/*.svg' 34 | 35 | binskim_options: 36 | files_to_scan: 37 | - exclude: 38 | - '**/*.svg' 39 | 40 | package_sources: 41 | npm: 42 | feeds: 43 | registry: https://powerbi.pkgs.visualstudio.com/_packaging/SDK.Externals/npm/registry/ 44 | 45 | version: 46 | major: 1 # <---- Required field but not being used for 'custom' scheme 47 | minor: 0 # <---- Required field but not being used for 'custom' scheme 48 | system: 'custom' # <---- Set this to 'custom'. we will build the version using package.json in versioning commands. 49 | 50 | versioning: 51 | commands: 52 | - !!defaultcommand 53 | name: 'Set Version' 54 | arguments: 'version.ps1' 55 | command: '.pipelines\cdpx_run_ps.cmd' 56 | 57 | restore: 58 | commands: 59 | - !!defaultcommand 60 | name: 'Install NPM' 61 | arguments: 'restore.ps1' 62 | command: '.pipelines\cdpx_run_ps.cmd' 63 | 64 | # Commands to run during the packaging stage. 65 | package: 66 | commands: 67 | - !!buildcommand 68 | name: 'Package' 69 | arguments: 'package.ps1' 70 | command: '.pipelines\cdpx_run_ps.cmd' 71 | artifacts: 72 | - to: 'tgz-package' 73 | include: 74 | - "**/*.tgz" 75 | -------------------------------------------------------------------------------- /.pipelines/restore.ps1: -------------------------------------------------------------------------------- 1 | Write-Host "Start build ..." 2 | Write-Host "Global node/npm paths ..." 3 | & where.exe npm 4 | & where.exe node 5 | 6 | Write-Host "Global node version" 7 | & node -v 8 | 9 | Write-Host "Global npm version" 10 | & npm -v 11 | 12 | $exitCode = 0; 13 | 14 | Write-Host "start: try install latest npm version" 15 | & npm install npm@latest -g 16 | Write-Host "done: try install latest npm version" 17 | 18 | # Do not update $exitCode because we do not want to fail if install latest npm version fails. 19 | 20 | exit $exitCode -------------------------------------------------------------------------------- /.pipelines/version.ps1: -------------------------------------------------------------------------------- 1 | try { 2 | # package.json is in root folder, while version.ps1 runs in .pipelines folder. 3 | $version = (Get-Content "package.json") -join "`n" | ConvertFrom-Json | Select -ExpandProperty "version" 4 | $revision = $env:CDP_DEFINITION_BUILD_COUNT_DAY 5 | $buildNumber = "$version.$revision" 6 | 7 | Write-Host "Build Number is" $buildNumber 8 | 9 | [Environment]::SetEnvironmentVariable("CustomBuildNumber", $buildNumber, "User") # This will allow you to use it from env var in later steps of the same phase 10 | Write-Host "##vso[build.updatebuildnumber]${buildNumber}" # This will update build number on your build 11 | } 12 | catch { 13 | Write-Error $_.Exception 14 | exit 1; 15 | } 16 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Setup 4 | 5 | Clone the repository: 6 | ``` 7 | git clone https://github.com/microsoft/PowerBI-Embedded-Showcases 8 | ``` 9 | 10 | ## Code structure 11 | 12 | The repository has the code of the following Showcases: 13 | 1) Personalize top insights 14 | 2) Capture report views 15 | 3) Customize report colors and mode 16 | 4) Go from insights to quick action 17 | 5) Quickly create and personalize visuals 18 | 19 | Each Showcase lives in a separate folder as a standalone app. 20 | 21 | ## Build and Run 22 | 23 | The showcases are a set of plain html and javascript files. A build is not required. 24 | To run the showcase, open the index.html in the relevant folder. 25 | -------------------------------------------------------------------------------- /Capture report views/css/content.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | .rotate { 5 | animation: rotation 1s infinite linear; 6 | } 7 | 8 | @keyframes rotation { 9 | from { 10 | transform: translate(-50%, -50%) rotate(0deg); 11 | } 12 | to { 13 | transform: translate(-50%, -50%) rotate(359deg); 14 | } 15 | } 16 | 17 | #overlay { 18 | position: absolute; 19 | width: inherit; 20 | height: inherit; 21 | } 22 | 23 | #overlay #spinner { 24 | position: fixed; 25 | top: 50%; 26 | left: 50%; 27 | } 28 | 29 | #main-div { 30 | background: #FFF; 31 | visibility: hidden; 32 | } 33 | 34 | .line { 35 | border-top: 0.5px solid rgba(0, 0, 0, .2); 36 | clear: both; 37 | margin: 109px 16px 0 16px; 38 | opacity: .7; 39 | width: auto; 40 | } -------------------------------------------------------------------------------- /Capture report views/css/header.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | body { 5 | height: 100%; 6 | scrollbar-color: #A0AEB2 transparent; 7 | scrollbar-width: thin; 8 | } 9 | 10 | /* Change custom scrollbar appearance */ 11 | body::-webkit-scrollbar-track { 12 | border-radius: 10px; 13 | background-color: transparent; 14 | } 15 | 16 | body::-webkit-scrollbar { 17 | background-color: transparent; 18 | height: 10px; 19 | width: 8px; 20 | } 21 | 22 | body::-webkit-scrollbar-thumb { 23 | background-color: #A0AEB2; 24 | border-radius: 10px; 25 | height: 30px; 26 | } 27 | 28 | #wrapper { 29 | margin: 0 auto; 30 | width: 100%; 31 | } 32 | 33 | #contoso-header { 34 | align-items: center; 35 | background: #1461F8; 36 | display: flex; 37 | height: 44px; 38 | margin: 0; 39 | } 40 | 41 | #contoso { 42 | height: 41px; 43 | margin-left: 24px; 44 | padding: 5px 0; 45 | width: 74px; 46 | } -------------------------------------------------------------------------------- /Capture report views/css/modal.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | .interactive-buttons { 5 | background: #FFF; 6 | display: flex; 7 | margin-top: 44px; 8 | } 9 | 10 | .btn-action { 11 | align-items: flex-end; 12 | background: transparent; 13 | border: 1px solid #1461F8; 14 | border-radius: 4px; 15 | color: #1461F8; 16 | font: 14px/20px "Segoe UI SemiBold", segoe_ui_semibold, Tahoma, Arial; 17 | outline: none; 18 | text-align: center; 19 | } 20 | 21 | .btn-action:hover { 22 | background: rgba(20, 97, 248, 0.25); 23 | } 24 | 25 | /* For keyboard focus */ 26 | .btn-action:focus-visible { 27 | box-shadow: 0 0 0 1.5px rgba(0,123,255,.5); 28 | } 29 | 30 | /* For mouse focus */ 31 | .btn-action:focus:not(:focus-visible) { 32 | box-shadow: none; 33 | } 34 | 35 | .btn[aria-expanded="true"] { 36 | background: rgba(20, 97, 248, .15); 37 | color: #1461F8; 38 | } 39 | 40 | .btn-img { 41 | margin: -3.5px 8px 0 0; 42 | } 43 | 44 | #display-btn { 45 | margin: 16px; 46 | } 47 | 48 | #capture-btn { 49 | margin: 16px 16px 16px 0; 50 | } 51 | 52 | .showcase-checkbox-container { 53 | cursor: pointer; 54 | display: flex; 55 | font-weight: normal; 56 | height: auto; 57 | margin: 0; 58 | padding: 8px 0; 59 | position: relative; 60 | } 61 | 62 | .close-dropdown { 63 | cursor: default; 64 | display: flex; 65 | height: 28px; 66 | margin: 1.5px 1.5px 0 0; 67 | } 68 | 69 | .showcase-checkbox-container input { 70 | cursor: pointer; 71 | opacity: 0; 72 | position: absolute; 73 | } 74 | 75 | .showcase-checkmark { 76 | visibility: hidden; 77 | } 78 | 79 | .showcase-checkbox-container input:checked~.showcase-checkmark { 80 | border-color: #3E65FF; 81 | } 82 | 83 | .showcase-checkmark:after { 84 | content: ""; 85 | display: none; 86 | position: absolute; 87 | } 88 | 89 | .showcase-checkbox-container input:checked~.showcase-checkmark:after { 90 | display: block; 91 | } 92 | 93 | .showcase-checkbox-container .showcase-checkmark:after { 94 | background: #3E65FF; 95 | border-radius: 50%; 96 | height: 8px; 97 | left: 3px; 98 | top: 3px; 99 | width: 8px; 100 | } 101 | 102 | .checkbox-title { 103 | display: block; 104 | margin: 0 24px; 105 | } 106 | 107 | #close-btn { 108 | background: transparent; 109 | border: 0; 110 | margin-left: auto; 111 | outline: none; 112 | padding: 0; 113 | } 114 | 115 | #close-list-btn { 116 | height: 24px; 117 | padding: 6px; 118 | width: 32px; 119 | } 120 | 121 | #bookmarks-list { 122 | background: #FFF; 123 | box-shadow: 0 1.2px 3.6px rgba(0, 0, 0, .108), 0 6.4px 14.4px rgba(90, 61, 61, .132); 124 | height: calc(100vh - 110px); 125 | margin: 16px 0 0 -16px; 126 | opacity: .98; 127 | /* Show scroll for small screen */ 128 | overflow-y: auto; 129 | padding-top: 0; 130 | position: absolute; 131 | top: 0; 132 | transform: translate3d(16px, 50px, 0) !important; 133 | width: 228px; 134 | } 135 | 136 | #bookmarks-list::-webkit-scrollbar-track { 137 | border-radius: 10px; 138 | background-color: transparent; 139 | } 140 | 141 | #bookmarks-list::-webkit-scrollbar { 142 | background-color: transparent; 143 | height: 10px; 144 | width: 5px; 145 | } 146 | 147 | #bookmarks-list::-webkit-scrollbar-thumb { 148 | background-color: #A0AEB2; 149 | border-radius: 10px; 150 | height: 30px; 151 | } 152 | 153 | .capture-link-note { 154 | margin-bottom: 0; 155 | } 156 | 157 | .vertical-alignment-helper { 158 | display: table; 159 | height: 100%; 160 | pointer-events: none; 161 | width: 100%; 162 | } 163 | 164 | #tick-icon { 165 | display: none; 166 | margin-top: -4px; 167 | } 168 | 169 | .vertical-align-center { 170 | display: table-cell; 171 | pointer-events: none; 172 | vertical-align: middle; 173 | } 174 | 175 | .modal-content { 176 | box-shadow: 0 4.8px 14.4px rgba(0, 0, 0, 0.18), 0 25.6px 57.6px rgba(0, 0, 0, 0.22); 177 | border-radius: 2px; 178 | height: 303px; 179 | margin: 0 auto; 180 | max-width: inherit; 181 | pointer-events: all; 182 | width: inherit; 183 | } 184 | 185 | .modal-body { 186 | display: flex; 187 | padding: 0; 188 | position: relative; 189 | } 190 | 191 | .btn-modal { 192 | background: transparent; 193 | border: none; 194 | border-bottom: .5px solid rgba(0, 0, 0, .2); 195 | color: #8A8886; 196 | font: 400 14px "Segoe UI", segoe_ui_regular, Tahoma, Arial; 197 | outline: none; 198 | text-align: center; 199 | width: 50%; 200 | } 201 | 202 | .btn.active.focus, 203 | .btn.active:focus, 204 | .btn.focus, 205 | .btn.focus:active, 206 | .btn:active:focus, 207 | .btn:hover { 208 | box-shadow: none; 209 | color: #1461F8; 210 | outline: 0; 211 | outline-offset: -2px; 212 | text-decoration: none; 213 | } 214 | 215 | .btn-active { 216 | border-bottom: 2px solid #1461F8; 217 | color: #1A1A1A; 218 | font-weight: 600; 219 | } 220 | 221 | .modal-title { 222 | font: 16px "Segoe UI SemiBold", segoe_ui_semibold, Tahoma, Arial; 223 | } 224 | 225 | #close-modal-btn { 226 | border: none; 227 | height: 40px; 228 | margin: 2px 2px 0 0; 229 | opacity: 1; 230 | outline: none; 231 | width: 39px; 232 | } 233 | 234 | #btn-cross { 235 | margin: 10px; 236 | } 237 | 238 | #capture-view-div { 239 | display: none; 240 | } 241 | 242 | .btn-links { 243 | padding: 0 0 8px 0; 244 | } 245 | 246 | .btn-links:focus-visible { 247 | box-shadow: 0 0 0 2.5px rgba(0,123,255,.5); 248 | outline: none; 249 | } 250 | 251 | .btn-links:focus:not(:focus-visible) { 252 | box-shadow: none; 253 | outline: none; 254 | } 255 | 256 | #save-bookmark-btn:focus-visible { 257 | box-shadow: 0 0 0 2.5px rgba(0,123,255,.5); 258 | } 259 | 260 | #save-bookmark-btn:focus:not(:focus-visible) { 261 | box-shadow: none; 262 | } 263 | 264 | .modal-btn:focus-visible { 265 | box-shadow: 0 0 0 2.5px rgba(0,123,255,.5); 266 | } 267 | 268 | .modal-btn:focus:not(:focus-visible) { 269 | box-shadow: none; 270 | } 271 | 272 | .close:focus-visible { 273 | box-shadow: 0 0 0 2.5px rgba(0,123,255,.5); 274 | outline: none; 275 | } 276 | 277 | .close:focus:not(:focus-visible) { 278 | box-shadow: none; 279 | outline: none; 280 | } 281 | 282 | #save-btn-div { 283 | margin-top: 28px; 284 | text-align: center; 285 | } 286 | 287 | .capture-btn-div { 288 | text-align: center; 289 | } 290 | 291 | #save-bookmark-btn { 292 | background: transparent; 293 | border-radius: 4px; 294 | border: 1px solid #1461F8; 295 | color: #1461F8; 296 | font-weight: 600; 297 | margin: 5px 20px 0 5px; 298 | outline: none; 299 | padding: 6px 16px; 300 | } 301 | 302 | #save-bookmark-btn:hover { 303 | background: #1461F8; 304 | color: #FFF; 305 | } 306 | 307 | #save-bookmark-btn:active { 308 | background: rgba(20, 97, 248, 0.15);; 309 | color: #1461F8; 310 | } 311 | 312 | #copy-btn { 313 | outline: none; 314 | margin: 6px 20px 0 5px; 315 | } 316 | 317 | #copy-btn:active { 318 | background: rgba(20, 97, 248, 0.15);; 319 | color: #1461F8; 320 | } 321 | 322 | .modal-text { 323 | background: transparent; 324 | height: 32px; 325 | margin-top: 15px; 326 | outline: 0; 327 | width: 95%; 328 | } 329 | 330 | .modal-text:focus-visible { 331 | box-shadow: 0 0 0 2.5px rgba(0,123,255,.5); 332 | } 333 | 334 | .modal-text:focus:not(:focus-visible) { 335 | box-shadow: none; 336 | } 337 | 338 | #copy-link-text { 339 | border: .5px solid rgba(0, 0, 0, .2); 340 | } 341 | 342 | #copy-link-success-msg { 343 | color: #1461F8; 344 | font: 12px "Segoe UI SemiBold", segoe_ui_semibold, Tahoma, Arial; 345 | margin-top: 11px; 346 | } 347 | 348 | .view-label { 349 | margin-bottom: 0; 350 | } 351 | 352 | #viewname { 353 | border: 0; 354 | border-bottom: .5px solid rgba(0, 0, 0, .2); 355 | padding: 0; 356 | } 357 | 358 | #viewname::placeholder { 359 | font: 400 14px/20px "Segoe UI", segoe_ui_regular, Tahoma, Arial; 360 | } 361 | 362 | .copy-bookmark { 363 | background: transparent; 364 | border-radius: 4px; 365 | border: 1px solid #1461F8; 366 | color: #1461F8; 367 | font-weight: 600; 368 | margin: 0 20px; 369 | padding: 6px 16px; 370 | } 371 | 372 | .copy-bookmark:hover { 373 | background: #1461F8; 374 | color: #FFF; 375 | } 376 | 377 | .modal-header { 378 | border: none; 379 | font: 16px "Segoe UI SemiBold", segoe_ui_semibold, Tahoma, Arial; 380 | margin: -15px 0 30px 25px; 381 | padding: 0; 382 | width: 80%; 383 | } 384 | 385 | .invalid-feedback{ 386 | margin-bottom: -21px; 387 | margin-top: 5px; 388 | } 389 | 390 | .inactive-bookmark { 391 | color: #323130; 392 | font: 400 14px/20px "Segoe UI", segoe_ui_regular, Tahoma, Arial; 393 | } 394 | 395 | #content-div { 396 | color: #201F1E; 397 | font: 400 14px "Segoe UI", segoe_ui_regular, Tahoma, Arial; 398 | margin: 30px 0 40px 24px; 399 | } 400 | 401 | .active-bookmark { 402 | color: #1461F8; 403 | font: 14px/20px "Segoe UI SemiBold", segoe_ui_semibold, Tahoma, Arial; 404 | } 405 | 406 | .selected-button { 407 | background: rgba(20, 97, 248, .15); 408 | border-radius: 4px; 409 | border: 1px solid rgba(20, 96, 247, .5); 410 | font-weight: 600; 411 | margin: 0 20px; 412 | outline: none; 413 | padding: 6px 16px; 414 | } 415 | 416 | .focused { 417 | box-shadow: 0 0 0 2px rgba(0, 123, 255, .5); 418 | } -------------------------------------------------------------------------------- /Capture report views/css/report.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | #report-container { 5 | margin: 125px 16px 16px 16px; 6 | width: auto; 7 | height: calc((9/16) * 99vw); 8 | } 9 | 10 | #report-container>iframe { 11 | border: 1px solid #C4C4C4; 12 | } -------------------------------------------------------------------------------- /Capture report views/css/share-bookmark.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | body { 5 | height: 100%; 6 | scrollbar-color: #A0AEB2 transparent; 7 | scrollbar-width: thin; 8 | } 9 | 10 | /* Change custom scrollbar appearance */ 11 | body::-webkit-scrollbar-track { 12 | border-radius: 10px; 13 | background-color: transparent; 14 | } 15 | 16 | body::-webkit-scrollbar { 17 | background-color: transparent; 18 | height: 10px; 19 | width: 8px; 20 | } 21 | 22 | body::-webkit-scrollbar-thumb { 23 | background-color: #A0AEB2; 24 | border-radius: 10px; 25 | height: 30px; 26 | } 27 | 28 | #wrapper { 29 | margin: 0 auto; 30 | width: 100%; 31 | } 32 | 33 | #contoso-header { 34 | align-items: center; 35 | background: #1461F8; 36 | display: flex; 37 | height: 44px; 38 | margin: 0; 39 | } 40 | 41 | #contoso { 42 | height: 41px; 43 | margin-left: 24px; 44 | padding: 5px 0; 45 | width: 74px; 46 | } 47 | 48 | #share-bookmark { 49 | height: calc(9/16 * 95vw); 50 | visibility: hidden; 51 | width: auto; 52 | } 53 | 54 | .rotate { 55 | animation: rotation 1s infinite linear; 56 | } 57 | 58 | @keyframes rotation { 59 | from { 60 | transform: translate(-50%, -50%) rotate(0deg); 61 | } 62 | to { 63 | transform: translate(-50%, -50%) rotate(359deg); 64 | } 65 | } 66 | 67 | #overlay { 68 | position: absolute; 69 | width: inherit; 70 | height: inherit; 71 | } 72 | 73 | #overlay #spinner { 74 | position: fixed; 75 | top: 50%; 76 | left: 50%; 77 | } 78 | 79 | #bookmark-container { 80 | height: 100%; 81 | margin-top: 44px; 82 | width: 100%; 83 | } 84 | 85 | #bookmark-container>iframe { 86 | border: none; 87 | } 88 | -------------------------------------------------------------------------------- /Capture report views/img/allviews.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Capture report views/img/captureview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Capture report views/img/contoso.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Capture report views/img/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Capture report views/img/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Capture report views/img/tickicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Capture report views/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Capture Report Views 24 | 25 | 26 | 27 | 28 | 73 | 74 | 75 |
76 |

77 | contoso 78 |

79 |
80 | loader 81 |
82 |
83 |
84 |
85 | 88 | 95 |
96 |
97 | 100 |
101 |
102 | 103 |
104 |
105 |
106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /Capture report views/js/error_listener.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | window.addEventListener('error', function(event) { 7 | // Protection against cross-origin failure 8 | try { 9 | if (window.parent.playground && window.parent.playground.logShowcaseError) { 10 | window.parent.playground.logShowcaseError("CaptureReportViews", event); 11 | } 12 | } catch { } 13 | }); -------------------------------------------------------------------------------- /Capture report views/js/globals.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | // To cache report config 7 | let reportConfig = { 8 | accessToken: undefined, 9 | embedUrl: undefined, 10 | reportId: undefined, 11 | type: "report" 12 | } 13 | 14 | // To cache bookmark state 15 | let bookmarkShowcaseState = { 16 | bookmarks: null, 17 | report: null, 18 | 19 | // Next bookmark ID counter 20 | bookmarkCounter: 1 21 | }; 22 | 23 | // Cache global DOM objects 24 | const listViewsBtn = $("#display-btn"); 25 | const copyLinkSuccessMsg = $("#copy-link-success-msg"); 26 | const viewName = $("#viewname"); 27 | const tickBtn = $("#tick-btn"); 28 | const tickIcon = $("#tick-icon"); 29 | const bookmarksList = $("#bookmarks-list"); 30 | const copyBtn = $("#copy-btn"); 31 | const copyLinkText = $("#copy-link-text"); 32 | const copyLinkBtn = $("#copy-link-btn"); 33 | const saveViewBtn = $("#save-view-btn"); 34 | const captureViewDiv = $("#capture-view-div"); 35 | const saveViewDiv = $("#save-view-div"); 36 | const overlay = $("#overlay"); 37 | const bookmarksDropdown = $(".bookmarks-dropdown"); 38 | const captureModal = $("#modal-action"); 39 | const closeModal = $("#close-modal-btn"); 40 | const viewLinkBtn = $("#copy-btn"); 41 | const saveBtn = $("#save-bookmark-btn"); 42 | const closeBtn = $("#close-btn"); 43 | 44 | // Store keycode for TAB key 45 | const KEYCODE_TAB = 9; 46 | 47 | // Enum for Keys 48 | const Keys = { 49 | TAB : "Tab" 50 | } 51 | 52 | // Freezing the contents of enum object 53 | Object.freeze(Keys); 54 | 55 | // Cache CSS classes 56 | const SELECTED_BUTTON = "selected-button"; 57 | const COPY_BOOKMARK = "copy-bookmark"; 58 | const ACTIVE_BUTTON = "btn-active"; 59 | const VISIBLE = "visible"; 60 | const INVISIBLE = "invisible"; 61 | const INACTIVE_BOOKMARK = "inactive-bookmark"; 62 | const ACTIVE_BOOKMARK = "active-bookmark"; 63 | const INVALID_FIELD = "is-invalid"; 64 | const FOCUSED = "focused"; 65 | const DISPLAY = "show"; 66 | const CHECKBOX = "input[type=checkbox]"; 67 | 68 | // Store IDs of the elements 69 | const SAVE_VIEW_BUTTON_ID = "save-view-btn"; 70 | const COPY_LINK_BUTTON_ID = "copy-link-btn"; 71 | 72 | // Cache the report containers 73 | const bookmarkContainer = $("#bookmark-container").get(0); 74 | const reportContainer = $("#report-container").get(0); 75 | 76 | // Store the state for the checkbox focus 77 | let checkBoxState = null; 78 | 79 | // Cache DOM elements to use for trapping the focus inside the modal 80 | const captureModalElements = { 81 | firstElement: closeModal, 82 | lastElement: { 83 | saveView: saveBtn, 84 | copyLink: viewLinkBtn 85 | } 86 | } 87 | 88 | // Store the last active element 89 | let lastActiveElement; 90 | 91 | // Using Regex to get the id parameter from the URL 92 | const regex = new RegExp("[?&]id(=([^&#]*)|&|#|$)"); 93 | -------------------------------------------------------------------------------- /Capture report views/js/session_utils.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | // API Endpoint to get the JSON response of Embed URL, Embed Token and reportId 7 | const reportEndpoint = "https://aka.ms/CaptureViewsReportEmbedConfig"; 8 | 9 | // Set minutes before the access token should get refreshed 10 | const minutesToRefreshBeforeExpiration = 2; 11 | 12 | // Store the amount of time left for refresh token 13 | let tokenExpiration; 14 | 15 | // This function will make the AJAX request to the endpoint and get the JSON response which it will set in the sessions 16 | function populateEmbedConfigIntoCurrentSession(updateCurrentToken) { 17 | 18 | try { 19 | let configFromParentWindow = window.parent.showcases.captureReportViews; 20 | if (configFromParentWindow) { 21 | let diffMs = new Date(configFromParentWindow.expiration) - new Date(); 22 | let diffMins = Math.round(((diffMs % 86400000) % 3600000) / 60000); 23 | 24 | embedConfig = { 25 | EmbedUrl: configFromParentWindow.embedUrl, 26 | EmbedToken: { 27 | Token: configFromParentWindow.token 28 | }, 29 | Id: configFromParentWindow.id, 30 | MinutesToExpiration: diffMins, 31 | }; 32 | 33 | handleNewEmbedConfig(embedConfig, updateCurrentToken); 34 | } 35 | 36 | return; 37 | } catch (error) { 38 | console.error(error); 39 | } 40 | 41 | // This returns the JSON response 42 | return $.getJSON(reportEndpoint, function (embedConfig) { 43 | handleNewEmbedConfig(embedConfig, updateCurrentToken); 44 | }); 45 | } 46 | 47 | function handleNewEmbedConfig(embedConfig, updateCurrentToken) { 48 | 49 | // Set Embed Token, Embed URL and Report Id 50 | setConfig(embedConfig.EmbedToken.Token, embedConfig.EmbedUrl, embedConfig.Id); 51 | if (updateCurrentToken) { 52 | 53 | // Get the reference to the embedded element 54 | const reportContainer = $("#report-container").get(0); 55 | if (reportContainer) { 56 | const report = powerbi.get(reportContainer); 57 | report.setAccessToken(embedConfig.EmbedToken.Token); 58 | } 59 | 60 | // Get the reference to the embedded element 61 | const bookmarkContainer = $("#bookmark-container").get(0); 62 | if (bookmarkContainer) { 63 | const bookmarkReport = powerbi.get(bookmarkContainer); 64 | bookmarkReport.setAccessToken(embedConfig.EmbedToken.Token); 65 | } 66 | } 67 | 68 | // Get the milliseconds after token will get refreshed 69 | tokenExpiration = embedConfig.MinutesToExpiration * 60 * 1000; 70 | 71 | // Set the tokenRefresh timer to count the seconds and request the JSON again when token expires 72 | setTokenExpirationListener(); 73 | } 74 | 75 | // Check the remaining time and call the API again 76 | function setTokenExpirationListener() { 77 | 78 | const safetyInterval = minutesToRefreshBeforeExpiration * 60 * 1000; 79 | 80 | // Time until token refresh in milliseconds 81 | const timeout = tokenExpiration - safetyInterval; 82 | if (timeout <= 0) { 83 | populateEmbedConfigIntoCurrentSession(true /* updateCurrentToken */); 84 | } 85 | else { 86 | console.log(`Report Embed Token will be updated in ${timeout} milliseconds.`); 87 | setTimeout(function () { 88 | populateEmbedConfigIntoCurrentSession(true /* updateCurrentToken */) 89 | }, timeout); 90 | } 91 | } 92 | 93 | // Add a listener to make sure token is updated after tab was inactive 94 | document.addEventListener("visibilitychange", function () { 95 | // Check the access token when the tab is visible 96 | if (!document.hidden) { 97 | setTokenExpirationListener(); 98 | } 99 | }); 100 | 101 | // Get the data from the API and pass it to the front-end 102 | function loadSampleReportIntoSession() { 103 | 104 | // Call the function for the first time to call the API, set the sessions values and return the response to the front-end 105 | return populateEmbedConfigIntoCurrentSession(false /* updateCurrentToken */); 106 | } 107 | 108 | // Set the embed config in globals 109 | function setConfig(accessToken, embedUrl, reportId) { 110 | 111 | // Fill the global object 112 | reportConfig.accessToken = accessToken; 113 | reportConfig.embedUrl = embedUrl; 114 | reportConfig.reportId = reportId; 115 | } -------------------------------------------------------------------------------- /Capture report views/js/share_bookmark.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | // Set props for accessibility insights 7 | function setReportAccessibilityProps(report) { 8 | report.setComponentTitle("Playground showcase sample report"); 9 | report.setComponentTabIndex(0); 10 | } 11 | 12 | $(document).ready(function () { 13 | 14 | // Bootstrap the bookmark container 15 | powerbi.bootstrap(bookmarkContainer, reportConfig); 16 | 17 | embedSharedBookmarkReport(); 18 | }); 19 | 20 | // Embed shared report with bookmark on load 21 | async function embedSharedBookmarkReport() { 22 | 23 | // Load sample report properties into session 24 | await loadSampleReportIntoSession() 25 | 26 | // Get models. models contains enums that can be used 27 | const models = window["powerbi-client"].models; 28 | 29 | // Use View permissions 30 | let permissions = models.Permissions.View; 31 | 32 | // Get the bookmark name from url param 33 | let bookmarkName = getBookmarkNameFromURL(); 34 | 35 | // Get the bookmark state from local storage 36 | // any type of database can be used 37 | let bookmarkState = localStorage.getItem(bookmarkName); 38 | 39 | // Embed configuration used to describe the what and how to embed 40 | // This object is used when calling powerbi.embed 41 | // This also includes settings and options such as filters 42 | // You can find more information at https://github.com/Microsoft/PowerBI-JavaScript/wiki/Embed-Configuration-Details 43 | let config = { 44 | type: "report", 45 | tokenType: models.TokenType.Embed, 46 | accessToken: reportConfig.accessToken, 47 | embedUrl: reportConfig.embedUrl, 48 | id: reportConfig.reportId, 49 | permissions: permissions, 50 | settings: { 51 | panes: { 52 | filters: { 53 | visible: false 54 | }, 55 | pageNavigation: { 56 | visible: false 57 | } 58 | } 59 | }, 60 | layoutType: models.LayoutType.Custom, 61 | customLayout: { 62 | displayOption: models.DisplayOption.FitToWidth 63 | }, 64 | // Adding bookmark attribute will apply the bookmark on load 65 | bookmark: { 66 | state: bookmarkState 67 | } 68 | }; 69 | 70 | // Embed the report and display it within the div container 71 | bookmarkShowcaseState.report = powerbi.embed(bookmarkContainer, config); 72 | 73 | // For accessibility insights 74 | setReportAccessibilityProps(bookmarkShowcaseState.report); 75 | 76 | bookmarkShowcaseState.report.on("loaded", function () { 77 | 78 | // Hide the loader and display the report 79 | overlay.addClass(INVISIBLE); 80 | $("#share-bookmark").addClass(VISIBLE); 81 | bookmarkShowcaseState.report.off("loaded"); 82 | }); 83 | } 84 | 85 | // Get the bookmark name from url "id" argument 86 | function getBookmarkNameFromURL() { 87 | let url = (window.location != window.parent.location) ? 88 | document.referrer : 89 | document.location.href; 90 | 91 | const results = regex.exec(url); 92 | 93 | // It can take id parameter from the URL using ? or & 94 | // If ? or & is not in the URL, returns NULL 95 | if (!results) { return null }; 96 | 97 | // Returns Empty String if id parameter's value is not specified 98 | if (!results[2]) { return "" }; 99 | 100 | // Returns the ID of the Bookmark 101 | return decodeURIComponent(results[2]); 102 | } -------------------------------------------------------------------------------- /Capture report views/share_bookmark.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Bookmarked Report 12 | 13 | 14 | 15 |
16 |

17 | contoso 18 |

19 |
20 | 21 |
22 |
23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Customize report colors and mode/css/container.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | /* The custom loader animation to be displayed till the report is embedded */ 5 | .rotate { 6 | animation: rotation 1s infinite linear; 7 | } 8 | 9 | @keyframes rotation { 10 | from { 11 | transform: translate(-50%, -50%) rotate(0deg); 12 | } 13 | to { 14 | transform: translate(-50%, -50%) rotate(359deg); 15 | } 16 | } 17 | 18 | .container { 19 | display: flex; 20 | height: auto; 21 | padding: 0; 22 | width: 100%; 23 | } 24 | 25 | #overlay { 26 | height: inherit; 27 | position: fixed; 28 | width: inherit; 29 | } 30 | 31 | #overlay #spinner { 32 | left: 50%; 33 | position: fixed; 34 | top: 50%; 35 | } 36 | 37 | .content { 38 | background: #FAF9F8; 39 | display: none; 40 | margin: 0 auto; 41 | text-align: left; 42 | width: 100%; 43 | } 44 | 45 | .content.dark { 46 | background: #3B3A39; 47 | } 48 | 49 | .horizontal-rule { 50 | border-top: .5px solid rgba(0, 0, 0, .2); 51 | clear: both; 52 | margin: 109px 16px 0 16px; 53 | opacity: .7; 54 | width: auto; 55 | z-index: 0; 56 | } 57 | 58 | .horizontal-rule.dark { 59 | border-top: .5px solid #605E5C; 60 | } 61 | -------------------------------------------------------------------------------- /Customize report colors and mode/css/dropdown.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | /* TODO: Change font URL in the final environment */ 5 | @font-face { 6 | font-family: "segoe_ui_regular"; 7 | src: url("https://content.powerapps.com/resource/powerbiwfe/fonts/SegoeUI-Regular-final.8956d1f5b4190f537497.woff") format("woff"); 8 | } 9 | 10 | @font-face { 11 | font-family: "segoe_ui_semibold"; 12 | src: url("https://content.powerapps.com/resource/powerbiwfe/fonts/SegoeUI-SemiBold-final.83b7261d0e6f3994ed6d.woff") format("woff"); 13 | } 14 | 15 | .dropdown { 16 | background: #FAF9F8; 17 | margin-top: 44px; 18 | } 19 | 20 | .dropdown.dark { 21 | background: #3B3A39; 22 | } 23 | 24 | /* Set the background 40% black when dropdown opens */ 25 | .dropdown.show:after { 26 | opacity: 1; 27 | position: fixed; 28 | visibility: visible; 29 | } 30 | 31 | .dropdown:after { 32 | background: rgba(0, 0, 0, .4); 33 | bottom: 0; 34 | content: ''; 35 | left: 0; 36 | right: 0; 37 | top: 0; 38 | transition: opacity .15s ease-in-out; 39 | } 40 | 41 | .btn-theme { 42 | background: #FAF9F8; 43 | border: 1px solid #4A565A; 44 | border-radius: 4px; 45 | color: #4A565A; 46 | display: flex; 47 | height: 32px; 48 | margin: 16px; 49 | } 50 | 51 | .btn-theme.dark { 52 | background: #3B3A39; 53 | border: 1px solid #D9E6EA; 54 | color: #D9E6EA; 55 | } 56 | 57 | .btn-theme:hover { 58 | background: #FFF; 59 | color: #4A565A; 60 | } 61 | 62 | .btn-theme.dark:hover { 63 | background: #393939; 64 | color: #D9E6EA; 65 | } 66 | 67 | /* For Keyboard focus */ 68 | .btn-theme.dark:focus-visible { 69 | box-shadow: 0 0 4px 1.5px #D9E6EA; 70 | } 71 | 72 | /* Remove focus when mouse navigation is used */ 73 | .btn-theme.dark:focus:not(:focus-visible) { 74 | box-shadow: none; 75 | } 76 | 77 | .btn-theme:focus-visible { 78 | box-shadow: 0 0 4px 1.5px rgba(0,123,255,.5); 79 | } 80 | 81 | .btn-theme:focus:not(:focus-visible) { 82 | box-shadow: none; 83 | } 84 | 85 | .btn.active.focus, 86 | .btn.active:focus, 87 | .btn:active:focus { 88 | box-shadow: none; 89 | outline: 0; 90 | outline-offset: -2px; 91 | text-decoration: none; 92 | } 93 | 94 | .btn-theme[aria-expanded="true"] { 95 | background: #D3D6D7; 96 | color: #4A565A; 97 | } 98 | 99 | .btn-theme.dark[aria-expanded="true"] { 100 | background: #393939; 101 | color: #D9E6EA; 102 | } 103 | 104 | input[type="checkbox"]:focus-visible + span.slider.round { 105 | box-shadow: 0 0 4px 2.5px rgba(0,123,255,.5); 106 | outline: none; 107 | } 108 | 109 | input[type="checkbox"]:focus:not(:focus-visible) + span.slider.round { 110 | box-shadow: none; 111 | outline: none; 112 | } 113 | 114 | input[type="checkbox"]:focus-visible + span.slider.round.dark { 115 | box-shadow: 0 0 4px 1.5px #D9E6EA; 116 | outline: none; 117 | } 118 | 119 | input[type="checkbox"]:focus:not(:focus-visible) + span.slider.round.dark { 120 | box-shadow: none; 121 | outline: none; 122 | } 123 | 124 | input[type="radio"]:focus { 125 | outline-offset: initial; 126 | } 127 | 128 | #bucket { 129 | display: flex; 130 | margin: 1px 8px 0 4px; 131 | } 132 | 133 | .bucket-theme.dark { 134 | fill: #D9E6EA; 135 | } 136 | 137 | .theme-selector-label { 138 | cursor: pointer; 139 | display: flex; 140 | font: 14px/20px "Segoe UI SemiBold", "segoe_ui_semibold", Tahoma, Arial, Helvetica; 141 | margin-top: -1.5px; 142 | } 143 | 144 | .theme-container { 145 | background: #FFF; 146 | border: 1px solid #FFF; 147 | border-radius: 4px; 148 | box-shadow: 0 6.4px 14.4px rgba(0, 0, 0, 0.132), 0 1.2px 3.6px rgba(0, 0, 0, 0.108); 149 | box-sizing: border-box; 150 | height: 244px; 151 | margin: 10px 0 0 0; 152 | padding: 16px; 153 | width: 336px; 154 | } 155 | 156 | .theme-container.dark { 157 | background: #3B3A39; 158 | border: 1px solid #69797E; 159 | } 160 | 161 | .theme-element-container { 162 | align-items: center; 163 | box-sizing: border-box; 164 | display: flex; 165 | height: 36px; 166 | padding: 0; 167 | position: relative; 168 | } 169 | 170 | .theme-element-container input { 171 | cursor: pointer; 172 | opacity: 1; 173 | position: relative; 174 | } 175 | 176 | input[type="radio"], 177 | input[type="checkbox"] { 178 | margin: 0; 179 | } 180 | 181 | .data-color-name { 182 | color: #323130; 183 | cursor: pointer; 184 | font: 14px/20px "Segoe UI Regular", "segoe_ui_regular", Tahoma, Arial, Helvetica; 185 | margin-left: 8px; 186 | } 187 | 188 | .data-color-name.dark { 189 | color: #FFF; 190 | } 191 | 192 | .data-color { 193 | display: inline-block; 194 | height: 20px; 195 | width: 20px; 196 | } 197 | 198 | .theme-colors { 199 | border: 1px solid #717171; 200 | cursor: pointer; 201 | display: inline; 202 | height: 22px; 203 | line-height: 0; 204 | margin-left: auto; 205 | } 206 | 207 | .theme-switch-label { 208 | color: #323130; 209 | font: 14px/20px "Segoe UI SemiBold", "segoe_ui_semibold", Tahoma, Arial, Helvetica; 210 | margin-left: 6px; 211 | } 212 | 213 | .theme-switch-label.dark { 214 | color: #FFF; 215 | } 216 | 217 | /* Box around the slider checkbox */ 218 | .switch { 219 | border: 1px solid #69797E; 220 | border-radius: 34px; 221 | box-sizing: border-box; 222 | display: flex; 223 | height: 20px; 224 | margin: auto 0; 225 | margin-left: auto; 226 | position: relative; 227 | width: 40px; 228 | } 229 | 230 | /* Hide default HTML checkbox */ 231 | .switch input { 232 | height: 0; 233 | opacity: 0; 234 | width: 0; 235 | } 236 | 237 | /* The toggle slider for the theme selection */ 238 | .slider { 239 | background: #FFF; 240 | bottom: 0; 241 | cursor: pointer; 242 | left: 0; 243 | position: absolute; 244 | right: 0; 245 | top: 0; 246 | transition: .4s; 247 | -webkit-transition: .4s; 248 | } 249 | 250 | .slider:before { 251 | background: #69797E; 252 | bottom: 3px; 253 | content: ""; 254 | height: 12px; 255 | left: 3px; 256 | position: absolute; 257 | transition: .4s; 258 | width: 12px; 259 | -webkit-transition: .4s; 260 | } 261 | 262 | .slider.dark:before { 263 | background: #FFF; 264 | } 265 | 266 | input:checked+.slider.dark { 267 | background: #69797E; 268 | } 269 | 270 | input+.slider.dark { 271 | background: #69797E; 272 | } 273 | 274 | input:checked+.slider:before { 275 | -webkit-transform: translateX(20px); 276 | -ms-transform: translateX(20px); 277 | transform: translateX(20px); 278 | } 279 | 280 | /* Rounded toggle slider for the theme selection */ 281 | .slider.round { 282 | border-radius: 34px; 283 | } 284 | 285 | .slider.round:before { 286 | border-radius: 50%; 287 | } 288 | 289 | .dropdown-separator { 290 | border-top: 0.5px solid rgba(0, 0, 0, 0.2); 291 | margin: 16px 0; 292 | opacity: 0.7; 293 | } 294 | 295 | .dropdown-separator.dark { 296 | border-top: 0.5px solid #69797E; 297 | } -------------------------------------------------------------------------------- /Customize report colors and mode/css/header.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | body { 5 | background: #FAF9F8; 6 | height: 100%; 7 | scrollbar-color: #A0AEB2 transparent; 8 | scrollbar-width: thin; 9 | } 10 | 11 | body.dark { 12 | background: #3B3A39; 13 | } 14 | 15 | /* Change custom scrollbar appearance */ 16 | body::-webkit-scrollbar-track { 17 | border-radius: 10px; 18 | background-color: transparent; 19 | } 20 | 21 | body::-webkit-scrollbar { 22 | background-color: transparent; 23 | height: 10px; 24 | width: 8px; 25 | } 26 | 27 | body::-webkit-scrollbar-thumb { 28 | background-color: #A0AEB2; 29 | border-radius: 10px; 30 | height: 30px; 31 | } 32 | 33 | .wrapper { 34 | margin: 0 auto; 35 | width: 100%; 36 | } 37 | 38 | .header { 39 | align-items: center; 40 | background: #69797E; 41 | display: flex; 42 | height: 44px; 43 | margin: 0; 44 | } 45 | 46 | .contoso { 47 | height: 41px; 48 | margin-left: 24px; 49 | width: 74px; 50 | } 51 | -------------------------------------------------------------------------------- /Customize report colors and mode/css/report.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | .report-container { 5 | height: calc((9/16) * 85vw + 10px); 6 | margin: 125px 16px 16px 16px; 7 | width: auto; 8 | } 9 | 10 | .report-container > iframe { 11 | border: none; 12 | } 13 | -------------------------------------------------------------------------------- /Customize report colors and mode/img/contoso.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Customize report colors and mode/img/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Customize report colors and mode/index.html: -------------------------------------------------------------------------------- 1 |  3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Customize report colors and theme 24 | 25 | 26 | 27 |
28 |

29 | Contoso 30 |

31 |
32 | loader 33 |
34 |
35 | 44 | 45 |
46 |
47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /Customize report colors and mode/js/error_listener.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | window.addEventListener('error', function(event) { 7 | // Protection against cross-origin failure 8 | try { 9 | if (window.parent.playground && window.parent.playground.logShowcaseError) { 10 | window.parent.playground.logShowcaseError("CustomizeColors", event); 11 | } 12 | } catch { } 13 | }); -------------------------------------------------------------------------------- /Customize report colors and mode/js/globals.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | // Constants used for report configurations as key-value pair 7 | const reportConfig = { 8 | accessToken: null, 9 | embedUrl: null, 10 | reportId: null, 11 | } 12 | 13 | // Maintain the state for the showcase 14 | const themesShowcaseState = { 15 | themesArray: null, 16 | themesReport: null, 17 | }; 18 | 19 | // Declare dynamic DOM objects 20 | let themeSlider; 21 | let dataColorNameElements; 22 | let themeSwitchLabel; 23 | let horizontalSeparator; 24 | let sliderCheckbox; 25 | let allUIElements; 26 | 27 | // Cache global DOM elements 28 | const bodyElement = $("body"); 29 | const overlay = $("#overlay"); 30 | const dropdownDiv = $(".dropdown"); 31 | const themesList = $("#theme-dropdown"); 32 | const contentElement = $(".content"); 33 | const themeContainer = $(".theme-container"); 34 | const horizontalRule = $(".horizontal-rule"); 35 | const themeButton = $(".btn-theme"); 36 | const themeBucket = $(".bucket-theme"); 37 | const embedContainer = $(".report-container").get(0); 38 | 39 | // Store keycode for TAB key 40 | const KEYCODE_TAB = 9; 41 | 42 | // Enum for Keys 43 | const Keys = { 44 | TAB: "Tab" 45 | } 46 | 47 | // Freezing the contents of enum object 48 | Object.freeze(Keys); -------------------------------------------------------------------------------- /Customize report colors and mode/js/index.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | // Perform events only after DOM is loaded 7 | $(document).ready(function () { 8 | 9 | // Bootstrap the embed-container for the report embedding 10 | powerbi.bootstrap(embedContainer, { 11 | "type": "report" 12 | }); 13 | 14 | // Embed the report in the report-container 15 | embedThemesReport(); 16 | 17 | // Build Theme palette 18 | buildThemePalette(); 19 | 20 | // Cache dynamic elements to toggle the theme 21 | themeSlider = $("#theme-slider"); 22 | dataColorNameElements = $(".data-color-name"); 23 | themeSwitchLabel = $(".theme-switch-label"); 24 | horizontalSeparator = $(".dropdown-separator"); 25 | sliderCheckbox = $(".slider"); 26 | 27 | // Move the focus back to the button which triggered the dropdown 28 | dropdownDiv.on("hidden.bs.dropdown", function () { 29 | themeButton.focus(); 30 | }); 31 | 32 | // Focus on the theme slider once the dropdown opens 33 | dropdownDiv.on("shown.bs.dropdown", function () { 34 | themeSlider.focus(); 35 | }); 36 | 37 | // Get all the UI elements to toggle the dark theme 38 | allUIElements = [bodyElement, contentElement, dropdownDiv, themeContainer, themeSwitchLabel, horizontalSeparator, horizontalRule, sliderCheckbox, themeButton, themeBucket, dataColorNameElements]; 39 | }); 40 | 41 | // Close the dropdown when focus moves to Choose theme button from Toggle slider 42 | $(document).on("keydown", "#theme-slider", function (e) { 43 | 44 | // Shift + Tab 45 | if (e.shiftKey && (e.key === Keys.TAB || e.keyCode === KEYCODE_TAB)) { 46 | dropdownDiv.removeClass("show"); 47 | themesList.removeClass("show"); 48 | $(".btn-theme")[0].setAttribute("aria-expanded", false); 49 | } 50 | }); 51 | 52 | // Set properties for Accessibility insights 53 | function setReportAccessibilityProps(report) { 54 | report.setComponentTitle("Playground showcase sample Theme report"); 55 | report.setComponentTabIndex(0); 56 | } 57 | 58 | // To stop the page load on click event inside dropdown 59 | $(document).on("click", ".allow-focus", function (element) { 60 | element.stopPropagation(); 61 | }); 62 | 63 | // Embed the report 64 | async function embedThemesReport() { 65 | 66 | // Load sample report properties into session 67 | await loadThemesShowcaseReportIntoSession(); 68 | 69 | // Get models. models contains enums that can be used 70 | const models = window["powerbi-client"].models; 71 | 72 | // Get embed application token from globals 73 | const accessToken = reportConfig.accessToken; 74 | 75 | // Get embed URL from globals 76 | const embedUrl = reportConfig.embedUrl; 77 | 78 | // Get report Id from globals 79 | const embedReportId = reportConfig.reportId; 80 | 81 | // Use View permissions 82 | const permissions = models.Permissions.View; 83 | 84 | // Create new theme object 85 | let newTheme = {}; 86 | 87 | // Append the data-colors and the theme 88 | $.extend(newTheme, jsonDataColors[0], themes[0]); 89 | 90 | // Embed configuration used to describe the what and how to embed 91 | // This object is used when calling powerbi.embed 92 | // This also includes settings and options such as filters 93 | // You can find more information at https://github.com/Microsoft/PowerBI-JavaScript/wiki/Embed-Configuration-Details 94 | let config = { 95 | type: "report", 96 | tokenType: models.TokenType.Embed, 97 | accessToken: accessToken, 98 | embedUrl: embedUrl, 99 | id: embedReportId, 100 | permissions: permissions, 101 | settings: { 102 | panes: { 103 | filters: { 104 | expanded: false, 105 | visible: false 106 | }, 107 | pageNavigation: { 108 | visible: false 109 | }, 110 | }, 111 | layoutType: models.LayoutType.Custom, 112 | customLayout: { 113 | displayOption: models.DisplayOption.FitToPage 114 | }, 115 | background: models.BackgroundType.Transparent 116 | }, 117 | // Adding theme attribute to the config, will apply the light theme and default data-colors on load 118 | theme: { themeJson: newTheme }, 119 | }; 120 | 121 | // Embed the report and display it within the div container 122 | themesShowcaseState.themesReport = powerbi.embed(embedContainer, config); 123 | 124 | // For accessibility insights 125 | setReportAccessibilityProps(themesShowcaseState.themesReport); 126 | 127 | // Clear any other loaded handler events 128 | themesShowcaseState.themesReport.off("loaded"); 129 | 130 | // Report.on will add an event handler for report loaded event. 131 | themesShowcaseState.themesReport.on("loaded", function () { 132 | 133 | // Hide the loader 134 | overlay.hide(); 135 | 136 | // Show the container 137 | $(".content").show(); 138 | 139 | // Set the first data-color on the list as active 140 | themesList.find("#datacolor0").prop("checked", true); 141 | }); 142 | 143 | // Clear any other loaded handler events 144 | themesShowcaseState.themesReport.off("rendered"); 145 | 146 | // Triggers when a report is successfully embedded in UI 147 | themesShowcaseState.themesReport.on("rendered", function () { 148 | themesShowcaseState.themesReport.off("rendered"); 149 | console.log("The customize colors and mode report rendered successfully"); 150 | 151 | // Protection against cross-origin failure 152 | try { 153 | if (window.parent.playground && window.parent.playground.logShowcaseDoneRendering) { 154 | window.parent.playground.logShowcaseDoneRendering("CustomizeColors"); 155 | } 156 | } catch { } 157 | }); 158 | } 159 | 160 | // Build the theme palette 161 | function buildThemePalette() { 162 | 163 | // Create Theme switcher 164 | buildThemeSwitcher(); 165 | 166 | // Create separator 167 | buildSeparator(); 168 | 169 | // Building the data-colors list 170 | for (let i = 0; i < jsonDataColors.length; i++) { 171 | themesList.append(buildDataColorElement(i)); 172 | } 173 | } 174 | 175 | // Build the theme switcher with the theme slider 176 | function buildThemeSwitcher() { 177 | 178 | // Div wrapper element 179 | let divElement = document.createElement("div"); 180 | divElement.setAttribute("class", "theme-element-container"); 181 | divElement.setAttribute("role", "menuitem"); 182 | 183 | let spanElement = document.createElement("span"); 184 | spanElement.setAttribute("class", "theme-switch-label"); 185 | spanElement.setAttribute("id", "dark-label-text"); 186 | let textNodeElement = document.createTextNode("Dark mode"); 187 | spanElement.appendChild(textNodeElement); 188 | divElement.appendChild(spanElement); 189 | 190 | // Build the checkbox slider 191 | let checkboxElement = document.createElement("label"); 192 | checkboxElement.setAttribute("class", "switch"); 193 | checkboxElement.setAttribute("aria-labelledby", "dark-label-text"); 194 | 195 | let inputCheckboxElement = document.createElement("input"); 196 | inputCheckboxElement.setAttribute("id", "theme-slider"); 197 | inputCheckboxElement.setAttribute("type", "checkbox"); 198 | inputCheckboxElement.setAttribute("onchange", "toggleTheme()"); 199 | 200 | let sliderElement = document.createElement("span"); 201 | sliderElement.setAttribute("class", "slider round"); 202 | 203 | checkboxElement.appendChild(inputCheckboxElement); 204 | checkboxElement.appendChild(sliderElement); 205 | 206 | // Attach the checox slider to text label 207 | divElement.appendChild(checkboxElement); 208 | 209 | // Append the element to the dropdown 210 | themesList.append(divElement); 211 | } 212 | 213 | // Build horizontal separator between the theme switcher and data color elements 214 | function buildSeparator() { 215 | 216 | // Build the separator between theme-switcher and data-colors 217 | let separatorElement = document.createElement("div"); 218 | separatorElement.setAttribute("class", "dropdown-separator"); 219 | separatorElement.setAttribute("role", "separator"); 220 | themesList.append(separatorElement); 221 | } 222 | 223 | // Build data-colors list based on the JSON object 224 | function buildDataColorElement(id) { 225 | 226 | // Div wrapper element for the data-color 227 | let divElement = document.createElement("div"); 228 | divElement.setAttribute("class", "theme-element-container"); 229 | divElement.setAttribute("role", "group"); 230 | 231 | let inputElement = document.createElement("input"); 232 | inputElement.setAttribute("role", "menuitemradio"); 233 | inputElement.setAttribute("type", "radio"); 234 | inputElement.setAttribute("name", "data-color"); 235 | inputElement.setAttribute("id", "datacolor" + id); 236 | inputElement.setAttribute("aria-label", jsonDataColors[id].name + " color theme"); 237 | inputElement.setAttribute("onclick", "onDataColorWrapperClicked(this);"); 238 | divElement.appendChild(inputElement); 239 | 240 | // Text-element based on the JSON object 241 | let secondSpanElement = document.createElement("span"); 242 | secondSpanElement.setAttribute("class", "data-color-name"); 243 | secondSpanElement.setAttribute("onclick", "onDataColorWrapperClicked(this)"); 244 | 245 | let radioTitleElement = document.createTextNode(jsonDataColors[id].name); 246 | secondSpanElement.appendChild(radioTitleElement); 247 | divElement.appendChild(secondSpanElement); 248 | 249 | // Div for displaying data-colors based on the JSON object 250 | let colorsDivElement = document.createElement("div"); 251 | colorsDivElement.setAttribute("class", "theme-colors"); 252 | colorsDivElement.setAttribute("onclick", "onDataColorWrapperClicked(this)"); 253 | 254 | const dataColors = jsonDataColors[id].dataColors; 255 | for (let i = 0; i < dataColors.length; i++) { 256 | let dataColorElement = document.createElement("div"); 257 | dataColorElement.setAttribute("class", "data-color"); 258 | dataColorElement.setAttribute("style", "background:#" + dataColors[i].substr(1)); 259 | colorsDivElement.appendChild(dataColorElement); 260 | } 261 | 262 | // Add the colors div to the label element 263 | divElement.appendChild(colorsDivElement); 264 | 265 | return divElement; 266 | } 267 | 268 | // Apply the selected data-color to the report from the wrapper element 269 | function onDataColorWrapperClicked(element) { 270 | 271 | // Deselect the previously selected data-color 272 | $("input[name=data-color]:checked", "#theme-dropdown").prop("checked", false); 273 | 274 | // Set the respective data-color as active from the wrapper element 275 | $(element.parentElement.firstElementChild).prop("checked", true); 276 | 277 | // Apply the theme to the report based on the data-color and the background 278 | applyTheme(); 279 | } 280 | 281 | // Apply the theme based on the mode and the data-color selected 282 | async function applyTheme() { 283 | 284 | // Get active data-color id 285 | activeDataColorId = Number($("input[name=data-color]:checked", "#theme-dropdown")[0].getAttribute("id").slice(-1)); 286 | 287 | // Get the theme state from the slider toggle 288 | let activeThemeSlider = $("#theme-slider").get(0); 289 | 290 | // Get the index of the theme based on the state of the slider 291 | // 1 => Dark theme 292 | // 0 => Light theme 293 | const themeId = (activeThemeSlider.checked) ? 1 : 0; 294 | 295 | // Create new theme object 296 | let newTheme = {}; 297 | 298 | // Append the data-colors and the theme 299 | $.extend(newTheme, jsonDataColors[activeDataColorId], themes[themeId]); 300 | 301 | // Apply the theme to the report 302 | await themesShowcaseState.themesReport.applyTheme({ themeJson: newTheme }); 303 | } 304 | 305 | // Apply theme to the report and toggle dark theme for the UI elements 306 | async function toggleTheme() { 307 | 308 | // Apply the theme in the report 309 | await applyTheme(); 310 | 311 | // Toggle the dark theme for all UI elements 312 | toggleDarkThemeOnElements(); 313 | } 314 | 315 | // Toggle dark theme for the UI elements 316 | function toggleDarkThemeOnElements() { 317 | 318 | // Toggle theme for all the UI elements 319 | allUIElements.forEach(element => { 320 | element.toggleClass("dark"); 321 | }); 322 | } 323 | -------------------------------------------------------------------------------- /Customize report colors and mode/js/session_utils.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | // API Endpoint to get the JSON response of Embed URL, Embed Token and reportId 7 | const reportEndpoint = "https://aka.ms/ThemeReportEmbedConfig"; 8 | 9 | // Set minutes before the access token should get refreshed 10 | const minutesToRefreshBeforeExpiration = 2; 11 | 12 | // Store the amount of time left for refresh token 13 | let tokenExpiration; 14 | 15 | // This function will make the AJAX request to the endpoint and get the JSON response which it will set in the sessions 16 | function populateEmbedConfigIntoCurrentSession(updateCurrentToken) { 17 | 18 | try { 19 | let configFromParentWindow = window.parent.showcases.personalizeReportDesign; 20 | if (configFromParentWindow) { 21 | let diffMs = new Date(configFromParentWindow.expiration) - new Date(); 22 | let diffMins = Math.round(((diffMs % 86400000) % 3600000) / 60000); 23 | 24 | embedConfig = { 25 | EmbedUrl: configFromParentWindow.embedUrl, 26 | EmbedToken: { 27 | Token: configFromParentWindow.token 28 | }, 29 | Id: configFromParentWindow.id, 30 | MinutesToExpiration: diffMins, 31 | }; 32 | 33 | handleNewEmbedConfig(embedConfig, updateCurrentToken); 34 | } 35 | 36 | return; 37 | } catch (error) { 38 | console.error(error); 39 | } 40 | 41 | // This returns the JSON response 42 | return $.getJSON(reportEndpoint, function (embedConfig) { 43 | handleNewEmbedConfig(embedConfig, updateCurrentToken); 44 | }); 45 | } 46 | 47 | function handleNewEmbedConfig(embedConfig, updateCurrentToken) { 48 | 49 | // Set Embed Token, Embed URL and Report Id 50 | setConfig(embedConfig.EmbedToken.Token, embedConfig.EmbedUrl, embedConfig.Id); 51 | if (updateCurrentToken) { 52 | 53 | // Get the reference to the embedded element 54 | const reportContainer = $(".report-container").get(0); 55 | if (reportContainer) { 56 | const report = powerbi.get(reportContainer); 57 | report.setAccessToken(embedConfig.EmbedToken.Token); 58 | } 59 | } 60 | 61 | // Get the milliseconds after token will get refreshed 62 | tokenExpiration = embedConfig.MinutesToExpiration * 60 * 1000; 63 | 64 | // Set the tokenRefresh timer to count the seconds and request the JSON again when token expires 65 | setTokenExpirationListener(); 66 | } 67 | 68 | // Check the remaining time and call the API again 69 | function setTokenExpirationListener() { 70 | 71 | const safetyInterval = minutesToRefreshBeforeExpiration * 60 * 1000; 72 | 73 | // Time until token refresh in milliseconds 74 | const timeout = tokenExpiration - safetyInterval; 75 | if (timeout <= 0) { 76 | populateEmbedConfigIntoCurrentSession(true /* updateCurrentToken */); 77 | } 78 | else { 79 | console.log(`Report Embed Token will be updated in ${timeout} milliseconds.`); 80 | setTimeout(function () { 81 | populateEmbedConfigIntoCurrentSession(true /* updateCurrentToken */) 82 | }, timeout); 83 | } 84 | } 85 | 86 | // Add a listener to make sure token is updated after tab was inactive 87 | document.addEventListener("visibilitychange", function () { 88 | // Check the access token when the tab is visible 89 | if (!document.hidden) { 90 | setTokenExpirationListener(); 91 | } 92 | }); 93 | 94 | // Get the data from the API and pass it to the front-end 95 | function loadThemesShowcaseReportIntoSession() { 96 | 97 | // Call the function for the first time to call the API, set the sessions values and return the response to the front-end 98 | return populateEmbedConfigIntoCurrentSession(false /* updateCurrentToken */); 99 | } 100 | 101 | // Set the embed config in globals 102 | function setConfig(accessToken, embedUrl, reportId) { 103 | 104 | // Fill the global object 105 | reportConfig.accessToken = accessToken; 106 | reportConfig.embedUrl = embedUrl; 107 | reportConfig.reportId = reportId; 108 | } 109 | -------------------------------------------------------------------------------- /Customize report colors and mode/js/themes.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | // For report themes documentation please check https://docs.microsoft.com/en-us/power-bi/desktop-report-themes 7 | const jsonDataColors = [{ 8 | "name": "Default", 9 | "dataColors": ["#1A81FB", "#142091", "#E16338", "#5F076E", "#DA3F9D", "#6945B8", "#D3AA22", "#CF404A"], 10 | "foreground": "#252423", 11 | "background": "#FFFFFF", 12 | "tableAccent": "#B73A3A" 13 | }, 14 | { 15 | "name": "Divergent", 16 | "dataColors": ["#B73A3A", "#EC5656", "#F28A90", "#F8BCBD", "#99E472", "#23C26F", "#0AAC00", "#026645"], 17 | "foreground": "#252423", 18 | "background": "#F4F4F4", 19 | "tableAccent": "#B73A3A" 20 | }, 21 | { 22 | "name": "Executive", 23 | "dataColors": ["#3257A8", "#37A794", "#8B3D88", "#DD6B7F", "#6B91C9", "#F5C869", "#77C4A8", "#DEA6CF"], 24 | "background": "#FFFFFF", 25 | "foreground": "#9C5252", 26 | "tableAccent": "#6076B4" 27 | }, 28 | { 29 | "name": "Tidal", 30 | "dataColors": ["#094782", "#0B72D7", "#098BF5", "#54B5FB", "#71C0A7", "#57B956", "#478F48", "#326633"], 31 | "tableAccent": "#094782", 32 | "visualStyles": { 33 | "*": { 34 | "*": { 35 | "background": [{ "show": true, "transparency": 3 }], 36 | "visualHeader": [{ 37 | "foreground": { "solid": { "color": "#094782" } }, 38 | "transparency": 3 39 | }] 40 | } 41 | }, 42 | "group": { "*": { "background": [{ "show": false }] } }, 43 | "basicShape": { "*": { "background": [{ "show": false }] } }, 44 | "image": { "*": { "background": [{ "show": false }] } }, 45 | "page": { 46 | "*": { 47 | "background": [{ "transparency": 100 }], 48 | } 49 | } 50 | } 51 | } 52 | ]; 53 | 54 | const themes = [{ 55 | "background": "#FFFFFF", 56 | "visualStyles": { 57 | "*": { 58 | "*": { 59 | "border": [{ 60 | "show": true, 61 | "color": { "solid": { "color": "#FFFFFF" } }, 62 | "radius": 2 63 | }], 64 | "dropShadow": [ 65 | { 66 | "color": { 67 | "solid": { 68 | "color": "#FFFFFF" 69 | } 70 | }, 71 | "show": true, 72 | "position": "Outer", 73 | "preset": "Custom", 74 | "shadowSpread": 1, 75 | "shadowBlur": 1, 76 | "angle": 45, 77 | "shadowDistance": 1, 78 | "transparency": 95 79 | } 80 | ] 81 | } 82 | } 83 | }, 84 | }, 85 | { 86 | "background": "#252423", 87 | "foreground": "#FFFFFF", 88 | "tableAccent": "#FFFFFF", 89 | "textClasses": { 90 | "title": { 91 | "color": "#FFF", 92 | "fontFace": "Segoe UI Bold" 93 | }, 94 | }, 95 | "visualStyles": { 96 | "*": { 97 | "*": { 98 | "*": [{ 99 | "fontFamily": "Segoe UI", 100 | "color": { "solid": { "color": "#252423" } }, 101 | "labelColor": { "solid": { "color": "#FFFFFF" } }, 102 | "secLabelColor": { "solid": { "color": "#FFFFFF" } }, 103 | "titleColor": { "solid": { "color": "#FFFFFF" } }, 104 | }], 105 | "labels": [{ 106 | "color": { "solid": { "color": "#FFFFFF" } } 107 | }], 108 | "categoryLabels": [{ 109 | "color": { "solid": { "color": "#FFFFFF" } } 110 | }], 111 | "border": [{ 112 | "show": true, 113 | "color": { "solid": { "color": "#484644" } }, 114 | "radius": 2 115 | }], 116 | "dropShadow": [ 117 | { 118 | "color": { 119 | "solid": { 120 | "color": "#FFFFFF" 121 | } 122 | }, 123 | "show": true, 124 | "position": "Outer", 125 | "preset": "Custom", 126 | "shadowSpread": 1, 127 | "shadowBlur": 1, 128 | "angle": 45, 129 | "shadowDistance": 1, 130 | "transparency": 95 131 | } 132 | ] 133 | } 134 | } 135 | } 136 | } 137 | ]; 138 | -------------------------------------------------------------------------------- /Go from insights to quick action/css/dialog_campaign_list.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | html { 5 | --table-header-padding: 0 8px 8px 30px; 6 | --table-cell-padding: 6px 0 6px 30px; 7 | --name-column: 170px; 8 | --region-column: 140px; 9 | --mail-column: 270px; 10 | --phone-column: 208px; 11 | --table-cell-font: 14px "Segoe UI Regular", "segoe_ui_regular", Tahoma, Arial, Helvetica; 12 | --table-cell-color: #323130; 13 | } 14 | 15 | .dialog-insight-to-action { 16 | background: #FFF; 17 | border-radius: 2px; 18 | box-shadow: 0 25.6px 57.6px rgba(0, 0, 0, .22), 0 4.8px 14.4px rgba(0, 0, 0, .18); 19 | display: none; 20 | position: relative; 21 | height: 427px; 22 | opacity: 1; 23 | top: 25%; 24 | width: 837px; 25 | } 26 | 27 | .btn-dialog-insight-to-action { 28 | background: #FFF; 29 | border: 1px solid #669B61; 30 | border-radius: 4px; 31 | color: #669B61; 32 | cursor: pointer; 33 | font: 14px/29px "Segoe UI SemiBold", "segoe_ui_semibold", Tahoma, Arial; 34 | height: 32px; 35 | line-height: 28px; 36 | margin-right: 8px; 37 | width: 116px; 38 | } 39 | 40 | .btn-dialog-insight-to-action:hover { 41 | background: rgba(102, 155, 97, 0.15); 42 | } 43 | 44 | .btn-dialog-insight-to-action:focus-visible { 45 | box-shadow: 0 0 0 2px rgba(0,123,255,.5); 46 | outline: none; 47 | } 48 | 49 | .btn-dialog-insight-to-action:focus:not(:focus-visible) { 50 | box-shadow: none; 51 | outline: none; 52 | } 53 | 54 | .btn-dialog-insight-to-action:active { 55 | background: #669B61; 56 | color: #FFF; 57 | } 58 | 59 | #dialog-table { 60 | color: #212121; 61 | height: 288px; 62 | overflow: hidden scroll; 63 | scrollbar-color: #D2D0CE #FFF; 64 | scrollbar-width: thin; 65 | width: 837px; 66 | } 67 | 68 | #dialog-table::-webkit-scrollbar-track { 69 | border-radius: 10px; 70 | background-color: transparent; 71 | } 72 | 73 | #dialog-table::-webkit-scrollbar { 74 | background-color: transparent; 75 | height: 10px; 76 | width: 8px; 77 | } 78 | 79 | #dialog-table::-webkit-scrollbar-thumb { 80 | background-color: #D2D0CE; 81 | border-radius: 10px; 82 | height: 30px; 83 | } 84 | 85 | .table-row { 86 | border-bottom: 1px solid #F3F2F1; 87 | display: flex; 88 | flex-direction: row; 89 | justify-content: flex-start; 90 | } 91 | 92 | .table-headers { 93 | color: #323130; 94 | font: 14px "Segoe UI SemiBold", "segoe_ui_semibold", Tahoma, Arial, Helvetica; 95 | text-align: left; 96 | } 97 | 98 | #name { 99 | margin-left: 42px; 100 | padding: 0 8px 8px 16px; 101 | width: var(--name-column); 102 | } 103 | 104 | #region { 105 | padding: var(--table-header-padding); 106 | width: var(--region-column); 107 | } 108 | 109 | #mail { 110 | padding: var(--table-header-padding); 111 | width: var(--mail-column); 112 | } 113 | 114 | #phone { 115 | padding: var(--table-header-padding); 116 | width: var(--phone-column); 117 | } 118 | 119 | .table-values { 120 | color: #323130; 121 | font: 14px "Segoe UI Regular", "segoe_ui_regular", Tahoma, Arial, Helvetica; 122 | } 123 | 124 | .name-cell { 125 | padding: 6px 0 6px 16px; 126 | width: var(--name-column); 127 | } 128 | 129 | .region-cell { 130 | padding: var(--table-cell-padding); 131 | width: var(--region-column); 132 | } 133 | 134 | .mail-cell { 135 | padding: var(--table-cell-padding); 136 | width: var(--mail-column); 137 | } 138 | 139 | .phone-cell { 140 | padding: 6px 30px; 141 | width: var(--phone-column); 142 | } 143 | 144 | #distribution-dialog>.dialog-footer { 145 | background: #FFF; 146 | bottom: 0; 147 | display: flex; 148 | flex-direction: row; 149 | justify-content: flex-end; 150 | height: 70px; 151 | padding: 19px 15px 16px; 152 | } 153 | 154 | .cell-checkbox { 155 | width: fit-content; 156 | } 157 | 158 | .checkbox-element { 159 | cursor: pointer; 160 | opacity: 0; 161 | } 162 | 163 | .table-checkbox { 164 | cursor: pointer; 165 | display: inline-block; 166 | height: 34px; 167 | width: 42px; 168 | margin: 0; 169 | position: relative; 170 | } 171 | 172 | .checkbox-circle { 173 | background: #FFF; 174 | border: 1px solid #D2D0CE; 175 | border-radius: 16px; 176 | height: 18px; 177 | left: 0; 178 | margin-left: 24px; 179 | position: absolute; 180 | top: 8px; 181 | width: 18px; 182 | } 183 | 184 | input[type="checkbox"]:focus-visible + .checkbox-circle { 185 | box-shadow: 0 0 4px 1.33px rgba(0,123,255,1); 186 | } 187 | 188 | input[type="checkbox"]:focus:not(:focus-visible) + .checkbox-circle { 189 | box-shadow: none; 190 | } 191 | 192 | .table-checkbox>input:checked~.checkbox-circle { 193 | background: #669B61; 194 | border: none; 195 | } 196 | 197 | .table-checkbox>input:checked~.checkbox-checkmark { 198 | border: 0 solid #FFF; 199 | border-width: 0 2.5px 2.5px 0; 200 | height: 11px; 201 | left: 30px; 202 | position: absolute; 203 | top: 10px; 204 | transform: rotate(45deg); 205 | width: 6px; 206 | } 207 | 208 | .overflow-hidden { 209 | overflow: hidden; 210 | } -------------------------------------------------------------------------------- /Go from insights to quick action/css/dialog_send_coupon.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | .send-dialog-field { 5 | font: 14px/20px "Segoe UI SemiBold", "segoe_ui_semibold", Tahoma, Arial, Helvetica; 6 | color: #323130; 7 | height: 20px; 8 | margin: 6px 0 0 24px; 9 | position: relative; 10 | width: 392px; 11 | } 12 | 13 | #send-coupon-title { 14 | margin-top: 0; 15 | } 16 | 17 | .title { 18 | border: 1px solid #A19F9D; 19 | border-radius: 2px; 20 | box-sizing: border-box; 21 | font: 14px/20px "Segoe UI Regular", "segoe_ui_regular", Tahoma, Arial, Helvetica; 22 | color: #323130; 23 | height: 30px; 24 | margin: 6px 24px 9px; 25 | padding: 5px 10px; 26 | width: 392px; 27 | } 28 | 29 | .input-content:focus:not(:focus-visible) { 30 | box-shadow: none; 31 | outline: none; 32 | } 33 | 34 | .input-content:focus-visible { 35 | box-shadow: 0 0 0 2px rgba(0,123,255,.5); 36 | outline: none; 37 | } 38 | 39 | .send-dialog-input { 40 | border: 1px solid #A19F9D; 41 | border-radius: 2px; 42 | box-sizing: border-box; 43 | font: 14px/20px "Segoe UI Regular", "segoe_ui_regular", Tahoma, Arial, Helvetica; 44 | height: 100px; 45 | margin: 6px 24px 0; 46 | padding: 5px 10px; 47 | resize: none; 48 | width: 392px; 49 | } 50 | 51 | .btn-insight-to-action-send { 52 | background: #FFF; 53 | border: 1px solid #669B61; 54 | border-radius: 4px; 55 | color: #669B61; 56 | cursor: pointer; 57 | font: 14px/28px "Segoe UI SemiBold", "segoe_ui_semibold", Tahoma, Arial; 58 | height: 30px; 59 | text-align: center; 60 | width: 64px; 61 | } 62 | 63 | .btn-insight-to-action-send:hover { 64 | background: rgba(102, 155, 97, 0.15); 65 | } 66 | 67 | .btn-insight-to-action-send:focus-visible { 68 | box-shadow: 0 0 0 2px rgba(0,123,255,.5); 69 | outline: none; 70 | } 71 | 72 | .btn-insight-to-action-send:focus:not(:focus-visible) { 73 | box-shadow: none; 74 | outline: none; 75 | } 76 | 77 | .btn-insight-to-action-send:active { 78 | background: #669B61; 79 | color: #FFF; 80 | } 81 | 82 | #send-dialog>.dialog-footer { 83 | border: none; 84 | bottom: 0; 85 | display: flex; 86 | flex-direction: row; 87 | justify-content: flex-end; 88 | height: 64px; 89 | left: 0; 90 | padding: 15px 24px 20px; 91 | right: 0; 92 | } -------------------------------------------------------------------------------- /Go from insights to quick action/css/dialog_success.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | #image-success { 5 | height: 36px; 6 | margin: 73px 0 0 204px; 7 | width: 36px; 8 | } 9 | 10 | .text-success { 11 | color: #323130; 12 | font: 18px "Segoe UI SemiBold", "segoe_ui_semibold", Tahoma, Arial, Helvetica; 13 | margin-top: 5px; 14 | } 15 | 16 | #success-dialog { 17 | text-align: center; 18 | } 19 | 20 | #success-dialog .dialog-header{ 21 | display: flex; 22 | justify-content: flex-end; 23 | } 24 | 25 | #success-content { 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: flex-start; 29 | } -------------------------------------------------------------------------------- /Go from insights to quick action/css/embed_area_style.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | /* TODO: Change font URL in the final environment */ 5 | @font-face { 6 | font-family: "segoe_ui_regular"; 7 | src: url("https://content.powerapps.com/resource/powerbiwfe/fonts/SegoeUI-Regular-final.8956d1f5b4190f537497.woff") format("woff"); 8 | } 9 | 10 | @font-face { 11 | font-family: "segoe_ui_semibold"; 12 | src: url("https://content.powerapps.com/resource/powerbiwfe/fonts/SegoeUI-SemiBold-final.83b7261d0e6f3994ed6d.woff") format("woff"); 13 | } 14 | 15 | #main-div { 16 | display: none; 17 | height: auto; 18 | min-width: 850px; 19 | width: 100%; 20 | } 21 | 22 | #overlay { 23 | height: inherit; 24 | width: inherit; 25 | } 26 | 27 | #overlay #spinner { 28 | left: 50%; 29 | position: fixed; 30 | top: 50%; 31 | } 32 | 33 | .rotate { 34 | animation: rotation 1s infinite linear; 35 | } 36 | 37 | @keyframes rotation { 38 | from { 39 | transform: translate(-50%, -50%) rotate(0deg); 40 | } 41 | to { 42 | transform: translate(-50%, -50%) rotate(359deg); 43 | } 44 | } 45 | 46 | #dialog-mask { 47 | background: rgba(17, 16, 15, .4); 48 | bottom: 0; 49 | display: flex; 50 | flex-direction: row; 51 | justify-content: center; 52 | height: max(calc(calc(9/16) * 100vw + 60px), 100%); 53 | left: 0; 54 | min-width: 850px; 55 | position: absolute; 56 | right: 0; 57 | top: 0; 58 | } 59 | 60 | #report-container { 61 | height: calc(calc(9/16) * 99vw); 62 | margin-top: 44px; 63 | width: 100%; 64 | } 65 | 66 | #report-container>iframe { 67 | border: none; 68 | } 69 | 70 | .text-dialog-header { 71 | font: 18px/24px "Segoe UI SemiBold", "segoe_ui_semibold", Tahoma, Arial, Helvetica; 72 | height: 24px; 73 | margin-left: 24px; 74 | position: relative; 75 | } 76 | 77 | .dialog-body { 78 | position: relative; 79 | } 80 | 81 | .dialog-header { 82 | height: 40px; 83 | display: flex; 84 | flex-direction: row; 85 | justify-content: space-between; 86 | margin-top: 18px; 87 | } 88 | 89 | .wrapper-close { 90 | background: transparent; 91 | border: 0; 92 | height: 40px; 93 | margin: -9px 10px 0 0; 94 | padding: 0; 95 | width: 40px; 96 | } 97 | 98 | .wrapper-close:focus-visible { 99 | box-shadow: 0 0 0 2px rgba(0,123,255,.5); 100 | outline: none; 101 | } 102 | 103 | .wrapper-close:focus:not(:focus-visible) { 104 | box-shadow: none; 105 | outline: none; 106 | } 107 | 108 | .btn-close-distribution-dialog { 109 | cursor: pointer; 110 | height: 16px; 111 | margin: 10px; 112 | position: relative; 113 | width: 16px; 114 | } 115 | 116 | .dialog-insight-to-action-small { 117 | background: #FFF; 118 | border-radius: 2px; 119 | box-shadow: 0 25.6px 57.6px rgba(0, 0, 0, .22), 0 4.8px 14.4px rgba(0, 0, 0, .18); 120 | display: none; 121 | height: 332px; 122 | opacity: 1; 123 | position: relative; 124 | top: 25%; 125 | width: 440px; 126 | } -------------------------------------------------------------------------------- /Go from insights to quick action/css/header_style.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | body { 5 | height: 100%; 6 | scrollbar-color: #A0AEB2 #FFF; 7 | scrollbar-width: thin; 8 | } 9 | 10 | /* Change custom scrollbar appearance */ 11 | body::-webkit-scrollbar-track { 12 | border-radius: 10px; 13 | background-color: transparent; 14 | } 15 | 16 | body::-webkit-scrollbar { 17 | background-color: transparent; 18 | height: 10px; 19 | width: 8px; 20 | } 21 | 22 | body::-webkit-scrollbar-thumb { 23 | background-color: #A0AEB2; 24 | border-radius: 10px; 25 | height: 30px; 26 | } 27 | 28 | #wrapper { 29 | margin: 0 auto; 30 | min-width: 838px; 31 | width: 100%; 32 | } 33 | 34 | #contoso-header { 35 | background: #094624; 36 | display: flex; 37 | align-items: center; 38 | height: 44px; 39 | margin: 0; 40 | min-width: 850px; 41 | } 42 | 43 | .fixed-top { 44 | position: fixed; 45 | top: 0; 46 | right: 0; 47 | left: 0; 48 | z-index: 1030; 49 | } 50 | 51 | #contoso { 52 | height: 41px; 53 | margin-left: 24px; 54 | width: 74px; 55 | } -------------------------------------------------------------------------------- /Go from insights to quick action/img/contoso.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Go from insights to quick action/img/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Go from insights to quick action/img/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Go from insights to quick action/img/success.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Go from insights to quick action/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Insights to action 25 | 26 | 27 | 28 | 29 |
30 |

31 | contoso 32 |

33 |
34 | loader 35 |
36 |
37 |
38 |
39 | 54 | 75 | 86 |
87 |
88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Go from insights to quick action/js/error_listener.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | window.addEventListener('error', function(event) { 7 | // Protection against cross-origin failure 8 | try { 9 | if (window.parent.playground && window.parent.playground.logShowcaseError) { 10 | window.parent.playground.logShowcaseError("InsightToAction", event); 11 | } 12 | } catch { } 13 | }); -------------------------------------------------------------------------------- /Go from insights to quick action/js/globals.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | // Constants used for report configurations as key-value pair 7 | const reportConfig = { 8 | accessToken: null, 9 | embedUrl: null, 10 | reportId: null 11 | } 12 | 13 | // To cache report 14 | const reportShowcaseState = { 15 | report: null, 16 | data: null, 17 | allChecked: false, 18 | tooltipNextPressed: false 19 | } 20 | 21 | // Initialize and cache global DOM objects 22 | const body = $("#insight-to-action"); 23 | const embedContainer = $("#report-container").get(0); 24 | const overlay = $("#overlay"); 25 | const distributionDialog = $("#distribution-dialog"); 26 | const dialogMask = $("#dialog-mask"); 27 | const sendDialog = $("#send-dialog"); 28 | const successDialog = $("#success-dialog"); 29 | const closeBtn1 = $("#close1"); 30 | const closeBtn2 = $("#close2"); 31 | const sendCouponBtn = $("#send-coupon"); 32 | const sendDiscountBtn = $("#send-discount"); 33 | const sendMessageBtn = $("#send-message"); 34 | const successCross = $("#success-cross"); 35 | 36 | // Cache CSS Class 37 | const HIDE_OVERFLOW = "overflow-hidden"; 38 | 39 | // Check if dialog box is closed 40 | let isDialogClosed = true; 41 | 42 | // Key codes 43 | const KEYCODE_TAB = 9; 44 | const KEYCODE_ESCAPE = 27; 45 | 46 | // Enum for keys 47 | const Keys = { 48 | TAB : "Tab", 49 | ESCAPE: "Escape" 50 | } 51 | 52 | // Freezing the contents of enum object 53 | Object.freeze(Keys); 54 | 55 | // Table visual GUID 56 | const TABLE_VISUAL_GUID = "1149606f2a101953b4ba"; 57 | let tableVisual; 58 | 59 | // Icon for the custom extension 60 | const base64Icon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABMCAYAAAD6BTBNAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuNvyMY98AAAZOSURBVHhe7ZrJSxxBFMbzNyXGLbgfXaLkIgqJmoPJzV0PHqNR/4TkoBfxkIgoKEhIwBjRi6gHcb2riBclF0Er/RX9iprJN0712DNu9eBH93zz+nXNm1dL18wzpZTnFlDR4w4VPe5Q0eMOFT3uUNHjDhU97lDR4w4VPe5Q0eMOFT3uUNHjDhU97lDR4w4VPe5Q0eMOFZ8YYuy9tFDxiaC2t7dVf3+/+vnzJ14yn7RQ8RGj9vf31adPn1RJSYl68eKFPobG/NNCxUeG2t3dVQMDA6q8vFy9fPlS5eXl6eThvL29HS4wdm1aqPgIUHt7e2p4eFiVlZXpZEnSJHF4nZ+fr2ZnZ+HOYjhBxQeK2tnZ0ZWGpCFBz58/10eApCFhQJKI89BYPCeo+EDQhjHt8+fPqrS01CRHEgbktZ1IgPPW1tYwCo3vBBXvOXpM6+vr02MaEpEqSdALCgpUR0eH2tjYUCMjI8YH7/369Qvh2D2coeI9xFSazJ5Igt1FRcMRie3p6VG/f//GpTAd4/Xr18YfPqEl3ysSVLwnqIODAzN7SvcEqCpJmHTHwsJCnbTNzU1cKmbinZ+fax+57uPHj9ohMPuekaHiHWLWaRjTJGHyoXG0x7CKigrV1dWlVlZWcCmMxdRMTk6aWKjcubm5QOa+UaBijjFjGltyJIMqwpi2tbWFS8VYXBvV2Nio4wJ8AaEx30hQMQeow8NDs06TAV+Oco5uiw9cVVWlE/znzx9cCmMxU3J5eanjSfy2trZA1kb9o0DFLKHXaXj2tCvNThoSJq9x3tnZqWfP0FhMJ758+WLuAX78+BHI3DcqVIwRPREMDQ0ljGnSRTEWyWskDpXW3d3tNKZFQDU0NOh7AHx5oTHfyFDxlujHqN7eXlVZWWmSJpUlyZIPJGPa+vo6LhVjcTPi4uJCz9q4F+7/4cOHQNZG/aNCxQwwz55Yp8nYJYmSoyQTsyeWHGtra7gUxmLGwtTUlL63tCOu2VegoiN6P03WadJIJEi6JkCjAaoAY5q1TmMx40Y1NTXpdkj7QmO+GUHFG9DrNKk0O2HJ4xlAF8aYtry8jEthLGbWwOwr7cHx/fv3gayN+mcCFZP4bz9NngRwnpxAdN8M1mlZ4evXr2Y4QdvinH0FKgbcuJ+Gc5vkBEKT5CaD95meDew2xj37CvaL/54IbCRJ6RB/O7F3hbQDxD37CvYLnUDseNTU1JhxI2oy4CvVJ42/K9AOacttt+5TQcUAdXR0ZLaP0O1YspJBQ+0uynxyhbQBicQxF104FfppQirTbpg0DkijZWG8urqKS8VY3KwzMTGRUImLi4uBzH0zhYo3oI6PjxOWMag4u8viXLTi4mLV0tIiO78wFjNrXF1dmS8a7UFbQqP+mUBFR/RMjW3y6upq01AkEo2Vc1QmEioLaes5F8bixolqbm427UE7QmO+GUHFDFCnp6dqdHQ04VEOyZMjkimgMvGDTi4W2NPT0+a+aMvMzEwgc99MoOIt0U8r2IHBmIlGy8N8MqgMqUxrrw/G4maEbCZIAuOejakYI7oy5WdH+RBSmYJUKHzevXunlpaWcCmMxYyKqq+vN/dGDwmN+UaGillCrzPRzevq6swHAji3gYbZPK7KHB8fT7jXwsJCIHPfqFAxB+jKRDdPNZtLMqG9evVKj5lJP1M6c319bcZlxHz79m0ga6P+UaBijkmoTPmgQBKKI7o5wHiGdWbE2dzMxgD3QFKt9zOGineIfgLCOtPeY5TKkbFSjqherO1cKvPbt28mDvj+/Xsgc98oUPGeoGdze50pFSRJEKDJmGktjWAm3t+/f7WPfCFxbS5Q8R6iTk5OdGXi5wBJJirRTqoci4qK9JgZ/vMUpmO8efPG+MU1G1PxnqOfgDAB1dbWmjETScERCZIuDvA+/r2AyhwcHEwYY+fn5xGO3cMZKj4g1NnZmdn4lQRKkiSJAElNHkPj+IGdig8UPZsjmXgCspOIoyRQkgcNPqGxeE5Q8RGgx8xU+5lIoCTxtrMxFR8ZdD9TKtT/yTwa/+1n4vk7NOafFio+EfRsPjY2JrvnzCctVHyCwJieFip63KGixx0qetyhoscdKnrcoaLHHSp63KGixx0qetyhoscdKnrcoaLHHSp63KGixx0qetyhoscV9ewf7lUFSJ1Dr2cAAAAASUVORK5CYII=" 61 | 62 | const distributionDialogElements = { 63 | firstElement: closeBtn1, 64 | lastElement: sendDiscountBtn 65 | } 66 | 67 | const sendDialogElements = { 68 | firstElement: closeBtn2, 69 | lastElement: sendMessageBtn 70 | } 71 | 72 | const successDialogElements = { 73 | firstElement: successCross, 74 | lastElement: successCross 75 | } -------------------------------------------------------------------------------- /Go from insights to quick action/js/session_utils.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | // API Endpoint to get the JSON response of Embed URL, Embed Token and reportId 7 | const insightToActionReportEndpoint = "https://aka.ms/InsightToActionReportEmbedConfig"; 8 | 9 | // Set minutes before the access token should get refreshed 10 | const minutesToRefreshBeforeExpiration = 2; 11 | 12 | // Store the amount of time left for refresh token 13 | let tokenExpiration; 14 | 15 | // This function will make the AJAX request to the endpoint and get the JSON response which it will set in the sessions 16 | function populateEmbedConfigIntoCurrentSession(updateCurrentToken) { 17 | 18 | try { 19 | let configFromParentWindow = window.parent.showcases.insightToAction; 20 | if (configFromParentWindow) { 21 | let diffMs = new Date(configFromParentWindow.expiration) - new Date(); 22 | let diffMins = Math.round(((diffMs % 86400000) % 3600000) / 60000); 23 | 24 | embedConfig = { 25 | EmbedUrl: configFromParentWindow.embedUrl, 26 | EmbedToken: { 27 | Token: configFromParentWindow.token 28 | }, 29 | Id: configFromParentWindow.id, 30 | MinutesToExpiration: diffMins, 31 | }; 32 | 33 | handleNewEmbedConfig(embedConfig, updateCurrentToken); 34 | } 35 | 36 | return; 37 | } catch (error) { 38 | console.error(error); 39 | } 40 | 41 | // This returns the JSON response 42 | return $.getJSON(insightToActionReportEndpoint, function (embedConfig) { 43 | handleNewEmbedConfig(embedConfig, updateCurrentToken); 44 | }); 45 | } 46 | 47 | function handleNewEmbedConfig(embedConfig, updateCurrentToken) { 48 | 49 | // Set Embed Token, Embed URL and Report Id 50 | setConfig(embedConfig.EmbedToken.Token, embedConfig.EmbedUrl, embedConfig.Id); 51 | if (updateCurrentToken) { 52 | 53 | // Get the reference to the embedded element 54 | const reportContainer = $("#report-container").get(0); 55 | if (reportContainer) { 56 | const report = powerbi.get(reportContainer); 57 | report.setAccessToken(embedConfig.EmbedToken.Token); 58 | } 59 | } 60 | 61 | // Get the milliseconds after token will get refreshed 62 | tokenExpiration = embedConfig.MinutesToExpiration * 60 * 1000; 63 | 64 | // Set the tokenRefresh timer to count the seconds and request the JSON again when token expires 65 | setTokenExpirationListener(); 66 | } 67 | 68 | // Check the remaining time and call the API again 69 | function setTokenExpirationListener() { 70 | 71 | const safetyInterval = minutesToRefreshBeforeExpiration * 60 * 1000; 72 | 73 | // Time until token refresh in milliseconds 74 | const timeout = tokenExpiration - safetyInterval; 75 | if (timeout <= 0) { 76 | populateEmbedConfigIntoCurrentSession(true /* updateCurrentToken */); 77 | } 78 | else { 79 | console.log(`Report Embed Token will be updated in ${timeout} milliseconds.`); 80 | setTimeout(function () { 81 | populateEmbedConfigIntoCurrentSession(true /* updateCurrentToken */) 82 | }, timeout); 83 | } 84 | } 85 | 86 | // Add a listener to make sure token is updated after tab was inactive 87 | document.addEventListener("visibilitychange", function () { 88 | // Check the access token when the tab is visible 89 | if (!document.hidden) { 90 | setTokenExpirationListener(); 91 | } 92 | }); 93 | 94 | // Get the data from the API and pass it to the front-end 95 | function loadReportIntoSession() { 96 | 97 | // Call the function for the first time to call the API, set the sessions values and return the response to the front-end 98 | return populateEmbedConfigIntoCurrentSession(false /* updateCurrentToken */); 99 | } 100 | 101 | // Set the embed config in global object 102 | function setConfig(accessToken, embedUrl, reportId) { 103 | 104 | // Fill the global object 105 | reportConfig.accessToken = accessToken; 106 | reportConfig.embedUrl = embedUrl; 107 | reportConfig.reportId = reportId; 108 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /Personalize top insights/css/content.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | .rotate { 5 | animation: rotation 1s infinite linear; 6 | } 7 | 8 | @keyframes rotation { 9 | from { 10 | transform: translate(-50%, -50%) rotate(0deg); 11 | } 12 | to { 13 | transform: translate(-50%, -50%) rotate(359deg); 14 | } 15 | } 16 | 17 | #overlay { 18 | height: inherit; 19 | position: absolute; 20 | width: inherit; 21 | } 22 | 23 | #overlay #spinner { 24 | position: fixed; 25 | top: 50%; 26 | left: 50%; 27 | } 28 | 29 | #main-div { 30 | background: #F6F6FA; 31 | } 32 | 33 | #main-div>* { 34 | display: none; 35 | } 36 | 37 | .line { 38 | border-top: 0.5px solid rgba(0, 0, 0, 0.2); 39 | clear: both; 40 | margin: 109px 16px 0 16px; 41 | opacity: 0.7; 42 | width: auto; 43 | z-index: 0; 44 | } -------------------------------------------------------------------------------- /Personalize top insights/css/dropdown.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | /* TODO: Change font URL in the final environment */ 5 | @font-face { 6 | font-family: "segoe_ui_regular"; 7 | src: url("https://content.powerapps.com/resource/powerbiwfe/fonts/SegoeUI-Regular-final.8956d1f5b4190f537497.woff") format("woff"); 8 | } 9 | 10 | @font-face { 11 | font-family: "segoe_ui_semibold"; 12 | src: url("https://content.powerapps.com/resource/powerbiwfe/fonts/SegoeUI-SemiBold-final.83b7261d0e6f3994ed6d.woff") format("woff"); 13 | } 14 | 15 | #action-bar { 16 | margin-top: 44px; 17 | background: #F6F6FA; 18 | } 19 | 20 | .dropdown { 21 | float: left; 22 | } 23 | 24 | .btn-action[aria-expanded="true"] { 25 | background: rgba(73, 70, 198, 0.15); 26 | color: #4946C6; 27 | } 28 | 29 | .btn-action { 30 | background: #F6F6FA; 31 | border: 1px solid #4946C6; 32 | color: #4946C6; 33 | font: 14px "Segoe UI SemiBold", segoe_ui_semibold, Tahoma, Arial; 34 | margin: 16px; 35 | } 36 | 37 | .btn-action:hover { 38 | background: rgba(73, 70, 198, 0.25); 39 | color: #4946C6; 40 | } 41 | 42 | /* For key-board focus */ 43 | .btn-action:focus-visible { 44 | box-shadow: 0 0 0 1.5px #4946C6; 45 | } 46 | 47 | /* Remove focus for the mouse navigation */ 48 | .btn-action:focus:not(:focus-visible) { 49 | box-shadow: none; 50 | } 51 | 52 | .btn-util:hover > img { 53 | filter: brightness(0) invert(1); 54 | } 55 | 56 | .btn.active.focus, 57 | .btn.active:focus, 58 | .btn.focus:active, 59 | .btn:active:focus { 60 | box-shadow: none; 61 | color: #4946C6; 62 | outline: 0; 63 | outline-offset: -2px; 64 | text-decoration: none; 65 | } 66 | 67 | #choose-layouts-btn { 68 | margin-left: 0 69 | } 70 | 71 | #caret-img { 72 | margin: 0 0 0 4px; 73 | } 74 | 75 | #visuals-list { 76 | border-radius: 2px; 77 | box-shadow: 0 0.6px 1.8px rgba(0, 0, 0, 0.108), 0 3.2px 7.2px rgba(0, 0, 0, 0.132); 78 | margin: 8px 0 0 0; 79 | padding: 0; 80 | width: 246px; 81 | padding: 8px 0; 82 | } 83 | 84 | /* Display the focus on the checkbox on keyboard navigation for the visual selection */ 85 | input[type="checkbox"]:focus-visible + .checkbox-checkmark { 86 | box-shadow: 0 0 0 2.5px #4946C6; 87 | } 88 | 89 | /* Remove focus for the mouse navigation */ 90 | input[type="checkbox"]:focus:not(:focus-visible) + .checkbox-checkmark { 91 | box-shadow: none; 92 | } 93 | 94 | .checkbox-container { 95 | cursor: pointer; 96 | display: block; 97 | font-size: 16px; 98 | height: 36px; 99 | padding: 8px 8px 8px 35px; 100 | position: relative; 101 | } 102 | 103 | .checkbox-container input { 104 | cursor: pointer; 105 | opacity: 0; 106 | position: absolute; 107 | } 108 | 109 | .checkbox-checkmark { 110 | background: #FFF; 111 | border: 1px solid #C8C8EE; 112 | border-radius: 2px; 113 | height: 20px; 114 | left: 8px; 115 | position: absolute; 116 | top: 8px; 117 | transition: background-color 100ms ease; 118 | width: 20px; 119 | } 120 | 121 | .checkbox-container input:checked~.checkbox-checkmark { 122 | background: rgba(73, 70, 198, 0.3); 123 | } 124 | 125 | .checkbox-container input:checked { 126 | background: #808080; 127 | } 128 | 129 | .checkbox-checkmark:after { 130 | content: ""; 131 | display: none; 132 | position: absolute; 133 | } 134 | 135 | .checkbox-container input:checked~.checkbox-checkmark:after { 136 | display: block; 137 | } 138 | 139 | .checkbox-title { 140 | color: #323130; 141 | display: block; 142 | font: 400 14px "Segoe UI", segoe_ui_regular, Tahoma, Arial; 143 | margin-top: -1px; 144 | } 145 | 146 | .checkbox-container .checkbox-checkmark:after { 147 | border: 1px solid #4946C6; 148 | border-width: 0 1px 1px 0; 149 | height: 12px; 150 | left: 6px; 151 | padding: 2px; 152 | top: 1px; 153 | transform: rotate(45deg); 154 | width: 6px; 155 | } 156 | 157 | .checkbox-menu label { 158 | clear: both; 159 | color: #333; 160 | font-weight: 400; 161 | margin: 0; 162 | white-space: nowrap; 163 | } 164 | 165 | #layouts-list { 166 | background: #FFF; 167 | border-radius: 4px; 168 | box-shadow: 0 0.3px 0.9px rgba(0, 0, 0, 0.11), 0 1.6px 3.6px rgba(0, 0, 0, 0.13); 169 | box-sizing: border-box; 170 | height: 32px; 171 | margin: 8px 0 0 -36px; 172 | padding: 0; 173 | position: absolute; 174 | width: 180px; 175 | z-index: 1; 176 | } 177 | 178 | #layouts-list.show { 179 | display: flex; 180 | } 181 | 182 | .btn-util { 183 | align-items: center; 184 | border: 0.25px solid transparent; 185 | box-sizing: border-box; 186 | display: flex; 187 | height: 32px; 188 | justify-content: center; 189 | outline: none; 190 | padding: 0; 191 | width: 36px; 192 | } 193 | 194 | .btn-util:hover { 195 | background: rgba(73, 70, 198, 0.8); 196 | } 197 | 198 | /* For key-board focus */ 199 | .btn-util:focus-visible { 200 | border: 2px solid #4946C6; 201 | outline: none; 202 | } 203 | 204 | /* Remove focus for the mouse navigation */ 205 | .btn-util:focus:not(:focus-visible) { 206 | border: none; 207 | outline: none; 208 | } 209 | 210 | .btn-layout { 211 | background: #FFF; 212 | } 213 | 214 | .layout-img { 215 | height: 16px; 216 | width: 16px; 217 | } 218 | 219 | .active-columns-btn { 220 | background: rgba(73, 70, 198, 0.3); 221 | } -------------------------------------------------------------------------------- /Personalize top insights/css/header.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | body { 5 | background: #F6F6FA; 6 | height: 100%; 7 | scrollbar-color: #A0AEB2 transparent; 8 | scrollbar-width: thin; 9 | } 10 | 11 | /* Change custom scrollbar appearance */ 12 | body::-webkit-scrollbar-track { 13 | background-color: transparent; 14 | border-radius: 10px; 15 | } 16 | 17 | body::-webkit-scrollbar { 18 | background-color: transparent; 19 | height: 10px; 20 | width: 8px; 21 | } 22 | 23 | body::-webkit-scrollbar-thumb { 24 | background-color: #A0AEB2; 25 | border-radius: 10px; 26 | height: 30px; 27 | } 28 | 29 | #wrapper { 30 | margin: 0 auto; 31 | width: 100%; 32 | } 33 | 34 | #contoso-header { 35 | align-items: center; 36 | background: #4946C6; 37 | display: flex; 38 | height: 44px; 39 | margin: 0; 40 | } 41 | 42 | #contoso { 43 | height: 41px; 44 | margin-left: 24px; 45 | padding: 5px 0; 46 | width: 74px; 47 | } -------------------------------------------------------------------------------- /Personalize top insights/css/report.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | #report-container { 5 | display: none; 6 | height: calc((9/16) * 98vw + 20px); 7 | margin-top: 109px; 8 | width: 100%; 9 | } 10 | 11 | #report-container>iframe { 12 | border: none; 13 | } -------------------------------------------------------------------------------- /Personalize top insights/img/caret.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Personalize top insights/img/colspan.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Personalize top insights/img/column1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Personalize top insights/img/column2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Personalize top insights/img/column3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Personalize top insights/img/contoso.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Personalize top insights/img/rowspan.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Personalize top insights/img/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Personalize top insights/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Personalize Top Insights 24 | 25 | 26 | 27 |
28 |

29 | contoso 30 |

31 |
32 | loader 33 |
34 |
35 |
36 | 43 |
44 | 47 | 74 |
75 |
76 | 77 |
78 |
79 |
80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /Personalize top insights/js/error_listener.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | window.addEventListener('error', function(event) { 7 | // Protection against cross-origin failure 8 | try { 9 | if (window.parent.playground && window.parent.playground.logShowcaseError) { 10 | window.parent.playground.logShowcaseError("PersonalizeTopInsights", event); 11 | } 12 | } catch { } 13 | }); -------------------------------------------------------------------------------- /Personalize top insights/js/globals.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | // For the decision of the layout 7 | const COLUMNS = { 8 | ONE: 1, 9 | TWO: 2, 10 | THREE: 3 11 | }; 12 | 13 | // Freezing the contents of COLUMNS object 14 | Object.freeze(COLUMNS); 15 | 16 | // For the decision of two custom layout with spanning 17 | const SPAN_TYPE = { 18 | NONE: 0, 19 | ROWSPAN: 1, 20 | COLSPAN: 2 21 | }; 22 | 23 | // Freezing the contents of SPAN_TYPE object 24 | Object.freeze(SPAN_TYPE); 25 | 26 | // To give consistent margin to each visual in the custom showcase 27 | const LAYOUT_SHOWCASE = { 28 | MARGIN: 16, 29 | VISUAL_ASPECT_RATIO: 9 / 16, 30 | }; 31 | 32 | // Constants used for report configurations as key-value pair 33 | let reportConfig = { 34 | accessToken: null, 35 | embedUrl: null, 36 | reportId: null, 37 | } 38 | 39 | // Maintain the state for the showcase 40 | let layoutShowcaseState = { 41 | columns: COLUMNS.TWO, 42 | span: SPAN_TYPE.NONE, 43 | layoutVisuals: null, 44 | layoutReport: null, 45 | layoutPageName: null 46 | }; 47 | 48 | // Get models. models contain enums that can be used 49 | const models = window["powerbi-client"].models; 50 | 51 | // Cache DOM elements 52 | const visualsDropdown = $("#visuals-list"); 53 | const visualsDiv = $(".dropdown"); 54 | const layoutsDiv = $(".layouts"); 55 | const layoutsDropdown = $("#layouts-list"); 56 | const layoutButtons = $(".btn-util"); 57 | const chooseVisualsBtn = $("#choose-visuals-btn"); 58 | const chooseLayoutBtn = $("#choose-layouts-btn"); 59 | 60 | // Store keycode for TAB key 61 | const KEYCODE_TAB = 9; 62 | 63 | const Keys = { 64 | TAB : "Tab" 65 | } 66 | 67 | // Freezing the contents of enum object 68 | Object.freeze(Keys); 69 | 70 | // Store id for the first visual 71 | let firstVisualId; 72 | 73 | // Store id for first button 74 | const firstButtonId = $(".btn-util")[0].id; 75 | 76 | // Cache the report containers 77 | const reportContainer = $("#report-container").get(0); -------------------------------------------------------------------------------- /Personalize top insights/js/session_utils.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | // API Endpoint to get the JSON response of Embed URL, Embed Token and reportId 7 | const layoutShowcaseReportEndpoint = "https://aka.ms/layoutReportEmbedConfig"; 8 | 9 | // Set minutes before the access token should get refreshed 10 | const minutesToRefreshBeforeExpiration = 2; 11 | 12 | // Store the amount of time left for refresh token 13 | let tokenExpiration; 14 | 15 | // This function will make the AJAX request to the endpoint and get the JSON response which it will set in the sessions 16 | function populateEmbedConfigIntoCurrentSession(updateCurrentToken) { 17 | 18 | try { 19 | let configFromParentWindow = window.parent.showcases.personalizeTopInsights; 20 | if (configFromParentWindow) { 21 | let diffMs = new Date(configFromParentWindow.expiration) - new Date(); 22 | let diffMins = Math.round(((diffMs % 86400000) % 3600000) / 60000); 23 | 24 | embedConfig = { 25 | EmbedUrl: configFromParentWindow.embedUrl, 26 | EmbedToken: { 27 | Token: configFromParentWindow.token 28 | }, 29 | Id: configFromParentWindow.id, 30 | MinutesToExpiration: diffMins, 31 | }; 32 | 33 | handleNewEmbedConfig(embedConfig, updateCurrentToken); 34 | } 35 | 36 | return; 37 | } catch (error) { 38 | console.error(error); 39 | } 40 | 41 | // This returns the JSON response 42 | return $.getJSON(layoutShowcaseReportEndpoint, function (embedConfig) { 43 | handleNewEmbedConfig(embedConfig, updateCurrentToken); 44 | }); 45 | } 46 | 47 | function handleNewEmbedConfig(embedConfig, updateCurrentToken) { 48 | 49 | // Set Embed Token, Embed URL and Report Id 50 | setConfig(embedConfig.EmbedToken.Token, embedConfig.EmbedUrl, embedConfig.Id); 51 | if (updateCurrentToken) { 52 | 53 | // Get the reference to the embedded element 54 | const reportContainer = $("#report-container").get(0); 55 | if (reportContainer) { 56 | const report = powerbi.get(reportContainer); 57 | report.setAccessToken(embedConfig.EmbedToken.Token); 58 | } 59 | } 60 | 61 | // Get the milliseconds after token will get refreshed 62 | tokenExpiration = embedConfig.MinutesToExpiration * 60 * 1000; 63 | 64 | // Set the tokenRefresh timer to count the seconds and request the JSON again when token expires 65 | setTokenExpirationListener(); 66 | } 67 | 68 | // Check the remaining time and call the API again 69 | function setTokenExpirationListener() { 70 | 71 | const safetyInterval = minutesToRefreshBeforeExpiration * 60 * 1000; 72 | 73 | // Time until token refresh in milliseconds 74 | const timeout = tokenExpiration - safetyInterval; 75 | if (timeout <= 0) { 76 | populateEmbedConfigIntoCurrentSession(true /* updateCurrentToken */); 77 | } 78 | else { 79 | console.log(`Report Embed Token will be updated in ${timeout} milliseconds.`); 80 | setTimeout(function () { 81 | populateEmbedConfigIntoCurrentSession(true /* updateCurrentToken */) 82 | }, timeout); 83 | } 84 | } 85 | 86 | // Add a listener to make sure token is updated after tab was inactive 87 | document.addEventListener("visibilitychange", function () { 88 | // Check the access token when the tab is visible 89 | if (!document.hidden) { 90 | setTokenExpirationListener(); 91 | } 92 | }); 93 | 94 | // Get the data from the API and pass it to the front-end 95 | function loadLayoutShowcaseReportIntoSession() { 96 | 97 | // Call the function for the first time to call the API, set the sessions values and return the response to the front-end 98 | return populateEmbedConfigIntoCurrentSession(false /* updateCurrentToken */); 99 | } 100 | 101 | // Set the embed config in global object 102 | function setConfig(accessToken, embedUrl, reportId) { 103 | 104 | // Fill the global object 105 | reportConfig.accessToken = accessToken; 106 | reportConfig.embedUrl = embedUrl; 107 | reportConfig.reportId = reportId; 108 | } -------------------------------------------------------------------------------- /Quickly create and personalize visuals/css/content.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | * { 5 | user-select: none; 6 | } 7 | 8 | /* The custom loader animation to be displayed till the report is embedded */ 9 | .rotate { 10 | animation: rotation 1s infinite linear; 11 | } 12 | 13 | @keyframes rotation { 14 | from { 15 | transform: translate(-50%, -50%) rotate(0deg); 16 | } 17 | to { 18 | transform: translate(-50%, -50%) rotate(359deg); 19 | } 20 | } 21 | 22 | .container { 23 | display: flex; 24 | height: auto; 25 | padding: 0; 26 | width: 100%; 27 | } 28 | 29 | #overlay { 30 | height: inherit; 31 | position: fixed; 32 | width: inherit; 33 | } 34 | 35 | #overlay #spinner { 36 | left: 50%; 37 | position: fixed; 38 | top: 50%; 39 | } 40 | 41 | .content { 42 | height: auto; 43 | min-width: 850px; 44 | width: 100%; 45 | } 46 | 47 | .content > * { 48 | display: none; 49 | } 50 | -------------------------------------------------------------------------------- /Quickly create and personalize visuals/css/header.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | body { 5 | background: #F7F8F6; 6 | height: 100%; 7 | min-width: 850px; 8 | overflow-x: auto !important; 9 | scrollbar-color: #A0AEB2 transparent; 10 | scrollbar-width: thin; 11 | } 12 | 13 | /* Change custom scrollbar appearance */ 14 | body::-webkit-scrollbar-track { 15 | background-color: transparent; 16 | border-radius: 10px; 17 | } 18 | 19 | body::-webkit-scrollbar { 20 | background-color: transparent; 21 | height: 10px; 22 | width: 8px; 23 | } 24 | 25 | body::-webkit-scrollbar-thumb { 26 | background-color: #A0AEB2; 27 | border-radius: 10px; 28 | height: 30px; 29 | } 30 | 31 | .wrapper { 32 | background: #F7F8F6; 33 | margin: 0 auto; 34 | width: 100%; 35 | } 36 | 37 | .header { 38 | align-items: center; 39 | background: #1B81EF; 40 | display: flex; 41 | height: 44px; 42 | margin: 0; 43 | } 44 | 45 | .contoso { 46 | height: 41px; 47 | margin-left: 24px; 48 | width: 74px; 49 | } 50 | -------------------------------------------------------------------------------- /Quickly create and personalize visuals/css/popup.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | /* TODO: Change font URL in the final environment */ 5 | @font-face { 6 | font-family: "segoe_ui_regular"; 7 | src: url("https://content.powerapps.com/resource/powerbiwfe/fonts/SegoeUI-Regular-final.8956d1f5b4190f537497.woff") format("woff"); 8 | } 9 | 10 | @font-face { 11 | font-family: "segoe_ui_semibold"; 12 | src: url("https://content.powerapps.com/resource/powerbiwfe/fonts/SegoeUI-SemiBold-final.83b7261d0e6f3994ed6d.woff") format("woff"); 13 | } 14 | 15 | *, *:hover{ 16 | outline: none; 17 | } 18 | 19 | body.modal-open { 20 | padding-right: 0 !important; 21 | } 22 | 23 | .fade { 24 | transition: opacity .25s linear; 25 | } 26 | 27 | @media screen and (max-width: 850px) { 28 | .modal { 29 | padding: 0 !important; 30 | position: absolute; 31 | width: 850px; 32 | } 33 | } 34 | 35 | .modal { 36 | scrollbar-color: #A0AEB2 transparent; 37 | scrollbar-width: thin; 38 | } 39 | 40 | /* Change custom scrollbar appearance */ 41 | .modal::-webkit-scrollbar-track { 42 | background-color: transparent; 43 | border-radius: 10px; 44 | } 45 | 46 | .modal::-webkit-scrollbar { 47 | background-color: transparent; 48 | height: 10px; 49 | width: 8px; 50 | } 51 | 52 | .modal::-webkit-scrollbar-thumb { 53 | background-color: #A0AEB2; 54 | border-radius: 10px; 55 | height: 30px; 56 | } 57 | 58 | .modal-backdrop.show { 59 | height: 100%; 60 | width: 100%; 61 | } 62 | 63 | .modal.show { 64 | display: flex !important; 65 | } 66 | 67 | .modal-container { 68 | align-items: center; 69 | border-radius: 2px 2px 0 0; 70 | display: flex; 71 | height: 647px; 72 | margin: auto; 73 | width: 784px; 74 | } 75 | 76 | .modal.modal-static .modal-dialog { 77 | transform: none; 78 | } 79 | 80 | .modal-content { 81 | height: 647px; 82 | } 83 | 84 | .modal-dialog { 85 | max-width: inherit; 86 | } 87 | 88 | .modal-body { 89 | display: flex; 90 | margin: 0 24px; 91 | padding: 0; 92 | } 93 | 94 | #visual-creator-options { 95 | box-sizing: border-box; 96 | margin-top: -9px; 97 | padding-right: 23px; 98 | width: 279px; 99 | } 100 | 101 | #visual-preview { 102 | background: #F2F5F8; 103 | border-radius: 4px; 104 | box-sizing: border-box; 105 | display: flex; 106 | height: 499px; 107 | width: 460px; 108 | } 109 | 110 | .authoring-area { 111 | background: #FFF; 112 | border: 1px solid #FFF; 113 | border-radius: 2px; 114 | box-sizing: border-box; 115 | box-shadow: 0 1.6px 3.6px rgba(0, 0, 0, 0.13), 0 0.3px 0.9px rgba(0, 0, 0, 0.11); 116 | display: flex; 117 | height: 252px; 118 | margin: auto; 119 | width: 426px; 120 | } 121 | 122 | #edit-image { 123 | display: block; 124 | margin: auto; 125 | } 126 | 127 | .select-items > div:focus { 128 | background-color: #E3E8EE; 129 | } 130 | 131 | .btn-create { 132 | align-items: center; 133 | background: #51ACF5; 134 | border: 1px solid #51ACF5; 135 | border-radius: 4px; 136 | color: #FDFDFD; 137 | display: inline-flex; 138 | font: 14px/20px "Segoe UI SemiBold", "segoe_ui_semibold", Tahoma, Arial, Helvetica; 139 | height: 32px; 140 | justify-content: space-around; 141 | margin: 0 4px 0 0; 142 | width: 94px; 143 | } 144 | 145 | .btn-create:hover { 146 | background: #3992F2; 147 | color: #FDFDFD; 148 | outline: none; 149 | } 150 | 151 | .btn-create:disabled { 152 | background: transparent; 153 | border: 1px solid #C3D4E1; 154 | color: #C3D4E1; 155 | } 156 | 157 | .btn-create:active { 158 | background: #1B81EF; 159 | } 160 | 161 | .btn-create:focus-visible { 162 | box-shadow: 0 0 4px 1.5px rgba(0,123,255,0.5); 163 | } 164 | 165 | .btn-create:focus:not(:focus-visible) { 166 | box-shadow: none; 167 | } 168 | 169 | .btn.active.focus, 170 | .btn.active:focus, 171 | .btn.focus:active, 172 | .btn:active:focus { 173 | box-shadow: none; 174 | outline: 0; 175 | outline-offset: -2px; 176 | text-decoration: none; 177 | } 178 | 179 | .modal-header { 180 | border: 0; 181 | padding: 22px 24px 33px 24px; 182 | } 183 | 184 | #close-modal { 185 | display: flex; 186 | margin-top: -14px; 187 | opacity: 1; 188 | outline: none; 189 | padding: 20px; 190 | } 191 | 192 | #close-modal:focus-visible { 193 | box-shadow: 0 0 0 2.5px rgba(0,123,255,.5); 194 | } 195 | 196 | #close-modal:focus:not(:focus-visible) { 197 | box-shadow: none; 198 | } 199 | 200 | .modal-footer { 201 | border: 0; 202 | padding: 8px 19px 24px 12px; 203 | } 204 | 205 | .modal-title { 206 | color: #1A1A1A; 207 | font: 20px/28px "Segoe UI SemiBold", "segoe_ui_semibold", Tahoma, Arial, Helvetica; 208 | margin-top: -5px; 209 | } 210 | 211 | .select-wrapper, .toggle-wrapper { 212 | display: flex; 213 | height: 32px; 214 | justify-content: space-between; 215 | margin-top: 5px; 216 | } 217 | 218 | .toggle-wrapper.disabled { 219 | pointer-events: none; 220 | } 221 | 222 | .custom-title-wrapper { 223 | align-items: center; 224 | display: flex; 225 | justify-content: space-between; 226 | } 227 | 228 | .custom-title-wrapper.disabled input[type="text"] { 229 | pointer-events: none; 230 | } 231 | 232 | /* Select Menu */ 233 | .styled-select { 234 | appearance: none; 235 | background: url('../img/enabled-images/chevron.svg') transparent no-repeat calc(100% - 10px) !important; 236 | border: 1px solid #ACACAC; 237 | border-radius: 2px; 238 | box-sizing: border-box; 239 | cursor: pointer; 240 | font: 14px/20px "Segoe UI Regular", "segoe_ui_regular", Tahoma, Arial, Helvetica; 241 | position: relative; 242 | width: 256px; 243 | -webkit-appearance: none; 244 | -moz-appearance: none; 245 | } 246 | 247 | .styled-select select { 248 | display: none; /*hide original SELECT element: */ 249 | } 250 | 251 | .generator-disabled { 252 | pointer-events: none; 253 | } 254 | 255 | .inline-select-text, .inline-toggle-text { 256 | color: #201F1E; 257 | font: 14px/20px "Segoe UI Regular", "segoe_ui_regular", Tahoma, Arial, Helvetica; 258 | line-height: 32px; 259 | } 260 | 261 | .inline-select { 262 | appearance: none; 263 | background: url('../img/enabled-images/chevron.svg') transparent no-repeat calc(100% - 10px) !important; 264 | border: 1px solid #ACACAC; 265 | border-radius: 2px; 266 | box-sizing: border-box; 267 | cursor: pointer; 268 | margin-left: 8px; 269 | width: 199px; 270 | -webkit-appearance: none; 271 | -moz-appearance: none; 272 | } 273 | 274 | #generator-fields, #generator-properties, .title-wrapper { 275 | margin-top: 22px; 276 | } 277 | 278 | /* Toggle Button */ 279 | .inline-toggle-text { 280 | width: auto; 281 | } 282 | 283 | /* The switch - the box around the slider */ 284 | .switch { 285 | height: 20px; 286 | margin: 7px 0 7px 16px; 287 | position: relative; 288 | width: 40px; 289 | } 290 | 291 | /* Hide default HTML checkbox */ 292 | .switch input { 293 | height: 0; 294 | opacity: 0; 295 | width: 0; 296 | } 297 | 298 | /* The slider */ 299 | .slider { 300 | border: 1px solid #54ADF4; 301 | background-color: #EDEFF0; 302 | bottom: 0; 303 | cursor: pointer; 304 | height: 20px; 305 | left: 0; 306 | position: absolute; 307 | right: 0; 308 | top: 0; 309 | transition: .4s; 310 | -webkit-transition: .4s; 311 | } 312 | 313 | .slider:before { 314 | background-color: #54ADF4; 315 | bottom: 3px; 316 | content: ""; 317 | height: 12px; 318 | left: 3px; 319 | position: absolute; 320 | top: 19%; 321 | transition: .4s; 322 | width: 12px; 323 | -webkit-transition: .4s; 324 | } 325 | 326 | input[type="checkbox"]:focus-visible + .slider.round { 327 | box-shadow: 0 0 4px 2.5px rgba(0,123,255,0.5); 328 | } 329 | 330 | input[type="checkbox"]:focus:not(:focus-visible) + .slider.round { 331 | box-shadow: none; 332 | } 333 | 334 | .alignment-block:focus-visible { 335 | box-shadow: 0 0 0 2.5px #118DFF; 336 | outline: none; 337 | } 338 | 339 | .alignment-block:focus:not(:focus-visible) { 340 | box-shadow: none; 341 | outline: none; 342 | } 343 | 344 | .slider.disabled-sliders:before { 345 | background: #BAC3C8; 346 | border: 0; 347 | top: 20%; 348 | } 349 | 350 | input:checked+.slider.disabled-sliders { 351 | background: #EDEFF0; 352 | border: 0; 353 | } 354 | 355 | input+.slider.disabled-sliders { 356 | background: #EDEFF0; 357 | border: 0; 358 | } 359 | 360 | input:checked + .slider { 361 | background-color: #2196F3; 362 | border: 0; 363 | } 364 | 365 | input:checked + .slider:before { 366 | background-color: #FFF; 367 | top: 20%; 368 | transform: translateX(22px); 369 | -webkit-transform: translateX(22px); 370 | -ms-transform: translateX(22px); 371 | } 372 | 373 | /* Rounded sliders */ 374 | .slider.round { 375 | border-radius: 40px; 376 | } 377 | 378 | .slider.round:before { 379 | border-radius: 50%; 380 | } 381 | 382 | #alignment-blocks-wrapper { 383 | height: 32px; 384 | margin-top: 4px; 385 | } 386 | 387 | #alignment-blocks-wrapper img { 388 | margin: auto; 389 | } 390 | 391 | .alignment-block { 392 | background: transparent; 393 | border: none; 394 | display: inline-flex; 395 | height: 25px; 396 | margin-right: 6px; 397 | padding: 1px 4px; 398 | user-select: none; 399 | width: 25px; 400 | } 401 | 402 | .alignment-block.selected { 403 | background: #BAC3C8; 404 | } 405 | 406 | .cursor { 407 | cursor: pointer; 408 | } 409 | 410 | .title-alignment-wrapper { 411 | display: flex; 412 | justify-content: space-between; 413 | margin-top: 10px; 414 | } 415 | 416 | #visual-title { 417 | border: 1px solid #A19F9D; 418 | border-radius: 2px; 419 | font: 14px/20px "Segoe UI Regular", "segoe_ui_regular", Tahoma, Arial, Helvetica; 420 | height: 32px; 421 | margin-top: 8px; 422 | padding-left: 8px; 423 | width: 206px; 424 | } 425 | 426 | #visual-title:focus { 427 | box-shadow: 0 0 0 2.5px rgba(0,123,255,.5); 428 | outline: none; 429 | } 430 | 431 | #visual-title::placeholder { 432 | color: #A19F9D; 433 | font-size: 14px; 434 | vertical-align: middle; 435 | } 436 | 437 | .eraser-button { 438 | background: transparent; 439 | border: none; 440 | margin-top: 5px; 441 | } 442 | 443 | #erase-tool-enabled { 444 | cursor: pointer; 445 | display: flex; 446 | margin-left: 8px; 447 | } 448 | 449 | #erase-tool-disabled { 450 | display: flex; 451 | margin-left: 8px; 452 | } 453 | 454 | .generator-options { 455 | color: #201F1E; 456 | font: 14px/20px "Segoe UI SemiBold", "segoe_ui_semibold", Tahoma, Arial, Helvetica; 457 | } 458 | 459 | #erase-tool-enabled { 460 | display: none; 461 | } 462 | 463 | #erase-tool-enabled:focus-visible { 464 | box-shadow: 0 0 0 2.5px #118DFF; 465 | outline: none; 466 | } 467 | 468 | #erase-tool-enabled:focus:not(:focus-visible) { 469 | box-shadow: none; 470 | outline: none; 471 | } 472 | 473 | #aligns-enabled { 474 | display: none; 475 | } 476 | 477 | /* Style the items (options), including the selected item: */ 478 | .select-selected, .select-items div { 479 | color: #000; 480 | cursor: pointer; 481 | padding: 5px 10px; 482 | } 483 | 484 | .select-selected { 485 | border-radius: 2px; 486 | } 487 | 488 | .select-selected:focus-visible{ 489 | box-shadow: 0 0 0 2.5px rgba(0,123,255,.5); 490 | } 491 | 492 | .select-selected:focus:not(:focus-visible){ 493 | box-shadow: none; 494 | } 495 | 496 | .select-items { 497 | background: #FFFFFF; 498 | box-shadow: 0 3.2px 7.2px rgba(0, 0, 0, 0.132), 0 0.6px 1.8px rgba(0, 0, 0, 0.108); 499 | border-radius: 2px; 500 | left: 0; 501 | position: absolute; 502 | right: 0; 503 | top: 100%; 504 | z-index: 99; 505 | } 506 | 507 | /* Hide the items when the select box is closed */ 508 | .select-hide { 509 | display: none; 510 | } 511 | 512 | .select-items div:hover, .same-as-selected { 513 | background-color: #E3E8EE; 514 | } 515 | 516 | .same-as-selected { 517 | background: #F2F5F8; 518 | font-weight: 600; 519 | } 520 | 521 | div.generator-type-disabled span { 522 | color: #BEBBB8; 523 | } 524 | 525 | div.generator-type-disabled div.select-selected { 526 | color: #BEBBB8; 527 | } 528 | 529 | div.generator-type-disabled div.styled-select { 530 | background: url('../img/disabled-images/chevron.svg') transparent no-repeat calc(100% - 10px) !important; 531 | } 532 | 533 | div.generator-fields-disabled span { 534 | color: #BEBBB8; 535 | } 536 | 537 | div.generator-fields-disabled div.select-selected { 538 | color: #BEBBB8; 539 | } 540 | 541 | div.generator-fields-disabled div.inline-select { 542 | background: url('../img/disabled-images/chevron.svg') transparent no-repeat calc(100% - 10px) !important; 543 | } 544 | 545 | div.generator-properties-disabled span { 546 | color: #BEBBB8; 547 | } -------------------------------------------------------------------------------- /Quickly create and personalize visuals/css/report.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | .report-container { 5 | background: #F7F8F6; 6 | height: calc((9/16) * 98vw + 20px); /* Remove the top empty space of the report and maintain 16:9 aspect ratio */ 7 | margin: 44px 16px 16px 16px; 8 | width: calc(100% - 33px); /* Position the report-container in center in all resolutions */ 9 | } 10 | 11 | .report-container > iframe { 12 | border: none; 13 | } 14 | 15 | #visual-authoring-container > iframe { 16 | border: none; 17 | } 18 | -------------------------------------------------------------------------------- /Quickly create and personalize visuals/img/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Quickly create and personalize visuals/img/contoso.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Quickly create and personalize visuals/img/disabled-images/center-align.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Quickly create and personalize visuals/img/disabled-images/chevron.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Quickly create and personalize visuals/img/disabled-images/eraser.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Quickly create and personalize visuals/img/disabled-images/left-align.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Quickly create and personalize visuals/img/disabled-images/right-align.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Quickly create and personalize visuals/img/enabled-images/center-align.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Quickly create and personalize visuals/img/enabled-images/chevron.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Quickly create and personalize visuals/img/enabled-images/eraser.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Quickly create and personalize visuals/img/enabled-images/left-align.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Quickly create and personalize visuals/img/enabled-images/right-align.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Quickly create and personalize visuals/img/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Quickly create and personalize visuals/img/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Quickly create and personalize visuals/js/error_listener.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | window.addEventListener('error', function(event) { 7 | // Protection against cross-origin failure 8 | try { 9 | if (window.parent.playground && window.parent.playground.logShowcaseError) { 10 | window.parent.playground.logShowcaseError("QuickCreate", event); 11 | } 12 | } catch { } 13 | }); -------------------------------------------------------------------------------- /Quickly create and personalize visuals/js/globals.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | const visualCreatorShowcaseState = { 7 | report: null, 8 | page: null, // The page from where the 3x3 visuals will be displayed 9 | newVisual: null, // New visual to be created on the page for the base-report 10 | visualType: null, 11 | dataRoles: { 12 | Legend: null, 13 | Values: null, 14 | Axis: null, 15 | Tooltips: null, 16 | }, 17 | dataFieldsCount: 0, 18 | properties: { 19 | legend: true, 20 | xAxis: true, 21 | yAxis: true, 22 | title: true, 23 | titleText: null, 24 | titleAlign: null 25 | }, 26 | } 27 | 28 | const selectedVisual = { 29 | visual: null, 30 | } 31 | 32 | const baseReportState = { 33 | report: null, 34 | visuals: null, 35 | page: null 36 | } 37 | 38 | const VISUAL_CREATOR_SHOWCASE = { 39 | COLUMNS: 3, 40 | MARGIN: 16, 41 | VISUAL_ASPECT_RATIO: 9 / 16 42 | } 43 | 44 | // Distance between the action button and the image visual inside the custom visual 45 | const DISTANCE = 18; 46 | 47 | // Constants used for report configurations as key-value pair 48 | const reportConfig = { 49 | accessToken: null, 50 | embedUrl: null, 51 | reportId: null, 52 | } 53 | 54 | // Visual overlapping 55 | const MAIN_VISUAL_GUID = "a6d74a71de4135e00a59"; 56 | 57 | const imageVisual = { 58 | name: "2270e4eea9242400a0cd", 59 | yPos: undefined, 60 | height: undefined, 61 | ratio: { 62 | widthRatioWithMainVisual: 36 / 426, 63 | heightRatioWithMainVisual: 36 / 252, 64 | xPositionRatioWithMainVisual: 195 / 426, 65 | yPositionRatioWithMainVisual: 90 / 252 66 | } 67 | } 68 | 69 | const actionButtonVisual = { 70 | name: "946862f32d49b6573406", 71 | height: 32, 72 | width: 151, 73 | } 74 | 75 | // Cache DOM Elements 76 | const overlay = $("#overlay"); 77 | const visualDisplayArea = $("#visual-authoring-container").get(0); 78 | const editArea = $("#edit-area"); 79 | const visualAuthoringArea = $("#visual-authoring-container"); 80 | const visualTypeDropdown = $("#selected-value-0"); 81 | const createVisualButton = $("#create-visual-btn"); 82 | const generatorType = $("#generator-type"); 83 | const generatorFields = $("#generator-fields"); 84 | const generatorProperties = $("#generator-properties"); 85 | const disabledEraseTool = $("#erase-tool-disabled"); 86 | const enabledEraseTool = $("#erase-tool-enabled"); 87 | const disabledAligns = $("#aligns-disabled"); 88 | const enabledAligns = $("#aligns-enabled"); 89 | const visualCreatorModal = $("#visual-creator"); 90 | const visualTitleText = $("#visual-title"); 91 | const legendToggle = $("#legend-toggle"); 92 | const xAxisToggle = $("#xAxis-toggle"); 93 | const yAxisToggle = $("#yAxis-toggle"); 94 | const titleToggle = $("#title-toggle"); 95 | const alignRight = $("#align-right"); 96 | const closeModalButton = $("#close-modal"); 97 | const alignLeft = $("#align-left"); 98 | const reportContainer = $(".report-container").get(0); 99 | const customTitleWrapper = $(".custom-title-wrapper"); 100 | const alignmentBlocks = $(".alignment-block"); 101 | const visualPropertiesCheckboxes = $(".property-checkbox"); 102 | const toggleWrappers = $(".toggle-wrapper"); 103 | const togglePropertiesSliders = $(".slider"); 104 | 105 | // Cache showcasePropertiesLength 106 | const showcasePropertiesLength = showcaseProperties.length; 107 | 108 | // Get models. models contain enums that can be used 109 | const models = window["powerbi-client"].models; 110 | 111 | // CSS Classes 112 | const DISABLED = "generator-disabled"; 113 | const HIDE = "select-hide"; 114 | const TYPES_DISABLED = "generator-type-disabled"; 115 | const FIELDS_DISABLED = "generator-fields-disabled"; 116 | const PROPERTIES_DISABLED = "generator-properties-disabled"; 117 | const SELECTED = "selected"; 118 | const SAME_AS_SELECTED = "same-as-selected"; 119 | const TOGGLE_WRAPPERS_DISABLED = "disabled"; 120 | const DISABLED_SLIDERS = "disabled-sliders"; 121 | const TYPE_DROPDOWN_ID = "selected-value-0"; 122 | 123 | // Key codes 124 | const KEYCODE_TAB = 9; 125 | const KEYCODE_ENTER = 13; 126 | const KEYCODE_ESCAPE = 27; 127 | const KEYCODE_SPACE = 32; 128 | 129 | // enum for keys 130 | const Keys = { 131 | TAB: "Tab", 132 | SPACE: "Space", 133 | ENTER: "Enter", 134 | ESCAPE: "Escape" 135 | } 136 | 137 | // Freezing the contents for enum object 138 | Object.freeze(Keys); 139 | 140 | // Store the position of the main visual [basicShape] 141 | let mainVisualState; 142 | 143 | // Get the reference for the iframe inside the modal to remove it from the tab-order 144 | let authoringiFrame; 145 | 146 | // Custom title for the visual 147 | let customVisualTitle = ""; 148 | 149 | // To store the state of the visual creation 150 | let visualCreationInProgress = false; 151 | 152 | // To apply setting to the new visual created in the Modal 153 | const visualHeaderReportSetting = { 154 | visualSettings: { 155 | visualHeaders: [ 156 | { 157 | settings: { 158 | visible: false 159 | } 160 | } 161 | ] 162 | } 163 | } 164 | 165 | // Headers 166 | const VISUAL_TYPE_HEADER = "Select visual type"; -------------------------------------------------------------------------------- /Quickly create and personalize visuals/js/session_utils.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | // API Endpoint to get the JSON response of Embed URL, Embed Token and reportId 7 | const quickVisualCreatorReportEndpoint = "https://aka.ms/QuickVisualCreatorReportEmbedConfig"; 8 | 9 | // Set minutes before the access token should get refreshed 10 | const minutesToRefreshBeforeExpiration = 2; 11 | 12 | // Store the amount of time left for refresh token 13 | let tokenExpiration; 14 | 15 | // This function will make the AJAX request to the endpoint and get the JSON response which it will set in the sessions 16 | function populateEmbedConfigIntoCurrentSession(updateCurrentToken) { 17 | 18 | try { 19 | let configFromParentWindow = window.parent.showcases.quickVisualCreator; 20 | if (configFromParentWindow) { 21 | let diffMs = new Date(configFromParentWindow.expiration) - new Date(); 22 | let diffMins = Math.round(((diffMs % 86400000) % 3600000) / 60000); 23 | 24 | embedConfig = { 25 | EmbedUrl: configFromParentWindow.embedUrl, 26 | EmbedToken: { 27 | Token: configFromParentWindow.token 28 | }, 29 | Id: configFromParentWindow.id, 30 | MinutesToExpiration: diffMins, 31 | }; 32 | 33 | handleNewEmbedConfig(embedConfig, updateCurrentToken); 34 | } 35 | 36 | return; 37 | } catch (error) { 38 | console.error(error); 39 | } 40 | 41 | // This returns the JSON response 42 | return $.getJSON(quickVisualCreatorReportEndpoint, function (embedConfig) { 43 | handleNewEmbedConfig(embedConfig, updateCurrentToken); 44 | }); 45 | } 46 | 47 | function handleNewEmbedConfig(embedConfig, updateCurrentToken) { 48 | 49 | // Set Embed Token, Embed URL and Report Id 50 | setConfig(embedConfig.EmbedToken.Token, embedConfig.EmbedUrl, embedConfig.Id); 51 | if (updateCurrentToken) { 52 | 53 | // Get the reference to the embedded element 54 | const reportContainer = $(".report-container").get(0); 55 | if (reportContainer) { 56 | const report = powerbi.get(reportContainer); 57 | report.setAccessToken(embedConfig.EmbedToken.Token); 58 | } 59 | 60 | // Get the reference to the embedded element 61 | const visualDisplayArea = $("#visual-authoring-container").get(0); 62 | if (visualDisplayArea) { 63 | const visualAuthoringReport = powerbi.get(visualDisplayArea); 64 | visualAuthoringReport.setAccessToken(embedConfig.EmbedToken.Token); 65 | } 66 | } 67 | 68 | // Get the milliseconds after token will get refreshed 69 | tokenExpiration = embedConfig.MinutesToExpiration * 60 * 1000; 70 | 71 | // Set the tokenRefresh timer to count the seconds and request the JSON again when token expires 72 | setTokenExpirationListener(); 73 | } 74 | 75 | // Check the remaining time and call the API again 76 | function setTokenExpirationListener() { 77 | 78 | const safetyInterval = minutesToRefreshBeforeExpiration * 60 * 1000; 79 | 80 | // Time until token refresh in milliseconds 81 | const timeout = tokenExpiration - safetyInterval; 82 | if (timeout <= 0) { 83 | populateEmbedConfigIntoCurrentSession(true /* updateCurrentToken */); 84 | } 85 | else { 86 | console.log(`Report Embed Token will be updated in ${timeout} milliseconds.`); 87 | setTimeout(function () { 88 | populateEmbedConfigIntoCurrentSession(true /* updateCurrentToken */) 89 | }, timeout); 90 | } 91 | } 92 | 93 | // Add a listener to make sure token is updated after tab was inactive 94 | document.addEventListener("visibilitychange", function () { 95 | // Check the access token when the tab is visible 96 | if (!document.hidden) { 97 | setTokenExpirationListener(); 98 | } 99 | }); 100 | 101 | // Get the data from the API and pass it to the front-end 102 | function loadQuickVisualCreatorReportConfigIntoSession() { 103 | 104 | // Call the function for the first time to call the API, set the sessions values and return the response to the front-end 105 | return populateEmbedConfigIntoCurrentSession(false /* updateCurrentToken */); 106 | } 107 | 108 | // Set the embed config in global object 109 | function setConfig(accessToken, embedUrl, reportId) { 110 | 111 | // Fill the global object 112 | reportConfig.accessToken = accessToken; 113 | reportConfig.embedUrl = embedUrl; 114 | reportConfig.reportId = reportId; 115 | } -------------------------------------------------------------------------------- /Quickly create and personalize visuals/js/themes.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | const theme = { 7 | "name": "visualsTheme", 8 | "dataColors": [ 9 | "#118dff", 10 | "#12239e", 11 | "#e66c37", 12 | "#6B007B", 13 | "#E044A7", 14 | "#744EC2", 15 | "#D9B300", 16 | "#D64550", 17 | "#000000" 18 | ], 19 | "visualStyles": { 20 | "*": { 21 | "*": { 22 | "dropShadow": [ 23 | { 24 | "color": { 25 | "solid": { 26 | "color": "#000000" 27 | } 28 | }, 29 | "show": true, 30 | "position": "Outer", 31 | "preset": "Custom", 32 | "shadowSpread": 1, 33 | "shadowBlur": 3, 34 | "angle": 45, 35 | "shadowDistance": 1, 36 | "transparency": 87 37 | } 38 | ] 39 | }, 40 | }, 41 | "image": { 42 | "*": { 43 | "dropShadow": [ 44 | { 45 | "show": false, 46 | } 47 | ] 48 | }, 49 | }, 50 | "actionButton": { 51 | "*": { 52 | "dropShadow": [ 53 | { 54 | "show": false, 55 | } 56 | ] 57 | }, 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /Quickly create and personalize visuals/js/visuals.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | // ---------------------------------------------------------------------------- 5 | 6 | // Define the available data roles for the visual types 7 | const visualTypeToDataRoles = [ 8 | { name: "columnChart", displayName: "Column chart", dataRoles: ["Axis", "Values", "Tooltips"], dataRoleNames: ["Category", "Y", "Tooltips"] }, 9 | { name: "areaChart", displayName: "Area chart", dataRoles: ["Axis", "Legend", "Values"], dataRoleNames: ["Category", "Series", "Y"] }, 10 | { name: "barChart", displayName: "Bar chart", dataRoles: ["Axis", "Values", "Tooltips"], dataRoleNames: ["Category", "Y", "Tooltips"] }, 11 | { name: "pieChart", displayName: "Pie chart", dataRoles: ["Legend", "Values", "Tooltips"], dataRoleNames: ["Category", "Y", "Tooltips"] }, 12 | { name: "lineChart", displayName: "Line chart", dataRoles: ["Axis", "Legend", "Values"], dataRoleNames: ["Category", "Series", "Y"] }, 13 | ]; 14 | 15 | // Define the available fields for each data role 16 | const dataRolesToFields = [ 17 | { dataRole: "Axis", Fields: ["Industry", "Opportunity Status", "Lead Rating", "Salesperson"] }, 18 | { dataRole: "Values", Fields: ["Actual Revenue", "Estimated Revenue", "Number of Opportunities", "Salesperson"] }, 19 | { dataRole: "Legend", Fields: ["Industry", "Lead Rating", "Opportunity Status", "Salesperson"] }, 20 | { dataRole: "Tooltips", Fields: ["Industry", "Actual Close Date", "Actual Revenue", "Estimated Revenue"] }, 21 | ]; 22 | 23 | // Define schemas for visuals API 24 | const schemas = { 25 | column: "http://powerbi.com/product/schema#column", 26 | measure: "http://powerbi.com/product/schema#measure", 27 | property: "http://powerbi.com/product/schema#property", 28 | default: "http://powerbi.com/product/schema#default", 29 | }; 30 | 31 | // Define mapping from fields to target table and column/measure 32 | const dataFieldsTargets = { 33 | ActualRevenue: { column: "Actual Revenue", table: "QVC Report", schema: schemas.column }, 34 | NumberofOpportunities: { measure: "Number of Opportunities", table: "QVC Report", schema: schemas.measure }, 35 | Salesperson: { column: "Salesperson", table: "QVC Report", schema: schemas.column }, 36 | EstimatedRevenue: { column: "Estimated Revenue", table: "QVC Report", schema: schemas.column }, 37 | OpportunityStatus: { column: "Opportunity Status", table: "QVC Report", schema: schemas.column }, 38 | Industry: { column: "Industry", table: "QVC Report", schema: schemas.column }, 39 | LeadRating: { column: "Lead Rating", table: "QVC Report", schema: schemas.column }, 40 | Salesperson: { column: "Salesperson", table: "QVC Report", schema: schemas.column }, 41 | ActualCloseDate: { column: "Actual Close Date", table: "QVC Report", schema: schemas.column }, 42 | }; 43 | 44 | const dataFieldsMappings = { 45 | ActualRevenue: "Actual Revenue", 46 | NumberofOpportunities: "Number of Opportunities", 47 | Salesperson: "Salesperson", 48 | EstimatedRevenue: "Estimated Revenue", 49 | OpportunityStatus: "Opportunity Status", 50 | Industry: "Industry", 51 | LeadRating: "Lead Rating", 52 | Salesperson: "Salesperson", 53 | ActualCloseDate: "Actual Close Date" 54 | } 55 | 56 | // Define the available properties 57 | const showcaseProperties = ["legend", "xAxis", "yAxis"]; 58 | 59 | // Define title related properties 60 | const titleProperties = ["title", "titleText", "titleAlign"]; 61 | 62 | const visualTypeProperties = { 63 | columnChart: ["xAxis", "yAxis"], 64 | areaChart: ["legend", "xAxis", "yAxis"], 65 | barChart: ["xAxis", "yAxis"], 66 | pieChart: ["legend"], 67 | lineChart: ["legend", "xAxis", "yAxis"] 68 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | Showcases for Power BI Embedded analytics playground 3 | 4 | # Contents 5 | Playground Showcases available in this package currently: 6 | 1. Personalize top insights 7 | 2. Capture report views 8 | 3. Customize report colors and mode 9 | 4. Go from insights to quick action 10 | 5. Quickly create and personalize visuals 11 | 12 | ## Contributing 13 | 14 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 15 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 16 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 17 | 18 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 19 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 20 | provided by the bot. You will only need to do this once across all repos using our CLA. 21 | 22 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 23 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 24 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 25 | 26 | ## Trademarks 27 | 28 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 29 | trademarks or logos is subject to and must follow 30 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 31 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 32 | Any use of third-party trademarks or logos are subject to those third-party's policies. 33 | 34 | ## Support 35 | 36 | - **Feature Requests:** Submit your ideas and suggestions to the [Fabric Ideas Portal](https://ideas.fabric.microsoft.com/), where you can also vote on ideas from other developers. 37 | - **Bug Reports and Technical Assistance:** Visit the [Fabric Developer Community Forum](https://community.fabric.microsoft.com/t5/Developer/bd-p/Developer). Our team and community experts are ready to assist you. 38 | - **Additional Support:** Contact your account manager or reach out to the [Fabric Support Team](https://support.fabric.microsoft.com/en-us/support/). 39 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? 4 | 5 | - **No CSS support:** Fill out this template with information about how to file issues and get help. 6 | - **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). 7 | - **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide. 8 | 9 | *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* 10 | 11 | # Support 12 | 13 | ## How to file issues and get help 14 | 15 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 16 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 17 | feature request as a new Issue. 18 | 19 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 20 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 21 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 22 | 23 | ## Microsoft Support Policy 24 | 25 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@powerbi/playground-showcases", 3 | "version": "2.5.2", 4 | "description": "Showcases for the Power BI Embedded playground", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://powerbi.visualstudio.com/DefaultCollection/Embedded/_git/playground-showcases" 8 | }, 9 | "author": "Microsoft", 10 | "files": [ 11 | "Personalize top insights", 12 | "Capture report views", 13 | "Customize report colors and mode", 14 | "Go from insights to quick action", 15 | "Quickly create and personalize visuals" 16 | ] 17 | } 18 | --------------------------------------------------------------------------------