├── .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 | 
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 | ||
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 | ||
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 | ||
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 | ||
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 | ||
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 | ||
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 | ||
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 | You need to enable JavaScript to run this app.
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 |
60 |
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 value, trigger the redirect
39 | const handleCallSelect = (callId: string) => {
40 | if (callId === PLACEHOLDER_ID) {
41 | return;
42 | }
43 |
44 | setRedirectId(callId);
45 | }
46 |
47 | const selectedId = props.isNew ? NEW_PLACEHOLDER_ID : (props.selectedCallId || PLACEHOLDER_ID);
48 |
49 | return (
50 |
51 | (Select a call)
52 | {props.calls.map(m => ({m.name || 'Broadcast Development Kit Demo'} ))}
53 | (Join a new Call)
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 |
112 | {stream == null ? 'START' : 'STOP'}
113 |
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 |
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 |
161 | {stream.state === StreamState.Disconnected ? 'START' : 'STOP'}
162 |
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 |
21 |
22 | {navBarLinks.map(o => (
23 | {o.label}
24 | ))}
25 |
26 |
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 |
52 | Join a new Call
53 |
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 |
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 | }
42 | shape="round"
43 | onClick={onClickSignIn}
44 | >
45 | Login with your account
46 |
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 |
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 |
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 | }
41 | shape="round"
42 | onClick={onClickSignOut}
43 | >
44 | Log out
45 |
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 |
--------------------------------------------------------------------------------