├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── docs ├── common │ └── images │ │ ├── call_details.png │ │ ├── cover.png │ │ ├── installing.png │ │ ├── invite_link.png │ │ └── join_call.png ├── how-to-install-nodejs-and-npm │ ├── README.md │ └── images │ │ └── node_version.png ├── how-to-run-the-solution-in-azure │ ├── README.md │ └── images │ │ ├── CORS.png │ │ ├── build.png │ │ ├── build_finished.png │ │ ├── connect_storage_explorer.png │ │ ├── connection_string.png │ │ ├── open_console.png │ │ ├── redirect_uri.png │ │ ├── static_website.png │ │ ├── storage_explorer.png │ │ ├── upload_build.png │ │ └── web_portal_login.png ├── how-to-run-the-solution-locally │ ├── README.md │ └── images │ │ ├── starting_webportal.png │ │ └── webportal_running.png └── how-to-use-the-solution │ ├── README.md │ └── images │ ├── bot-service-status-deprovisioned.png │ ├── bot-service-status-provisioned.png │ ├── call-details-view-change-protocol.png │ ├── call-details-view-extraction-card-main-stream-expanded.png │ ├── call-details-view-extraction-card-main-stream.png │ ├── call-details-view-extraction-card-participant-stream-expanded.png │ ├── call-details-view-extraction-card-participant-stream.png │ ├── call-details-view-extraction-stream-advance.png │ ├── call-details-view-extraction-stream-rtmp.png │ ├── call-details-view-extraction-stream.png │ ├── call-details-view-info.png │ ├── call-details-view-injection-card-expanded.png │ ├── call-details-view-injection-card.png │ ├── call-details-view-injection-stream.png │ ├── call-details-view-select-protocol.png │ ├── call-details-view-stream-key.png │ ├── call-details-view-streams-sections.png │ ├── call-details-view.png │ ├── join-call.png │ ├── joining-call.png │ ├── login-page.png │ └── login-popup.png ├── package-lock.json ├── package.json ├── public ├── config.json ├── favicon.png ├── index.html ├── manifest.json ├── robots.txt └── routes.json ├── src ├── App.tsx ├── hooks │ └── useInterval.tsx ├── images │ └── logo.png ├── index.css ├── index.tsx ├── middlewares │ └── errorToastMiddleware.ts ├── models │ ├── auth │ │ └── types.ts │ ├── calls │ │ └── types.ts │ ├── error │ │ ├── helpers.ts │ │ └── types.ts │ ├── service │ │ └── types.ts │ └── toast │ │ └── types.ts ├── react-app-env.d.ts ├── sample.md ├── services │ ├── api │ │ └── index.ts │ ├── auth │ │ └── index.ts │ ├── helpers.ts │ └── store │ │ ├── IAppState.ts │ │ ├── index.ts │ │ └── mock.ts ├── setupEnzyme.ts ├── stores │ ├── auth │ │ ├── actions.ts │ │ ├── asyncActions.ts │ │ └── reducer.ts │ ├── base │ │ ├── BaseAction.ts │ │ └── BaseReducer.ts │ ├── calls │ │ ├── actions │ │ │ ├── disconnectCall.ts │ │ │ ├── getActiveCalls.ts │ │ │ ├── getCall.ts │ │ │ ├── index.ts │ │ │ ├── joinCall.ts │ │ │ ├── muteBot.ts │ │ │ ├── newInjectionStreamDrawer.ts │ │ │ ├── newStreamDrawer.ts │ │ │ ├── refreshStreamKey.ts │ │ │ ├── startInjectionStream.ts │ │ │ ├── startStream.ts │ │ │ ├── stopInjectionStream.ts │ │ │ ├── stopStream.ts │ │ │ ├── unmuteBot.ts │ │ │ ├── updateDefaults.ts │ │ │ └── updateStreamPhoto.ts │ │ ├── asyncActions.ts │ │ ├── reducer.ts │ │ └── selectors.ts │ ├── config │ │ ├── actions.ts │ │ ├── asyncActions.ts │ │ ├── constants.ts │ │ ├── loader.ts │ │ ├── reducer.ts │ │ └── types.ts │ ├── error │ │ ├── actions.ts │ │ └── reducer.ts │ ├── requesting │ │ ├── reducer.ts │ │ └── selectors.ts │ ├── service │ │ ├── actions.ts │ │ ├── asyncActions.ts │ │ └── reducer.ts │ └── toast │ │ ├── actions.ts │ │ └── reducer.ts └── views │ ├── call-details │ ├── CallDetails.tsx │ ├── components │ │ ├── CallInfo.css │ │ ├── CallInfo.tsx │ │ ├── CallSelector.tsx │ │ ├── CallStreams.css │ │ ├── CallStreams.tsx │ │ ├── InjectionCard.css │ │ ├── InjectionCard.tsx │ │ ├── NewInjectionStreamDrawer.css │ │ ├── NewInjectionStreamDrawer.tsx │ │ ├── NewStreamDrawer.css │ │ ├── NewStreamDrawer.tsx │ │ ├── StreamCard.css │ │ └── StreamCard.tsx │ └── types.ts │ ├── components │ ├── Footer.tsx │ ├── Header.css │ ├── Header.tsx │ ├── NavBar.css │ ├── NavBar.tsx │ ├── NotFound.tsx │ └── PrivateRoute.tsx │ ├── home │ ├── Home.css │ ├── Home.tsx │ ├── components │ │ ├── ActiveCallCard.css │ │ └── ActiveCallCard.tsx │ └── types.ts │ ├── join-call │ ├── JoinCall.css │ └── JoinCall.tsx │ ├── login │ └── LoginPage.tsx │ ├── service │ ├── BotServiceStatus.css │ ├── BotServiceStatus.tsx │ ├── BotServiceStatusCard.css │ └── BotServiceStatusCard.tsx │ ├── toast │ ├── Toasts.css │ ├── Toasts.tsx │ └── toast-card │ │ └── ToastCard.tsx │ └── unauthorized │ └── Unauthorized.tsx ├── test.config.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2020": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaFeatures": { 16 | "jsx": true 17 | }, 18 | "ecmaVersion": 11, 19 | "sourceType": "module" 20 | }, 21 | "plugins": [ 22 | "react", 23 | "@typescript-eslint" 24 | ], 25 | "rules": {}, 26 | "settings": { 27 | "react": { 28 | "version": "detect" 29 | } 30 | }, 31 | "overrides": [ 32 | { 33 | "files": [ 34 | "**/*.tsx" 35 | ], 36 | "rules": { 37 | "react/prop-types": "off" 38 | } 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | 352 | # Misc 353 | build/ 354 | .env.local 355 | .env.development.local 356 | .env.test.local 357 | .env.production.local -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Here you can find the changelog for the pre-release versions that are available as [releases in this repo](https://github.com/microsoft/Broadcast-Development-Kit-Web-UI/releases). 4 | 5 | ## Notice 6 | 7 | This is a **PRE-RELEASE** project and is still in development. This project uses the [application-hosted media bot](https://docs.microsoft.com/en-us/microsoftteams/platform/bots/calls-and-meetings/requirements-considerations-application-hosted-media-bots) SDKs and APIs, which are still in **BETA**. 8 | 9 | The code in this repository is provided "AS IS", without any warranty of any kind. Check the [LICENSE](LICENSE) for more information. 10 | 11 | ## 0.5.0-dev 12 | 13 | - Updated the UI to support version 0.5.0-dev of the Broadcast Development Kit. This includes: 14 | - Changes to start extractions using RTMP/RTMPS in pull mode. 15 | - Changes to support the new statuses for the bot service. 16 | - Fixed a minor issue where unnecessary parameters were sent to the backend when starting an extraction (e.g. SRT latency, when the selected protocol was RTMP). 17 | - Updated dependencies. 18 | 19 | ## 0.4.0-dev 20 | 21 | - Initial pre-release of the solution. 22 | -------------------------------------------------------------------------------- /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 | This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 2 | 3 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. 4 | 5 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 6 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notice 2 | 3 | This is a **PRE-RELEASE** project and is still in development. This project uses the [application-hosted media bot](https://docs.microsoft.com/en-us/microsoftteams/platform/bots/calls-and-meetings/requirements-considerations-application-hosted-media-bots) SDKs and APIs, which are still in **BETA**. 4 | 5 | The code in this repository is provided "AS IS", without any warranty of any kind. Check the [LICENSE](LICENSE) for more information. 6 | 7 | # Web UI for the Broadcast Development Kit 8 | 9 | This repository contains a sample Web UI for the [Broadcast Development Kit](https://github.com/microsoft/Broadcast-Development-Kit) solution, developed as a single page application (SPA) in React and TypeScript. 10 | 11 | ![Screenshot of the web UI](docs/common/images/cover.png) 12 | 13 | ## Dependencies 14 | 15 | - This is not an standalone application. It requires an instance of the [Broadcast Development Kit](https://github.com/microsoft/Broadcast-Development-Kit) to work with. Check the documentation in that repository to run the **Broadcast Development Kit** (either locally or in the cloud) before using this application. 16 | - [Node JS and npm](docs/how-to-install-nodejs-and-npm/README.md) are needed to build and run the application. 17 | 18 | ## Getting started 19 | 20 | ### How to run the solution 21 | 22 | You can follow these documents to run and deploy the sample UI: 23 | - [How to run the solution locally](docs/how-to-run-the-solution-locally/README.md) 24 | - [How to run the solution in Azure](docs/how-to-run-the-solution-in-azure/README.md) 25 | 26 | You can find more information on how to use the UI in the following document: 27 | - [How to use the Web UI solution](docs/how-to-use-the-solution/README.md) 28 | 29 | ### Exploring the repository 30 | 31 | The repository is structured in the following directories: 32 | - **src**: Contains the source code of the application. 33 | - **public**: Contains static files that are used in the application, including configuration files. 34 | - **docs**: Contains the documentation of the solution. 35 | 36 | ## Reporting issues 37 | 38 | Security issues and bugs should be reported privately to the Microsoft Security Response Center (MSRC) at https://msrc.microsoft.com/create-report, or via email to secure@microsoft.com. Please check [SECURITY](SECURITY.md) for more information on the reporting process. 39 | 40 | For non-security related issues, feel free to file an new issue through [GitHub Issues](https://github.com/microsoft/Broadcast-Development-Kit-Web-UI/issues/new). 41 | 42 | ## Contributing 43 | 44 | This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 45 | 46 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. 47 | 48 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 49 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 50 | 51 | ## Trademarks 52 | 53 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow 54 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 55 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 56 | Any use of third-party trademarks or logos are subject to those third-party's policies. 57 | 58 | ## Acknowledgments 59 | 60 | The architecture used in this solution was inspired by the sample in [codeBelt/react-redux-architecture](https://github.com/codeBelt/react-redux-architecture). 61 | 62 | ## License 63 | 64 | Copyright (c) Microsoft Corporation. All rights reserved. 65 | 66 | Licensed under the [MIT](LICENSE) license. 67 | -------------------------------------------------------------------------------- /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 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new Issue. 6 | 7 | For help and questions about using this project, please refer to [our contributing guidelines](CONTRIBUTING.md). 8 | 9 | ## Microsoft Support Policy 10 | 11 | Support for this project is limited to the resources listed above. 12 | 13 | -------------------------------------------------------------------------------- /docs/common/images/call_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/common/images/call_details.png -------------------------------------------------------------------------------- /docs/common/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/common/images/cover.png -------------------------------------------------------------------------------- /docs/common/images/installing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/common/images/installing.png -------------------------------------------------------------------------------- /docs/common/images/invite_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/common/images/invite_link.png -------------------------------------------------------------------------------- /docs/common/images/join_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/common/images/join_call.png -------------------------------------------------------------------------------- /docs/how-to-install-nodejs-and-npm/README.md: -------------------------------------------------------------------------------- 1 | ## How to install Node.js and npm 2 | 3 | 1. Check if you already have Node.js and npm installed by running in the terminal the following commands 4 | 5 | ```bash 6 | node -v 7 | ``` 8 | 9 | ```bash 10 | npm -v 11 | ``` 12 | 13 | You will get a message (version number may change depending on the installed version) like the following: 14 | 15 | |![node.js and npm versions](images/node_version.png)| 16 | |:--:| 17 | |*node.js and npm installed versions*| 18 | 19 | > You can open a console by pressing `Win + R` keys, write `cmd` in the `Open` input and press `Ok` 20 | 21 | If Node.js and/or npm are/is not installed, proceed with the following step. 22 | 23 | 2. Download [node.js](https://nodejs.org/en/) and [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). If you use the [node.js installer](https://nodejs.org/en/download/), npm is already included so you don't need to download them separately. After that, you can run the commands mentioned in the step **1** and verify that were correctly installed. -------------------------------------------------------------------------------- /docs/how-to-install-nodejs-and-npm/images/node_version.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-install-nodejs-and-npm/images/node_version.png -------------------------------------------------------------------------------- /docs/how-to-run-the-solution-in-azure/images/CORS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-run-the-solution-in-azure/images/CORS.png -------------------------------------------------------------------------------- /docs/how-to-run-the-solution-in-azure/images/build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-run-the-solution-in-azure/images/build.png -------------------------------------------------------------------------------- /docs/how-to-run-the-solution-in-azure/images/build_finished.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-run-the-solution-in-azure/images/build_finished.png -------------------------------------------------------------------------------- /docs/how-to-run-the-solution-in-azure/images/connect_storage_explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-run-the-solution-in-azure/images/connect_storage_explorer.png -------------------------------------------------------------------------------- /docs/how-to-run-the-solution-in-azure/images/connection_string.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-run-the-solution-in-azure/images/connection_string.png -------------------------------------------------------------------------------- /docs/how-to-run-the-solution-in-azure/images/open_console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-run-the-solution-in-azure/images/open_console.png -------------------------------------------------------------------------------- /docs/how-to-run-the-solution-in-azure/images/redirect_uri.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-run-the-solution-in-azure/images/redirect_uri.png -------------------------------------------------------------------------------- /docs/how-to-run-the-solution-in-azure/images/static_website.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-run-the-solution-in-azure/images/static_website.png -------------------------------------------------------------------------------- /docs/how-to-run-the-solution-in-azure/images/storage_explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-run-the-solution-in-azure/images/storage_explorer.png -------------------------------------------------------------------------------- /docs/how-to-run-the-solution-in-azure/images/upload_build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-run-the-solution-in-azure/images/upload_build.png -------------------------------------------------------------------------------- /docs/how-to-run-the-solution-in-azure/images/web_portal_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-run-the-solution-in-azure/images/web_portal_login.png -------------------------------------------------------------------------------- /docs/how-to-run-the-solution-locally/README.md: -------------------------------------------------------------------------------- 1 | # How to run the solution locally 2 | 3 | ## Getting Started 4 | The objective of this document is to explain the necessary steps to configure and run the Web Portal solution in a local environment. This includes: 5 | 6 | - [Install the Solution](#install-the-solution) 7 | - [Configure the Solution](#configure-the-solution) 8 | - [Run the Solution](#run-the-solution) 9 | - [Test the solution](#test-the-solution) 10 | 11 | ### Install the Solution 12 | 13 | Go to the main directory of the solution open a terminal in that directory and enter the command `npm i`. It will start the installation of the packages used by the solution which may take a few seconds. 14 | 15 | |![npm i running](../common/images/installing.png)| 16 | |:--:| 17 | |*`npm i` command is running*| 18 | 19 | Once finished you will notice that a directory called node_modules and a package-lock.json file have been created. 20 | 21 | ### Configure the Solution 22 | To configure the solution open the `config.json` file located in the `public` folder of the solution's root directory and replace with the following configuration: 23 | 24 | ```json 25 | { 26 | "buildNumber": "0.0.0", 27 | "apiBaseUrl": "https://localhost:8442/api", 28 | "msalConfig": { 29 | "spaClientId": "", 30 | "apiClientId": "", 31 | "groupId": "", 32 | "authority": "", 33 | "redirectUrl": "" 34 | }, 35 | "featureFlags": { 36 | "DISABLE_AUTHENTICATION": { 37 | "description": "Disable authentication flow when true", 38 | "isActive": true 39 | } 40 | } 41 | } 42 | 43 | ``` 44 | 45 | ### Run the Solution 46 | Once the solution is configured to run, go to the root directory of the solution, open a terminal and type the following command `npm run start`, a message like the following will appear and a new tab will open in the browser: 47 | 48 | |![npm run start](images/starting_webportal.png)| 49 | |:--:| 50 | |*After entering the command `npm run start` the solution will start to run.*| 51 | 52 | > Having the solution already configured, it will only be necessary to run the start command every time you want to use it. 53 | 54 | Once the web portal finishes launching, the view of the opened tab in the browser will be refreshed showing the following: 55 | 56 | |![web portal](images/webportal_running.png)| 57 | |:--:| 58 | |*Web portal after startup is complete*| 59 | 60 | ### Test the solution 61 | 62 | [Create](https://support.microsoft.com/en-us/office/schedule-a-meeting-in-teams-943507a9-8583-4c58-b5d2-8ec8265e04e5) a new Microsoft Teams meeting and join it. 63 | 64 | |![Microsoft Teams invite link](../common/images/invite_link.png)| 65 | |:--:| 66 | |*Steps to copy the invite Link from Microsoft Teams*| 67 | 68 | Once you have joined the meeting copy the invitation link from the meeting, we will use it to join the bot to that meeting. 69 | 70 | In the web portal solution click on the `Join a Call` tab in the top menu, copy the Microsoft Teams meeting invitation link to the `Invite URL` field and click on the `Join Call` button below. 71 | 72 | |![Join call menu](../common/images/join_call.png)| 73 | |:--:| 74 | |*Complete the "Invitation Url" field with the Microsoft Teams meeting invitation link.*| 75 | 76 | After a few seconds the bot will join the Microsoft Teams meeting and the call details will be displayed on the web portal. 77 | 78 | |![Call details view](../common/images/call_details.png)| 79 | |:--:| 80 | |*When the bot joins, the call details will be displayed.*| -------------------------------------------------------------------------------- /docs/how-to-run-the-solution-locally/images/starting_webportal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-run-the-solution-locally/images/starting_webportal.png -------------------------------------------------------------------------------- /docs/how-to-run-the-solution-locally/images/webportal_running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-run-the-solution-locally/images/webportal_running.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/bot-service-status-deprovisioned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/bot-service-status-deprovisioned.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/bot-service-status-provisioned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/bot-service-status-provisioned.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/call-details-view-change-protocol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/call-details-view-change-protocol.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/call-details-view-extraction-card-main-stream-expanded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/call-details-view-extraction-card-main-stream-expanded.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/call-details-view-extraction-card-main-stream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/call-details-view-extraction-card-main-stream.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/call-details-view-extraction-card-participant-stream-expanded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/call-details-view-extraction-card-participant-stream-expanded.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/call-details-view-extraction-card-participant-stream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/call-details-view-extraction-card-participant-stream.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/call-details-view-extraction-stream-advance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/call-details-view-extraction-stream-advance.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/call-details-view-extraction-stream-rtmp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/call-details-view-extraction-stream-rtmp.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/call-details-view-extraction-stream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/call-details-view-extraction-stream.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/call-details-view-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/call-details-view-info.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/call-details-view-injection-card-expanded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/call-details-view-injection-card-expanded.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/call-details-view-injection-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/call-details-view-injection-card.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/call-details-view-injection-stream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/call-details-view-injection-stream.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/call-details-view-select-protocol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/call-details-view-select-protocol.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/call-details-view-stream-key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/call-details-view-stream-key.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/call-details-view-streams-sections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/call-details-view-streams-sections.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/call-details-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/call-details-view.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/join-call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/join-call.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/joining-call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/joining-call.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/login-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/login-page.png -------------------------------------------------------------------------------- /docs/how-to-use-the-solution/images/login-popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/docs/how-to-use-the-solution/images/login-popup.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "teams-tx", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@azure/msal-browser": "^2.11.1", 7 | "@material-ui/core": "^4.11.0", 8 | "@material-ui/icons": "^4.9.1", 9 | "antd": "^4.4.2", 10 | "axios": "^0.21.1", 11 | "connected-react-router": "^6.8.0", 12 | "jwt-decode": "^3.1.2", 13 | "moment": "^2.27.0", 14 | "react": "^16.12.0", 15 | "react-dom": "^16.12.0", 16 | "react-redux": "^7.2.0", 17 | "react-router-dom": "^5.2.0", 18 | "react-scripts": "3.4.4", 19 | "redux": "^4.0.5", 20 | "redux-devtools-extension": "^2.13.8", 21 | "redux-thunk": "^2.3.0", 22 | "reselect": "^4.0.0", 23 | "uuid": "^8.3.2" 24 | }, 25 | "scripts": { 26 | "lint": "eslint .", 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "jest --coverage", 30 | "test:watch": "jest --watch --coverage", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@testing-library/jest-dom": "^4.2.4", 50 | "@testing-library/react": "^9.5.0", 51 | "@testing-library/user-event": "^7.1.2", 52 | "@types/enzyme": "^3.10.5", 53 | "@types/enzyme-adapter-react-16": "^1.0.6", 54 | "@types/jest": "^26.0.0", 55 | "@types/react-redux": "^7.1.9", 56 | "@types/react-router-dom": "^5.1.5", 57 | "@types/redux-mock-store": "^1.0.2", 58 | "@types/uuid": "^8.3.0", 59 | "@typescript-eslint/eslint-plugin": "^3.2.0", 60 | "@typescript-eslint/parser": "^3.2.0", 61 | "enzyme": "^3.11.0", 62 | "enzyme-adapter-react-16": "^1.15.2", 63 | "enzyme-to-json": "^3.5.0", 64 | "eslint-plugin-react": "^7.20.0", 65 | "jest": "^24.9.0", 66 | "jest-sonar-reporter": "^2.0.0", 67 | "redux-mock-store": "^1.5.4", 68 | "ts-jest": "^26.1.0", 69 | "typescript": "^3.9.5" 70 | }, 71 | "jest": { 72 | "testResultsProcessor": "jest-sonar-reporter" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /public/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildNumber": "0.5.0-dev", 3 | "apiBaseUrl": "https://localhost:8442/api", 4 | "releaseDummyVariable": "empty", 5 | "msalConfig": { 6 | "spaClientId": "", 7 | "apiClientId": "", 8 | "groupId": "", 9 | "authority": "", 10 | "redirectUrl": "" 11 | }, 12 | "featureFlags": { 13 | "DISABLE_AUTHENTICATION": { 14 | "description": "Disable authentication flow when true", 15 | "isActive": false 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/public/favicon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 26 | Broadcast Development Kit 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Broadcast Dev. Kit", 3 | "name": "Broadcast Development Kit", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "route": "/*", 5 | "serve": "/index.html", 6 | "statusCode": 200 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { Spin } from "antd"; 4 | import React, { Fragment, useEffect } from "react"; 5 | import { useDispatch, useSelector } from "react-redux"; 6 | import { Route, Switch } from "react-router-dom"; 7 | import { AuthStatus } from "./models/auth/types"; 8 | import IAppState from "./services/store/IAppState"; 9 | import { setAuthenticationDisabled } from "./stores/auth/actions"; 10 | import { initilizeAuthentication } from "./stores/auth/asyncActions"; 11 | import { FEATUREFLAG_DISABLE_AUTHENTICATION } from "./stores/config/constants"; 12 | import CallDetails from "./views/call-details/CallDetails"; 13 | import Footer from "./views/components/Footer"; 14 | import Header from "./views/components/Header"; 15 | import NotFound from "./views/components/NotFound"; 16 | import PrivateRoute from "./views/components/PrivateRoute"; 17 | import Home from "./views/home/Home"; 18 | import JoinCall from "./views/join-call/JoinCall"; 19 | import LoginPage from "./views/login/LoginPage"; 20 | import BotServiceStatus from "./views/service/BotServiceStatus"; 21 | import Unauthorized from "./views/unauthorized/Unauthorized"; 22 | 23 | const App: React.FC = () => { 24 | const dispatch = useDispatch(); 25 | const { initialized: authInitialized, status: authStatus } = useSelector((state: IAppState) => state.auth); 26 | const { app: appConfig, initialized: configInitialized } = useSelector((state: IAppState) => state.config); 27 | const disableAuthFlag = appConfig?.featureFlags && appConfig.featureFlags[FEATUREFLAG_DISABLE_AUTHENTICATION]; 28 | const isAuthenticated = authStatus === AuthStatus.Authenticated; 29 | 30 | useEffect(() => { 31 | if (appConfig) { 32 | disableAuthFlag?.isActive 33 | ? dispatch(setAuthenticationDisabled()) 34 | : dispatch(initilizeAuthentication(appConfig.msalConfig)); 35 | } 36 | }, [configInitialized]); 37 | 38 | if (!authInitialized) { 39 | return ( 40 |
41 | 42 |
43 | ); 44 | } else { 45 | return ( 46 |
47 | {isAuthenticated &&
} 48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
59 |
61 | ); 62 | } 63 | }; 64 | 65 | export default App; 66 | -------------------------------------------------------------------------------- /src/hooks/useInterval.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { useEffect, useRef } from "react"; 4 | 5 | function useInterval(callback: () => any, delay: number | null) { 6 | const savedCallback = useRef(null); 7 | 8 | // Remember the latest callback. 9 | useEffect(() => { 10 | savedCallback.current = callback; 11 | }, [callback]); 12 | 13 | // Set up the interval. 14 | useEffect(() => { 15 | function tick() { 16 | const currentCallback = savedCallback.current; 17 | if (currentCallback) { 18 | currentCallback(); 19 | } 20 | } 21 | if (delay !== null) { 22 | const id = setInterval(tick, delay); 23 | return () => clearInterval(id); 24 | } 25 | }, [delay]); 26 | } 27 | 28 | export default useInterval; 29 | -------------------------------------------------------------------------------- /src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/Broadcast-Development-Kit-Web-UI/2910f4c018749eb0af7d8aafb9ad1bf7f47a46a6/src/images/logo.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | body { 4 | padding: 100px 0 0px 0; 5 | background-color: #f3f2f1; 6 | } 7 | 8 | #root, 9 | #HeaderInner { 10 | min-width: 920px; 11 | } 12 | #main { 13 | margin: 0 6%; 14 | padding-bottom: 40px; 15 | } 16 | 17 | .PageBody { 18 | background-color: #fff; 19 | padding: 15px; 20 | margin-bottom: 20px; 21 | min-height: 180px; 22 | 23 | /* shadow */ 24 | -webkit-box-shadow: 1px 1px 2px 0px rgba(0, 0, 0, 0.1); 25 | -moz-box-shadow: 1px 1px 2px 0px rgba(0, 0, 0, 0.1); 26 | box-shadow: 1px 1px 2px 0px rgba(0, 0, 0, 0.1); 27 | } 28 | 29 | /* footer */ 30 | body, 31 | #root, 32 | #app { 33 | height: 100%; 34 | } 35 | #app { 36 | display: flex; 37 | flex-direction: column; 38 | } 39 | 40 | #Footer { 41 | margin-top: auto; 42 | border-top: 1px solid #ddd; 43 | padding: 8px; 44 | text-align: center; 45 | font-size: 0.8em; 46 | } 47 | 48 | /* misc */ 49 | .break { 50 | display: block; 51 | clear: both; 52 | } 53 | 54 | h2 { 55 | margin: 20px 0; 56 | } 57 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { Provider as ReduxProvider } from "react-redux"; 6 | import { ConnectedRouter } from "connected-react-router"; 7 | import configureStore, { DispatchExts, history } from "./services/store"; 8 | 9 | import 'antd/dist/antd.css'; 10 | import './index.css'; 11 | import App from './App'; 12 | import { loadConfig } from './stores/config/asyncActions'; 13 | import { pollCurrentCallAsync } from './stores/calls/asyncActions'; 14 | 15 | const store = configureStore(); 16 | const storeDispatch = store.dispatch as DispatchExts; 17 | 18 | // triger config loading 19 | storeDispatch(loadConfig()) 20 | 21 | // trigger automatic polling of selected call 22 | storeDispatch(pollCurrentCallAsync()); 23 | 24 | ReactDOM.render(<> 25 | 26 | 27 | 28 | 29 | 30 | 31 | , document.getElementById('root')); 32 | -------------------------------------------------------------------------------- /src/middlewares/errorToastMiddleware.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { Middleware } from 'redux'; 4 | import IAppState from '../services/store/IAppState'; 5 | import { notification } from 'antd'; 6 | 7 | const errorToastMiddleware: Middleware<{}, IAppState> = (store) => (next) => (action) => { 8 | if (action.error) { 9 | const errorAction = action; 10 | 11 | errorAction.payload.status === 401 12 | ? notification.error({ description: 'Unauthorized: Please, Sing in again.', message: `Error` }) 13 | : notification.error({ description: errorAction.payload.message, message: `Error` }); 14 | } 15 | 16 | next(action); 17 | }; 18 | 19 | export default errorToastMiddleware; 20 | -------------------------------------------------------------------------------- /src/models/auth/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | export enum AuthStatus { 4 | Unauthenticated, 5 | Unauthorized, 6 | Authenticating, 7 | Authenticated, 8 | } 9 | 10 | export interface UserProfile { 11 | id: string; 12 | username: string; 13 | role: UserRoles 14 | } 15 | 16 | export enum UserRoles { 17 | Producer = "Producer", 18 | Attendee = "Attendee", 19 | } 20 | -------------------------------------------------------------------------------- /src/models/calls/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | export interface Call { 4 | id: string; 5 | joinUrl: string; 6 | displayName: string; // Call/Room name 7 | state: CallState; // Initializing, Established, Terminating, Terminated 8 | errorMessage: string | null; // Error message (if any) 9 | createdAt: Date; 10 | meetingType: CallType; // Unknown, Normal, Event 11 | botFqdn: string | null; 12 | botIp: string | null; 13 | connectionPool: ConnectionPool; 14 | defaultProtocol: StreamProtocol; 15 | defaultPassphrase: string; 16 | defaultKeyLength: KeyLength; 17 | defaultLatency: number; 18 | streams: Stream[]; 19 | injectionStream: InjectionStream | null; 20 | privateContext: PrivateContext | null; 21 | } 22 | 23 | export enum StreamProtocol { 24 | SRT = 0, 25 | RTMP = 1, 26 | } 27 | 28 | export enum CallState { 29 | Establishing, 30 | Established, 31 | Terminating, 32 | Terminated, 33 | } 34 | 35 | export enum CallType { 36 | Default, 37 | Event, 38 | } 39 | 40 | export interface ConnectionPool { 41 | used: number; 42 | available: number; 43 | } 44 | 45 | export interface Stream { 46 | id: string; //internal id 47 | callId: string; 48 | participantGraphId: string; //id form teams meeting 49 | displayName: string; // User name or Stream name 50 | photoUrl: string | null; 51 | photo: string | undefined; 52 | type: StreamType; // VbSS, DominantSpeaker, Participant 53 | state: StreamState; // Disconnected, Initializing, Established, Disconnecting, Error 54 | isHealthy: boolean; 55 | healthMessage: string; 56 | isSharingScreen: boolean; 57 | isSharingVideo: boolean; 58 | isSharingAudio: boolean; 59 | audioMuted: boolean; 60 | details: StreamDetails | null; 61 | enableSsl: boolean; 62 | } 63 | 64 | export interface StartStreamRequest { 65 | participantId?: string; 66 | participantGraphId?: string; 67 | type: StreamType; 68 | callId: string; 69 | protocol: StreamProtocol; 70 | config: StreamConfiguration; 71 | } 72 | 73 | export interface StopStreamRequest { 74 | callId: string; 75 | type: StreamType; 76 | participantId?: string; 77 | participantGraphId?: string; 78 | participantName?: string; 79 | } 80 | 81 | export interface NewInjectionStream { 82 | callId: string; 83 | streamUrl?: string; 84 | streamKey?: string; 85 | protocol?: StreamProtocol; 86 | mode?: StreamMode; 87 | latency?: number; 88 | enableSsl?: boolean; 89 | keyLength?: KeyLength; 90 | } 91 | 92 | export interface StopInjectionRequest { 93 | callId: string; 94 | streamId: string; 95 | } 96 | 97 | export type StreamConfiguration = { 98 | streamUrl: string; 99 | streamKey?: string; 100 | unmixedAudio: boolean; 101 | audioFormat: number; 102 | timeOverlay: boolean; 103 | enableSsl: boolean; 104 | }; 105 | 106 | export interface StreamSrtConfiguration extends StreamConfiguration { 107 | mode: StreamMode; 108 | latency: number; 109 | keyLength: KeyLength; 110 | } 111 | 112 | export interface NewCall { 113 | callUrl: string; 114 | status: CallState; 115 | errorMessage?: string; 116 | } 117 | 118 | export interface CallDefaults { 119 | protocol: StreamProtocol; 120 | latency: number; 121 | passphrase: string; 122 | keyLength: KeyLength; 123 | } 124 | 125 | export enum StreamState { 126 | Disconnected, 127 | Starting, 128 | Ready, 129 | Receiving, 130 | NotReceiving, 131 | Stopping, 132 | StartingError, 133 | StoppingError, 134 | Error, 135 | } 136 | 137 | export const ActiveStatuses = [StreamState.Ready, StreamState.Receiving, StreamState.NotReceiving, StreamState.Stopping]; 138 | 139 | export const InactiveStatuses = [StreamState.Disconnected, StreamState.Starting]; 140 | 141 | export enum StreamType { 142 | VbSS = 0, 143 | PrimarySpeaker = 1, 144 | Participant = 2, 145 | TogetherMode = 3, 146 | LargeGallery = 4, 147 | LiveEvent = 5, 148 | } 149 | 150 | export const SpecialStreamTypes = [ 151 | StreamType.VbSS, 152 | StreamType.PrimarySpeaker, 153 | StreamType.LargeGallery, 154 | StreamType.LiveEvent, 155 | StreamType.TogetherMode, 156 | ]; 157 | 158 | export enum StreamMode { 159 | Caller = 1, 160 | Listener = 2, 161 | } 162 | 163 | export enum RtmpMode { 164 | Pull = 1, 165 | Push = 2, 166 | } 167 | 168 | export enum KeyLength { 169 | None = 0, 170 | SixteenBytes = 16, 171 | TwentyFourBytes = 24, 172 | ThirtyTwoBytes = 32, 173 | } 174 | 175 | export interface NewStream { 176 | callId: string; 177 | participantId?: string; 178 | participantName?: string; 179 | streamType: StreamType; 180 | mode?: StreamMode; 181 | advancedSettings: { 182 | url?: string; 183 | latency?: number; 184 | key?: string; 185 | unmixedAudio: boolean; 186 | keyLength?: KeyLength; 187 | enableSsl: boolean; 188 | }; 189 | } 190 | 191 | export interface StreamDetails { 192 | streamUrl: string; 193 | passphrase: string; 194 | latency: number; 195 | previewUrl: string; 196 | audioDemuxed: boolean; 197 | } 198 | 199 | export interface InjectionStream { 200 | id: string; 201 | callId: string; 202 | injectionUrl?: string; 203 | protocol: StreamProtocol; 204 | streamMode: StreamMode; 205 | state?: StreamState; 206 | startingAt: string; 207 | startedAt: string; 208 | endingAt: string; 209 | endedAt: string; 210 | latency: number; 211 | passphrase: string; 212 | audioMuted: boolean; 213 | keylength: KeyLength; 214 | } 215 | 216 | export interface NewStreamDrawerOpenParameters { 217 | callId: string; 218 | streamType: StreamType; 219 | participantId?: string; 220 | participantName?: string; 221 | } 222 | 223 | export interface NewInjectionStreamDrawerOpenParameters { 224 | callId: string; 225 | } 226 | 227 | export interface PrivateContext { 228 | streamKey: string; 229 | } 230 | 231 | export interface CallStreamKey { 232 | callId: string; 233 | streamKey: string; 234 | } 235 | -------------------------------------------------------------------------------- /src/models/error/helpers.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { ApiError } from "./types"; 4 | 5 | export const fillApiErrorWithDefaults = (error: Partial, requestUrl: string): ApiError => { 6 | const errorResponse = new ApiError(); 7 | 8 | errorResponse.status = error.status || 0; 9 | errorResponse.message = error.message || 'An error ocurred'; //TODO: Change this message 10 | errorResponse.errors = error.errors!.length ? error.errors! : ['An error ocurred']; 11 | errorResponse.url = error.url || requestUrl; 12 | 13 | return errorResponse; 14 | }; -------------------------------------------------------------------------------- /src/models/error/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { v4 as uuidv4 } from 'uuid'; 4 | 5 | export type ApplicationError = ApiError | DefaultError; 6 | 7 | export interface ErrorDefaults { 8 | id: string; 9 | message: string; 10 | } 11 | 12 | export class DefaultError implements ErrorDefaults { 13 | id: string = uuidv4(); 14 | message: string = ''; 15 | raw: any = null; 16 | 17 | constructor(message: string, raw?: any) { 18 | this.message = message; 19 | if (raw) { 20 | this.raw = raw; 21 | } 22 | } 23 | } 24 | 25 | export class ApiError implements ErrorDefaults { 26 | id: string = uuidv4(); 27 | message: string = ''; 28 | status: number = 0; 29 | errors: string[] = []; 30 | url: string = ''; 31 | raw: any = null; 32 | } 33 | -------------------------------------------------------------------------------- /src/models/service/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | export enum BotServiceInfrastructureState { 4 | Running = "PowerState/running", 5 | Deallocating = "PowerState/deallocating", 6 | Deallocated = "PowerState/deallocated", 7 | Starting = "PowerState/starting", 8 | Stopped = "PowerState/stopped", 9 | Stopping = "PowerState/stopping", 10 | Unknown = "PowerState/unknown", 11 | } 12 | 13 | export interface BotService { 14 | id: string; 15 | name: string; 16 | callId: string; 17 | state: BotServiceStates; 18 | infrastructure: Infrastructure; 19 | } 20 | 21 | export interface Infrastructure { 22 | virtualMachineName: string; 23 | resourceGroup: string; 24 | subscriptionId: string; 25 | powerState: BotServiceInfrastructureState; 26 | provisioningDetails: ProvisioningDetails; 27 | } 28 | 29 | export interface ProvisioningDetails { 30 | state: ProvisioningState; 31 | message: string; 32 | } 33 | 34 | export interface ProvisioningState { 35 | id: ProvisioningStateValues; 36 | name: string; 37 | } 38 | 39 | export enum ProvisioningStateValues { 40 | Provisioning = 0, 41 | Provisioned = 1, 42 | Deprovisioning = 2, 43 | Deprovisioned = 3, 44 | Error = 4, 45 | Unknown = 5 46 | } 47 | 48 | export enum TeamsColors { 49 | Red = "#D74654", 50 | Purple = "#6264A7", 51 | Black = "#11100F", 52 | Green = "#7FBA00", 53 | Grey = "#BEBBB8", 54 | MiddleGrey = "#3B3A39", 55 | DarkGrey = "#201F1E", 56 | White = "white", 57 | } 58 | 59 | export enum TeamsMargins { 60 | micro = "4px", 61 | small = "8px", 62 | medium = "20px", 63 | large = "40px", 64 | } 65 | 66 | export enum BotServiceStates { 67 | Unavailable = 0, 68 | Available = 1, 69 | Busy = 2, 70 | } -------------------------------------------------------------------------------- /src/models/toast/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | export enum ToastStatusEnum { 4 | Error = 'error', 5 | Warning = 'warning', 6 | Success = 'success', 7 | } -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | /// 4 | -------------------------------------------------------------------------------- /src/sample.md: -------------------------------------------------------------------------------- 1 | #WIP -------------------------------------------------------------------------------- /src/services/api/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import Axios, { Method, AxiosRequestConfig } from 'axios'; 4 | 5 | import { fillApiErrorWithDefaults } from '../../models/error/helpers'; 6 | import { ApiError } from '../../models/error/types'; 7 | import AuthService from '../../services/auth'; 8 | import { FEATUREFLAG_DISABLE_AUTHENTICATION } from '../../stores/config/constants'; 9 | import { getConfig } from '../../stores/config/loader'; 10 | 11 | export enum RequestMethod { 12 | Get = 'GET', 13 | Post = 'POST', 14 | Put = 'PUT', 15 | Delete = 'DELETE', 16 | Options = 'OPTIONS', 17 | Head = 'HEAD', 18 | Patch = 'PATCH', 19 | } 20 | 21 | export interface RequestParameters { 22 | url: string; 23 | isSecured: boolean; 24 | shouldOverrideBaseUrl?: boolean; 25 | payload?: unknown; 26 | method?: RequestMethod; 27 | config?: AxiosRequestConfig; 28 | } 29 | 30 | export class ApiClient { 31 | public static async post({ 32 | url, 33 | isSecured, 34 | shouldOverrideBaseUrl: shouldOverrideUrl, 35 | payload, 36 | config, 37 | }: RequestParameters): Promise> { 38 | return baseRequest({ url, isSecured, shouldOverrideBaseUrl: shouldOverrideUrl, payload, method: RequestMethod.Post, config }); 39 | } 40 | 41 | public static async put({ 42 | url, 43 | isSecured, 44 | shouldOverrideBaseUrl: shouldOverrideUrl, 45 | payload, 46 | config, 47 | }: RequestParameters): Promise> { 48 | return baseRequest({ url, isSecured, shouldOverrideBaseUrl: shouldOverrideUrl, payload, method: RequestMethod.Put, config }); 49 | } 50 | 51 | public static async get({ 52 | url, 53 | isSecured, 54 | shouldOverrideBaseUrl: shouldOverrideUrl, 55 | payload, 56 | config, 57 | }: RequestParameters): Promise> { 58 | return baseRequest({ url, isSecured, shouldOverrideBaseUrl: shouldOverrideUrl, payload, method: RequestMethod.Get, config }); 59 | } 60 | 61 | public static async delete({ 62 | url, 63 | isSecured, 64 | shouldOverrideBaseUrl: shouldOverrideUrl, 65 | payload, 66 | config, 67 | }: RequestParameters): Promise> { 68 | return baseRequest({ url, isSecured, shouldOverrideBaseUrl: shouldOverrideUrl, payload, method: RequestMethod.Delete, config }); 69 | } 70 | } 71 | 72 | const baseRequest = async ({ 73 | url, 74 | isSecured, 75 | shouldOverrideBaseUrl: shouldOverrideUrl, 76 | payload, 77 | method, 78 | config, 79 | }: RequestParameters): Promise> => { 80 | try { 81 | const { 82 | apiBaseUrl, 83 | msalConfig: { apiClientId }, 84 | featureFlags, 85 | } = await getConfig(); 86 | 87 | const disableAuthFlag = featureFlags && featureFlags[FEATUREFLAG_DISABLE_AUTHENTICATION]; 88 | 89 | let headers: any; 90 | if (isSecured && !disableAuthFlag?.isActive) { 91 | const token = await refreshAccessToken(apiClientId); 92 | headers = { 93 | Authorization: `Bearer ${token}`, 94 | }; 95 | } 96 | 97 | const requestConfig: AxiosRequestConfig = { 98 | url: shouldOverrideUrl ? url : `${apiBaseUrl}${url}`, 99 | method: method as Method, 100 | data: payload, 101 | headers: { 102 | 'x-client': 'Management Portal', 103 | ...headers, 104 | }, 105 | ...config, 106 | }; 107 | 108 | const [response] = await Promise.all([Axios(requestConfig), delay()]); 109 | 110 | const { status, data, request } = response; 111 | 112 | if (data.success === false) { 113 | const errorResponse = fillApiErrorWithDefaults( 114 | { 115 | status, 116 | message: data.errors.join(' - '), 117 | errors: data.errors, 118 | url: request ? request.responseURL : url, 119 | raw: response, 120 | }, 121 | url 122 | ); 123 | 124 | return errorResponse; 125 | } 126 | 127 | return data as T; 128 | } catch (error) { 129 | //The request was made and the server responded with an status code different of 2xx 130 | if (error.response) { 131 | const { value } = error.response.data; 132 | 133 | //TODO: Modify how we parse de error. Acording to our exception responses, we should look the property value 134 | 135 | const errors: string[] = 136 | value && Object.prototype.hasOwnProperty.call(value, 'errors') 137 | ? [value?.title, value?.detail, concatErrorMessages(value?.errors)] 138 | : [value?.title, value?.detail]; 139 | 140 | const serverError = fillApiErrorWithDefaults( 141 | { 142 | status: error.response.status, 143 | message: errors.filter(Boolean).join(' - '), 144 | errors, 145 | url: error.request.responseURL, 146 | raw: error.response, 147 | }, 148 | url 149 | ); 150 | 151 | return serverError; 152 | } 153 | 154 | //The request was made but no response was received 155 | if (error.request) { 156 | const { status, statusText, responseURL } = error.request; 157 | 158 | const unknownError = fillApiErrorWithDefaults( 159 | { 160 | status, 161 | message: `${error.message} ${statusText}`, 162 | errors: [statusText], 163 | url: responseURL, 164 | raw: error.request, 165 | }, 166 | url 167 | ); 168 | 169 | return unknownError; 170 | } 171 | 172 | //Something happened during the setup 173 | const defaultError = fillApiErrorWithDefaults( 174 | { 175 | status: 0, 176 | message: error.message, 177 | errors: [error.message], 178 | url: url, 179 | raw: error, 180 | }, 181 | url 182 | ); 183 | 184 | return defaultError; 185 | } 186 | }; 187 | 188 | export const refreshAccessToken = async (apiClientId: string): Promise => { 189 | const accounts = AuthService.getAccounts(); 190 | 191 | if (accounts && accounts.length > 0) { 192 | try { 193 | const authResult = await AuthService.requestSilentToken(accounts[0], apiClientId); 194 | 195 | return authResult.accessToken; 196 | } catch (error) { 197 | console.error(error); 198 | } 199 | } 200 | }; 201 | 202 | const concatErrorMessages = (errors: Record): string[] => { 203 | const errorsArray: string[] = []; 204 | 205 | Object.values(errors).forEach((element) => { 206 | Array.isArray(element) ? errorsArray.push(element.join(' - ')) : errorsArray.push(JSON.stringify(element)); 207 | }); 208 | 209 | return errorsArray; 210 | }; 211 | 212 | const delay = (duration: number = 250): Promise => { 213 | return new Promise((resolve) => setTimeout(resolve, duration)); 214 | }; 215 | 216 | export type RequestResponse = T | ApiError; 217 | 218 | export interface Resource { 219 | id: string; 220 | resource: T; 221 | } 222 | 223 | //TODO: Remove after migrating async actions 224 | export const api = async (url: string, method: Method, json?: unknown): Promise => { 225 | try { 226 | const { 227 | apiBaseUrl, 228 | msalConfig: { apiClientId }, 229 | featureFlags, 230 | } = await getConfig(); 231 | 232 | const disableAuthFlag = featureFlags && featureFlags[FEATUREFLAG_DISABLE_AUTHENTICATION]; 233 | const token = !disableAuthFlag?.isActive ? await refreshAccessToken(apiClientId) : ''; 234 | const headersConfig = !disableAuthFlag?.isActive ? { Authorization: `Bearer ${token}` } : {}; 235 | 236 | // Request Auth 237 | const request: AxiosRequestConfig = { 238 | url: `${apiBaseUrl}${url}`, 239 | method: method, 240 | data: json, 241 | headers: { 242 | ...headersConfig, 243 | 'X-Client': 'Management Portal', 244 | }, 245 | }; 246 | 247 | // TODO: Handle proper return codes 248 | 249 | const response = await Axios(request); 250 | return response.data as T; 251 | } catch (err) { 252 | // Handle HTTP errors 253 | const errorMessage = !err.response?.data?.error_description ? err.toString() : err.response.data.error_description; 254 | 255 | throw new Error(errorMessage); 256 | } 257 | }; 258 | -------------------------------------------------------------------------------- /src/services/auth/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { 4 | AccountInfo, 5 | AuthenticationResult, 6 | Configuration, 7 | EndSessionRequest, 8 | PublicClientApplication, 9 | SilentRequest, 10 | } from '@azure/msal-browser'; 11 | import jwtDecode from 'jwt-decode'; 12 | import { UserProfile, UserRoles } from '../../models/auth/types'; 13 | import { MsalConfig } from '../../stores/config/types'; 14 | 15 | interface DecodedToken { 16 | groups: string[]; 17 | } 18 | 19 | export default class AuthService { 20 | private static msalClient: PublicClientApplication; 21 | private static appConfig: MsalConfig; 22 | public static configure(config: MsalConfig): void { 23 | 24 | AuthService.appConfig = config; 25 | const msalConfig: Configuration = { 26 | auth: { 27 | authority: config?.authority, 28 | clientId: config?.spaClientId || '', 29 | redirectUri: config?.redirectUrl, 30 | }, 31 | cache: { 32 | cacheLocation: 'localStorage', 33 | storeAuthStateInCookie: false, 34 | }, 35 | }; 36 | 37 | AuthService.msalClient = new PublicClientApplication(msalConfig); 38 | } 39 | 40 | public static async signIn(apiClientId: string | undefined): Promise { 41 | const loginRequest = { 42 | scopes: ['openid', 'profile', 'offline_access', `api://${apiClientId}/.default`], 43 | }; 44 | return await AuthService.msalClient.loginPopup(loginRequest); 45 | } 46 | 47 | public static async signOut(username: string): Promise { 48 | const request: EndSessionRequest = { 49 | account: AuthService.msalClient.getAccountByUsername(username) || undefined, 50 | }; 51 | 52 | await AuthService.msalClient.logout(request); 53 | } 54 | 55 | public static getAccounts(): AccountInfo[] { 56 | return AuthService.msalClient.getAllAccounts(); 57 | } 58 | 59 | public static async requestSilentToken(account: AccountInfo, apiClientId: string): Promise { 60 | const request: SilentRequest = { 61 | account, 62 | scopes: ['openid', 'profile', 'offline_access', `api://${apiClientId}/.default`], 63 | }; 64 | 65 | return await AuthService.msalClient.acquireTokenSilent(request); 66 | } 67 | 68 | public static getUserProfile(authResult: AuthenticationResult): UserProfile { 69 | const userRole = AuthService.getUserRole(authResult.accessToken); 70 | const userProfile: UserProfile = { 71 | id: authResult.account?.localAccountId || '', 72 | username: authResult.account?.username || '', 73 | role: userRole, 74 | }; 75 | 76 | return userProfile; 77 | } 78 | 79 | private static getUserRole(jwtToken: string): UserRoles { 80 | const groupId = AuthService.appConfig.groupId; 81 | 82 | if(!groupId){ 83 | return UserRoles.Producer; 84 | } 85 | 86 | const decodedToken = jwtDecode(jwtToken) as DecodedToken; 87 | // If users are in the RBAC group then they have the Producer/Broadcast role 88 | const role = decodedToken.groups && decodedToken.groups.includes(groupId) ? UserRoles.Producer: UserRoles.Attendee; 89 | 90 | return role; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/services/helpers.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | export const extractLinks = (rawHTML: string): string[] => { 4 | 5 | const doc = document.createElement('html'); 6 | doc.innerHTML = rawHTML; 7 | const links = doc.getElementsByTagName('a') 8 | const urls = []; 9 | 10 | for (let i = 0; i < links.length; i++) { 11 | urls.push(links[i].getAttribute('href')); 12 | } 13 | 14 | return urls.filter(Boolean) as string[]; 15 | }; 16 | -------------------------------------------------------------------------------- /src/services/store/IAppState.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { RouterState } from 'connected-react-router' 4 | import { AuthState } from '../../stores/auth/reducer'; 5 | import { ICallsState } from '../../stores/calls/reducer'; 6 | import { ConfigState } from '../../stores/config/reducer'; 7 | import { ErrorState } from '../../stores/error/reducer'; 8 | import { RequestingState } from '../../stores/requesting/reducer'; 9 | import { BotServiceAppState } from '../../stores/service/reducer'; 10 | import { IToastState } from '../../stores/toast/reducer'; 11 | 12 | export default interface IAppState { 13 | router: RouterState, 14 | config: ConfigState, 15 | auth: AuthState; 16 | calls: ICallsState; 17 | errors: ErrorState; 18 | requesting: RequestingState; 19 | toast: IToastState; 20 | botServiceStatus: BotServiceAppState; 21 | } 22 | -------------------------------------------------------------------------------- /src/services/store/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { applyMiddleware, combineReducers, createStore, AnyAction, CombinedState, Store } from 'redux'; 4 | import { connectRouter, routerMiddleware } from 'connected-react-router'; 5 | import { History } from 'history'; 6 | import { composeWithDevTools } from 'redux-devtools-extension'; 7 | import thunkMiddleware, { ThunkMiddleware, ThunkDispatch } from 'redux-thunk'; 8 | import { createBrowserHistory } from 'history'; 9 | 10 | import IAppState from './IAppState'; 11 | import { callsReducer } from '../../stores/calls/reducer'; 12 | import { configReducer } from '../../stores/config/reducer'; 13 | import { authReducer } from '../../stores/auth/reducer'; 14 | import errorReducer from '../../stores/error/reducer'; 15 | import requestingReducer from '../../stores/requesting/reducer'; 16 | import { toastReducer } from '../../stores/toast/reducer'; 17 | import errorToastMiddleware from '../../middlewares/errorToastMiddleware'; 18 | import { serviceReducer } from '../../stores/service/reducer'; 19 | 20 | const createRootReducer = (history: History) => 21 | combineReducers({ 22 | router: connectRouter(history), 23 | config: configReducer, 24 | auth: authReducer, 25 | calls: callsReducer, 26 | errors: errorReducer, 27 | toast: toastReducer, 28 | requesting: requestingReducer, 29 | botServiceStatus: serviceReducer, 30 | }); 31 | 32 | const configureStore = (): Store, AnyAction> => 33 | createStore( 34 | createRootReducer(history), 35 | composeWithDevTools( 36 | applyMiddleware( 37 | routerMiddleware(history), 38 | errorToastMiddleware, 39 | thunkMiddleware as ThunkMiddleware 40 | ) 41 | ) 42 | ); 43 | 44 | export const history = createBrowserHistory(); 45 | 46 | export default configureStore; 47 | 48 | export type DispatchExts = ThunkDispatch; 49 | -------------------------------------------------------------------------------- /src/services/store/mock.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { AnyAction } from "redux"; 4 | import { MockStore } from "redux-mock-store"; 5 | 6 | // Test Helpers 7 | export function findAction(store: MockStore, type: string): AnyAction { 8 | return store.getActions().find((action) => action.type === type); 9 | } 10 | 11 | export function getAction(store: MockStore, type: string): Promise { 12 | const action = findAction(store, type); 13 | if (action) { 14 | return Promise.resolve(action); 15 | } 16 | 17 | return new Promise((resolve) => { 18 | store.subscribe(() => { 19 | const eventualAction = findAction(store, type); 20 | if (eventualAction) { 21 | resolve(eventualAction); 22 | } 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/setupEnzyme.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import * as Enzyme from "enzyme"; 4 | import Adapter from "enzyme-adapter-react-16"; 5 | Enzyme.configure({ adapter: new Adapter() }) -------------------------------------------------------------------------------- /src/stores/auth/actions.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { UserProfile } from '../../models/auth/types'; 4 | import { DefaultError } from '../../models/error/types'; 5 | import { AuthStatus } from '../../models/auth/types'; 6 | import BaseAction from '../base/BaseAction'; 7 | 8 | export type AuthActions = 9 | | AuthStateInitialized 10 | | UserAuthenticating 11 | | UserAuthenticated 12 | | UserUnauthorized 13 | | UserAuthenticationError; 14 | 15 | export const AUTH_STATE_INITIALIZED = 'AUTH_STATE_INITIALIZED'; 16 | export interface AuthStateInitialized extends BaseAction<{ initialized: boolean }> {} 17 | 18 | export const authStateInitialized = (): AuthStateInitialized => ({ 19 | type: AUTH_STATE_INITIALIZED, 20 | payload: { 21 | initialized: true, 22 | }, 23 | }); 24 | 25 | export const USER_AUTHENTICATING = 'USER_AUTHENTICATING'; 26 | export interface UserAuthenticating extends BaseAction<{ authStatus: AuthStatus }> {} 27 | 28 | export const userAuthenticating = (): UserAuthenticating => ({ 29 | type: USER_AUTHENTICATING, 30 | payload: { 31 | authStatus: AuthStatus.Authenticating, 32 | }, 33 | }); 34 | 35 | export const USER_AUTHENTICATED = 'USER_AUTHENTICATED'; 36 | export interface UserAuthenticated extends BaseAction<{ userProfile: UserProfile; authStatus: AuthStatus }> {} 37 | 38 | export const userAuthenticated = (userProfile: UserProfile): UserAuthenticated => ({ 39 | type: USER_AUTHENTICATED, 40 | payload: { 41 | userProfile, 42 | authStatus: AuthStatus.Authenticated, 43 | }, 44 | }); 45 | 46 | export const USER_UNAUTHENTICATED = 'USER_UNAUTHENTICATED'; 47 | export interface UserUnauthenticated extends BaseAction<{ authStatus: AuthStatus }> {} 48 | 49 | export const userUnauthenticated = (): UserUnauthenticated => ({ 50 | type: USER_UNAUTHENTICATED, 51 | payload: { 52 | authStatus: AuthStatus.Unauthenticated, 53 | }, 54 | }); 55 | 56 | export const USER_UNAUTHORIZED = 'USER_UNAUTHORIZED'; 57 | export interface UserUnauthorized extends BaseAction<{ userProfile: UserProfile; authStatus: AuthStatus }> {} 58 | 59 | export const userUnauthorized = (userProfile: UserProfile): UserUnauthorized => ({ 60 | type: USER_UNAUTHORIZED, 61 | payload: { 62 | userProfile, 63 | authStatus: AuthStatus.Unauthorized, 64 | }, 65 | }); 66 | 67 | export const USER_AUTHENTICATION_ERROR = 'USER_AUTHENTICATION_ERROR'; 68 | export interface UserAuthenticationError extends BaseAction {} 69 | 70 | export const userAuthenticationError = (message: string, rawError: any): UserAuthenticationError => ({ 71 | type: USER_AUTHENTICATION_ERROR, 72 | payload: new DefaultError(message, rawError), 73 | error: true, 74 | }); 75 | 76 | export const SET_AUTHENTICATION_DISABLED = "SET_AUTHENTICATION_DISABLED"; 77 | export interface SetAuthenticationDisabled extends BaseAction {}; 78 | 79 | export const setAuthenticationDisabled = (): SetAuthenticationDisabled => ({ 80 | type: SET_AUTHENTICATION_DISABLED, 81 | }) -------------------------------------------------------------------------------- /src/stores/auth/asyncActions.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { push } from 'connected-react-router'; 4 | import { AnyAction } from 'redux'; 5 | import { ThunkAction } from 'redux-thunk'; 6 | import { UserProfile, UserRoles } from '../../models/auth/types'; 7 | import AuthService from '../../services/auth'; 8 | import IAppState from '../../services/store/IAppState'; 9 | import { MsalConfig } from '../config/types'; 10 | import { authStateInitialized, userAuthenticated, userAuthenticating, userAuthenticationError, userUnauthenticated, userUnauthorized } from './actions'; 11 | 12 | const UNAUTHORIZE_ENDPOINT = "/login/unauthorized"; 13 | 14 | export const initilizeAuthentication = 15 | (config: MsalConfig): ThunkAction => 16 | async (dispatch, getState) => { 17 | AuthService.configure(config); 18 | 19 | // Check for accounts in browser 20 | const accounts = AuthService.getAccounts(); 21 | 22 | if (accounts && accounts.length > 0) { 23 | try { 24 | dispatch(userAuthenticating()); 25 | const authResult = await AuthService.requestSilentToken(accounts[0], config.apiClientId); 26 | const userProfile = AuthService.getUserProfile(authResult); 27 | 28 | if (userIsProducer(userProfile)) { 29 | dispatch(userAuthenticated(userProfile)); 30 | } else { 31 | dispatch(userUnauthorized(userProfile)); 32 | dispatch(push(UNAUTHORIZE_ENDPOINT)); 33 | } 34 | } catch (error) { 35 | dispatch(userAuthenticationError("Error has ocurred while trying to initilize authentication", error)); 36 | } 37 | } 38 | // Dispatch MSAL config loaded 39 | dispatch(authStateInitialized()); 40 | }; 41 | 42 | export const signIn = (): ThunkAction => async (dispatch, getState) => { 43 | const msalConfig = getState().config.app?.msalConfig; 44 | dispatch(userAuthenticating()); 45 | 46 | try { 47 | const authResult = await AuthService.signIn(msalConfig?.apiClientId); 48 | const userProfile = AuthService.getUserProfile(authResult); 49 | 50 | if (userIsProducer(userProfile)) { 51 | dispatch(userAuthenticated(userProfile)); 52 | } else { 53 | dispatch(userUnauthorized(userProfile)); 54 | dispatch(push(UNAUTHORIZE_ENDPOINT)); 55 | } 56 | } catch (error) { 57 | console.error(error); 58 | dispatch(userAuthenticationError("Error has ocurred while trying to sign in", error)); 59 | } 60 | } 61 | 62 | export const signOut = ( 63 | username: string 64 | ): ThunkAction => async (dispatch) => { 65 | try { 66 | await AuthService.signOut(username); 67 | // Dispatch sign-out action 68 | dispatch(userUnauthenticated()); 69 | } catch (error) { 70 | console.error(error); 71 | 72 | // Dispatch error action 73 | dispatch(userAuthenticationError("Error has ocurred while trying to sign out", error)); 74 | } 75 | }; 76 | 77 | const userIsProducer = (userProfile: UserProfile): boolean => { 78 | return userProfile.role === UserRoles.Producer; 79 | } -------------------------------------------------------------------------------- /src/stores/auth/reducer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { AuthStatus, UserProfile, UserRoles } from '../../models/auth/types'; 4 | import * as AuthActions from './actions'; 5 | import baseReducer from '../base/BaseReducer'; 6 | 7 | export interface AuthState { 8 | status: AuthStatus; 9 | userProfile: UserProfile | null; 10 | initialized: boolean; 11 | } 12 | 13 | export const INITIAL_STATE: AuthState = { 14 | status: AuthStatus.Unauthenticated, 15 | userProfile: null, 16 | initialized: false, 17 | }; 18 | 19 | export const authReducer = baseReducer(INITIAL_STATE, { 20 | [AuthActions.USER_AUTHENTICATED](state: AuthState, action: AuthActions.UserAuthenticated): AuthState { 21 | return { 22 | ...state, 23 | status: action.payload!.authStatus, 24 | userProfile: action.payload!.userProfile, 25 | } 26 | }, 27 | [AuthActions.USER_UNAUTHENTICATED](state: AuthState, action: AuthActions.UserUnauthenticated): AuthState { 28 | return { 29 | ...state, 30 | status: action.payload!.authStatus, 31 | } 32 | }, 33 | [AuthActions.USER_AUTHENTICATING](state: AuthState, action: AuthActions.UserAuthenticating): AuthState { 34 | return { 35 | ...state, 36 | status: action.payload!.authStatus, 37 | } 38 | }, 39 | [AuthActions.USER_UNAUTHORIZED](state: AuthState, action: AuthActions.UserUnauthorized): AuthState { 40 | return { 41 | ...state, 42 | status: action.payload!.authStatus, 43 | userProfile: action.payload!.userProfile, 44 | } 45 | }, 46 | [AuthActions.AUTH_STATE_INITIALIZED](state: AuthState, action: AuthActions.AuthStateInitialized): AuthState { 47 | return { 48 | ...state, 49 | initialized: action.payload!.initialized, 50 | } 51 | }, 52 | [AuthActions.SET_AUTHENTICATION_DISABLED](state: AuthState, action: AuthActions.SetAuthenticationDisabled): AuthState { 53 | return { 54 | ...state, 55 | status: AuthStatus.Authenticated, 56 | userProfile: { 57 | id: '', 58 | username: 'Local User', 59 | role: UserRoles.Producer, 60 | }, 61 | initialized: true, 62 | } 63 | }, 64 | }) -------------------------------------------------------------------------------- /src/stores/base/BaseAction.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { Action } from 'redux'; 4 | import { RequestResponse } from '../../services/api'; 5 | 6 | export interface RequestFinishedActionParameters { 7 | payload: RequestResponse; 8 | meta?: any; 9 | } 10 | 11 | export default interface BaseAction extends Action { 12 | type: string; 13 | payload?: T; 14 | error?: boolean; 15 | meta?: any; 16 | } 17 | 18 | export const createBaseAction = ({ 19 | type, 20 | payload, 21 | error = false, 22 | meta = null, 23 | }: BaseAction): BaseAction => { 24 | return { type, payload, error, meta }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/stores/base/BaseReducer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { Reducer } from 'redux'; 4 | import BaseAction from './BaseAction'; 5 | 6 | type ReducerMethod = (state: T, action: BaseAction) => T; 7 | type ReducerMethods = { [actionType: string]: ReducerMethod }; 8 | 9 | /* 10 | The API related reducers have to implement this baseReducer. If an action of type 11 | REQUEST_SOMETHING_FINISHED is flagged with error, it doesn't have to be processed. 12 | */ 13 | export default function baseReducer(initialState: T, methods: ReducerMethods): Reducer { 14 | return (state: T = initialState, action: BaseAction): T => { 15 | const method: ReducerMethod | undefined = methods[action.type]; 16 | 17 | // if the action doesn't have a method or it has been flagged as error 18 | // we return the current state and do not mutate the state 19 | if (!method || action.error) { 20 | return state; 21 | } 22 | 23 | return method(state, action); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/stores/calls/actions/disconnectCall.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { Call } from "../../../models/calls/types"; 4 | import { ApiError } from "../../../models/error/types"; 5 | import { RequestResponse, Resource } from "../../../services/api"; 6 | import BaseAction, { RequestFinishedActionParameters } from "../../base/BaseAction"; 7 | 8 | export const REQUEST_DISCONNECT_CALL = 'REQUEST_DISCONNECT_CALL'; 9 | export const REQUEST_DISCONNECT_CALL_FINISHED = 'REQUEST_DISCONNECT_CALL_FINISHED'; 10 | 11 | export interface RequestDisconnectCall extends BaseAction {} 12 | export interface RequestDisconnectCallFinished extends BaseAction>> {} 13 | 14 | export const requestDisconnectCall = (): RequestDisconnectCall => ({ 15 | type: REQUEST_DISCONNECT_CALL, 16 | }); 17 | 18 | export const requestDisconnectCallFinished = ({ 19 | payload, 20 | meta, 21 | }: RequestFinishedActionParameters>): RequestDisconnectCallFinished => ({ 22 | type: REQUEST_DISCONNECT_CALL_FINISHED, 23 | payload: payload, 24 | error: payload instanceof ApiError, 25 | }); -------------------------------------------------------------------------------- /src/stores/calls/actions/getActiveCalls.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { Call } from '../../../models/calls/types'; 4 | import { ApiError } from '../../../models/error/types'; 5 | import { RequestResponse } from '../../../services/api'; 6 | import BaseAction, { RequestFinishedActionParameters } from '../../base/BaseAction'; 7 | 8 | export const REQUEST_ACTIVE_CALLS = 'REQUEST_ACTIVE_CALLS'; 9 | export const REQUEST_ACTIVE_CALLS_FINISHED = 'REQUEST_ACTIVE_CALLS_FINISHED'; 10 | 11 | export interface RequestActiveCalls extends BaseAction {} 12 | export interface RequestActiveCallsFinished extends BaseAction> {} 13 | 14 | export const requestActiveCalls = (): RequestActiveCalls => ({ 15 | type: REQUEST_ACTIVE_CALLS, 16 | }); 17 | 18 | export const requestActiveCallsFinished = ({ 19 | payload, 20 | meta, 21 | }: RequestFinishedActionParameters): RequestActiveCallsFinished => ({ 22 | type: REQUEST_ACTIVE_CALLS_FINISHED, 23 | payload: payload, 24 | error: payload instanceof ApiError, 25 | }); 26 | -------------------------------------------------------------------------------- /src/stores/calls/actions/getCall.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { Call } from '../../../models/calls/types'; 4 | import { ApiError } from '../../../models/error/types'; 5 | import { RequestResponse } from '../../../services/api'; 6 | import BaseAction, { RequestFinishedActionParameters } from '../../base/BaseAction'; 7 | 8 | export enum RequestCallType { 9 | NewCall = 'NewCall', 10 | ExistingCall = 'ExistingCall', 11 | } 12 | 13 | export const REQUEST_CALL = 'REQUEST_CALL'; 14 | export const REQUEST_CALL_FINISHED = 'REQUEST_CALL_FINISHED'; 15 | 16 | export interface RequestCall extends BaseAction {} 17 | export interface RequestCallFinished extends BaseAction> {} 18 | 19 | export const requestCall = (): BaseAction => ({ 20 | type: REQUEST_CALL, 21 | }); 22 | 23 | export const requestCallFinished = ({ 24 | payload, 25 | meta, 26 | }: RequestFinishedActionParameters): BaseAction> => ({ 27 | type: REQUEST_CALL_FINISHED, 28 | payload: payload, 29 | error: payload instanceof ApiError, 30 | }); 31 | 32 | export const REQUEST_POLLING_CALL = 'REQUEST_POLLING_CALL'; 33 | export const REQUEST_POLLING_CALL_FINISHED = 'REQUEST_POLLING_CALL_FINISHED'; 34 | 35 | export interface RequestPollingCall extends BaseAction {} 36 | export interface RequestPollingCallFinished extends BaseAction> {} 37 | 38 | export const requestPollingCall = (): BaseAction => ({ 39 | type: REQUEST_POLLING_CALL, 40 | }); 41 | 42 | export const requestPollingCallFinished = ({ 43 | payload, 44 | meta, 45 | }: RequestFinishedActionParameters): BaseAction> => ({ 46 | type: REQUEST_POLLING_CALL_FINISHED, 47 | payload: payload, 48 | meta: meta, 49 | error: payload instanceof ApiError, 50 | }); 51 | -------------------------------------------------------------------------------- /src/stores/calls/actions/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | export * from './disconnectCall'; 4 | export * from './getCall'; 5 | export * from './joinCall'; 6 | export * from './newStreamDrawer'; 7 | export * from './startStream'; 8 | export * from './stopStream'; 9 | export * from './getActiveCalls'; 10 | export * from './startInjectionStream'; 11 | export * from './stopInjectionStream'; 12 | export * from './muteBot'; 13 | export * from './unmuteBot'; 14 | export * from './newInjectionStreamDrawer'; 15 | export * from './updateDefaults'; 16 | export * from './refreshStreamKey'; 17 | export * from './updateStreamPhoto'; 18 | -------------------------------------------------------------------------------- /src/stores/calls/actions/joinCall.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { Call } from '../../../models/calls/types'; 4 | import { ApiError } from '../../../models/error/types'; 5 | import { RequestResponse } from '../../../services/api'; 6 | import BaseAction, { RequestFinishedActionParameters } from '../../base/BaseAction'; 7 | 8 | export const REQUEST_JOIN_CALL = 'REQUEST_JOIN_CALL'; 9 | export const REQUEST_JOIN_CALL_FINISHED = 'REQUEST_JOIN_CALL_FINISHED'; 10 | 11 | export interface RequestJoinCall extends BaseAction<{ callUrl: string }> {} 12 | export interface RequestJoinCallFinished extends BaseAction> {} 13 | 14 | export const requestJoinCall = (callUrl: string): BaseAction<{ callUrl: string }> => ({ 15 | type: REQUEST_JOIN_CALL, 16 | payload: { 17 | callUrl, 18 | }, 19 | }); 20 | 21 | export const requestJoinCallFinished = ({ 22 | payload, 23 | meta, 24 | }: RequestFinishedActionParameters): BaseAction> => ({ 25 | type: REQUEST_JOIN_CALL_FINISHED, 26 | payload: payload, 27 | error: payload instanceof ApiError, 28 | }); 29 | -------------------------------------------------------------------------------- /src/stores/calls/actions/muteBot.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { InjectionStream } from '../../../models/calls/types'; 4 | import { RequestResponse } from '../../../services/api'; 5 | import BaseAction, { RequestFinishedActionParameters } from '../../base/BaseAction'; 6 | import { ApiError } from '../../../models/error/types'; 7 | 8 | export const REQUEST_MUTE_BOT = 'REQUEST_MUTE_BOT'; 9 | export const REQUEST_MUTE_BOT_FINISHED = 'REQUEST_MUTE_BOT_FINISHED'; 10 | 11 | export interface RequestMuteBot extends BaseAction {} 12 | export interface RequestMuteBotFinished extends BaseAction> {} 13 | 14 | export const requestMuteBot = (): RequestMuteBot => ({ 15 | type: REQUEST_MUTE_BOT, 16 | }); 17 | 18 | export const requestMuteBotFinished = ({ 19 | payload, 20 | meta, 21 | }: RequestFinishedActionParameters): RequestMuteBotFinished => ({ 22 | type: REQUEST_MUTE_BOT_FINISHED, 23 | payload: payload, 24 | error: payload instanceof ApiError, 25 | }); 26 | -------------------------------------------------------------------------------- /src/stores/calls/actions/newInjectionStreamDrawer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { NewInjectionStreamDrawerOpenParameters } from "../../../models/calls/types"; 4 | import BaseAction from "../../base/BaseAction"; 5 | 6 | export const OPEN_NEW_INJECTION_STREAM_DRAWER = 'OPEN_NEW_INJECTION_STREAM_DRAWER'; 7 | export const CLOSE_NEW_INJECTION_STREAM_DRAWER = 'CLOSE_NEW_INJECTION_STREAM_DRAWER'; 8 | 9 | export interface OpenNewInjectionStreamDrawer extends BaseAction {} 10 | export interface CloseNewInjectionStreamDrawer extends BaseAction {} 11 | 12 | export const openNewInjectionStreamDrawer = ({ 13 | callId, 14 | }: NewInjectionStreamDrawerOpenParameters): OpenNewInjectionStreamDrawer => ({ 15 | type: OPEN_NEW_INJECTION_STREAM_DRAWER, 16 | payload: { 17 | callId, 18 | }, 19 | }); 20 | 21 | export const closeNewInjectionStreamDrawer = (): CloseNewInjectionStreamDrawer =>({ 22 | type: CLOSE_NEW_INJECTION_STREAM_DRAWER, 23 | }) -------------------------------------------------------------------------------- /src/stores/calls/actions/newStreamDrawer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { NewStreamDrawerOpenParameters } from "../../../models/calls/types"; 4 | import BaseAction from "../../base/BaseAction"; 5 | 6 | export const OPEN_NEW_STREAM_DRAWER = 'OPEN_NEW_STREAM_DRAWER'; 7 | export const CLOSE_NEW_STREAM_DRAWER = 'CLOSE_NEW_STREAM_DRAWER'; 8 | 9 | export interface OpenNewStreamDrawer extends BaseAction {} 10 | export interface CloseNewStreamDrawer extends BaseAction {} 11 | 12 | export const openNewStreamDrawer = ({ 13 | callId, 14 | streamType, 15 | participantId, 16 | participantName, 17 | }: NewStreamDrawerOpenParameters): OpenNewStreamDrawer => ({ 18 | type: OPEN_NEW_STREAM_DRAWER, 19 | payload: { 20 | callId, 21 | streamType, 22 | participantId, 23 | participantName, 24 | }, 25 | }); 26 | 27 | export const closeNewStreamDrawer = (): CloseNewStreamDrawer =>({ 28 | type: CLOSE_NEW_STREAM_DRAWER, 29 | }) -------------------------------------------------------------------------------- /src/stores/calls/actions/refreshStreamKey.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { CallStreamKey } from '../../../models/calls/types'; 4 | import { ApiError } from '../../../models/error/types'; 5 | import { RequestResponse } from '../../../services/api'; 6 | import BaseAction, { RequestFinishedActionParameters } from '../../base/BaseAction'; 7 | 8 | export const REQUEST_REFRESH_STREAM_KEY = 'REQUEST_REFRESH_STREAM_KEY'; 9 | export const REQUEST_REFRESH_STREAM_KEY_FINISHED = 'REQUEST_REFRESH_STREAM_KEY_FINISHED'; 10 | 11 | export interface RequestRefreshStreamKey extends BaseAction {} 12 | export interface RequestRefreshStreamKeyFinished extends BaseAction> {} 13 | 14 | export const requestRefreshStreamKey = (): RequestRefreshStreamKey => ({ 15 | type: REQUEST_REFRESH_STREAM_KEY, 16 | }); 17 | 18 | export const requestRefreshStreamKeyFinished = ({ 19 | payload, 20 | meta 21 | }: RequestFinishedActionParameters): RequestRefreshStreamKeyFinished => ({ 22 | type: REQUEST_REFRESH_STREAM_KEY_FINISHED, 23 | payload: payload, 24 | error: payload instanceof ApiError, 25 | }); 26 | -------------------------------------------------------------------------------- /src/stores/calls/actions/startInjectionStream.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { InjectionStream } from '../../../models/calls/types'; 4 | import { ApiError } from '../../../models/error/types'; 5 | import { RequestResponse } from '../../../services/api'; 6 | import BaseAction, { RequestFinishedActionParameters } from '../../base/BaseAction'; 7 | 8 | export const REQUEST_START_INJECTION_STREAM = 'REQUEST_START_INJECTION_STREAM'; 9 | export const REQUEST_START_INJECTION_STREAM_FINISHED = 'REQUEST_START_INJECTION_STREAM_FINISHED'; 10 | 11 | export interface RequestStartInjectionStream extends BaseAction {} 12 | export interface RequestStartInjectionStreamFinished extends BaseAction> {} 13 | 14 | export const requestStartInjectionStream = (): RequestStartInjectionStream => ({ 15 | type: REQUEST_START_INJECTION_STREAM, 16 | }); 17 | 18 | export const requestStartInjectionStreamFinished = ({ 19 | payload, 20 | meta, 21 | }: RequestFinishedActionParameters): RequestStartInjectionStreamFinished => ({ 22 | type: REQUEST_START_INJECTION_STREAM_FINISHED, 23 | payload: payload, 24 | error: payload instanceof ApiError, 25 | }); 26 | -------------------------------------------------------------------------------- /src/stores/calls/actions/startStream.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { Stream } from '../../../models/calls/types'; 4 | import { ApiError } from '../../../models/error/types'; 5 | import { RequestResponse, Resource } from '../../../services/api'; 6 | import BaseAction, { RequestFinishedActionParameters } from '../../base/BaseAction'; 7 | 8 | export const REQUEST_START_STREAM = 'REQUEST_START_STREAM'; 9 | export const REQUEST_START_STREAM_FINISHED = 'REQUEST_START_STREAM_FINISHED'; 10 | 11 | export interface RequestStartStream extends BaseAction {} 12 | export interface RequestStartStreamFinished extends BaseAction>> {} 13 | 14 | export const requestStartStream = (): RequestStartStream => ({ 15 | type: REQUEST_START_STREAM, 16 | }); 17 | 18 | export const requestStartStreamFinished = ({ 19 | payload, 20 | meta, 21 | }: RequestFinishedActionParameters>): RequestStartStreamFinished => ({ 22 | type: REQUEST_START_STREAM_FINISHED, 23 | payload: payload, 24 | error: payload instanceof ApiError, 25 | }); 26 | -------------------------------------------------------------------------------- /src/stores/calls/actions/stopInjectionStream.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { InjectionStream } from "../../../models/calls/types"; 4 | import { ApiError } from "../../../models/error/types"; 5 | import { RequestResponse } from "../../../services/api"; 6 | import BaseAction, { RequestFinishedActionParameters } from "../../base/BaseAction"; 7 | 8 | export const REQUEST_STOP_INJECTION_STREAM = 'REQUEST_STOP_INJECTION_STREAM'; 9 | export const REQUEST_STOP_INJECTION_STREAM_FINISHED = 'REQUEST_STOP_INJECTION_STREAM_FINISHED'; 10 | 11 | export interface RequestStoptInjectionStream extends BaseAction {} 12 | export interface RequestStoptInjectionStreamFinished extends BaseAction> {} 13 | 14 | export const requestStoptInjectionStream = (): RequestStoptInjectionStream => ({ 15 | type: REQUEST_STOP_INJECTION_STREAM, 16 | }); 17 | 18 | export const requestStoptInjectionStreamFinished = ({ 19 | payload, 20 | meta, 21 | }: RequestFinishedActionParameters): RequestStoptInjectionStreamFinished => ({ 22 | type: REQUEST_STOP_INJECTION_STREAM_FINISHED, 23 | payload: payload, 24 | error: payload instanceof ApiError, 25 | }); -------------------------------------------------------------------------------- /src/stores/calls/actions/stopStream.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { Stream } from '../../../models/calls/types'; 4 | import { ApiError } from '../../../models/error/types'; 5 | import { RequestResponse, Resource } from '../../../services/api'; 6 | import BaseAction, { RequestFinishedActionParameters } from '../../base/BaseAction'; 7 | 8 | export const REQUEST_STOP_STREAM = 'REQUEST_STOP_STREAM'; 9 | export const REQUEST_STOP_STREAM_FINISHED = 'REQUEST_STOP_STREAM_FINISHED'; 10 | 11 | export interface RequestStopStream extends BaseAction {} 12 | export interface RequestStopStreamFinished extends BaseAction>> {} 13 | 14 | export const requestStopStream = (): RequestStopStream => ({ 15 | type: REQUEST_STOP_STREAM, 16 | }); 17 | 18 | export const requestStopStreamFinished = ({ 19 | payload, 20 | meta, 21 | }: RequestFinishedActionParameters>): RequestStopStreamFinished => ({ 22 | type: REQUEST_STOP_STREAM_FINISHED, 23 | payload: payload, 24 | error: payload instanceof ApiError, 25 | }); 26 | -------------------------------------------------------------------------------- /src/stores/calls/actions/unmuteBot.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { InjectionStream } from "../../../models/calls/types"; 4 | import { ApiError } from "../../../models/error/types"; 5 | import { RequestResponse } from "../../../services/api"; 6 | import BaseAction, { RequestFinishedActionParameters } from "../../base/BaseAction"; 7 | 8 | export const REQUEST_UNMUTE_BOT = 'REQUEST_UNMUTE_BOT'; 9 | export const REQUEST_UNMUTE_BOT_FINISHED = 'REQUEST_UNMUTE_BOT_FINISHED'; 10 | 11 | export interface RequestUnmuteBot extends BaseAction {} 12 | export interface RequestUnmuteBotFinished extends BaseAction> {} 13 | 14 | export const requestUnmuteBot = (): RequestUnmuteBot => ({ 15 | type: REQUEST_UNMUTE_BOT, 16 | }); 17 | 18 | export const requestUnmuteBotFinished = ({ 19 | payload, 20 | meta, 21 | }: RequestFinishedActionParameters): RequestUnmuteBotFinished => ({ 22 | type: REQUEST_UNMUTE_BOT_FINISHED, 23 | payload: payload, 24 | error: payload instanceof ApiError, 25 | }); -------------------------------------------------------------------------------- /src/stores/calls/actions/updateDefaults.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { CallDefaults } from '../../../models/calls/types'; 4 | import BaseAction from '../../base/BaseAction'; 5 | 6 | export const UPDATE_CALL_DEFAULTS = 'UPDATE_CALL_DEFAULTS'; 7 | export interface UpdateCallDefaults 8 | extends BaseAction<{ 9 | callId: string; 10 | defaults: CallDefaults; 11 | }> {} 12 | 13 | export const updateCallDefaults = ({ 14 | callId, 15 | defaults, 16 | }: { 17 | callId: string; 18 | defaults: CallDefaults; 19 | }): UpdateCallDefaults => ({ 20 | type: UPDATE_CALL_DEFAULTS, 21 | payload: { 22 | callId, 23 | defaults, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /src/stores/calls/actions/updateStreamPhoto.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import BaseAction from '../../base/BaseAction'; 4 | 5 | export const UPDATE_STREAM_PHOTO = 'UPDATE_STREAM_PHOTO'; 6 | 7 | export interface UpdateStreamPhoto extends BaseAction<{streamId: string, photo: string, callId: string}> {} 8 | 9 | export const updateStreamPhoto = (streamId: string, photo: string, callId: string): UpdateStreamPhoto => ({ 10 | type: UPDATE_STREAM_PHOTO, 11 | payload: {streamId, photo, callId}, 12 | }); -------------------------------------------------------------------------------- /src/stores/calls/selectors.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { createSelector} from 'reselect'; 4 | import { ActiveStatuses, Call, CallState, CallType, InactiveStatuses, KeyLength, SpecialStreamTypes, StreamProtocol, StreamType } from '../../models/calls/types'; 5 | import IAppState from '../../services/store/IAppState'; 6 | import { CallInfoProps, CallStreamsProps, NewInjectionStreamDrawerProps, NewStreamDrawerProps } from '../../views/call-details/types'; 7 | import { ICallsState } from './reducer'; 8 | 9 | export const selectNewCall = createSelector( 10 | (state: IAppState) => state.calls, 11 | (state: ICallsState) => state.newCall 12 | ); 13 | 14 | export const selectActiveCalls = createSelector( 15 | (state: IAppState) => state.calls, 16 | (state: ICallsState) => state.activeCalls 17 | ); 18 | 19 | export const selectCallStreams = createSelector( 20 | (state: IAppState) => state.calls, 21 | (state: IAppState, callId: string) => callId, 22 | _selectCallStreams 23 | ) 24 | 25 | export const selectNewInjectionStreamDrawerProps = createSelector( 26 | (state: IAppState) => state.calls, 27 | (state: IAppState, callId: string) => callId, 28 | _selectNewInjectionStreamDrawerProps 29 | ) 30 | 31 | export const selectNewStreamDrawerProps = createSelector( 32 | (state: IAppState) => state.calls, 33 | (state: IAppState, callId: string) => callId, 34 | _selectNewStreamDrawerProps 35 | ) 36 | 37 | export const selectCallInfoProps = createSelector( 38 | (state: IAppState) => state.calls, 39 | (state: IAppState, callId: string) => callId, 40 | _selectCallInfoProps 41 | ) 42 | 43 | 44 | function _selectCallStreams(callState: ICallsState, callId: string): CallStreamsProps { 45 | const call = callState.activeCalls.find(call => call.id === callId); 46 | 47 | if(!call){ 48 | return { 49 | callId, 50 | callEnabled: false, 51 | mainStreams: [], 52 | participantStreams: [], 53 | activeStreams: [], 54 | primarySpeakerEnabled: false, 55 | stageEnabled: false, 56 | injectionStream: null, 57 | callProtocol: 0, 58 | } 59 | } 60 | 61 | return { 62 | callId, 63 | callEnabled: call.state === CallState.Established, 64 | mainStreams: call.streams.filter((o) => SpecialStreamTypes.includes(o.type) && InactiveStatuses.includes(o.state)), 65 | participantStreams: call.streams.filter( 66 | (o) => o.type === StreamType.Participant && InactiveStatuses.includes(o.state) 67 | ), 68 | activeStreams: call.streams.filter((o) => ActiveStatuses.includes(o.state)), 69 | primarySpeakerEnabled: 70 | call.streams.filter( 71 | (o) => o.type === StreamType.Participant && o.isSharingVideo && o.isSharingAudio && !o.audioMuted 72 | ).length > 0, 73 | stageEnabled: call.streams.filter((o) => o.type === StreamType.Participant && o.isSharingScreen).length > 0, 74 | injectionStream: call.injectionStream, 75 | callProtocol: call.defaultProtocol, 76 | } 77 | } 78 | 79 | function _selectNewInjectionStreamDrawerProps(callState: ICallsState, callId: string): NewInjectionStreamDrawerProps { 80 | const call = callState.activeCalls.find((o) => o.id === callId); 81 | const newInjectionStream = callState.newInjectionStream; 82 | if (!call) { 83 | return { 84 | call: null, 85 | newInjectionStream, 86 | }; 87 | } 88 | 89 | return { 90 | call, 91 | newInjectionStream, 92 | }; 93 | } 94 | 95 | function _selectNewStreamDrawerProps(callState: ICallsState, callId: string): NewStreamDrawerProps { 96 | const call = callState.activeCalls.find((o) => o.id === callId); 97 | const newStream = callState.newStream; 98 | if (!call) { 99 | return { 100 | call: null, 101 | newStream, 102 | }; 103 | } 104 | 105 | return { 106 | call, 107 | newStream, 108 | }; 109 | } 110 | 111 | function _selectCallInfoProps(callState: ICallsState, callId: string): CallInfoProps { 112 | const call = callState.activeCalls.find((o) => o.id === callId); 113 | 114 | if (!call) { 115 | return { 116 | call: { 117 | ...CALL_INITIALIZING_PLACEHOLDER, 118 | }, 119 | streams: [], 120 | }; 121 | } 122 | 123 | return { 124 | call, 125 | streams: call.streams, 126 | }; 127 | } 128 | 129 | const CALL_INITIALIZING_PLACEHOLDER: Call = { 130 | id: '0', 131 | displayName: 'Loading Call', 132 | botFqdn: '', 133 | botIp: '', 134 | connectionPool: { 135 | available: 0, 136 | used: 0, 137 | }, 138 | createdAt: new Date(), 139 | defaultProtocol: StreamProtocol.SRT, 140 | defaultLatency: 0, 141 | defaultPassphrase: '', 142 | defaultKeyLength: KeyLength.None, 143 | errorMessage: null, 144 | joinUrl: '', 145 | state: CallState.Establishing, 146 | meetingType: CallType.Default, 147 | streams: [], 148 | injectionStream: null, 149 | privateContext: null, 150 | }; 151 | -------------------------------------------------------------------------------- /src/stores/config/actions.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { ApiError } from '../../models/error/types'; 4 | import BaseAction from '../base/BaseAction'; 5 | import { AppConfig } from './types'; 6 | 7 | export const LOAD_CONFIG = 'LOAD_CONFIG'; 8 | export const LOAD_CONFIG_ERROR = 'LOAD_CONFIG_ERROR'; 9 | 10 | export interface LoadConfig extends BaseAction {} 11 | export interface LoadConfigError extends BaseAction {} 12 | 13 | export const loadConfig = (payload: AppConfig): LoadConfig => ({ 14 | type: LOAD_CONFIG, 15 | payload: payload, 16 | }); 17 | 18 | export const loadConfigError = (error: ApiError): LoadConfigError => ({ 19 | type: LOAD_CONFIG_ERROR, 20 | payload: error, 21 | error: true, 22 | }); 23 | -------------------------------------------------------------------------------- /src/stores/config/asyncActions.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { AnyAction } from 'redux'; 4 | import { ThunkAction } from 'redux-thunk'; 5 | import IAppState from '../../services/store/IAppState'; 6 | import { getConfig } from './loader'; 7 | import { loadConfig as loadConfigAction, loadConfigError } from './actions'; 8 | 9 | export const loadConfig = (): ThunkAction => async (dispatch, getState) => { 10 | try { 11 | const config = await getConfig(); 12 | dispatch(loadConfigAction(config)); 13 | } catch (error) { 14 | dispatch(loadConfigError(error)); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/stores/config/constants.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | export const FEATUREFLAG_DISABLE_AUTHENTICATION = "DISABLE_AUTHENTICATION"; 4 | -------------------------------------------------------------------------------- /src/stores/config/loader.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { AppConfig } from './types'; 4 | import Axios from 'axios'; 5 | import { DefaultError } from '../../models/error/types'; 6 | const configUrl = '/config.json'; 7 | 8 | const loader = new Promise((resolve, reject) => { 9 | Axios.get(configUrl) 10 | .then((o) => resolve(o.data as AppConfig)) 11 | .catch((err) => { 12 | console.error('Error loading config:', err); 13 | const errorResponse = new DefaultError('Error loading config', err); 14 | 15 | reject(errorResponse); 16 | }); 17 | }); 18 | 19 | export const getConfig = (): Promise => loader; 20 | -------------------------------------------------------------------------------- /src/stores/config/reducer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import baseReducer from "../base/BaseReducer"; 4 | import { AppConfig } from "./types"; 5 | import * as ConfigActions from "./actions"; 6 | export interface ConfigState { 7 | initialized: boolean; 8 | app: AppConfig | null; 9 | } 10 | 11 | export const INITIAL_STATE: ConfigState = { 12 | initialized: false, 13 | app: null, 14 | } 15 | 16 | export const configReducer = baseReducer(INITIAL_STATE, { 17 | [ConfigActions.LOAD_CONFIG](state: ConfigState, action: ConfigActions.LoadConfig): ConfigState{ 18 | return { 19 | initialized: true, 20 | app: action.payload! 21 | } 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /src/stores/config/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | export interface AppConfig { 4 | buildNumber: string; 5 | apiBaseUrl: string; 6 | msalConfig: MsalConfig; 7 | featureFlags: FeatureFlags | undefined; 8 | } 9 | 10 | export interface BaseFeatureFlag { 11 | description: string; 12 | isActive: boolean; 13 | } 14 | 15 | export type FeatureFlagsTypes = BaseFeatureFlag; 16 | 17 | export interface FeatureFlags { 18 | readonly [key: string]: FeatureFlagsTypes; 19 | } 20 | 21 | export interface MsalConfig { 22 | spaClientId: string, 23 | apiClientId: string, 24 | groupId: string, 25 | authority: string, 26 | redirectUrl: string 27 | } 28 | -------------------------------------------------------------------------------- /src/stores/error/actions.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import BaseAction from '../base/BaseAction'; 4 | 5 | export const REMOVE_ERROR: string = 'REMOVE_ERROR'; 6 | export const CLEAR_ALL_ERROR: string = 'CLEAR_ALL_ERROR'; 7 | 8 | export function removeById(id: string): BaseAction { 9 | return { 10 | type: REMOVE_ERROR, 11 | payload: id, 12 | }; 13 | } 14 | 15 | export function clearAll(): BaseAction { 16 | return { 17 | type: CLEAR_ALL_ERROR, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/stores/error/reducer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | /* 4 | * Note: This reducer breaks convention on how reducers should be setup. 5 | */ 6 | 7 | import { ApplicationError, DefaultError } from '../../models/error/types'; 8 | import BaseAction from '../base/BaseAction'; 9 | import * as ErrorAction from './actions'; 10 | 11 | export interface ErrorState { 12 | [key: string]: ApplicationError; 13 | } 14 | 15 | export const initialState: ErrorState = {}; 16 | 17 | export default function errorReducer(state: ErrorState = initialState, action: BaseAction): ErrorState { 18 | const { type, error, payload } = action; 19 | 20 | /* 21 | * Removes an ErrorRseponse by it's id that is in the action payload. 22 | */ 23 | 24 | if (type === ErrorAction.REMOVE_ERROR) { 25 | // Create a new state without the error that has the same id as the payload. 26 | return Object.entries(state).reduce((newState: ErrorState, [key, value]: [string, ApplicationError]) => { 27 | if (value.id !== payload) { 28 | newState[key] = value; 29 | } 30 | 31 | return newState; 32 | }, {}); 33 | } 34 | 35 | /* 36 | * Removes all errors by returning the initial state which is an empty object. 37 | */ 38 | if (type === ErrorAction.CLEAR_ALL_ERROR) { 39 | return initialState; 40 | } 41 | 42 | /* 43 | * Checking if is a default error 44 | */ 45 | 46 | const isDefaultError = payload instanceof DefaultError && Boolean(error); 47 | if (isDefaultError) { 48 | //Adds the default error 49 | return { 50 | ...state, 51 | [type]: payload, 52 | }; 53 | } 54 | 55 | /* 56 | * APi Errors logic 57 | */ 58 | 59 | /* 60 | * True if the action type has the key word '_FINISHED' then the action is finished. 61 | */ 62 | const isFinishedRequestType = type.includes('_FINISHED'); 63 | /* 64 | * True if the action type has the key word 'REQUEST_' and not '_FINISHED'. 65 | */ 66 | const isStartRequestType = type.includes('REQUEST_') && !isFinishedRequestType; 67 | 68 | /* 69 | * If an action is started we want to remove any old errors because there is a new action has been re-dispatched. 70 | */ 71 | if (isStartRequestType) { 72 | // Using ES7 Object Rest Spread operator to omit properties from an object. 73 | const { [`${type}_FINISHED`]: value, ...stateWithoutFinishedType } = state; 74 | 75 | return stateWithoutFinishedType; 76 | } 77 | 78 | /* 79 | * True if the action is finished and the error property is true. 80 | */ 81 | const isError: boolean = isFinishedRequestType && Boolean(error); 82 | 83 | /* 84 | * For any start and finished actions that don't have errors we return the current state. 85 | */ 86 | if (isError === false) { 87 | return state; 88 | } 89 | 90 | /* 91 | * At this point the "type" will be a finished action type (e.g. "SomeAction.REQUEST_*_FINISHED"). 92 | * The payload will be a ErrorRseponse. 93 | */ 94 | return { 95 | ...state, 96 | [type]: payload, 97 | }; 98 | } 99 | -------------------------------------------------------------------------------- /src/stores/requesting/reducer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | /* 4 | * Note: This reducer breaks convention on how reducers should be setup. 5 | */ 6 | 7 | import BaseAction from "../base/BaseAction"; 8 | 9 | export interface RequestingState { 10 | readonly [key: string]: boolean; 11 | } 12 | 13 | export const initialState: RequestingState = {}; 14 | 15 | export default function requestingReducer(state: RequestingState = initialState, action: BaseAction): RequestingState { 16 | // We only take actions that include 'REQUEST_' in the type. 17 | const isRequestType: boolean = action.type.includes('REQUEST_'); 18 | 19 | if (isRequestType === false) { 20 | return state; 21 | } 22 | 23 | // Remove the string '_FINISHED' from the action type so we can use the first part as the key on the state. 24 | const requestName: string = action.type.replace('_FINISHED', ''); 25 | // If the action type includes '_FINISHED'. The boolean value will be false. Otherwise we 26 | // assume it is a starting request and will be set to true. 27 | const isFinishedRequestType: boolean = action.type.includes('_FINISHED'); 28 | 29 | return { 30 | ...state, 31 | [requestName]: isFinishedRequestType === false, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/stores/requesting/selectors.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { createSelector, ParametricSelector } from "reselect"; 4 | import IAppState from "../../services/store/IAppState"; 5 | import { RequestingState } from "./reducer"; 6 | 7 | export const selectRequesting: ParametricSelector = createSelector( 8 | (state: IAppState) => state.requesting, 9 | (state: IAppState, actionTypes: string[]) => actionTypes, 10 | _selectRequesting 11 | ); 12 | 13 | function _selectRequesting(requestingState: RequestingState, actionTypes: string[]): boolean { 14 | return actionTypes.some((actionType: string) => requestingState[actionType]); 15 | } -------------------------------------------------------------------------------- /src/stores/service/actions.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { ApiError } from "../../models/error/types"; 4 | import { BotService } from "../../models/service/types"; 5 | import { RequestResponse, Resource } from "../../services/api"; 6 | import BaseAction, { RequestFinishedActionParameters } from "../base/BaseAction"; 7 | 8 | export const REQUEST_START_SERVICE = "REQUEST_START_SERVICE"; 9 | export const REQUEST_START_SERVICE_FINISHED = "REQUEST_START_SERVICE_FINISHED"; 10 | 11 | export interface RequestStartService extends BaseAction {} 12 | export interface RequestStartServiceFinished extends BaseAction>> {} 13 | 14 | export const requestStartService = (): RequestStartService => ({ 15 | type: REQUEST_START_SERVICE, 16 | }); 17 | 18 | export const requestStartServiceFinished = ({ 19 | payload, 20 | meta, 21 | }: RequestFinishedActionParameters>): RequestStartServiceFinished => ({ 22 | type: REQUEST_START_SERVICE_FINISHED, 23 | payload: payload, 24 | error: payload instanceof ApiError, 25 | }); 26 | 27 | export const REQUEST_STOP_SERVICE = "REQUEST_STOP_SERVICE"; 28 | export const REQUEST_STOP_SERVICE_FINISHED = "REQUEST_STOP_SERVICE_FINISHED"; 29 | 30 | export interface RequestStopService extends BaseAction {} 31 | export interface RequestStopServiceFinished extends BaseAction>> {} 32 | 33 | export const requestStopService = (): RequestStopService => ({ 34 | type: REQUEST_STOP_SERVICE, 35 | }); 36 | 37 | export const requestStopServiceFinished = ({ 38 | payload, 39 | meta, 40 | }: RequestFinishedActionParameters>): RequestStopServiceFinished => ({ 41 | type: REQUEST_STOP_SERVICE_FINISHED, 42 | payload: payload, 43 | error: payload instanceof ApiError, 44 | }); 45 | 46 | export const REQUEST_BOT_SERVICE = "REQUEST_BOT_SERVICE"; 47 | export const REQUEST_BOT_SERVICE_FINISHED = "REQUEST_BOT_SERVICE_FINISHED"; 48 | 49 | export interface RequestBotService extends BaseAction {} 50 | export interface RequestBotServiceFinished extends BaseAction>> {} 51 | 52 | export const requestBotService = (): RequestBotService => ({ 53 | type: REQUEST_BOT_SERVICE, 54 | }); 55 | 56 | export const requestBotServiceFinished = ({ 57 | payload, 58 | meta, 59 | }: RequestFinishedActionParameters>): RequestBotServiceFinished => ({ 60 | type: REQUEST_BOT_SERVICE_FINISHED, 61 | payload: payload, 62 | error: payload instanceof ApiError, 63 | }); 64 | 65 | -------------------------------------------------------------------------------- /src/stores/service/asyncActions.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { AnyAction } from 'redux'; 4 | import { ThunkAction } from 'redux-thunk'; 5 | import { BotService } from '../../models/service/types'; 6 | import { ApiClient, Resource } from '../../services/api'; 7 | import IAppState from '../../services/store/IAppState'; 8 | import { 9 | requestBotService, 10 | requestBotServiceFinished, 11 | requestStartService, 12 | requestStartServiceFinished, 13 | requestStopService, 14 | requestStopServiceFinished, 15 | } from './actions'; 16 | 17 | /* 18 | TODO: Warning 19 | The default service id is temporary 20 | */ 21 | 22 | const DEFAULT_SERVICE_ID = '00000000-0000-0000-0000-000000000000'; 23 | 24 | export const startBotServiceAsync = (): ThunkAction => async (dispatch) => { 25 | dispatch(requestStartService()); 26 | 27 | const startServiceResponse = await ApiClient.post>({ 28 | url: `/service/${DEFAULT_SERVICE_ID}/start`, 29 | isSecured: true, 30 | }); 31 | 32 | dispatch(requestStartServiceFinished({ payload: startServiceResponse })); 33 | }; 34 | 35 | export const stopBotServiceAsync = (): ThunkAction => async (dispatch) => { 36 | dispatch(requestStopService()); 37 | 38 | const stopBotServiceResponse = await ApiClient.post>({ 39 | url: `/service/${DEFAULT_SERVICE_ID}/stop`, 40 | isSecured: true, 41 | }); 42 | 43 | dispatch(requestStopServiceFinished({ payload: stopBotServiceResponse })); 44 | }; 45 | 46 | export const getBotServiceAsync = (): ThunkAction => async (dispatch) => { 47 | dispatch(requestBotService()); 48 | 49 | const getBotServiceResponse = await ApiClient.get>({ 50 | url: `/service/${DEFAULT_SERVICE_ID}/state`, 51 | isSecured: true, 52 | }); 53 | 54 | dispatch(requestBotServiceFinished({ payload: getBotServiceResponse })); 55 | }; 56 | -------------------------------------------------------------------------------- /src/stores/service/reducer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { BotService } from "../../models/service/types"; 4 | import { Resource } from "../../services/api"; 5 | import baseReducer from "../base/BaseReducer"; 6 | import * as BotServiceActions from "./actions"; 7 | 8 | export interface BotServiceAppState { 9 | botServices: BotService[]; 10 | loading: boolean; 11 | } 12 | 13 | export const INITIAL_STATE: BotServiceAppState = { 14 | botServices: [], 15 | loading: true, 16 | }; 17 | 18 | export const serviceReducer = baseReducer(INITIAL_STATE, { 19 | [BotServiceActions.REQUEST_BOT_SERVICE_FINISHED](state: BotServiceAppState, action: BotServiceActions.RequestBotServiceFinished){ 20 | const botService = action.payload! as Resource; 21 | return { 22 | ...state, 23 | botServices: [botService.resource], 24 | }; 25 | }, 26 | [BotServiceActions.REQUEST_START_SERVICE_FINISHED](state: BotServiceAppState, action: BotServiceActions.RequestStartService){ 27 | const botService = action.payload! as Resource; 28 | return { 29 | ...state, 30 | botServices: [botService.resource], 31 | }; 32 | }, 33 | [BotServiceActions.REQUEST_STOP_SERVICE_FINISHED](state: BotServiceAppState, action: BotServiceActions.RequestStopServiceFinished){ 34 | const botService = action.payload! as Resource; 35 | return { 36 | ...state, 37 | botServices: [botService.resource], 38 | }; 39 | } 40 | }) -------------------------------------------------------------------------------- /src/stores/toast/actions.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { v4 as uuid4 } from 'uuid'; 4 | import BaseAction from '../base/BaseAction'; 5 | import { IToastItem } from './reducer'; 6 | 7 | export const ADD_TOAST = 'ADD_TOAST'; 8 | export const REMOVE_TOAST = 'REMOVE_TOAST'; 9 | 10 | export interface AddToastMessage extends BaseAction { } 11 | export interface RemoveToastMessage extends BaseAction { } 12 | 13 | export const addToast = (message: string, type: string): AddToastMessage => ({ 14 | type: ADD_TOAST, 15 | payload: { id: uuid4(), message, type } 16 | }); 17 | 18 | export const removeToastById = (toastId: string): RemoveToastMessage => ({ 19 | type: REMOVE_TOAST, 20 | payload: toastId 21 | }); 22 | 23 | -------------------------------------------------------------------------------- /src/stores/toast/reducer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { Reducer } from 'redux'; 4 | import * as ToastsAction from './actions'; 5 | import baseReducer from '../base/BaseReducer'; 6 | 7 | export interface IToastItem { 8 | id?: string; 9 | type: string; 10 | message: string; 11 | } 12 | 13 | export interface IToastState{ 14 | items: IToastItem[]; 15 | } 16 | 17 | const INITIAL_STATE: IToastState = { 18 | items: [], 19 | }; 20 | 21 | export const toastReducer: Reducer = baseReducer(INITIAL_STATE, { 22 | [ToastsAction.ADD_TOAST](state: IToastState, action: ToastsAction.AddToastMessage): IToastState { 23 | return { 24 | ...state, 25 | items: [...state.items, action.payload!], 26 | }; 27 | }, 28 | [ToastsAction.REMOVE_TOAST](state: IToastState, action: ToastsAction.RemoveToastMessage) { 29 | const toastId = action.payload; 30 | 31 | return { 32 | ...state, 33 | items: state.items.filter((model) => model.id !== toastId), 34 | }; 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /src/views/call-details/CallDetails.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import React, { Fragment } from 'react'; 4 | import CallInfo from './components/CallInfo'; 5 | import CallStreams from './components/CallStreams'; 6 | import NewInjectionStreamDrawer from './components/NewInjectionStreamDrawer'; 7 | import NewStreamDrawer from './components/NewStreamDrawer'; 8 | 9 | const CallDetails: React.FC<{}> = () => { 10 | return ( 11 | 12 | 13 | 14 |
15 | 16 | 17 |
18 |
19 | ); 20 | }; 21 | 22 | export default CallDetails; 23 | -------------------------------------------------------------------------------- /src/views/call-details/components/CallInfo.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | #CallInfo { 4 | min-height: 140px; 5 | display: grid; 6 | grid-template-columns: 250px 1fr auto; 7 | justify-items: center; 8 | align-items: flex-start; 9 | padding: 0 !important; 10 | } 11 | 12 | #CallInfoSettings { 13 | width: 100%; 14 | display: grid; 15 | grid-template-rows: 3fr 1fr; 16 | justify-items: center; 17 | align-items: center; 18 | } 19 | 20 | .CallSetting { 21 | width: 200px; 22 | } 23 | 24 | #CallInfoProperties { 25 | width: 100%; 26 | height: 220px; 27 | margin: 20px; 28 | 29 | display: flex; 30 | flex-direction: column; 31 | flex-wrap: wrap; 32 | } 33 | 34 | .CallInfoProperty { 35 | width: 50%; 36 | display: flex; 37 | vertical-align: middle; 38 | padding: 5px 0; 39 | } 40 | .CallInfoStatusBadge { 41 | margin-left: 10px; 42 | } 43 | 44 | #CallInfoForm { 45 | align-self: flex-end; 46 | width: 50%; 47 | max-width: 600px; 48 | } 49 | 50 | #CallInfoForm h4 { 51 | padding-bottom: 10px; 52 | } 53 | 54 | #CallInfoOptions { 55 | padding: 20px; 56 | } 57 | 58 | #CallInfoOptions button { 59 | margin-bottom: 10px; 60 | } 61 | -------------------------------------------------------------------------------- /src/views/call-details/components/CallSelector.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import React, { useState } from 'react'; 4 | import { connect } from 'react-redux'; 5 | import { RouteComponentProps, withRouter, Redirect } from 'react-router'; 6 | import { Select } from 'antd' 7 | 8 | import IAppState from '../../../services/store/IAppState'; 9 | 10 | const { Option } = Select; 11 | 12 | interface ICallSelectorDataProps extends RouteComponentProps<{ id: string }> { 13 | selectedCallId?: string; 14 | isNew: boolean; 15 | calls: { 16 | id: string, 17 | name: string 18 | }[] 19 | } 20 | 21 | type ICallSelectorProps = ICallSelectorDataProps; 22 | 23 | const PLACEHOLDER_ID = '_'; 24 | const NEW_PLACEHOLDER_ID = '*'; 25 | const JOIN_CALL_ROUTE = '/call/join'; 26 | 27 | const CallSelector: React.FC = (props) => { 28 | // Handle redirect using 29 | const [redirectId, setRedirectId] = useState(''); 30 | if (redirectId && redirectId !== props.selectedCallId) { 31 | if (redirectId === NEW_PLACEHOLDER_ID) { 32 | return 33 | } else { 34 | return 35 | } 36 | } 37 | 38 | // When changing the 51 | 52 | {props.calls.map(m => ())} 53 | 54 | 55 | ) 56 | } 57 | 58 | const mapStateToPros = (appState: IAppState, ownProps: RouteComponentProps<{ id: string }>): ICallSelectorDataProps => ({ 59 | ...ownProps, 60 | calls: appState.calls.activeCalls.map(o => ({ 61 | id: o.id, 62 | name: o.displayName 63 | })), 64 | selectedCallId: ownProps.match.params.id, 65 | isNew: ownProps.match.path === JOIN_CALL_ROUTE 66 | }) 67 | 68 | export default withRouter(connect(mapStateToPros)(CallSelector)); 69 | -------------------------------------------------------------------------------- /src/views/call-details/components/CallStreams.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | #CallStreams h3 { 4 | margin-top: 20px; 5 | display: block; 6 | clear: both; 7 | } 8 | -------------------------------------------------------------------------------- /src/views/call-details/components/CallStreams.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import React from 'react'; 4 | import {useSelector } from 'react-redux'; 5 | import IAppState from '../../../services/store/IAppState'; 6 | import StreamCard from './StreamCard'; 7 | import './CallStreams.css'; 8 | import NewStreamPopUpDrawer from './NewStreamDrawer'; 9 | import InjectionCard from './InjectionCard'; 10 | import { useParams } from 'react-router-dom'; 11 | import { selectCallStreams } from '../../../stores/calls/selectors'; 12 | import { Stream } from '../../../models/calls/types'; 13 | import { CallStreamsProps } from '../types'; 14 | 15 | 16 | const CallStreams: React.FC = () => { 17 | const { id: callId } = useParams<{id: string}>(); 18 | const callStreams = useSelector((state: IAppState) => selectCallStreams(state, callId)); 19 | 20 | if (!callStreams.mainStreams.length && !callStreams.participantStreams.length && !callStreams.activeStreams.length) { 21 | // Empty Call? 22 | return null; 23 | } 24 | 25 | const hasMainStreams = callStreams.mainStreams.length > 0; 26 | const hasParticipants = callStreams.participantStreams.length > 0; 27 | const hasActiveStreams = callStreams.activeStreams.length > 0; 28 | 29 | return ( 30 |
31 | 32 | 33 |

Injection Stream

34 | 35 |

Active Streams

36 | {(hasActiveStreams && renderStreams(callStreams.activeStreams, callStreams)) || ( 37 |

38 | Start a stream from below 39 |

40 | )} 41 | 42 | {hasMainStreams && ( 43 | <> 44 |

Main Streams

45 | {renderStreams(callStreams.mainStreams, callStreams)} 46 | 47 | )} 48 | 49 | {hasParticipants && ( 50 | <> 51 |

Participants

52 | {renderStreams(callStreams.participantStreams, callStreams)} 53 | 54 | )} 55 | 56 |
57 |
58 | ); 59 | }; 60 | 61 | const renderStreams = (streams: Stream[], callStreams: CallStreamsProps): React.ReactElement[] => 62 | streams.map((stream) => ( 63 | 64 | )); 65 | 66 | export default CallStreams; 67 | -------------------------------------------------------------------------------- /src/views/call-details/components/InjectionCard.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | .injectionCard { 4 | float: left; 5 | min-width: 400px; 6 | position: relative; 7 | } 8 | 9 | @media only screen and (max-width: 1300px) { 10 | .injectionCard { 11 | min-width: 460px; 12 | width: 50%; 13 | } 14 | } 15 | 16 | @media only screen and (min-width: 1300px) { 17 | .injectionCard { 18 | min-width: auto; 19 | width: 33%; 20 | } 21 | } 22 | 23 | @media only screen and (min-width: 2000px) { 24 | .injectionCard { 25 | min-width: auto; 26 | width: 25%; 27 | } 28 | } 29 | 30 | .injectionCard .injectionCardContent { 31 | height: 160px; 32 | margin: 10px; 33 | border: 3px solid; 34 | border-radius: 77px 24px 24px 77px; 35 | background-color: #fff; 36 | transition: all 0.1s ease-in-out; 37 | 38 | /* shadow */ 39 | -webkit-box-shadow: 1px 1px 2px 0px rgba(0, 0, 0, 0.1); 40 | -moz-box-shadow: 1px 1px 2px 0px rgba(0, 0, 0, 0.1); 41 | box-shadow: 1px 1px 2px 0px rgba(0, 0, 0, 0.1); 42 | 43 | flex-wrap: nowrap; 44 | overflow: hidden; 45 | } 46 | .injectionCard .toggler { 47 | transition: all 0.1s ease-in-out; 48 | height: 160px; 49 | } 50 | .injectionCard.expanded .injectionCardContent { 51 | height: 340px; 52 | border-radius: 77px 24px 24px 24px; 53 | } 54 | .injectionCard.expanded .toggler { 55 | height: 340px; 56 | } 57 | 58 | /* content */ 59 | .injectionCard .injectionCardContent > .ant-row > .ant-col { 60 | padding: 20px; 61 | } 62 | 63 | .injectionCard .injectionCardContent > .ant-row > .ant-col.injectionMain { 64 | padding-left: 0; 65 | } 66 | 67 | /* Avatar */ 68 | .injectionCard .ant-avatar-string { 69 | font-size: 1.8em; 70 | } 71 | 72 | /* Display name */ 73 | .injectionCard h4 { 74 | margin: 0; 75 | padding: 0; 76 | font-size: 1.4em; 77 | } 78 | 79 | /* Status */ 80 | .injectionCard .InjectionState { 81 | font-size: 0.9em; 82 | text-transform: uppercase; 83 | font-weight: bold; 84 | } 85 | 86 | /* icons & btns */ 87 | .injectionCard .injectionActions { 88 | height: 60px; 89 | } 90 | .injectionCard .injectionActions .anticon { 91 | font-size: 18px; 92 | margin-right: 4px; 93 | } 94 | 95 | /* main info */ 96 | .injectionCard .injectionMain { 97 | flex-grow: 1; 98 | } 99 | 100 | .injectionCard .injectionDetails p { 101 | padding: 0; 102 | margin: 0 0 4px 0; 103 | } 104 | .injectionCard .injectionOptions { 105 | text-align: right; 106 | padding-right: 15px; 107 | } 108 | 109 | /* more details toggler */ 110 | .injectionCard .toggler { 111 | position: absolute; 112 | right: 10px; 113 | top: 10px; 114 | width: 28px; 115 | border-radius: 0px 21px 21px 0px; 116 | cursor: pointer; 117 | } 118 | .injectionCard .toggler span { 119 | color: white; 120 | font-weight: bold; 121 | 122 | display: block; 123 | position: absolute; 124 | 125 | transform: rotate(-90deg); 126 | width: 150px; 127 | bottom: 60px; 128 | right: -60px; 129 | text-align: center; 130 | white-space: nowrap; 131 | } 132 | 133 | /* palette - disconnected / non-injectioning */ 134 | .injectionCard .injectionCardContent { 135 | border-color: #bdbdbd; 136 | } 137 | .injectionCard .toggler { 138 | background-color: #bdbdbd; 139 | } 140 | .injectionCard .injectionCardContent .InjectionState { 141 | color: #bdbdbd; 142 | } 143 | 144 | /* palette - establised */ 145 | .injectionCard.established .injectionCardContent { 146 | border-color: #73c856; 147 | } 148 | .injectionCard.established .toggler { 149 | background-color: #73c856; 150 | } 151 | .injectionCard.established .injectionCardContent .InjectionState { 152 | color: #73c856; 153 | } 154 | 155 | /* palette - initializing/connecting */ 156 | .injectionCard.initializing .injectionCardContent { 157 | border-color: #f3ca3e; 158 | } 159 | .injectionCard.initializing .toggler { 160 | background-color: #f3ca3e; 161 | } 162 | .injectionCard.initializing .injectionCardContent .InjectionState { 163 | color: #f3ca3e; 164 | } 165 | 166 | /* palette - error/unhealthy */ 167 | .injectionCard.error .injectionCardContent { 168 | border-color: #df3639; 169 | } 170 | .injectionCard.error .toggler { 171 | background-color: #df3639; 172 | } 173 | .injectionCard.error .injectionCardContent .InjectionState { 174 | color: #df3639; 175 | } 176 | -------------------------------------------------------------------------------- /src/views/call-details/components/InjectionCard.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { IconButton } from '@material-ui/core'; 4 | import { Mic, MicOff } from '@material-ui/icons'; 5 | import { Avatar, Button, Col, Row, Typography } from 'antd'; 6 | import React, { useState } from 'react'; 7 | import { useDispatch } from 'react-redux'; 8 | import { InjectionStream, StreamMode, StreamProtocol, StreamState } from '../../../models/calls/types'; 9 | import { openNewInjectionStreamDrawer } from '../../../stores/calls/actions'; 10 | import { muteBotAsync, stopInjectionAsync, unmuteBotAsync } from '../../../stores/calls/asyncActions'; 11 | import { CallStreamsProps } from '../types'; 12 | import './InjectionCard.css'; 13 | 14 | interface InjectionCardProps { 15 | callStreams: CallStreamsProps; 16 | } 17 | 18 | const avatarSize = 112; 19 | const OBFUSCATION_PATTERN = '********'; 20 | 21 | const InjectionCard: React.FC = (props) => { 22 | const dispatch = useDispatch(); 23 | const { callStreams } = props; 24 | const callId = callStreams.callId; 25 | const stream = callStreams.injectionStream; 26 | const streamId = stream?.id; 27 | const hasStream = callStreams.injectionStream && stream?.state !== StreamState.Disconnected; 28 | 29 | const startInjection = () => { 30 | if (callId) { 31 | dispatch( 32 | openNewInjectionStreamDrawer({ 33 | callId, 34 | }) 35 | ); 36 | } 37 | }; 38 | 39 | const stopInjection = () => { 40 | if (callId && streamId) { 41 | dispatch(stopInjectionAsync(callId, streamId)); 42 | } 43 | }; 44 | const [audioMuted, setAudioMuted] = useState(false); 45 | const [expanded, setExpanded] = useState(false); 46 | const toggleExpand = () => setExpanded(!expanded); 47 | 48 | // collapse disabled streams 49 | if (expanded && !stream) { 50 | setExpanded(false); 51 | } 52 | 53 | const classes = ['injectionCard', getConnectionClass(stream), expanded ? 'expanded' : '']; 54 | const status = getConnectionStatus(stream); 55 | const injectionUrl = stream ? getInjectionUrl(stream) : ''; 56 | 57 | const protocolText = () => { 58 | switch (stream?.protocol) { 59 | case StreamProtocol.RTMP: 60 | return 'RTMP'; 61 | case StreamProtocol.SRT: 62 | return 'SRT'; 63 | default: 64 | return ''; 65 | } 66 | }; 67 | 68 | const streamModeText = () => { 69 | switch (stream?.streamMode) { 70 | case StreamMode.Caller: 71 | return stream?.protocol === StreamProtocol.RTMP ? 'Pull' : 'Caller'; 72 | case StreamMode.Listener: 73 | return stream?.protocol === StreamProtocol.RTMP ? 'Push' : 'Listener'; 74 | default: 75 | return ''; 76 | } 77 | }; 78 | 79 | const toggleBotAudio = () => { 80 | if (callId) { 81 | if (!audioMuted) { 82 | dispatch(muteBotAsync(callId)); 83 | setAudioMuted(true); 84 | } else { 85 | dispatch(unmuteBotAsync(callId)); 86 | setAudioMuted(false); 87 | } 88 | } 89 | }; 90 | 91 | return ( 92 |
93 |
94 | 95 | 96 | 97 | 98 | 99 |

Injection Stream

100 | {status} 101 | 102 | 103 | {audioMuted ? : } 104 | 105 | 106 | 114 | 115 | 116 | 117 |
118 | 119 | 120 | {stream && ( 121 | <> 122 |
123 | Injection URL: 124 | 125 | {'' + injectionUrl} 126 | 127 |
128 | {stream.protocol === StreamProtocol.SRT && ( 129 | <> 130 |
131 | Passphrase:{' '} 132 | 133 | 134 | {stream.passphrase ? '********' : 'None'} 135 | 136 | 137 |
138 |
139 | Latency: {stream.latency}ms 140 |
141 | 142 | )} 143 |
144 | Protocol: {protocolText()} 145 |
146 |
147 | Stream Mode: {streamModeText()} 148 |
149 | 150 | )} 151 | 152 |
153 |
154 | {hasStream && ( 155 |
156 | {expanded ? '- less info' : '+ more info'} 157 |
158 | )} 159 |
160 | ); 161 | }; 162 | 163 | const getConnectionClass = (stream: InjectionStream | null): string => { 164 | switch (stream?.state) { 165 | case StreamState.Stopping: 166 | case StreamState.Disconnected: 167 | return 'disconnected'; 168 | case StreamState.Starting: 169 | return 'initializing'; 170 | case StreamState.Ready: 171 | case StreamState.Receiving: 172 | case StreamState.NotReceiving: 173 | return 'established'; 174 | case StreamState.Error: 175 | case StreamState.StartingError: 176 | case StreamState.StoppingError: 177 | return 'error'; 178 | default: 179 | return ''; 180 | } 181 | }; 182 | 183 | const getConnectionStatus = (stream: InjectionStream | null): string => { 184 | switch (stream?.state) { 185 | case StreamState.Disconnected: 186 | return 'Available Stream'; 187 | case StreamState.Stopping: 188 | return 'Stopping'; 189 | case StreamState.Starting: 190 | return 'Starting'; 191 | case StreamState.Error: 192 | case StreamState.StartingError: 193 | case StreamState.StoppingError: 194 | return 'Unhealthy Stream'; 195 | case StreamState.Ready: 196 | return 'Ready'; 197 | case StreamState.Receiving: 198 | return 'Receiving'; 199 | case StreamState.NotReceiving: 200 | return 'Not Receiving'; 201 | default: 202 | return 'Available Stream'; 203 | } 204 | }; 205 | 206 | const getInjectionUrl = (stream: InjectionStream): string => { 207 | if (stream.protocol === StreamProtocol.RTMP && stream.injectionUrl) { 208 | const rtmpUrl = stream.injectionUrl.replace(stream.passphrase, OBFUSCATION_PATTERN); 209 | 210 | return rtmpUrl; 211 | } 212 | 213 | return stream.injectionUrl ?? ''; 214 | }; 215 | 216 | export default InjectionCard; 217 | -------------------------------------------------------------------------------- /src/views/call-details/components/NewInjectionStreamDrawer.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | #NewStreamDrawerBody { 5 | height: 100%; 6 | 7 | } 8 | #NewStreamDrawerFooter{ 9 | height: 100%; 10 | width: 95%; 11 | display: grid; 12 | grid-template-columns: 1fr; 13 | justify-items: flex-start; 14 | align-items: center; 15 | } 16 | #NewStreamDrawerFooterInner{ 17 | height: 100%; 18 | width: 60%; 19 | display: grid; 20 | justify-items: center; 21 | grid-template-columns: 1fr 1fr; 22 | } 23 | #cancelButton { 24 | margin-left: 5%; 25 | } 26 | 27 | .chooseAFlowText { 28 | font-size: 1.2em; 29 | } 30 | .insertUrlText { 31 | font-size: 1.1em; 32 | } 33 | 34 | #ParticipantsListContainer{ 35 | width: 90%; 36 | margin-left: 10%; 37 | height: 100%; 38 | 39 | } 40 | 41 | 42 | .selectedFlowText{ 43 | font-weight: bold; 44 | font-size: 1.1em; 45 | } 46 | .NewStreamSettingBox{ 47 | margin-bottom: 2em; 48 | } 49 | .NewStreamSettingText{ 50 | padding-bottom: 0.5em; 51 | display: block; 52 | } 53 | 54 | .DrawerButton{ 55 | width: 120px; 56 | } 57 | .NewStreamInput{ 58 | width: 300px; 59 | } 60 | 61 | .settingsText{ 62 | width: 90%; 63 | } 64 | 65 | .KeyLengthSelect { 66 | width: 300px; 67 | } -------------------------------------------------------------------------------- /src/views/call-details/components/NewInjectionStreamDrawer.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import React, { ReactText, useReducer } from 'react'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { useParams } from 'react-router'; 6 | import { Drawer, Button, Input, Radio, InputNumber, Tooltip, Typography, Select } from 'antd'; 7 | import { ReloadOutlined } from '@ant-design/icons'; 8 | import IAppState from '../../../services/store/IAppState'; 9 | import './NewInjectionStreamDrawer.css'; 10 | import Form from 'antd/lib/form'; 11 | import { Switch } from 'antd'; 12 | import { Call, NewInjectionStream, StreamMode, StreamProtocol, KeyLength } from '../../../models/calls/types'; 13 | import { selectNewInjectionStreamDrawerProps } from '../../../stores/calls/selectors'; 14 | import { closeNewInjectionStreamDrawer } from '../../../stores/calls/actions'; 15 | import { startInjectionAsync, refreshStreamKeyAsync } from '../../../stores/calls/asyncActions'; 16 | 17 | interface DrawerState { 18 | protocol?: StreamProtocol; 19 | injectionUrl?: string; 20 | streamMode?: StreamMode; 21 | latency?: number; 22 | passphrase?: string; 23 | enableSsl?: boolean; 24 | keyLength?: KeyLength; 25 | } 26 | 27 | const DEFAULT_LATENCY = 750; 28 | const OBFUSCATION_PATTERN = '********'; 29 | 30 | const NewInjectionStreamDrawer: React.FC = () => { 31 | const dispatch = useDispatch(); 32 | const { id: callId } = useParams<{ id: string }>(); 33 | const drawerProps = useSelector((state: IAppState) => selectNewInjectionStreamDrawerProps(state, callId)); 34 | 35 | const visible = !!drawerProps.newInjectionStream; 36 | 37 | //Drawer's state 38 | const initialState: DrawerState = { 39 | protocol: drawerProps.newInjectionStream?.protocol || StreamProtocol.SRT, 40 | streamMode: drawerProps.newInjectionStream?.mode || StreamMode.Caller, 41 | injectionUrl: drawerProps.newInjectionStream?.streamUrl, 42 | latency: drawerProps.newInjectionStream?.latency || DEFAULT_LATENCY, 43 | passphrase: drawerProps.newInjectionStream?.streamKey, 44 | enableSsl: drawerProps.newInjectionStream?.enableSsl, 45 | keyLength: drawerProps.newInjectionStream?.keyLength || KeyLength.None, 46 | }; 47 | 48 | //Warning! It wasn't tested with nested objects 49 | const [state, setState] = useReducer( 50 | (state: DrawerState, newState: Partial) => ({ ...state, ...newState }), 51 | {} 52 | ); 53 | 54 | const loadDefaultSettings = () => { 55 | const protocol = drawerProps.newInjectionStream?.protocol || StreamProtocol.SRT; 56 | const streamMode = drawerProps.newInjectionStream?.mode || StreamMode.Caller; 57 | const injectionUrl = drawerProps.newInjectionStream?.streamUrl; 58 | const latency = drawerProps.newInjectionStream?.latency || DEFAULT_LATENCY; 59 | const passphrase = drawerProps.newInjectionStream?.streamKey; 60 | const enableSsl = drawerProps.newInjectionStream?.enableSsl; 61 | const keyLength = drawerProps.newInjectionStream?.keyLength || KeyLength.None; 62 | 63 | setState({ protocol, streamMode, injectionUrl, latency, passphrase, enableSsl, keyLength }); 64 | }; 65 | 66 | const handleChange = (e: any) => { 67 | setState({ [e.target.name]: e.target.value }); 68 | }; 69 | 70 | const handleSwitchChange = (checked: boolean) => { 71 | setState({ enableSsl: checked }); 72 | }; 73 | 74 | const handleLatencyChange = (value?: ReactText) => { 75 | const latency = parseInt(value?.toString() ?? '0', 10); 76 | setState({ latency }); 77 | }; 78 | 79 | const handleKeyLengthSelect = (keyLength: number) => { 80 | setState({ keyLength }); 81 | }; 82 | 83 | const handleRefreshStremKey = () => { 84 | dispatch(refreshStreamKeyAsync(callId)); 85 | }; 86 | 87 | const handleClose = () => { 88 | dispatch(closeNewInjectionStreamDrawer()); 89 | }; 90 | 91 | const handleSave = () => { 92 | if (!drawerProps.newInjectionStream) { 93 | return; 94 | } 95 | 96 | const newInjectionStream: NewInjectionStream = { 97 | callId: drawerProps.newInjectionStream.callId, 98 | protocol: state.protocol || StreamProtocol.SRT, 99 | mode: state.streamMode || StreamMode.Caller, 100 | streamUrl: state.injectionUrl, 101 | latency: state.protocol === StreamProtocol.SRT ? state.latency : undefined, 102 | streamKey: state.protocol === StreamProtocol.SRT ? state.passphrase : undefined, 103 | enableSsl: state.protocol === StreamProtocol.RTMP ? state.enableSsl : undefined, 104 | keyLength: (state.protocol === StreamProtocol.SRT && state.passphrase) ? state.keyLength : undefined, 105 | }; 106 | 107 | dispatch(startInjectionAsync(newInjectionStream)); 108 | }; 109 | 110 | const getKeyLengthValues = () => { 111 | return Object.keys(KeyLength).filter((k) => typeof KeyLength[k as any] !== 'number'); 112 | }; 113 | 114 | const getRtmpPushStreamUrl = (call: Call | null, enableSsl: boolean): string => { 115 | const protocol = enableSsl ? 'rtmps' : 'rtmp'; 116 | const port = enableSsl ? 2936 : 1936; 117 | const ingest = enableSsl ? 'secure-ingest' : 'ingest'; 118 | 119 | if (call) { 120 | const domain = call.botFqdn?.split(':')[0]; 121 | return `${protocol}://${domain}:${port}/${ingest}/${OBFUSCATION_PATTERN}?callId=${call?.id}`; 122 | } 123 | 124 | return ''; 125 | }; 126 | 127 | const rtmpPushStreamKey = drawerProps.call?.privateContext?.streamKey ?? ''; 128 | const rtmpPushStreamUrl = getRtmpPushStreamUrl(drawerProps.call, !!state.enableSsl); 129 | 130 | return ( 131 | 141 |
142 | 145 | 148 |
149 | 150 | } 151 | > 152 |
153 |
154 |
155 | Start injection 156 |
157 | 158 | 159 | 160 | SRT 161 | RTMP 162 | 163 | 164 | 165 | 166 | 167 | 168 | {state.protocol === StreamProtocol.SRT ? 'Caller' : 'Pull'} 169 | 170 | 171 | {state.protocol === StreamProtocol.SRT ? 'Listener' : 'Push'} 172 | 173 | 174 | 175 | 176 | {state.streamMode === StreamMode.Caller && ( 177 | 188 | 194 | 195 | )} 196 | 197 | {state.protocol === StreamProtocol.SRT && ( 198 | <> 199 | 200 | 207 | 208 | 209 | 210 | 216 | 217 | 218 | 229 | 230 | 231 | )} 232 | 233 | {state.protocol === StreamProtocol.RTMP && state.streamMode === StreamMode.Listener && ( 234 | <> 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 249 | 250 | 251 | 252 | 253 | 254 | {rtmpPushStreamUrl} 255 | 256 | 257 | 258 | )} 259 |
260 |
261 |
262 | ); 263 | }; 264 | 265 | export default NewInjectionStreamDrawer; 266 | -------------------------------------------------------------------------------- /src/views/call-details/components/NewStreamDrawer.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | 4 | #NewStreamDrawerBody { 5 | height: 100%; 6 | 7 | } 8 | #NewStreamDrawerFooter{ 9 | height: 100%; 10 | width: 95%; 11 | display: grid; 12 | grid-template-columns: 1fr; 13 | justify-items: flex-start; 14 | align-items: center; 15 | } 16 | #NewStreamDrawerFooterInner{ 17 | height: 100%; 18 | width: 60%; 19 | display: grid; 20 | justify-items: center; 21 | grid-template-columns: 1fr 1fr; 22 | } 23 | #cancelButton { 24 | margin-left: 5%; 25 | } 26 | 27 | .chooseAFlowText { 28 | font-size: 1.2em; 29 | } 30 | .insertUrlText { 31 | font-size: 1.1em; 32 | } 33 | 34 | #ParticipantsListContainer{ 35 | width: 90%; 36 | margin-left: 10%; 37 | height: 100%; 38 | 39 | } 40 | 41 | .selectedFlowText{ 42 | font-weight: bold; 43 | font-size: 1.1em; 44 | } 45 | .NewStreamSettingBox{ 46 | margin-bottom: 2em; 47 | } 48 | .NewStreamSettingControl{ 49 | margin-bottom: 0.5em; 50 | } 51 | .NewStreamSettingText{ 52 | padding-bottom: 0.5em; 53 | display: block; 54 | } 55 | .NewStreamSettingTopLabel{ 56 | display: block; 57 | } 58 | .NewStreamSettingInlineLabel{ 59 | margin-right: 0.5em; 60 | } 61 | 62 | .DrawerButton{ 63 | width: 120px; 64 | } 65 | .NewStreamInput{ 66 | width: 300px; 67 | } 68 | 69 | .settingsText{ 70 | width: 90%; 71 | } -------------------------------------------------------------------------------- /src/views/call-details/components/StreamCard.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | .streamCard { 4 | float: left; 5 | min-width: 400px; 6 | position: relative; 7 | } 8 | 9 | @media only screen and (max-width: 1300px) { 10 | .streamCard { 11 | min-width: 460px; 12 | width: 50%; 13 | } 14 | } 15 | 16 | @media only screen and (min-width: 1300px) { 17 | .streamCard { 18 | min-width: auto; 19 | width: 33%; 20 | } 21 | } 22 | 23 | @media only screen and (min-width: 2000px) { 24 | .streamCard { 25 | min-width: auto; 26 | width: 25%; 27 | } 28 | } 29 | 30 | .streamCard .streamCardContent { 31 | height: 160px; 32 | margin: 10px; 33 | border: 3px solid; 34 | border-radius: 77px 24px 24px 77px; 35 | background-color: #fff; 36 | transition: all 0.1s ease-in-out; 37 | 38 | /* shadow */ 39 | -webkit-box-shadow: 1px 1px 2px 0px rgba(0, 0, 0, 0.1); 40 | -moz-box-shadow: 1px 1px 2px 0px rgba(0, 0, 0, 0.1); 41 | box-shadow: 1px 1px 2px 0px rgba(0, 0, 0, 0.1); 42 | 43 | flex-wrap: nowrap; 44 | overflow: hidden; 45 | } 46 | .streamCard .toggler { 47 | transition: all 0.1s ease-in-out; 48 | height: 160px; 49 | } 50 | .streamCard.expanded .streamCardContent { 51 | height: 340px; 52 | border-radius: 77px 24px 24px 24px; 53 | } 54 | .streamCard.expanded .toggler { 55 | height: 340px; 56 | } 57 | 58 | /* content */ 59 | .streamCard .streamCardContent > .ant-row > .ant-col { 60 | padding: 20px; 61 | } 62 | 63 | .streamCard .streamCardContent > .ant-row > .ant-col.streamMain { 64 | padding-left: 0; 65 | } 66 | 67 | /* Avatar */ 68 | .streamCard .ant-avatar-string { 69 | font-size: 1.8em; 70 | } 71 | 72 | /* Display name */ 73 | .streamCard h4 { 74 | margin: 0; 75 | padding: 0; 76 | font-size: 1.4em; 77 | } 78 | 79 | /* Status */ 80 | .streamCard .StreamState { 81 | font-size: 0.9em; 82 | text-transform: uppercase; 83 | font-weight: bold; 84 | } 85 | 86 | /* icons & btns */ 87 | .streamCard .streamActions { 88 | height: 60px; 89 | } 90 | .streamCard .streamActions .anticon { 91 | font-size: 18px; 92 | margin-right: 4px; 93 | } 94 | 95 | /* main info */ 96 | .streamCard .streamMain { 97 | flex-grow: 1; 98 | } 99 | 100 | .streamCard .streamDetails p { 101 | padding: 0; 102 | margin: 0 0 4px 0; 103 | } 104 | .streamCard .streamOptions { 105 | text-align: right; 106 | padding-right: 15px; 107 | } 108 | 109 | /* more details toggler */ 110 | .streamCard .toggler { 111 | position: absolute; 112 | right: 10px; 113 | top: 10px; 114 | width: 28px; 115 | border-radius: 0px 21px 21px 0px; 116 | cursor: pointer; 117 | } 118 | .streamCard .toggler span { 119 | color: white; 120 | font-weight: bold; 121 | 122 | display: block; 123 | position: absolute; 124 | 125 | transform: rotate(-90deg); 126 | width: 150px; 127 | bottom: 60px; 128 | right: -60px; 129 | text-align: center; 130 | white-space: nowrap; 131 | } 132 | 133 | /* palette - disconnected / non-streaming */ 134 | .streamCard .streamCardContent { 135 | border-color: #bdbdbd; 136 | } 137 | .streamCard .toggler { 138 | background-color: #bdbdbd; 139 | } 140 | .streamCard .streamCardContent .StreamState { 141 | color: #bdbdbd; 142 | } 143 | 144 | /* palette - establised */ 145 | .streamCard.established .streamCardContent { 146 | border-color: #73c856; 147 | } 148 | .streamCard.established .toggler { 149 | background-color: #73c856; 150 | } 151 | .streamCard.established .streamCardContent .StreamState { 152 | color: #73c856; 153 | } 154 | 155 | /* palette - initializing/connecting */ 156 | .streamCard.initializing .streamCardContent { 157 | border-color: #f3ca3e; 158 | } 159 | .streamCard.initializing .toggler { 160 | background-color: #f3ca3e; 161 | } 162 | .streamCard.initializing .streamCardContent .StreamState { 163 | color: #f3ca3e; 164 | } 165 | 166 | /* palette - error/unhealthy */ 167 | .streamCard.error .streamCardContent { 168 | border-color: #df3639; 169 | } 170 | .streamCard.error .toggler { 171 | background-color: #df3639; 172 | } 173 | .streamCard.error .streamCardContent .StreamState { 174 | color: #df3639; 175 | } 176 | -------------------------------------------------------------------------------- /src/views/call-details/components/StreamCard.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import React, { useEffect, useState } from 'react'; 4 | import { useDispatch } from 'react-redux'; 5 | import { Avatar, Row, Col, Button, Typography } from 'antd'; 6 | import Videocam from '@material-ui/icons/Videocam'; 7 | import VideocamOff from '@material-ui/icons/VideocamOff'; 8 | import Mic from '@material-ui/icons/Mic'; 9 | import MicOff from '@material-ui/icons/MicOff'; 10 | import ScreenShare from '@material-ui/icons/ScreenShare'; 11 | import StopScreenShare from '@material-ui/icons/StopScreenShare'; 12 | import './StreamCard.css'; 13 | import { Stream, StreamProtocol, StreamState, StreamType } from '../../../models/calls/types'; 14 | import { openNewStreamDrawer, updateStreamPhoto } from '../../../stores/calls/actions'; 15 | import { stopStreamAsync } from '../../../stores/calls/asyncActions'; 16 | import { CallStreamsProps } from '../types'; 17 | import { ApiClient } from '../../../services/api'; 18 | import { ApiError } from '../../../models/error/types'; 19 | 20 | interface StreamCardProps { 21 | callProtocol: StreamProtocol; 22 | stream: Stream; 23 | callStreams: CallStreamsProps; 24 | } 25 | 26 | const OBFUSCATION_PATTERN = '********'; 27 | 28 | const StreamCard: React.FC = (props) => { 29 | const dispatch = useDispatch(); 30 | 31 | const { callProtocol: protocol, stream, callStreams } = props; 32 | const [expanded, setExpanded] = useState(false); 33 | const toggleExpand = () => setExpanded(!expanded); 34 | 35 | useEffect(() => { 36 | if (stream.photoUrl && stream.photo === undefined) { 37 | ApiClient.get({ 38 | url: stream.photoUrl, 39 | isSecured: true, 40 | shouldOverrideBaseUrl: true, 41 | config: { 42 | responseType: 'blob', 43 | }, 44 | }).then((response) => { 45 | const isError = response instanceof ApiError; 46 | if (isError) { 47 | const error = response as ApiError; 48 | console.error(error.raw); 49 | dispatch(updateStreamPhoto(stream.id, '', callStreams.callId)); 50 | return; 51 | } 52 | 53 | const urlCreator = window.URL || window.webkitURL; 54 | const imageUrl = urlCreator.createObjectURL(response); 55 | dispatch(updateStreamPhoto(stream.id, imageUrl, callStreams.callId)); 56 | }); 57 | } 58 | }, []); 59 | 60 | // collapse disabled streams 61 | if (expanded && !callStreams.callEnabled) { 62 | setExpanded(false); 63 | } 64 | 65 | const isStreamDisconnected = stream.state === StreamState.Disconnected; 66 | const toggleStreamOperation = () => { 67 | if (!isStreamDisconnected && expanded) { 68 | // active & expanded, collapse 69 | setExpanded(false); 70 | } 71 | 72 | if (isStreamDisconnected) { 73 | dispatch( 74 | openNewStreamDrawer({ 75 | callId: callStreams.callId, 76 | streamType: stream.type, 77 | participantId: stream.id, 78 | participantName: stream.displayName, 79 | }) 80 | ); 81 | } 82 | 83 | if (!isStreamDisconnected) { 84 | dispatch( 85 | stopStreamAsync({ 86 | callId: callStreams.callId, 87 | type: stream.type, 88 | participantId: stream.id, 89 | participantName: stream.displayName, 90 | }) 91 | ); 92 | } 93 | }; 94 | 95 | const getStreamUrl = (): string => { 96 | if (stream.details?.streamUrl) { 97 | const rtmpUrl = stream.details.streamUrl.replace(stream.details.passphrase, OBFUSCATION_PATTERN); 98 | 99 | return rtmpUrl; 100 | } 101 | 102 | return stream.details?.streamUrl ?? ''; 103 | }; 104 | 105 | const initials = stream.displayName 106 | .split(' ') 107 | .map((s) => s[0].toUpperCase()) 108 | .join(''); 109 | const hasStream = callStreams.callEnabled && stream.state !== StreamState.Disconnected; 110 | const status = getConnectionStatus(stream); 111 | 112 | const operationEnabled = 113 | callStreams.callEnabled && 114 | ((stream.state === StreamState.Ready || stream.state === StreamState.Receiving || stream.state === StreamState.NotReceiving) || 115 | (stream.state === StreamState.Disconnected && 116 | ((stream.type === StreamType.VbSS && callStreams.stageEnabled) || 117 | (stream.type === StreamType.PrimarySpeaker && callStreams.primarySpeakerEnabled) || 118 | ([StreamType.Participant, StreamType.LargeGallery, StreamType.LiveEvent, StreamType.TogetherMode].includes( 119 | stream.type 120 | ) && 121 | stream.isSharingVideo)))); 122 | 123 | const classes = ['streamCard', getConnectionClass(stream), expanded ? 'expanded' : '']; 124 | const avatarSize = 112; 125 | const avatarIcon = stream.photo ? ( 126 | dispatch(updateStreamPhoto(stream.id, '', callStreams.callId))} 130 | /> 131 | ) : ( 132 | <>{initials} 133 | ); 134 | 135 | const isRtmp = protocol === StreamProtocol.RTMP; 136 | const streamUrl = getStreamUrl(); 137 | 138 | return ( 139 |
140 |
141 | 142 | 143 | 144 | 145 | 146 |

{props.stream.displayName}

147 | {status} 148 | 149 | {(stream.type === StreamType.Participant && ( 150 | 151 | {stream.isSharingVideo && } 152 | {!stream.isSharingVideo && } 153 | {stream.isSharingAudio && !stream.audioMuted && } 154 | {stream.isSharingAudio && stream.audioMuted && } 155 | {stream.isSharingScreen && } 156 | {!stream.isSharingScreen && } 157 | 158 | )) || } 159 | 160 | 163 | 164 | 165 | 166 |
167 | 168 | 169 | {stream.details && ( 170 | <> 171 |
172 | Stream URL:{' '} 173 | 174 | {streamUrl} 175 | 176 |
177 | 178 |
179 | {isRtmp ? 'StreamKey: ' : 'Passphrase: '} 180 | 181 | 182 | {stream.details.passphrase ? '********' : 'None'} 183 | 184 | 185 |
186 | 187 | {!isRtmp && ( 188 |
189 | Latency: {stream.details.latency}ms 190 |
191 | )} 192 | 193 | )} 194 | 195 |
196 |
197 | {hasStream && ( 198 |
199 | {expanded ? '- less info' : '+ more info'} 200 |
201 | )} 202 |
203 | ); 204 | }; 205 | 206 | const getConnectionClass = (stream: Stream): string => { 207 | switch (stream.state) { 208 | case StreamState.Stopping: 209 | return 'disconnected'; 210 | case StreamState.Disconnected: 211 | return 'disconnected'; 212 | case StreamState.Starting: 213 | return 'initializing'; 214 | case StreamState.Ready: 215 | case StreamState.Receiving: 216 | case StreamState.NotReceiving: 217 | return stream.isHealthy ? 'established' : 'error'; 218 | case StreamState.Error: 219 | case StreamState.StartingError: 220 | case StreamState.StoppingError: 221 | return 'error'; 222 | } 223 | }; 224 | 225 | const getConnectionStatus = (stream: Stream): string => { 226 | switch (stream.state) { 227 | case StreamState.Disconnected: 228 | return 'Available Stream'; 229 | case StreamState.Stopping: 230 | return 'Stopping'; 231 | case StreamState.Starting: 232 | return 'Starting'; 233 | case StreamState.Error: 234 | case StreamState.StartingError: 235 | case StreamState.StoppingError: 236 | return 'Unhealthy Stream'; 237 | case StreamState.Ready: 238 | case StreamState.Receiving: 239 | case StreamState.NotReceiving: 240 | return stream.isHealthy ? 'Active Stream' : 'Unhealthy Stream'; 241 | } 242 | }; 243 | 244 | export default StreamCard; 245 | -------------------------------------------------------------------------------- /src/views/call-details/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import { Call, InjectionStream, NewInjectionStream, NewStream, Stream, StreamProtocol } from "../../models/calls/types"; 4 | 5 | export interface NewInjectionStreamDrawerProps { 6 | call: Call | null; 7 | newInjectionStream: NewInjectionStream | null; 8 | } 9 | 10 | export interface NewStreamDrawerProps { 11 | call: Call | null; 12 | newStream: NewStream | null; 13 | } 14 | 15 | export interface CallInfoProps { 16 | call: Call | null; 17 | streams: Stream[]; 18 | } 19 | 20 | export interface CallStreamsProps { 21 | callId: string; 22 | callEnabled: boolean; 23 | mainStreams: Stream[]; 24 | participantStreams: Stream[]; 25 | activeStreams: Stream[]; 26 | injectionStream: InjectionStream | null; 27 | primarySpeakerEnabled: boolean; 28 | stageEnabled: boolean; 29 | callProtocol: StreamProtocol; 30 | } -------------------------------------------------------------------------------- /src/views/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import React from 'react'; 4 | import { useSelector } from 'react-redux'; 5 | import IAppState from '../../services/store/IAppState'; 6 | 7 | const Footer: React.FC = () => { 8 | const { initialized, app } = useSelector((state: IAppState) => state.config); 9 | 10 | const versionString = initialized ? app?.buildNumber ?? '' : ''; 11 | return ; 12 | }; 13 | 14 | export default Footer; 15 | -------------------------------------------------------------------------------- /src/views/components/Header.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | #Header { 4 | background-color: #f3f2f1; 5 | position: fixed; 6 | top: 0; 7 | right: 0; 8 | z-index: 10; 9 | width: 100%; 10 | height: 70px; 11 | border-bottom: 1px solid #ddd; 12 | 13 | /* shadow */ 14 | -webkit-box-shadow: 1px 1px 2px 0px rgba(0, 0, 0, 0.025); 15 | -moz-box-shadow: 1px 1px 2px 0px rgba(0, 0, 0, 0.025); 16 | box-shadow: 1px 1px 2px 0px rgba(0, 0, 0, 0.025); 17 | } 18 | 19 | #Header h1 { 20 | margin: 0; 21 | padding: 0; 22 | } 23 | 24 | #Header h1 img { 25 | display: inline-block; 26 | height: 70px; 27 | width: 70px; 28 | padding: 10px; 29 | } 30 | 31 | #Header h1 span { 32 | padding-left: 15px; 33 | font-weight: bold; 34 | position: relative; 35 | top: 3px; 36 | } 37 | 38 | #Header h1 a { 39 | color: black; 40 | } 41 | #Header h1 a:hover { 42 | text-decoration: underline; 43 | } 44 | 45 | #Header .profile { 46 | text-align: right; 47 | padding: 10px; 48 | } 49 | 50 | #Header .profile .ant-avatar { 51 | float: right; 52 | } 53 | 54 | #Header .profile .profileDetails { 55 | display: inline-block; 56 | padding: 8px 12px; 57 | font-size: 0.8em; 58 | } 59 | -------------------------------------------------------------------------------- /src/views/components/Header.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import React from 'react'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { Link } from 'react-router-dom'; 6 | import { Row, Col, Avatar } from 'antd'; 7 | import { UserOutlined } from '@ant-design/icons'; 8 | import NavBar from './NavBar'; 9 | import './Header.css'; 10 | import Logo from '../../images/logo.png'; 11 | import IAppState from '../../services/store/IAppState'; 12 | import { AuthStatus } from '../../models/auth/types'; 13 | import { signOut } from '../../stores/auth/asyncActions'; 14 | import { FEATUREFLAG_DISABLE_AUTHENTICATION } from '../../stores/config/constants'; 15 | 16 | const links = [ 17 | { 18 | label: 'Join a Call', 19 | to: '/call/join', 20 | }, 21 | { 22 | label: 'Calls', 23 | to: '/', 24 | }, 25 | { 26 | label: 'Bot Service Status', 27 | to: '/botservice', 28 | }, 29 | ]; 30 | 31 | const Header: React.FC = () => { 32 | const dispatch = useDispatch(); 33 | const { status: authStatus, userProfile } = useSelector((state: IAppState) => state.auth); 34 | const { app: appConfig } = useSelector((state: IAppState) => state.config); 35 | const userName = userProfile?.username || ''; 36 | const role = authStatus === AuthStatus.Authenticated ? 'Producer' : 'None'; 37 | const disableAuthFlag = appConfig?.featureFlags && appConfig.featureFlags[FEATUREFLAG_DISABLE_AUTHENTICATION]; 38 | 39 | const onClickSignOut = () => { 40 | dispatch(signOut(userName)); 41 | }; 42 | 43 | return ( 44 | 78 | ); 79 | }; 80 | 81 | export default Header; 82 | -------------------------------------------------------------------------------- /src/views/components/NavBar.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | #Nav ul { 4 | list-style: none; 5 | margin: 0; 6 | padding: 0; 7 | /* border: 1px solid red; */ 8 | 9 | /* text-align: center; */ 10 | /* position: absolute; */ 11 | text-align: center; 12 | /* bottom: 0; */ 13 | } 14 | #Nav ul li { 15 | display: inline-block; 16 | padding-right: 20px; 17 | padding-top: 24px; 18 | } 19 | 20 | #Nav ul a { 21 | display: block; 22 | padding: 10px; 23 | border-color: transparent; 24 | color: 000; 25 | 26 | transition: all 0.1s ease-in-out; 27 | } 28 | 29 | #Nav ul .selected { 30 | font-weight: bold; 31 | } 32 | #Nav ul .selected a, 33 | #Nav ul a:hover { 34 | border-bottom: 4px solid #6364a9; 35 | color: #6364a9; 36 | } 37 | -------------------------------------------------------------------------------- /src/views/components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import React from 'react'; 4 | import { Link, withRouter, RouteComponentProps } from 'react-router-dom'; 5 | 6 | import './NavBar.css'; 7 | 8 | interface INavBarDataProps extends RouteComponentProps { 9 | links: Array<{label:string, to: string}> 10 | } 11 | 12 | const NavBar: React.FC = (props) => { 13 | 14 | const navBarLinks = props.links.map(o => ({ 15 | ...o, 16 | selected: props.location.pathname === o.to 17 | })); 18 | 19 | return ( 20 | 27 | ); 28 | } 29 | 30 | export default withRouter(NavBar); 31 | -------------------------------------------------------------------------------- /src/views/components/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const NotFound: React.FC = () => { 5 | return ( 6 |
7 |

404 - Not Found

8 |

Sorry we can’t find that page.

9 |

Maybe the page you are looking for has been removed, or you typed in the wrong URL.

10 | Go Home 11 |
12 | ); 13 | }; 14 | 15 | export default NotFound; -------------------------------------------------------------------------------- /src/views/components/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import React from "react"; 4 | import { useSelector } from "react-redux"; 5 | import { Route, Redirect } from "react-router-dom"; 6 | import { AuthStatus } from "../../models/auth/types"; 7 | import IAppState from "../../services/store/IAppState"; 8 | 9 | interface PrivateRouteProps { 10 | component: React.ComponentType; 11 | path: string; 12 | exact?: boolean; 13 | } 14 | 15 | const PrivateRoute: React.FC = ({ 16 | component: Component, 17 | ...rest 18 | }) => { 19 | const authStatus = useSelector((state: IAppState) => state.auth.status); 20 | const isAuthenticated = authStatus === AuthStatus.Authenticated; 21 | 22 | return ( 23 | 26 | isAuthenticated ? : 27 | } 28 | /> 29 | ); 30 | }; 31 | 32 | export default PrivateRoute; -------------------------------------------------------------------------------- /src/views/home/Home.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | #CallsInfoActions p { 4 | margin: 20px 0; 5 | } 6 | 7 | .joinNew { 8 | clear: both; 9 | } 10 | -------------------------------------------------------------------------------- /src/views/home/Home.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import React, { useEffect } from 'react'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { Link } from 'react-router-dom'; 6 | import { Button, Spin } from 'antd'; 7 | 8 | import './Home.css'; 9 | import ActiveCallCard from './components/ActiveCallCard'; 10 | import { getActiveCallsAsync } from '../../stores/calls/asyncActions'; 11 | import * as CallsActions from '../../stores/calls/actions'; 12 | import { selectRequesting } from '../../stores/requesting/selectors'; 13 | import IAppState from '../../services/store/IAppState'; 14 | import { selectActiveCalls } from '../../stores/calls/selectors'; 15 | 16 | const Home: React.FC = () => { 17 | 18 | const dispatch = useDispatch(); 19 | const isRequesting = useSelector((state: IAppState) => selectRequesting(state, [CallsActions.REQUEST_ACTIVE_CALLS])); 20 | const activeCalls = useSelector((state: IAppState) => selectActiveCalls(state)); 21 | 22 | useEffect(() => { 23 | dispatch(getActiveCallsAsync()); 24 | }, []); 25 | 26 | const hasCalls = activeCalls.length > 0; 27 | return ( 28 |
29 |

Active Calls

30 | 31 | {!hasCalls && ( 32 | <> 33 | {isRequesting && ( 34 |
35 | )} 36 | {!isRequesting && ( 37 | <> 38 |

There are no active calls.

39 |

40 | Please join the bot to an active call to start. 41 |

42 | 43 | )} 44 | 45 | )} 46 | 47 | {hasCalls && ( 48 | <> 49 | {activeCalls.map(o => )} 50 |
51 | 54 |
55 | 56 | )} 57 |
58 | ) 59 | } 60 | 61 | export default Home; 62 | -------------------------------------------------------------------------------- /src/views/home/components/ActiveCallCard.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | .activeCallCard { 4 | float: left; 5 | min-width: 400px; 6 | position: relative; 7 | } 8 | 9 | @media only screen and (max-width: 1300px) { 10 | .activeCallCard { 11 | min-width: 460px; 12 | width: 50%; 13 | } 14 | } 15 | 16 | @media only screen and (min-width: 1300px) { 17 | .activeCallCard { 18 | min-width: auto; 19 | width: 33%; 20 | } 21 | } 22 | 23 | @media only screen and (min-width: 2000px) { 24 | .activeCallCard { 25 | min-width: auto; 26 | width: 25%; 27 | } 28 | } 29 | 30 | .activeCallCard .content { 31 | position: relative; 32 | 33 | height: 200px; 34 | margin: 0px 20px 20px 0; 35 | border-radius: 4px 4px 24px 4px; 36 | background-color: #fff; 37 | transition: all 0.1s ease-in-out; 38 | padding: 20px; 39 | 40 | /* shadow */ 41 | -webkit-box-shadow: 1px 1px 2px 0px rgba(0, 0, 0, 0.1); 42 | -moz-box-shadow: 1px 1px 2px 0px rgba(0, 0, 0, 0.1); 43 | box-shadow: 1px 1px 2px 0px rgba(0, 0, 0, 0.1); 44 | 45 | flex-wrap: nowrap; 46 | overflow: hidden; 47 | } 48 | 49 | .activeCallCard .content h3 { 50 | font-size: 1.4em; 51 | font-weight: bold; 52 | } 53 | 54 | .activeCallCard .status { 55 | text-transform: uppercase; 56 | font-weight: bold; 57 | } 58 | 59 | /* palette - disconnected / non-streaming */ 60 | .activeCallCard .content .status { 61 | color: #bdbdbd; 62 | } 63 | 64 | /* palette - establised */ 65 | .activeCallCard.healthy .content .status { 66 | color: #73c856; 67 | } 68 | 69 | /* palette - initializing/connecting */ 70 | .activeCallCard.initializing .content .status { 71 | color: #f3ca3e; 72 | } 73 | 74 | /* palette - error/unhealthy */ 75 | .activeCallCard.error .content .status { 76 | color: #df3639; 77 | } 78 | 79 | .activeCallCard p { 80 | padding: 0; 81 | margin: 0 0 4px 0; 82 | } 83 | 84 | .activeCallCard a.action { 85 | position: absolute; 86 | bottom: 20px; 87 | right: 20px; 88 | display: inline-flex; 89 | width: 60px; 90 | height: 60px; 91 | text-align: center; 92 | border-radius: 60px; 93 | vertical-align: middle; 94 | 95 | transition: all 0.1s ease-in-out; 96 | background-color: #1890ff; 97 | color: #fff; 98 | } 99 | 100 | .activeCallCard a.action:hover { 101 | background-color: #000; 102 | color: #fff; 103 | } 104 | 105 | .activeCallCard a.action svg { 106 | display: block; 107 | margin: auto; 108 | } 109 | -------------------------------------------------------------------------------- /src/views/home/components/ActiveCallCard.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import React from 'react'; 4 | import ChevronRightIcon from '@material-ui/icons/ChevronRight'; 5 | import { Link } from 'react-router-dom'; 6 | import moment from 'moment' 7 | import './ActiveCallCard.css'; 8 | import { Call, CallState, CallType } from '../../../models/calls/types'; 9 | 10 | interface ICallCardDataProps { 11 | call: Call; 12 | } 13 | 14 | type ICallCardProps = ICallCardDataProps; 15 | 16 | const ActiveCallCard: React.FC = ({ call }) => { 17 | 18 | const status = getConnectionStatus(call); 19 | const classes = ['activeCallCard', getConnectionClass(call)]; 20 | 21 | // creation formatting 22 | const creationDate = moment(call.createdAt); 23 | const datePart = creationDate.format('L'); // 07/03/2020 24 | const timePart = creationDate.format('LTS'); // 5:29:19 PM 25 | // const minutesPassed = creationDate.startOf('hour').fromNow(); // 14 minutes ago 26 | const formattedCreation = datePart + " " + timePart; // + " (" + minutesPassed + ")"; 27 | 28 | 29 | return ( 30 |
31 |
32 |

{call.displayName}

33 |

Status: {status}

34 |

{CallTypeStrings[call.meetingType]} | Created : {formattedCreation}

35 | 36 | 37 | 38 |
39 |
40 | ); 41 | } 42 | 43 | export default ActiveCallCard; 44 | 45 | const getConnectionStatus = (call: Call): string => { 46 | switch (call.state) { 47 | case CallState.Establishing: return 'Connecting'; 48 | case CallState.Established: return 'Connected'; 49 | case CallState.Terminating: return 'Disconnecting'; 50 | case CallState.Terminated: return 'Disconnected'; 51 | } 52 | } 53 | 54 | const getConnectionClass = (call: Call): string => { 55 | switch (call.state) { 56 | case CallState.Establishing: return 'initializing'; 57 | case CallState.Established: return 'healthy'; 58 | case CallState.Terminating: return 'disconnecting'; 59 | case CallState.Terminated: return 'disconnected'; 60 | } 61 | } 62 | 63 | const CallTypeStrings = { 64 | [CallType.Default]: 'Normal call', 65 | [CallType.Event]: 'Event call' 66 | }; 67 | -------------------------------------------------------------------------------- /src/views/home/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | export enum TeamsColors { 4 | Red = "#D74654", 5 | Purple = "#6264A7", 6 | Black = "#11100F", 7 | Green = "#7FBA00", 8 | Grey = "#BEBBB8", 9 | MiddleGrey = "#3B3A39", 10 | DarkGrey = "#201F1E", 11 | White = "white" 12 | } 13 | 14 | export enum TeamsMargins { 15 | micro = "4px", 16 | small = "8px", 17 | medium = "20px", 18 | large = "40px", 19 | } 20 | -------------------------------------------------------------------------------- /src/views/join-call/JoinCall.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | #HomeBody { 4 | padding: 30px; 5 | } 6 | 7 | .StatusIcon { 8 | margin-right: 20px; 9 | } 10 | 11 | .CallUrl { 12 | display: inline-block; 13 | max-width: 700px; 14 | text-overflow: ellipsis; 15 | overflow: hidden; 16 | white-space: nowrap; 17 | } 18 | 19 | .errorRow { 20 | color: red; 21 | } 22 | -------------------------------------------------------------------------------- /src/views/join-call/JoinCall.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import React, { createRef } from "react"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import { Input, Button, Form, Row, Col } from "antd"; 6 | import { Store } from "antd/lib/form/interface"; 7 | import { Rule, FormInstance } from "antd/lib/form"; 8 | import HourglassEmpty from "@material-ui/icons/HourglassEmpty"; 9 | 10 | import "./JoinCall.css"; 11 | import { selectNewCall } from "../../stores/calls/selectors"; 12 | import IAppState from "../../services/store/IAppState"; 13 | import { selectRequesting } from "../../stores/requesting/selectors"; 14 | import { extractLinks } from "../../services/helpers"; 15 | import * as CallsActions from '../../stores/calls/actions'; 16 | import { joinCallAsync } from "../../stores/calls/asyncActions"; 17 | 18 | const { Item } = Form; 19 | 20 | const MEETING_URL_PATTERN = /https:\/\/teams\.microsoft\.com\/l\/meetup-join\/(.*)/; 21 | 22 | const JoinCall: React.FC = (props) => { 23 | 24 | const dispatch = useDispatch(); 25 | const connectingCall = useSelector((state: IAppState) => selectNewCall(state)); 26 | const isRequesting: boolean = useSelector((state: IAppState) => selectRequesting(state, [CallsActions.REQUEST_JOIN_CALL])); 27 | 28 | 29 | const formRef = createRef(); 30 | const handlePaste = (data: DataTransfer) => { 31 | const html = data.getData("text/html"); 32 | if (html && html.indexOf('href="https://teams.microsoft.com/l/meetup-join"') > -1) { 33 | // extract links 34 | const links = extractLinks(html); 35 | const meetingLink = links.find((o) => MEETING_URL_PATTERN.test(o)); 36 | if (meetingLink) { 37 | setTimeout(() => formRef.current?.setFieldsValue({ callUrl: meetingLink }), 1); 38 | } 39 | } 40 | }; 41 | 42 | // When form is completed correctly 43 | const onFinish = (form: Store) => { 44 | // Trigger JoinCall AsyncAction 45 | dispatch(joinCallAsync(form.callUrl)); 46 | }; 47 | 48 | // Validation 49 | const callUrlRules: Rule[] = [ 50 | { 51 | required: true, 52 | whitespace: false, 53 | message: "Please add your Teams Invite URL.", 54 | pattern: MEETING_URL_PATTERN, 55 | }, 56 | ]; 57 | 58 | return ( 59 | <> 60 |
61 |
62 |
63 |

Connect to a new call

64 |
65 | 66 | handlePaste(e.clipboardData)} 70 | /> 71 | 72 |
73 |
74 |
75 | 76 | 79 | 80 |
81 |
82 | 83 | {isRequesting && ( 84 | 85 | 86 | 87 | 88 | 89 |

90 | Joining {connectingCall?.callUrl} 91 |

92 |

Please wait while the bot joins...

93 | 94 |
95 | )} 96 |
97 | 98 | ); 99 | }; 100 | 101 | export default JoinCall 102 | -------------------------------------------------------------------------------- /src/views/login/LoginPage.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import React from "react"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import { Button, Card } from "antd"; 6 | import { LoginOutlined } from "@ant-design/icons"; 7 | import { Redirect } from "react-router-dom"; 8 | import { signIn } from "../../stores/auth/asyncActions"; 9 | import IAppState from "../../services/store/IAppState"; 10 | import { AuthStatus } from "../../models/auth/types"; 11 | 12 | const LoginPage: React.FC = () => { 13 | const dispatch = useDispatch(); 14 | const authStatus = useSelector((state: IAppState) => state.auth.status); 15 | const isAuthenticated = authStatus === AuthStatus.Authenticated; 16 | 17 | const { Meta } = Card; 18 | const onClickSignIn = () => { 19 | dispatch(signIn()); 20 | } 21 | return isAuthenticated ? ( 22 | 23 | ) : ( 24 |
31 | 37 | 38 |
39 | 47 |
48 |
49 | ); 50 | }; 51 | 52 | export default LoginPage; -------------------------------------------------------------------------------- /src/views/service/BotServiceStatus.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | #CallsInfoActions p { 4 | margin: 20px 0; 5 | } 6 | 7 | .joinNew { 8 | clear: both; 9 | } -------------------------------------------------------------------------------- /src/views/service/BotServiceStatus.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import React, { useCallback, useEffect, useState } from 'react'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { Spin } from 'antd'; 6 | import './BotServiceStatus.css'; 7 | import BotServiceStatusCard from './BotServiceStatusCard'; 8 | import * as BotServiceActions from '../../stores/service/actions'; 9 | import { selectRequesting } from '../../stores/requesting/selectors'; 10 | import IAppState from '../../services/store/IAppState'; 11 | import { getBotServiceAsync, startBotServiceAsync, stopBotServiceAsync } from '../../stores/service/asyncActions'; 12 | import useInterval from '../../hooks/useInterval'; 13 | 14 | const ServicePage: React.FC = () => { 15 | const dispatch = useDispatch(); 16 | const { botServices } = useSelector((state: IAppState) => state.botServiceStatus); 17 | const [isPollingEnabled, setIsPollingEnabled] = useState(false) 18 | const isRequesting: boolean = useSelector((state: IAppState) => 19 | selectRequesting(state, [BotServiceActions.REQUEST_BOT_SERVICE]) 20 | ); 21 | 22 | useInterval(useCallback(() => dispatch(getBotServiceAsync()), [dispatch, getBotServiceAsync]), isPollingEnabled ? 3000 : null); 23 | 24 | useEffect(()=>{ 25 | setIsPollingEnabled(true) 26 | },[]) 27 | 28 | const hasBotServices = botServices.length > 0; 29 | return ( 30 |
31 |

Bot Services

32 | {!hasBotServices && ( 33 | <> 34 | {isRequesting && ( 35 |
36 | 37 |
38 | )} 39 | {isRequesting && ( 40 | <> 41 |

There are no Bot Services.

42 | 43 | )} 44 | 45 | )} 46 | {hasBotServices && ( 47 | <> 48 | {botServices.map((botService, i) => ( 49 | dispatch(startBotServiceAsync())} 54 | onStop={() => dispatch(stopBotServiceAsync())} 55 | /> 56 | ))} 57 | 58 | )} 59 |
60 | ); 61 | }; 62 | 63 | export default ServicePage; 64 | -------------------------------------------------------------------------------- /src/views/service/BotServiceStatusCard.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | #CardHeader { 4 | background-color: #1890FF; 5 | } 6 | 7 | #BotServiceStatusCard { 8 | float: left; 9 | min-width: 300px; 10 | position: relative; 11 | margin: 5px; 12 | background-color: #FFFFFF; 13 | } 14 | 15 | @-webkit-keyframes infiniteRotate { 16 | 0% { -webkit-transform: rotate(0deg); } 17 | 100% { -webkit-transform: rotate(360deg); } 18 | } 19 | /* Standard syntax */ 20 | @keyframes infiniteRotate { 21 | 0% { -webkit-transform: rotate(0deg); } 22 | 100% { -webkit-transform: rotate(360deg); } 23 | } 24 | -------------------------------------------------------------------------------- /src/views/service/BotServiceStatusCard.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import React from 'react'; 4 | import PowerOffIcon from '@material-ui/icons/PowerOff'; 5 | import CloseIcon from '@material-ui/icons/Close'; 6 | import './BotServiceStatusCard.css'; 7 | import Card from '@material-ui/core/Card'; 8 | import CardHeader from '@material-ui/core/CardHeader'; 9 | import CardContent from '@material-ui/core/CardContent'; 10 | import StorageIcon from '@material-ui/icons/Storage'; 11 | import Avatar from '@material-ui/core/Avatar'; 12 | import IconButton from '@material-ui/core/IconButton'; 13 | import { makeStyles, createStyles } from '@material-ui/core/styles'; 14 | import StopRoundedIcon from '@material-ui/icons/StopRounded'; 15 | import PlayArrowRoundedIcon from '@material-ui/icons/PlayArrowRounded'; 16 | import HourglassEmptyIcon from '@material-ui/icons/HourglassEmpty'; 17 | import RecordVoiceOverIcon from '@material-ui/icons/RecordVoiceOver'; 18 | import PersonIcon from '@material-ui/icons/Person'; 19 | import List from '@material-ui/core/List'; 20 | import ListItem from '@material-ui/core/ListItem'; 21 | import ListItemText from '@material-ui/core/ListItemText'; 22 | import ListItemAvatar from '@material-ui/core/ListItemAvatar'; 23 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; 24 | import { BotService, BotServiceStates, ProvisioningStateValues } from '../../models/service/types'; 25 | 26 | interface BotServiceStatusDataProps { 27 | name: string; 28 | botService: BotService; 29 | onStart: () => void; 30 | onStop: () => void; 31 | } 32 | 33 | const useStyles = makeStyles(() => 34 | createStyles({ 35 | rotateIcon: { 36 | animation: 'infiniteRotate 2s linear infinite', 37 | }, 38 | avatarColor: { 39 | backgroundColor: 'lightgrey', 40 | } 41 | }) 42 | ); 43 | 44 | const BotServiceStatusCard: React.FC = (props) => { 45 | const classes = useStyles(); 46 | 47 | const { botService, name, onStart, onStop } = props; 48 | 49 | const serviceState = BotServiceStates[botService.state]; 50 | const { id: provisioningStateValue, name: provisioningStateDisplayName } = 51 | botService.infrastructure.provisioningDetails.state; 52 | const hasTransitioningState: boolean = 53 | provisioningStateValue === ProvisioningStateValues.Provisioning || 54 | provisioningStateValue === ProvisioningStateValues.Deprovisioning; 55 | const stateDisplayName = 56 | provisioningStateValue === ProvisioningStateValues.Provisioned ? serviceState : provisioningStateDisplayName; 57 | 58 | const provisionedIcon = () => { 59 | switch(botService.state) { 60 | case BotServiceStates.Available: 61 | return ; 62 | case BotServiceStates.Busy: 63 | return ; 64 | default: 65 | return ; 66 | } 67 | } 68 | 69 | return ( 70 | 71 | 75 | 76 | 77 | } 78 | title={{name} || ' [Bot Service] '} 79 | // subheader="teamstx-demo_group" // resourceGroup 80 | > 81 | 82 | 83 | 84 | 85 | 86 | {provisioningStateValue === ProvisioningStateValues.Provisioned && ( 87 | provisionedIcon() 88 | )} 89 | {provisioningStateValue === ProvisioningStateValues.Deprovisioned && ( 90 | 91 | )} 92 | {hasTransitioningState && } 93 | 94 | 95 | 96 | 97 | 98 | {![ProvisioningStateValues.Provisioning, ProvisioningStateValues.Provisioned].includes( 99 | provisioningStateValue 100 | ) && ( 101 | 102 | 103 | 104 | )} 105 | {[ProvisioningStateValues.Provisioning, ProvisioningStateValues.Provisioned].includes( 106 | provisioningStateValue 107 | ) && ( 108 | 109 | 110 | 111 | )} 112 | 113 | 114 | 115 | 116 | ); 117 | }; 118 | 119 | export default BotServiceStatusCard; 120 | -------------------------------------------------------------------------------- /src/views/toast/Toasts.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Microsoft Corporation. 2 | Licensed under the MIT license. */ 3 | .wrapper { 4 | display: flex; 5 | flex-direction: column; 6 | overflow: hidden; 7 | padding: 16px; 8 | position: fixed; 9 | right: 0; 10 | top: 0; 11 | z-index: 10; 12 | } -------------------------------------------------------------------------------- /src/views/toast/Toasts.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import React from 'react'; 4 | import { useSelector } from 'react-redux'; 5 | import IAppState from '../../services/store/IAppState'; 6 | import { ToastCard } from './toast-card/ToastCard'; 7 | import './Toasts.css'; 8 | 9 | export const Toasts: React.FC = () => { 10 | const items = useSelector((state:IAppState) => state.toast.items); 11 | return ( 12 |
13 | {items.map((item) => ( 14 | 15 | ))} 16 |
17 | ); 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/views/toast/toast-card/ToastCard.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import * as React from 'react'; 4 | import Card from "@material-ui/core/Card"; 5 | import CardHeader from "@material-ui/core/CardHeader"; 6 | import Divider from "@material-ui/core/Divider"; 7 | import CardContent from "@material-ui/core/CardContent"; 8 | import { Typography } from 'antd'; 9 | 10 | import { IToastItem } from '../../../stores/toast/reducer'; 11 | 12 | const { Text } = Typography 13 | 14 | interface ToastCardProps { 15 | item: IToastItem; 16 | } 17 | 18 | export const ToastCard: React.FC = (props) => { 19 | return ( 20 | 21 | {props.item.type} || 25 | " Toast error "} 26 | > 27 | 28 | 29 | {props.item.message} 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/views/unauthorized/Unauthorized.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | import React from 'react'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { Redirect } from 'react-router-dom'; 6 | import { Button, Card } from 'antd'; 7 | import { LoginOutlined } from '@ant-design/icons'; 8 | import { AuthStatus } from '../../models/auth/types'; 9 | import IAppState from '../../services/store/IAppState'; 10 | import { signOut } from '../../stores/auth/asyncActions'; 11 | 12 | const Unauthorized: React.FC = () => { 13 | const dispatch = useDispatch(); 14 | const { status: authStatus, userProfile } = useSelector((state: IAppState) => state.auth); 15 | const isUnauthorized = authStatus === AuthStatus.Unauthorized; 16 | 17 | const onClickSignOut = () => { 18 | const userName = userProfile?.username || ''; 19 | dispatch(signOut(userName)); 20 | }; 21 | 22 | const { Meta } = Card; 23 | 24 | return isUnauthorized ? ( 25 |
32 | 33 | 37 |
38 | 46 |
47 |
48 | ) : ( 49 | 50 | ); 51 | }; 52 | 53 | export default Unauthorized; 54 | -------------------------------------------------------------------------------- /test.config.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | 5 | module.exports = { 6 | roots: ["/src"], 7 | transform: { 8 | "^.+\\.tsx?$": "ts-jest" 9 | }, 10 | collectCoverage: true, 11 | testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", 12 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], 13 | snapshotSerializers: ["enzyme-to-json/serializer"], 14 | setupFilesAfterEnv: "/src/setupEnzyme.ts", 15 | reporters: [ 16 | "default", 17 | ["jest-sonar-reporter", { reportPath: "build/sonar-test", reportFile: "test.xml" }], 18 | ] 19 | 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src", "test.config.js" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------