├── .codecov.yml ├── .dependabot └── config.yml ├── .editorconfig ├── .env ├── .env.development ├── .env.production ├── .env.test ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .nvmrc ├── .sonarcloud.properties ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Procfile ├── README.md ├── app-icons ├── icon.icns ├── icon.ico └── icon.png ├── azure-pipelines.yml ├── azure-pipelines ├── create-release.yml ├── linux │ ├── continuous-build-linux.yml │ └── xvfb.init ├── mac │ └── continuous-build-mac.yml ├── templates │ ├── build-artifact.yml │ ├── create-github-release.yml │ ├── create-web-release.yml │ ├── git-pull-current-branch.yml │ └── npm-version-bump.yml ├── upload-plato-report.yml └── windows │ └── continuous-build-windows.yml ├── cocoSSDModel ├── classes.json ├── group1-shard1of5 ├── group1-shard2of5 ├── group1-shard3of5 ├── group1-shard4of5 ├── group1-shard5of5 └── model.json ├── config ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js ├── docs ├── CI.md ├── DEBUG.md ├── PACKAGING.md ├── PLATO.md ├── REACTAPP.md ├── RELEASE_GUIDE.md ├── STYLE.md ├── images │ ├── export-labels.jpg │ ├── label-image.jpg │ ├── ml-workflow.png │ ├── new-connection.jpg │ ├── new-project.jpg │ ├── project-settings.jpg │ ├── release-process.png │ ├── reorder-tag.jpg │ ├── security-tokens.jpg │ └── video-player.jpg └── wireframes.bmpr ├── electron-builder.yml ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── manifest.json └── tags.svg ├── scripts ├── build.sh ├── complexity-analysis.js ├── generate-changelog.sh ├── generate-report.sh ├── generate-web-artifact.sh ├── git-pull-current-branch.sh ├── release-pr.sh ├── update-report.sh └── version-bump-commit.sh ├── server ├── .env.template ├── .vscode │ ├── launch.json │ └── tasks.json ├── azure-deploy.yml ├── jest.config.js ├── package-lock.json ├── package.json ├── public │ ├── test.html │ └── views │ │ ├── account.ejs │ │ ├── index.ejs │ │ └── layout.ejs ├── src │ ├── __tests__ │ │ ├── api.test.ts │ │ └── extra.test.ts │ ├── app.ts │ ├── config.ts │ ├── graph.ts │ └── oauth2.ts ├── tsconfig.json └── tslint.json ├── src ├── App.scss ├── App.test.tsx ├── App.tsx ├── assets │ ├── css │ │ └── bootstrap-theme-slate.css │ └── sass │ │ └── theme.scss ├── common │ ├── appInfo.ts │ ├── clipboard.test.ts │ ├── clipboard.ts │ ├── constants.ts │ ├── crypto.test.ts │ ├── crypto.ts │ ├── deferred.test.ts │ ├── deferred.ts │ ├── environment.ts │ ├── extensions │ │ ├── array.test.ts │ │ ├── array.ts │ │ ├── map.test.ts │ │ └── map.ts │ ├── guard.test.ts │ ├── guard.ts │ ├── hostProcess.test.ts │ ├── hostProcess.ts │ ├── htmlFileReader.test.ts │ ├── htmlFileReader.ts │ ├── ipcRendererProxy.test.ts │ ├── ipcRendererProxy.ts │ ├── layout.test.tsx │ ├── layout.ts │ ├── localization │ │ ├── en-us.ts │ │ ├── es-cl.ts │ │ ├── ja.ts │ │ ├── ko-kr.ts │ │ ├── zh-ch.ts │ │ └── zh-tw.ts │ ├── mockFactory.ts │ ├── strings.test.ts │ ├── strings.ts │ ├── utils.test.ts │ └── utils.ts ├── electron │ ├── common │ │ ├── ipcMainProxy.test.ts │ │ ├── ipcMainProxy.ts │ │ └── ipcProxy.ts │ ├── main.ts │ ├── providers │ │ └── storage │ │ │ ├── localFileSystem.test.ts │ │ │ └── localFileSystem.ts │ └── start.js ├── history.ts ├── index.scss ├── index.tsx ├── logo.svg ├── models │ ├── applicationState.ts │ └── v1Models.ts ├── providers │ ├── activeLearning │ │ ├── electronProxyHandler.test.ts │ │ ├── electronProxyHandler.ts │ │ ├── objectDetection.test.ts │ │ └── objectDetection.ts │ ├── export │ │ ├── azureCustomVision.json │ │ ├── azureCustomVision.test.ts │ │ ├── azureCustomVision.ts │ │ ├── azureCustomVision.ui.json │ │ ├── azureCustomVision │ │ │ ├── azureCustomVisionService.test.ts │ │ │ └── azureCustomVisionService.ts │ │ ├── cntk.json │ │ ├── cntk.test.ts │ │ ├── cntk.ts │ │ ├── cntk.ui.json │ │ ├── csv.json │ │ ├── csv.test.ts │ │ ├── csv.ts │ │ ├── csv.ui.json │ │ ├── exportProvider.test.ts │ │ ├── exportProvider.ts │ │ ├── exportProviderFactory.test.ts │ │ ├── exportProviderFactory.ts │ │ ├── pascalVOC.json │ │ ├── pascalVOC.test.ts │ │ ├── pascalVOC.ts │ │ ├── pascalVOC.ui.json │ │ ├── pascalVOC │ │ │ └── pascalVOCTemplates.ts │ │ ├── tensorFlowRecords.json │ │ ├── tensorFlowRecords.test.ts │ │ ├── tensorFlowRecords.ts │ │ ├── tensorFlowRecords.ui.json │ │ ├── tensorFlowRecords │ │ │ ├── tensorFlowBuilder.test.ts │ │ │ ├── tensorFlowBuilder.ts │ │ │ ├── tensorFlowHelpers.test.ts │ │ │ ├── tensorFlowHelpers.ts │ │ │ ├── tensorFlowReader.test.ts │ │ │ ├── tensorFlowReader.ts │ │ │ ├── tensorFlowRecordsProtoBuf.proto │ │ │ └── tensorFlowRecordsProtoBuf_pb.js │ │ ├── testAssetsSplitHelper.test.ts │ │ ├── testAssetsSplitHelper.ts │ │ ├── vottJson.json │ │ ├── vottJson.test.ts │ │ ├── vottJson.ts │ │ └── vottJson.ui.json │ ├── storage │ │ ├── assetProviderFactory.test.ts │ │ ├── assetProviderFactory.ts │ │ ├── azureBlobStorage.json │ │ ├── azureBlobStorage.test.ts │ │ ├── azureBlobStorage.ts │ │ ├── azureBlobStorage.ui.json │ │ ├── bingImageSearch.json │ │ ├── bingImageSearch.test.ts │ │ ├── bingImageSearch.ts │ │ ├── bingImageSearch.ui.json │ │ ├── localFileSystemProxy.json │ │ ├── localFileSystemProxy.test.ts │ │ ├── localFileSystemProxy.ts │ │ ├── localFileSystemProxy.ui.json │ │ ├── storageProviderFactory.test.ts │ │ └── storageProviderFactory.ts │ └── toolbar │ │ ├── toolbarItemFactory.test.ts │ │ └── toolbarItemFactory.ts ├── react-app-env.d.ts ├── react │ └── components │ │ ├── common │ │ ├── alert │ │ │ ├── alert.test.tsx │ │ │ └── alert.tsx │ │ ├── arrayField │ │ │ └── arrayFieldTemplate.tsx │ │ ├── assetPreview │ │ │ ├── assetPreview.test.tsx │ │ │ ├── assetPreview.tsx │ │ │ ├── imageAsset.test.tsx │ │ │ ├── imageAsset.tsx │ │ │ ├── tfrecordAsset.test.tsx │ │ │ ├── tfrecordAsset.tsx │ │ │ ├── videoAsset.test.tsx │ │ │ └── videoAsset.tsx │ │ ├── cloudFilePicker │ │ │ ├── cloudFilePicker.test.tsx │ │ │ └── cloudFilePicker.tsx │ │ ├── colorPicker.tsx │ │ ├── common.scss │ │ ├── condensedList │ │ │ ├── condensedList.scss │ │ │ ├── condensedList.test.tsx │ │ │ └── condensedList.tsx │ │ ├── conditionalNavLink │ │ │ ├── conditionalNavLink.test.tsx │ │ │ └── conditionalNavLink.tsx │ │ ├── confirm │ │ │ ├── confirm.test.tsx │ │ │ └── confirm.tsx │ │ ├── connectionPicker │ │ │ ├── connectionPicker.test.tsx │ │ │ └── connectionPicker.tsx │ │ ├── connectionProviderPicker │ │ │ ├── connectionProviderPicker.test.tsx │ │ │ └── connectionProviderPicker.tsx │ │ ├── customField │ │ │ ├── customField.tsx │ │ │ ├── customFieldTemplate.test.tsx │ │ │ └── customFieldTemplate.tsx │ │ ├── errorHandler │ │ │ ├── errorHandler.test.tsx │ │ │ └── errorHandler.tsx │ │ ├── exportProviderPicker │ │ │ ├── exportProviderPicker.test.tsx │ │ │ └── exportProviderPicker.tsx │ │ ├── externalPicker │ │ │ ├── externalPicker.test.tsx │ │ │ └── externalPicker.tsx │ │ ├── filePicker │ │ │ ├── filePicker.test.tsx │ │ │ └── filePicker.tsx │ │ ├── keyboardBinding │ │ │ ├── keyboardBinding.test.tsx │ │ │ └── keyboardBinding.tsx │ │ ├── keyboardManager │ │ │ ├── keyboardManager.test.tsx │ │ │ ├── keyboardManager.tsx │ │ │ ├── keyboardRegistrationManager.test.ts │ │ │ └── keyboardRegistrationManager.ts │ │ ├── localFolderPicker │ │ │ ├── localFolderPicker.test.tsx │ │ │ └── localFolderPicker.tsx │ │ ├── messageBox │ │ │ ├── messageBox.test.tsx │ │ │ └── messageBox.tsx │ │ ├── objectField │ │ │ └── objectFieldTemplate.tsx │ │ ├── protectedInput │ │ │ ├── protectedInput.test.tsx │ │ │ └── protectedInput.tsx │ │ ├── securityTokenPicker │ │ │ ├── securityTokenPicker.test.tsx │ │ │ └── securityTokenPicker.tsx │ │ ├── slider │ │ │ ├── slider.test.tsx │ │ │ └── slider.tsx │ │ ├── tagColors.json │ │ ├── tagInput │ │ │ ├── tagInput.scss │ │ │ ├── tagInput.test.tsx │ │ │ ├── tagInput.tsx │ │ │ ├── tagInputItem.test.tsx │ │ │ ├── tagInputItem.tsx │ │ │ └── tagInputToolbar.tsx │ │ └── videoPlayer │ │ │ ├── customVideoPlayerButton.test.tsx │ │ │ └── customVideoPlayerButton.tsx │ │ ├── pages │ │ ├── activeLearning │ │ │ ├── activeLearningForm.json │ │ │ ├── activeLearningForm.test.tsx │ │ │ ├── activeLearningForm.tsx │ │ │ ├── activeLearningForm.ui.json │ │ │ ├── activeLearningPage.test.tsx │ │ │ └── activeLearningPage.tsx │ │ ├── appSettings │ │ │ ├── appSettingsForm.json │ │ │ ├── appSettingsForm.test.tsx │ │ │ ├── appSettingsForm.tsx │ │ │ ├── appSettingsForm.ui.json │ │ │ ├── appSettingsPage.scss │ │ │ ├── appSettingsPage.test.tsx │ │ │ └── appSettingsPage.tsx │ │ ├── connections │ │ │ ├── connectionForm.json │ │ │ ├── connectionForm.test.tsx │ │ │ ├── connectionForm.tsx │ │ │ ├── connectionForm.ui.json │ │ │ ├── connectionItem.tsx │ │ │ ├── connectionsPage.scss │ │ │ ├── connectionsPage.test.tsx │ │ │ └── connectionsPage.tsx │ │ ├── editorPage │ │ │ ├── canvas.test.tsx │ │ │ ├── canvas.tsx │ │ │ ├── canvasHelpers.test.ts │ │ │ ├── canvasHelpers.ts │ │ │ ├── editorPage.scss │ │ │ ├── editorPage.test.tsx │ │ │ ├── editorPage.tsx │ │ │ ├── editorSideBar.test.tsx │ │ │ ├── editorSideBar.tsx │ │ │ ├── editorToolbar.scss │ │ │ ├── editorToolbar.test.tsx │ │ │ └── editorToolbar.tsx │ │ ├── export │ │ │ ├── exportForm.json │ │ │ ├── exportForm.test.tsx │ │ │ ├── exportForm.tsx │ │ │ ├── exportForm.ui.json │ │ │ ├── exportPage.test.tsx │ │ │ └── exportPage.tsx │ │ ├── homepage │ │ │ ├── homePage.scss │ │ │ ├── homePage.test.tsx │ │ │ ├── homePage.tsx │ │ │ └── recentProjectItem.tsx │ │ └── projectSettings │ │ │ ├── projectForm.json │ │ │ ├── projectForm.test.tsx │ │ │ ├── projectForm.tsx │ │ │ ├── projectForm.ui.json │ │ │ ├── projectMetrics.test.tsx │ │ │ ├── projectMetrics.tsx │ │ │ ├── projectSettingsPage.scss │ │ │ ├── projectSettingsPage.test.tsx │ │ │ └── projectSettingsPage.tsx │ │ ├── shell │ │ ├── helpMenu.scss │ │ ├── helpMenu.test.tsx │ │ ├── helpMenu.tsx │ │ ├── mainContentRouter.test.tsx │ │ ├── mainContentRouter.tsx │ │ ├── sidebar.test.tsx │ │ ├── sidebar.tsx │ │ ├── statusBar.scss │ │ ├── statusBar.test.tsx │ │ ├── statusBar.tsx │ │ ├── statusBarMetrics.test.tsx │ │ ├── statusBarMetrics.tsx │ │ ├── titleBar.scss │ │ ├── titleBar.test.tsx │ │ └── titleBar.tsx │ │ └── toolbar │ │ ├── exportProject.tsx │ │ ├── saveProject.test.tsx │ │ ├── saveProject.tsx │ │ ├── toolbarItem.test.tsx │ │ └── toolbarItem.tsx ├── redux │ ├── actions │ │ ├── actionCreators.ts │ │ ├── actionTypes.ts │ │ ├── appErrorActions.test.ts │ │ ├── appErrorActions.ts │ │ ├── applicationActions.test.ts │ │ ├── applicationActions.ts │ │ ├── connectionActions.test.ts │ │ ├── connectionActions.ts │ │ ├── projectActions.test.ts │ │ └── projectActions.ts │ ├── middleware │ │ ├── appInsights.test.ts │ │ ├── appInsights.ts │ │ └── localStorage.ts │ ├── reducers │ │ ├── appErrorReducer.test.ts │ │ ├── appErrorReducer.ts │ │ ├── applicationReducer.test.ts │ │ ├── applicationReducer.ts │ │ ├── connectionsReducer.test.ts │ │ ├── connectionsReducer.ts │ │ ├── currentProjectReducer.test.ts │ │ ├── currentProjectReducer.ts │ │ ├── index.ts │ │ ├── recentProjectsReducer.test.ts │ │ └── recentProjectsReducer.ts │ └── store │ │ ├── initialState.ts │ │ └── store.ts ├── registerMixins.ts ├── registerProviders.test.ts ├── registerProviders.ts ├── registerToolbar.ts ├── serviceWorker.ts ├── services │ ├── activeLearningService.test.ts │ ├── activeLearningService.ts │ ├── assetService.test.ts │ ├── assetService.ts │ ├── connectionService.test.ts │ ├── connectionService.ts │ ├── importService.test.tsx │ ├── importService.ts │ ├── projectService.test.ts │ └── projectService.ts ├── setupTests.js ├── telemetry.test.ts └── telemetry.ts ├── tsconfig.json ├── tslint.json ├── typings.json └── typings └── react-jsonschema-form └── index.d.ts /.codecov.yml: -------------------------------------------------------------------------------- 1 | # Validate changes 2 | # cat .codecov.yml | curl --data-binary @- https://codecov.io/validate 3 | 4 | comment: on # enable pull request comment 5 | coverage: 6 | range: 70..100 7 | round: down # round down to the precision point 8 | precision: 2 9 | status: 10 | project: # compare project coverage against the base of pr 11 | default: 12 | target: 75% # min coverage ratio to be considered a success 13 | threshold: null # allow coverage to drop by X% 14 | base: auto 15 | patch: # provides an indication on how well the pull request is tested 16 | default: 17 | target: 70% # min coverage ratio to be considered a success 18 | ignore: 19 | - "src/electron/start.js" 20 | - "src/providers/export/tensorFlowRecords/tensorFlowRecordsProtoBuf_pb.js" 21 | - "src/redux/store/store.ts" 22 | -------------------------------------------------------------------------------- /.dependabot/config.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | update_configs: 4 | target_branch: "develop" 5 | 6 | # Keep package.json (& lockfiles) up to date weekly 7 | - package_manager: "javascript" 8 | directory: "/" 9 | update_schedule: "weekly" 10 | 11 | default_labels: 12 | - "dependencies" 13 | - "dependabot" 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # Default: 4 | # https://github.com/editorconfig/editorconfig-defaults/blob/master/editorconfig-defaults.json 5 | 6 | # top-most EditorConfig file 7 | root = true 8 | 9 | # Unix-style newlines with a newline ending every file 10 | [*] 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [*.yml] 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # react-scripts build use this to generate the right path for assets 2 | # relative to index.html 3 | # without it, you'll see error like this 4 | # Failed to load resource: net::ERR_FILE_NOT_FOUND /favicon.ico:1 5 | PUBLIC_URL=. 6 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | REACT_APP_INSTRUMENTATION_KEY=40a80c0c-b913-45b7-afc9-c7eb3ed62900 2 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | REACT_APP_INSTRUMENTATION_KEY=0b9e5117-c78d-40c9-9338-921092cde49a 2 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | HOST_TYPE=electron 2 | INSTRUMENTATION_KEY=40a80c0c-b913-45b7-afc9-c7eb3ed62900 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | /.vscode 4 | 5 | # dependencies 6 | /node_modules 7 | /.pnp 8 | /test-output 9 | .pnp.js 10 | 11 | # testing 12 | /coverage 13 | 14 | # packaging 15 | /build 16 | 17 | # releases 18 | /releases/ 19 | 20 | # misc 21 | .DS_Store 22 | .env 23 | .env.local 24 | .env.development.local 25 | .env.test.local 26 | .env.production.local 27 | 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # dev 33 | secrets.sh 34 | 35 | # ide 36 | .idea 37 | 38 | # complexity reports 39 | es6-src/ 40 | report/ 41 | 42 | # VoTT Server 43 | server/lib 44 | server/node_modules 45 | server/coverage 46 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.15.1 2 | -------------------------------------------------------------------------------- /.sonarcloud.properties: -------------------------------------------------------------------------------- 1 | # Path to sources 2 | sonar.sources=src/ 3 | sonar.exclusions=**/*.test.ts*,src/common/localization/* 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016-2019 Microsoft Commercial Software Engineering 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | react: npm run react-start 2 | electron: npm run electron-start -------------------------------------------------------------------------------- /app-icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/app-icons/icon.icns -------------------------------------------------------------------------------- /app-icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/app-icons/icon.ico -------------------------------------------------------------------------------- /app-icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/app-icons/icon.png -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - greenkeeper/* # enable CI to run on greenkeeper branches 3 | - master # run build for every merge to master 4 | pr: 5 | - dev* # kick off for pr targeting dev or prefix dev 6 | - master # trigger build for pr targeting master 7 | 8 | variables: 9 | - group: CODE_COV 10 | 11 | jobs: 12 | - job: Linux 13 | pool: 14 | vmImage: ubuntu-16.04 15 | timeoutInMinutes: 60 # how long to run the job before automatically cancelling 16 | steps: 17 | - checkout: self # self represents the repo where the initial Pipelines YAML file was found 18 | fetchDepth: 1 # the depth of commits to ask Git to fetch 19 | - template: azure-pipelines/linux/continuous-build-linux.yml 20 | 21 | - job: MacOS 22 | pool: 23 | vmImage: macOS-10.15 24 | timeoutInMinutes: 60 # how long to run the job before automatically cancelling 25 | steps: 26 | - checkout: self # self represents the repo where the initial Pipelines YAML file was found 27 | fetchDepth: 1 # the depth of commits to ask Git to fetch 28 | - template: azure-pipelines/mac/continuous-build-mac.yml 29 | 30 | - job: Windows 31 | pool: 32 | vmImage: "windows-2019" 33 | timeoutInMinutes: 60 # how long to run the job before automatically cancelling 34 | steps: 35 | - checkout: self # self represents the repo where the initial Pipelines YAML file was found 36 | fetchDepth: 1 # the depth of commits to ask Git to fetch 37 | - template: azure-pipelines/windows/continuous-build-windows.yml 38 | -------------------------------------------------------------------------------- /azure-pipelines/create-release.yml: -------------------------------------------------------------------------------- 1 | name: "GitHub & Web Release [$(SourceBranchName)] - $(Date:yyyyMMdd)$(Rev:.r)" 2 | 3 | trigger: none # manual queue only when we're ready to release 4 | pr: none # disable CI build for PR 5 | 6 | variables: 7 | azureSubscription: "pj-little-sub" 8 | DEV_STORAGE_ACCOUNT: 'vottdev' 9 | PROD_STORAGE_ACCOUNT: 'vott' 10 | DEV_URL: "https://vottdev.z5.web.core.windows.net/" 11 | PROD_URL: "https://vott.z22.web.core.windows.net/" 12 | 13 | stages: 14 | - stage: version_bump_commit 15 | jobs: 16 | - template: templates/npm-version-bump.yml 17 | parameters: 18 | versionType: $(NPM_VERSION_TYPE) 19 | 20 | - stage: github_release 21 | dependsOn: version_bump_commit 22 | jobs: 23 | - template: templates/create-github-release.yml 24 | parameters: 25 | GitHubConnection: 'GitHub connection' # defaults for any parameters that aren't specified 26 | repositoryName: '$(Build.Repository.Name)' # microsoft/VoTT 27 | isPreRelease: $(IS_PRERELEASE) # set when queuing build 28 | isDraft: $(IS_DRAFT) 29 | 30 | - stage: web_release 31 | dependsOn: version_bump_commit 32 | jobs: 33 | - template: templates/create-web-release.yml 34 | -------------------------------------------------------------------------------- /azure-pipelines/linux/continuous-build-linux.yml: -------------------------------------------------------------------------------- 1 | # Node.js with React 2 | # Build a Node.js project that uses React. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | steps: 6 | - bash: | 7 | set -e 8 | 9 | sudo apt-get update 10 | sudo apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 libgconf-2-4 dbus xvfb libgtk-3-0 11 | sudo cp azure-pipelines/linux/xvfb.init /etc/init.d/xvfb 12 | sudo chmod +x /etc/init.d/xvfb 13 | sudo update-rc.d xvfb defaults 14 | sudo service xvfb start 15 | displayName: 'Install dependencies' 16 | 17 | - task: NodeTool@0 18 | inputs: 19 | versionSpec: '10.x' 20 | displayName: 'Install Node.js' 21 | 22 | - bash: | 23 | set -e 24 | 25 | export DISPLAY=:99.0 26 | 27 | npm ci # do a clean install 28 | npm run compile 29 | npm run test:coverage # run tests and coverage 30 | displayName: 'Run tests and coverage' 31 | 32 | - bash: | 33 | set -e 34 | 35 | token=$(CODECOV_TOKEN) 36 | if [[ -z "${token}" ]]; then 37 | echo "Need to set CODECOV_TOKEN" 38 | exit 1 39 | fi 40 | 41 | # https://docs.codecov.io/docs/about-the-codecov-bash-uploader 42 | bash <(curl -s https://codecov.io/bash) -t ${token} 43 | displayName: 'Upload coverage report' 44 | -------------------------------------------------------------------------------- /azure-pipelines/linux/xvfb.init: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # /etc/rc.d/init.d/xvfbd 4 | # 5 | # chkconfig: 345 95 28 6 | # description: Starts/Stops X Virtual Framebuffer server 7 | # processname: Xvfb 8 | # 9 | ### BEGIN INIT INFO 10 | # Provides: xvfb 11 | # Required-Start: $remote_fs $syslog 12 | # Required-Stop: $remote_fs $syslog 13 | # Default-Start: 2 3 4 5 14 | # Default-Stop: 0 1 6 15 | # Short-Description: Start xvfb at boot time 16 | # Description: Enable xvfb provided by daemon. 17 | ### END INIT INFO 18 | 19 | [ "${NETWORKING}" = "no" ] && exit 0 20 | 21 | PROG="/usr/bin/Xvfb" 22 | PROG_OPTIONS=":10 -ac" 23 | PROG_OUTPUT="/tmp/Xvfb.out" 24 | 25 | case "$1" in 26 | start) 27 | echo "Starting : X Virtual Frame Buffer " 28 | $PROG $PROG_OPTIONS>>$PROG_OUTPUT 2>&1 & 29 | disown -ar 30 | ;; 31 | stop) 32 | echo "Shutting down : X Virtual Frame Buffer" 33 | killproc $PROG 34 | RETVAL=$? 35 | [ $RETVAL -eq 0 ] && /bin/rm -f /var/lock/subsys/Xvfb 36 | /var/run/Xvfb.pid 37 | echo 38 | ;; 39 | restart|reload) 40 | $0 stop 41 | $0 start 42 | RETVAL=$? 43 | ;; 44 | status) 45 | status Xvfb 46 | RETVAL=$? 47 | ;; 48 | *) 49 | echo $"Usage: $0 (start|stop|restart|reload|status)" 50 | exit 1 51 | esac 52 | 53 | exit $RETVAL -------------------------------------------------------------------------------- /azure-pipelines/mac/continuous-build-mac.yml: -------------------------------------------------------------------------------- 1 | # Node.js with React 2 | # Build a Node.js project that uses React. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | steps: 6 | - task: NodeTool@0 7 | inputs: 8 | versionSpec: '10.x' 9 | displayName: 'Install Node.js' 10 | 11 | - bash: | 12 | set -e 13 | 14 | export DISPLAY=:99.0 15 | 16 | npm ci # do a clean install 17 | npm run compile 18 | npm run test:ci # don't watch and just run all the tests 19 | displayName: 'Run tests' 20 | -------------------------------------------------------------------------------- /azure-pipelines/templates/build-artifact.yml: -------------------------------------------------------------------------------- 1 | # https://docs.microsoft.com/en-us/azure/devops/pipelines/process/templates?view=azure-devops#job-templates-with-parameters 2 | jobs: 3 | - job: ${{ parameters.name }} 4 | pool: ${{ parameters.pool }} 5 | timeoutInMinutes: 15 # how long to run the job before automatically cancelling 6 | steps: 7 | - template: git-pull-current-branch.yml 8 | 9 | - task: NodeTool@0 10 | displayName: 'Use Node 10.x' 11 | inputs: 12 | versionSpec: 10.x 13 | 14 | - bash: | 15 | set -ex 16 | 17 | # clean install 18 | npm ci 19 | npm run release-ci 20 | 21 | OS=${{ parameters.os }} 22 | ARTIFACT_NAME=${{ parameters.artifact }} 23 | 24 | mkdir -p ${OS} 25 | cp releases/${ARTIFACT_NAME} ${OS}/ 26 | 27 | displayName: Build 28 | 29 | - publish: $(System.DefaultWorkingDirectory)/${{ parameters.os }} 30 | artifact: ${{ parameters.os }} 31 | -------------------------------------------------------------------------------- /azure-pipelines/templates/create-web-release.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - job: "Web_Release" 3 | pool: 4 | vmImage: 'windows-latest' 5 | 6 | steps: 7 | - template: git-pull-current-branch.yml 8 | 9 | - task: NodeTool@0 10 | displayName: 'Use Node 10.x' 11 | inputs: 12 | versionSpec: 10.x 13 | 14 | - task: Npm@1 15 | displayName: 'npm ci' 16 | inputs: 17 | command: custom 18 | workingDir: . 19 | verbose: false 20 | customCommand: ci 21 | 22 | - task: Bash@3 23 | displayName: 'Create artifact' 24 | inputs: 25 | targetType: filePath 26 | filePath: './scripts/generate-web-artifact.sh' 27 | 28 | - task: AzureFileCopy@3 29 | displayName: 'AzureBlob File Copy to $DEV_STORAGE_ACCOUNT' 30 | inputs: 31 | SourcePath: './build' 32 | azureSubscription: $(azureSubscription) 33 | destination: azureBlob 34 | storage: $(DEV_STORAGE_ACCOUNT) 35 | containerName: '$web' 36 | 37 | - task: AzureFileCopy@3 38 | displayName: 'AzureBlob File Copy to $PROD_STORAGE_ACCOUNT' 39 | inputs: 40 | SourcePath: './build' 41 | azureSubscription: $(azureSubscription) 42 | destination: azureBlob 43 | storage: $(PROD_STORAGE_ACCOUNT) 44 | containerName: '$web' 45 | condition: eq(variables['Build.SourceBranch'], 'refs/heads/master') 46 | 47 | - bash: | 48 | echo 49 | echo "Dev url: $DEV_URL" 50 | displayName: "Dev URL" 51 | 52 | - bash: | 53 | echo 54 | echo "Prod url: $PROD_URL" 55 | displayName: "Prod URL" 56 | condition: eq(variables['Build.SourceBranch'], 'refs/heads/master') 57 | -------------------------------------------------------------------------------- /azure-pipelines/templates/git-pull-current-branch.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: Bash@3 3 | displayName: 'Pull latest from branch' 4 | inputs: 5 | targetType: filePath 6 | filePath: './scripts/git-pull-current-branch.sh' 7 | env: 8 | SOURCE_BRANCH: $(Build.SourceBranch) 9 | -------------------------------------------------------------------------------- /azure-pipelines/templates/npm-version-bump.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | versionType: '' 3 | 4 | jobs: 5 | - job: "version_bump" 6 | 7 | variables: 8 | - group: GitHub-Deploy-Creds 9 | 10 | timeoutInMinutes: 30 # timeout on job if deploy is not completed in 30 minutes 11 | cancelTimeoutInMinutes: 1 # time limit to wait for job to cancel 12 | 13 | pool: 14 | vmImage: macOS-latest # ssh key was generated on a Mac so using the same type of OS here 15 | 16 | steps: 17 | - task: NodeTool@0 18 | inputs: 19 | versionSpec: 10.x 20 | displayName: 'Install Node.js' 21 | 22 | # Download secure file 23 | # Download a secure file to the agent machine 24 | - task: DownloadSecureFile@1 25 | # name: sshKey # The name with which to reference the secure file's path on the agent, like $(mySecureFile.secureFilePath) 26 | inputs: 27 | secureFile: vott_id_rsa 28 | 29 | # Install an SSH key prior to a build or deployment 30 | - task: InstallSSHKey@0 # https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/install-ssh-key?view=azure-devops 31 | inputs: 32 | knownHostsEntry: $(KNOWN_HOSTS_ENTRY) 33 | sshPublicKey: $(SSH_PUBLIC_KEY) 34 | #sshPassphrase: # Optional 35 | sshKeySecureFile: vott_id_rsa 36 | env: 37 | KNOWN_HOSTS_ENTRY: $(KNOWN_HOSTS_ENTRY) 38 | SSH_PUBLIC_KEY: $(SSH_PUBLIC_KEY) # map to the right format (camelCase) that Azure credentials understand 39 | 40 | - task: Bash@3 41 | name: BumpNpmVersion 42 | displayName: Bump NPM Version 43 | inputs: 44 | targetType: filePath 45 | filePath: ./scripts/version-bump-commit.sh 46 | arguments: 47 | ${{ parameters.versionType }} 48 | env: 49 | SOURCE_BRANCH: $(Build.SourceBranch) 50 | 51 | - bash: | 52 | printenv | sort 53 | mkdir -p $(System.DefaultWorkingDirectory)/variables 54 | echo "$NEXT_VERSION" > $(System.DefaultWorkingDirectory)/variables/NEXT_VERSION 55 | echo "$CURRENT_VERSION" > $(System.DefaultWorkingDirectory)/variables/CURRENT_VERSION 56 | displayName: "Prep variables for publishing" 57 | 58 | # Publish the variables folder as pipeline artifact 59 | - publish: $(System.DefaultWorkingDirectory)/variables 60 | artifact: variables 61 | -------------------------------------------------------------------------------- /azure-pipelines/upload-plato-report.yml: -------------------------------------------------------------------------------- 1 | name: $(Date:yyyyMMdd).$(Hours)$(Minutes)$(Seconds) 2 | variables: 3 | AZURE_STORAGE_ACCOUNT: vottv2 4 | azureSubscription: 'pj-little-sub' 5 | REPORT_URL: 'https://vottv2.z22.web.core.windows.net/' 6 | 7 | trigger: 8 | - dev* 9 | - master 10 | 11 | pr: none # disable CI build for PR 12 | 13 | pool: 14 | vmImage: 'Ubuntu-16.04' 15 | 16 | steps: 17 | - bash: | 18 | set -e 19 | 20 | COMMIT_SHA=`echo ${BUILD_SOURCEVERSION} | cut -c1-8` 21 | echo "sha: " $COMMIT_SHA 22 | 23 | # rewrite build number 24 | echo "##vso[build.updatebuildnumber]Report-${COMMIT_SHA}-${BUILD_BUILDNUMBER}" 25 | displayName: "Rewrite build number" 26 | 27 | - bash: | 28 | set -e 29 | 30 | ACCOUNT=$(AZURE_STORAGE_ACCOUNT) 31 | if [[ -z "${ACCOUNT}" ]]; then 32 | echo "Need to set AZURE_STORAGE_ACCOUNT" 33 | exit 1 34 | fi 35 | displayName: "Verify storage account cred exists" 36 | 37 | - task: NodeTool@0 38 | displayName: "Use Node 10.x" 39 | inputs: 40 | versionSpec: 10.x 41 | 42 | - task: Npm@1 43 | displayName: 'Run `npm ci`' 44 | inputs: 45 | command: custom 46 | verbose: false 47 | customCommand: ci 48 | 49 | - task: AzureCLI@1 50 | displayName: "Pull down old report and add updates" 51 | inputs: 52 | azureSubscription: $(azureSubscription) 53 | scriptLocation: inlineScript 54 | inlineScript: './scripts/update-report.sh' 55 | 56 | - bash: | 57 | set -e 58 | 59 | cat /tmp/download.log 60 | displayName: "print download log" 61 | condition: succeededOrFailed() 62 | 63 | - task: AzureFileCopy@3 64 | displayName: 'AzureBlob File Copy to $(AZURE_STORAGE_ACCOUNT)' 65 | inputs: 66 | SourcePath: './report' 67 | azureSubscription: $(azureSubscription) 68 | destination: azureBlob 69 | storage: $(AZURE_STORAGE_ACCOUNT) 70 | containerName: '$web' 71 | 72 | - bash: | 73 | echo "See report: $(REPORT_URL) " 74 | displayName: "Report URl" 75 | -------------------------------------------------------------------------------- /azure-pipelines/windows/continuous-build-windows.yml: -------------------------------------------------------------------------------- 1 | # Node.js with React 2 | # Build a Node.js project that uses React. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | steps: 6 | - task: NodeTool@0 7 | inputs: 8 | versionSpec: '10.x' 9 | displayName: 'Install Node.js' 10 | 11 | - bash: | 12 | set -e 13 | 14 | export DISPLAY=:99.0 15 | 16 | npm ci # do a clean install 17 | npm run compile 18 | 19 | npm run test:ci # don't watch and just run all the tests 20 | displayName: 'Run tests' 21 | -------------------------------------------------------------------------------- /cocoSSDModel/group1-shard1of5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/cocoSSDModel/group1-shard1of5 -------------------------------------------------------------------------------- /cocoSSDModel/group1-shard2of5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/cocoSSDModel/group1-shard2of5 -------------------------------------------------------------------------------- /cocoSSDModel/group1-shard3of5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/cocoSSDModel/group1-shard3of5 -------------------------------------------------------------------------------- /cocoSSDModel/group1-shard4of5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/cocoSSDModel/group1-shard4of5 -------------------------------------------------------------------------------- /cocoSSDModel/group1-shard5of5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/cocoSSDModel/group1-shard5of5 -------------------------------------------------------------------------------- /config/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | node: { 5 | __dirname: false, 6 | }, 7 | target: "electron-main", 8 | entry: "./src/electron/main.ts", 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.ts?$/, 13 | use: [{ 14 | loader: "ts-loader", 15 | options: { 16 | compilerOptions: { 17 | noEmit: false 18 | } 19 | } 20 | }], 21 | exclude: /node_modules/ 22 | } 23 | ] 24 | }, 25 | resolve: { 26 | extensions: [".ts", ".js"] 27 | }, 28 | output: { 29 | filename: "main.js", 30 | path: path.resolve(__dirname, "../build") 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /config/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const common = require('./webpack.common.js') 3 | 4 | module.exports = merge(common, { 5 | mode: 'development', 6 | devtool: "inline-source-map", 7 | }) 8 | -------------------------------------------------------------------------------- /config/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const common = require('./webpack.common.js') 3 | 4 | module.exports = merge(common, { 5 | mode: 'production', 6 | devtool: 'cheap-module-source-map', 7 | }) -------------------------------------------------------------------------------- /docs/CI.md: -------------------------------------------------------------------------------- 1 | # Overview of CI setup 2 | 3 | ## Issues found and solutions 4 | 5 | ### Environment variables in scripts sections is not cross-platform friendly 6 | 7 | 1. Option 1 8 | * install `cross-env` npm package 9 | * use `cross-env` in npm script 10 | * https://github.com/facebook/create-react-app/issues/1137#issuecomment-279180815 11 | 12 | 1. Option 2 13 | * add `--coverage` flag so test will run only once without needing to set `CI=true` 14 | * no need for extra package, cross platform support 15 | * https://github.com/facebook/create-react-app/issues/1137#issuecomment-279191193 16 | 17 | ### Logs are cut off on Windows agent when using shell 18 | 19 | 1. make sure to use Bash task and not shell -------------------------------------------------------------------------------- /docs/DEBUG.md: -------------------------------------------------------------------------------- 1 | # Debugging Guide 2 | ## Electron Process 3 | TODO: create a vscode launch.json configuration that attaches to the elctron process. https://stackoverflow.com/a/41073851 4 | 5 | ## Renderer Process 6 | ### Chrome Debugger 7 | 1. Start app: `npm run start`. 8 | 2. Open Chrome Dev Tools: `F12` in browser, `Ctrl + Shift + I` in electron. 9 | 3. Open `Sources` tab. 10 | 4. `Ctrl + Shift + F` and search for the file/function/code you would like to break into. 11 | 5. Place breakpoint. 12 | 6. Trigger breakpoint. 13 | > Note: Reloading the page may be required if the code executes at startup. 14 | 15 | ### VSCode Debugger 16 | 1. Download & install the `Debugger for Chrome` VSCode plugin. 17 | 2. Start app: `npm run start`. 18 | 3. `F5` 19 | 4. Select `Chrome` 20 | 5. Change `http://localhost:8080` to `http://localhost:3000` 21 | 6. `F5` 22 | 7. Chrome should startup pointed at the app running at `http://localhost:3000`, and breakpoints in VSCode should work as expected. 23 | 24 | ## Unit Tests 25 | ### Happy Path 26 | 1. Download & install the `Jest` VSCode plugin. 27 | 2. Open the test's source file. 28 | 3. A `Debug` button should appear above every test. Place breakpoints, and click debug. 29 | > Note: If this button does not appear, here are some troubleshooting steps... 30 | > 1. `Ctrl + Shift + P` and select `Jest: Start Runner`. 31 | > 2. Restarting VSCode. 32 | 33 | ### Alternate Proof 34 | 1. Download & install the `Jest` VSCode plugin. 35 | 2. Open the test's source file. 36 | 3. Place breakpoints. 37 | 4. Go to the Debug view, select the `Jest All` configuration. 38 | 5. `F5` or press the green play button. 39 | 6. Your breakpoint should now be hit. 40 | -------------------------------------------------------------------------------- /docs/PACKAGING.md: -------------------------------------------------------------------------------- 1 | # Order of Operation 2 | 3 | ## Command 4 | 5 | ```bash 6 | npm run release 7 | ``` 8 | 9 | ### Underneath the hood 10 | 11 | 1. Create the `react` bundle inside the `build` directory, 12 | * have to run this first because it will override everything in the `build` directory; 13 | * create `index.html` & `static/` 14 | 15 | ```bash 16 | react-scripts build 17 | ``` 18 | 19 | 1. Create `build/electron` directory and the entry point file `bundle.js` 20 | 21 | ```bash 22 | webpack -p --config ./config/webpack.dev.config.js 23 | ``` 24 | 25 | 1. Now you can build the os-specific executable 26 | ```bash 27 | electron-builder 28 | ``` 29 | 30 | ### Relevant files 31 | 32 | 1. `.env` 33 | * environment variables use by `react-script` to generate the correct content for `build/index.html` 34 | 35 | 1. `electron-builder.yml` 36 | * configuration for electron-builder 37 | 38 | 1. `package.json` 39 | * dependencies and scripts 40 | -------------------------------------------------------------------------------- /docs/PLATO.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | We're using [es6-plato](https://github.com/the-simian/es6-plato) to analyze code complexity. VoTT first has to be transpiled to ES6, as es6-plato won't work direcly on TypeScript. 4 | 5 | We're also using the [eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react) eslint plugin for React-specific linting rules. 6 | 7 | ## Coverage Reports 8 | 9 | The [VoTT coverage report](https://vottv2.z5.web.core.windows.net/) is updated on each merged PR, to reflect complexity analysis over time. 10 | 11 | ### V1 12 | 13 | For posterity, a copy of the [V1 coverage report](https://vottv1.z5.web.core.windows.net/) is also available. This report represents a snapshot of the v1 codebase (v1 branch) and can be generated by running: 14 | 15 | ```bash 16 | git checkout v1 17 | npm i plato --no-save 18 | plato -r -d report -t 'VoTT v1 Complexity Analysis' -x 'public/js|test_|main.js' src 19 | ``` 20 | 21 | ## Local development 22 | 23 | 1. The following command will generate a `report` directory. 24 | 25 | ```bash 26 | npm run plato 27 | ``` 28 | 29 | 2. Open `report/index.html` in your browser to see the report locally. 30 | 31 | ## Debugging 32 | 33 | 1. If `npm run plato` is not working for you, try running the script manually: 34 | 35 | * Make sure you have `typescript` & `es6-plato` installed locally: 36 | 37 | ```bash 38 | npm install -g typescript 39 | npm install -g es6-plato 40 | ``` 41 | 42 | * Run following command from the root of the repository: 43 | 44 | ```bash 45 | ./scripts/generate-report.sh -o report -v $(node -pe "require('./package.json').version") -c $(git rev-parse --short HEAD) 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/REACTAPP.md: -------------------------------------------------------------------------------- 1 | ## Available Scripts 2 | 3 | In the project directory, you can run: 4 | 5 | ### `npm start` 6 | 7 | Runs the app in the development mode.
8 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 9 | 10 | The page will reload if you make edits.
11 | You will also see any lint errors in the console. 12 | 13 | ### `npm test` 14 | 15 | Launches the test runner in the interactive watch mode.
16 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 17 | 18 | ### `npm run build` 19 | 20 | Builds the app for production to the `build` folder.
21 | It correctly bundles React in production mode and optimizes the build for the best performance. 22 | 23 | The build is minified and the filenames include the hashes.
24 | Your app is ready to be deployed! 25 | 26 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 27 | 28 | ### `npm run eject` 29 | 30 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 31 | 32 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 33 | 34 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 35 | 36 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 37 | 38 | ## Learn More 39 | 40 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 41 | 42 | To learn React, check out the [React documentation](https://reactjs.org/). -------------------------------------------------------------------------------- /docs/RELEASE_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | [![Build Status](https://dev.azure.com/msft-vott/VoTT/_apis/build/status/VoTT/Create%20Release?branchName=master)](https://dev.azure.com/msft-vott/VoTT/_build/latest?definitionId=55&branchName=master) 4 | 5 | Instruction on how to create new GitHub & Web Releases. 6 | 7 | ## Release Process 8 | 9 | ![alt text](./images/release-process.png "Create Release Process") 10 | 11 | ### AzDO Tasks 12 | 13 | [GitHub Release Task](https://github.com/microsoft/azure-pipelines-tasks/tree/master/Tasks/GitHubReleaseV1) 14 | 15 | [Azure File Copy](https://github.com/microsoft/azure-pipelines-tasks/tree/master/Tasks/AzureFileCopyV3) 16 | 17 | ## Versioning 18 | 19 | Follow [NPM Semantic Versioning](https://docs.npmjs.com/about-semantic-versioning#incrementing-semantic-versions-in-published-packages) 20 | 21 | | Code status | Stage | Rule | Example version | 22 | | ----------------------------------------- | ------------- | ------------------------------------------------------------------ | --------------- | 23 | | First release | New product | Start with 1.0.0 | 1.0.0 | 24 | | Backward compatible bug fixes | Patch release | Increment the third digit | 1.0.1 | 25 | | Backward compatible new features | Minor release | Increment the middle digit and reset last digit to zero | 1.1.0 | 26 | | Changes that break backward compatibility | Major release | Increment the first digit and reset middle and last digits to zero | 2.0.0 | 27 | 28 | ### Commands 29 | 30 | The pipeline use [npm-version](https://docs.npmjs.com/cli/version) to update version 31 | 32 | #### Pre 33 | 34 | All version with `pre`, ie. `preminor` will bump the appropriate didgit & append `-0` to the new version 35 | 36 | Examples: 37 | 38 | `npm version prepatch` 39 | 40 | 1. v2.3.0 --> v2.3.1-0 41 | 1. v2.3.1-0 --> v2.3.2-0 42 | 43 | `npm version preminor` 44 | 45 | 1. v2.3.0 --> v2.4.0-0 46 | 1. v2.4.0-0 --> v2.5.0-0 47 | 48 | ##### Exception 49 | 50 | `prerelease` behave similar to prepatch, but would increment the last digit. 51 | 52 | Examples: 53 | 54 | `npm version prerelease` 55 | 56 | v2.3.0 --> v2.3.1-0 57 | 58 | v2.3.0-0 --> v2.3.0-1 59 | 60 | #### Major 61 | 62 | v2.x.x --> v3.0.0 63 | 64 | #### Minor 65 | 66 | v2.2.0 --> v2.3.0 67 | -------------------------------------------------------------------------------- /docs/STYLE.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | We're using [EditorConfig](https://editorconfig.org/) to help maintain consistent coding styles 4 | and also avoid unneccessary changes due to space/tab configuration. 5 | 6 | ## How to use 7 | 8 | Some editors required installing additional plugin to work while others don't. 9 | 10 | * Find your editor [here](https://editorconfig.org/). 11 | 12 | ### Editors that require plugins 13 | 14 | * VsCode 15 | * Notepad 16 | * Sublime Text 17 | -------------------------------------------------------------------------------- /docs/images/export-labels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/docs/images/export-labels.jpg -------------------------------------------------------------------------------- /docs/images/label-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/docs/images/label-image.jpg -------------------------------------------------------------------------------- /docs/images/ml-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/docs/images/ml-workflow.png -------------------------------------------------------------------------------- /docs/images/new-connection.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/docs/images/new-connection.jpg -------------------------------------------------------------------------------- /docs/images/new-project.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/docs/images/new-project.jpg -------------------------------------------------------------------------------- /docs/images/project-settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/docs/images/project-settings.jpg -------------------------------------------------------------------------------- /docs/images/release-process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/docs/images/release-process.png -------------------------------------------------------------------------------- /docs/images/reorder-tag.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/docs/images/reorder-tag.jpg -------------------------------------------------------------------------------- /docs/images/security-tokens.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/docs/images/security-tokens.jpg -------------------------------------------------------------------------------- /docs/images/video-player.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/docs/images/video-player.jpg -------------------------------------------------------------------------------- /docs/wireframes.bmpr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/docs/wireframes.bmpr -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | # only use if package.json doesn't contain a "build" 2 | 3 | directories: 4 | output: releases 5 | buildResources: app-icons # this is where app-icons is store 6 | appId: com.microsoft.vott 7 | artifactName: '${productName}-${version}-${platform}.${ext}' 8 | extends: null # need this otherwise it won't use the entry point we set in "main" in package.json 9 | files: 10 | - filter: 11 | - build/ # copy this directory to the asar directory that electron-builder use to look for the main entry file 12 | mac: 13 | icon: app-icons/icon.icns 14 | target: dmg 15 | identity: null # don't sign the app 16 | win: 17 | icon: app-icons/icon.ico 18 | linux: 19 | target: 20 | - snap 21 | publish: null 22 | electronVersion: 3.0.13 23 | extraFiles: 24 | - "cocoSSDModel" 25 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | Visual Object Tagging Tool (VoTT) v%REACT_APP_VERSION% 23 | 24 | 25 | 28 |
29 |
VoTT v%REACT_APP_VERSION%, commit=%REACT_APP_COMMIT_SHA%
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "VoTT", 3 | "name": "Visual Object Tagging Tool", 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/tags.svg: -------------------------------------------------------------------------------- 1 | > -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # NOTE: this script should be ran from the root of the repository; the CWD should reflect this 5 | VERSION=$(node -pe "require('./package.json').version") 6 | COMMIT_SHA=$(git rev-parse --short HEAD) 7 | NPM_BIN_DIR=$(npm bin) 8 | 9 | echo "cwd=$(pwd)" 10 | echo "version=${VERSION}" 11 | echo "commit=${COMMIT_SHA}" 12 | 13 | export REACT_APP_VERSION=${VERSION} 14 | export REACT_APP_COMMIT_SHA=${COMMIT_SHA} 15 | 16 | npx react-scripts build 17 | npx webpack -p --config ./config/webpack.prod.js 18 | npx electron-builder 19 | -------------------------------------------------------------------------------- /scripts/complexity-analysis.js: -------------------------------------------------------------------------------- 1 | 2 | const plato = require('es6-plato'); 3 | 4 | // parse command line args 5 | const optionDefinitions = [ 6 | { name: 'src', type: String, defaultValue: './es6-src/**' }, 7 | { name: 'output', type: String, defaultValue: './report' }, 8 | { name: 'version', type: String, defaultValue: '2.0.0' }, 9 | { name: 'commit', type: String, defaultValue: '' } 10 | ] 11 | const commandLineArgs = require('command-line-args'); 12 | const options = commandLineArgs(optionDefinitions) 13 | 14 | // close(ish) eslint config for ES6 + React 15 | let lintRules = { 16 | extends: [ 17 | 'eslint:recommended', 18 | 'plugin:react/recommended' 19 | ], 20 | plugins: [ 21 | 'react' 22 | ], 23 | env: { 24 | es6: true, 25 | browser: true, 26 | serviceworker: true 27 | }, 28 | parserOptions: { 29 | 'ecmaVersion': 6, 30 | 'sourceType': 'module', 31 | 'ecmaFeatures': { 32 | 'jsx': true 33 | } 34 | }, 35 | rules: { 36 | quotes: [2, 'double'] 37 | } 38 | }; 39 | 40 | // exclude all tests, toolbar/mockFactory and localization 41 | let exclude = /\.test|registerToolbar|en-us|es-cl|mockFactory/; 42 | let complexityRules = {}; 43 | 44 | let platoArgs = { 45 | title: `VoTT Complexity Analysis
v${options.version}
commit: ${options.commit}`, 46 | exclude: exclude, 47 | eslint: lintRules, 48 | complexity: complexityRules 49 | }; 50 | 51 | console.info(`Running complexity analysis on \`${options.src}\`, writing results to \`${options.output}\`...`); 52 | plato.inspect(options.src, options.output, platoArgs); 53 | -------------------------------------------------------------------------------- /scripts/generate-changelog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # NOTE: To generate a changlelog, a git revision range is required. This can be commit SHAs, 5 | # but for all links in the template to work, tags are expected. The CWD should be set to the 6 | # root of the repository. 7 | echo "cwd=$(pwd)" 8 | 9 | PARAMS="" 10 | while (( "$#" )); do 11 | case "$1" in 12 | -t|--to) 13 | TO_COMMIT=$2 14 | shift 2 15 | ;; 16 | -f|--from) 17 | FROM_COMMIT=$2 18 | shift 2 19 | ;; 20 | --) # end argument parsing 21 | shift 22 | break 23 | ;; 24 | -*|--*=) # unsupported flags 25 | echo "Error: Unsupported flag $1" >&2 26 | exit 1 27 | ;; 28 | *) # preserve positional arguments 29 | PARAMS="$PARAMS $1" 30 | shift 31 | ;; 32 | esac 33 | done 34 | # set positional arguments in their proper place 35 | eval set -- "$PARAMS" 36 | 37 | BASE_GITHUB_URL=https://github.com/Microsoft/VoTT 38 | RELEASE_DATE=$(date +"%m-%d-%Y") 39 | TEMPLATE="# [${TO_COMMIT}](${BASE_GITHUB_URL}/compare/${FROM_COMMIT}...${TO_COMMIT}) (${RELEASE_DATE})\n[GitHub Release](${BASE_GITHUB_URL}/releases/tag/${TO_COMMIT})\n\n" 40 | CL_START='' 41 | 42 | # Grab all non-merge commits (from PRs). Current PR policy is squash and merge, 43 | # using the fast-forward option, so merge commits *shouldn't* be present in the commit history. 44 | COMMITS=$(git log --pretty=%s --no-merges ${FROM_COMMIT}..${TO_COMMIT}) 45 | 46 | echo "Generating changlog from ${FROM_COMMIT} to ${TO_COMMIT}..." 47 | while read -r line; 48 | do 49 | echo "${line}" 50 | TEMPLATE="${TEMPLATE}- ${line}\n" 51 | done <<< "${COMMITS}" 52 | 53 | # Attemped to use `sed` here, but between new lines and escape characters, 54 | # quickly became untenable. Python, perl and a couple other solutions come to mind, 55 | # but npm/JS are very xplat friendly and we're already using that tooling. 56 | # sed -i -e "s/${CL_START}/${CL_START}\n${TEMPLATE}/" CHANGELOG.md 57 | 58 | npm install replace-in-file --no-save 59 | ./node_modules/.bin/replace-in-file "${CL_START}" "$(echo -e ${CL_START}\\n\\n${TEMPLATE})" CHANGELOG.md 60 | -------------------------------------------------------------------------------- /scripts/generate-report.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # -e: immediately exit if any command has a non-zero exit status 5 | # -o: prevents errors in a pipeline from being masked 6 | 7 | BASEDIR=$(dirname "$0") 8 | ES6_SRC=$(pwd)/es6-src 9 | 10 | PARAMS="" 11 | while (( "$#" )); do 12 | case "$1" in 13 | -o|--output) 14 | REPORT_DIR=$2 15 | shift 2 16 | ;; 17 | -v|--version) 18 | VERSION=$2 19 | shift 2 20 | ;; 21 | -c|--commit) 22 | COMMIT_SHA=$2 23 | shift 2 24 | ;; 25 | --) # end argument parsing 26 | shift 27 | break 28 | ;; 29 | -*|--*=) # unsupported flags 30 | echo "Error: Unsupported flag $1" >&2 31 | exit 1 32 | ;; 33 | *) # preserve positional arguments 34 | PARAMS="$PARAMS $1" 35 | shift 36 | ;; 37 | esac 38 | done 39 | # set positional arguments in their proper place 40 | eval set -- "$PARAMS" 41 | 42 | echo "cwd=$(pwd)" 43 | echo "basedir=${BASEDIR}" 44 | echo "source=${ES6_SRC}" 45 | echo "output=${REPORT_DIR}" 46 | echo "version=${VERSION}" 47 | echo "commit=${COMMIT_SHA}" 48 | 49 | # these are just needed for the reports; just install ad-hoc and don't save to package.json 50 | npm install es6-plato eslint-plugin-react command-line-args --no-save 51 | 52 | echo 53 | echo "------> Finish installing dependencies" 54 | 55 | echo 56 | echo "------> Transpile TS to ES6" 57 | # we can't do complexity analysis on TypeScript directly; transpile to ES6 58 | rm -rf ${ES6_SRC} 59 | tsc --noEmit false --outDir ${ES6_SRC} 60 | 61 | echo 62 | echo "------> Running complexity analasis ..." 63 | node ${BASEDIR}/complexity-analysis.js --src ${ES6_SRC} --output ${REPORT_DIR} --version ${VERSION} --commit ${COMMIT_SHA} 64 | -------------------------------------------------------------------------------- /scripts/generate-web-artifact.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # NOTE: this script should be ran from the root of the repository; the CWD should reflect this 5 | VERSION=$(node -pe "require('./package.json').version") 6 | COMMIT_SHA=$(git rev-parse --short HEAD) 7 | 8 | echo "cwd=$(pwd)" 9 | echo "version=${VERSION}" 10 | echo "commit=${COMMIT_SHA}" 11 | 12 | # use by web pack 13 | export REACT_APP_VERSION=${VERSION} 14 | export REACT_APP_COMMIT_SHA=${COMMIT_SHA} 15 | 16 | # npm install will be in a standalone task 17 | npm run release-web 18 | -------------------------------------------------------------------------------- /scripts/git-pull-current-branch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Get full branch name excluding refs/head from the env var SOURCE_BRANCH 5 | SOURCE_BRANCH_NAME=${SOURCE_BRANCH/refs\/heads\/} 6 | 7 | echo "SOURCE_BRANCH: ${SOURCE_BRANCH_NAME}" 8 | git pull origin ${SOURCE_BRANCH_NAME} 9 | git checkout ${SOURCE_BRANCH_NAME} 10 | echo "Checked out branch: ${SOURCE_BRANCH_NAME}" 11 | -------------------------------------------------------------------------------- /scripts/release-pr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | PARAMS="" 5 | while (( "$#" )); do 6 | case "$1" in 7 | -p|--previous) 8 | PREVIOUS_VERSION=$2 9 | shift 2 10 | ;; 11 | -n|--new) 12 | NEW_VERSION=$2 13 | shift 2 14 | ;; 15 | --) # end argument parsing 16 | shift 17 | break 18 | ;; 19 | -*|--*=) # unsupported flags 20 | echo "Error: Unsupported flag $1" >&2 21 | exit 1 22 | ;; 23 | *) # preserve positional arguments 24 | PARAMS="$PARAMS $1" 25 | shift 26 | ;; 27 | esac 28 | done 29 | # set positional arguments in their proper place 30 | eval set -- "$PARAMS" 31 | 32 | BASEDIR=$(dirname "$0") 33 | PROMPT=$(echo -e "This will create changes to open a release PR for VoTT v${NEW_VERSION}?\nNOTE: a clean working git directory is required.\nDo you want to continue? [Y/n] ") 34 | RELEASE_BRANCH=release-${NEW_VERSION} 35 | 36 | read -p "${PROMPT}" -r 37 | echo 38 | if [[ $REPLY =~ ^[Yy]$ ]] 39 | then 40 | echo "cwd=$(pwd)" 41 | echo "basedir=${BASEDIR}" 42 | echo "version=${NEW_VERSION}" 43 | 44 | # get the latest from v2, create a release branch 45 | git checkout master 46 | git pull 47 | git checkout -b ${RELEASE_BRANCH} 48 | echo "Creating local tag ${NEW_VERSION}" 49 | git tag -a ${NEW_VERSION} -m "VoTT v${NEW_VERSION}" 50 | # update package.json version and the changelog 51 | npm install json --no-save 52 | # NOTE: at some point, we need to move to `npm version` and do all of this via build system 53 | ./node_modules/.bin/json -I -f package.json -4 -e "this.version=\"${NEW_VERSION}\"" 54 | ./node_modules/.bin/json -I -f package-lock.json -4 -e "this.version=\"${NEW_VERSION}\"" 55 | ${BASEDIR}/generate-changelog.sh --from ${PREVIOUS_VERSION} --to ${NEW_VERSION} 56 | git commit -am "ci: update package version and changelog for ${NEW_VERSION} release" 57 | git push -u origin ${RELEASE_BRANCH} 58 | # remove the local tag, used for the changelog 59 | echo "Deleting local tag ${NEW_VERSION}" 60 | git tag -d ${NEW_VERSION} 61 | fi 62 | -------------------------------------------------------------------------------- /scripts/update-report.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | # NOTE: this script should be ran from the root of the repository; the CWD should reflect this 5 | BASEDIR=$(dirname "$0") 6 | REPORT_DIR=$(pwd)/report 7 | VERSION=$(node -pe "require('./package.json').version") 8 | COMMIT_SHA=$(git rev-parse --short HEAD) 9 | 10 | echo "cwd=$(pwd)" 11 | echo "basedir=${BASEDIR}" 12 | echo "reportdir=${REPORT_DIR}" 13 | echo "version=${VERSION}" 14 | echo "commit=${COMMIT_SHA}" 15 | 16 | # This script appends code complexity reports over time. Given the amount of files, report files 17 | # are now stored on Azure Blob Storage. It's the source of truth - we download the current report, 18 | # run complexity analysis again, then push everything back to blob. 19 | rm -rf ${REPORT_DIR} 20 | mkdir -p ${REPORT_DIR} 21 | 22 | #NOTE: be sure to set AZURE_STORAGE_ACCOUNT and AZURE_STORAGE_KEY environment variables 23 | az storage blob download-batch --no-progress -d report -s '$web' > /tmp/download.log 24 | 25 | ${BASEDIR}/generate-report.sh -o ${REPORT_DIR} -v ${VERSION} -c ${COMMIT_SHA} 26 | -------------------------------------------------------------------------------- /scripts/version-bump-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | NPM_VERSION_TYPE=${1:-"prepatch --preid=preview"} 5 | echo "Next version type: ----->$NPM_VERSION_TYPE<-----" 6 | 7 | PACKAGE_VERSION=$(node -pe "require('./package.json').version") 8 | CURRENT_VERSION="v$PACKAGE_VERSION" 9 | 10 | # Get full branch name excluding refs/head from the env var SOURCE_BRANCH 11 | SOURCE_BRANCH_NAME=${SOURCE_BRANCH/refs\/heads\/} 12 | 13 | # Configure git to commit as VoTT Service Account 14 | echo "Configuring git to use deploy key..." 15 | git config --local user.email "vott@microsoft.com" 16 | git config --local user.name "Vott" 17 | 18 | echo "SOURCE_BRANCH: ${SOURCE_BRANCH_NAME}" 19 | git pull origin ${SOURCE_BRANCH_NAME} 20 | git checkout ${SOURCE_BRANCH_NAME} 21 | echo "Checked out branch: ${SOURCE_BRANCH_NAME}" 22 | 23 | ## format: v2.2.0 24 | NEXT_VERSION=`npm version ${NPM_VERSION_TYPE} -m "release: Update ${NPM_VERSION_TYPE} version to %s ***NO_CI***"` 25 | echo "Set next version to: ${NEXT_VERSION}" 26 | 27 | # There is currently no way to pass variables between stages, hence this workaround 28 | echo 29 | echo "##vso[task.setvariable variable=NEXT_VERSION]$NEXT_VERSION" 30 | echo "##vso[task.setvariable variable=CURRENT_VERSION]$CURRENT_VERSION" 31 | 32 | #### Push new tag 33 | SHA=`git rev-parse HEAD` 34 | 35 | export GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" 36 | git remote add authOrigin git@github.com:microsoft/VoTT.git 37 | git push authOrigin ${SOURCE_BRANCH_NAME} --tags 38 | 39 | echo 40 | echo "Pushed new tag: ${NEXT_VERSION} @ SHA: ${SHA:0:8}" 41 | -------------------------------------------------------------------------------- /server/.env.template: -------------------------------------------------------------------------------- 1 | APP_ID=xyz 2 | APP_SECRET=asdf 3 | COOKIE_SECRETS="[ { key: '12345678901234567890123456789012', iv: '123456789012' }, { key: 'abcdefghijklmnopqrstuvwxyzabcdef', iv: 'abcdefghijkl' }, ])" 4 | ALLOW_HTTP=true 5 | BASE_URL=http://localhost:3000/ 6 | -------------------------------------------------------------------------------- /server/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch via NPM", 11 | "runtimeExecutable": "npm", 12 | "runtimeArgs": [ 13 | "run-script", 14 | "debug" 15 | ], 16 | "port": 9229 17 | }, 18 | { 19 | "type": "node", 20 | "request": "launch", 21 | "name": "Launch Program", 22 | "program": "${workspaceFolder}/lib/app.js", //"${workspaceFolder}\\lib\\app.js", 23 | "args": [ 24 | "|", 25 | "bunyan" 26 | ], 27 | "outFiles": [ 28 | "${workspaceFolder}/**/*.js" 29 | ], 30 | "console": "internalConsole", 31 | "outputCapture": "std", 32 | } 33 | 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /server/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "build", 9 | "problemMatcher": [ 10 | "$tsc" 11 | ], 12 | "group": "build" 13 | }, 14 | { 15 | "type": "typescript", 16 | "tsconfig": "tsconfig.json", 17 | "option": "watch", 18 | "problemMatcher": [ 19 | "$tsc-watch" 20 | ], 21 | "group": "build" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /server/azure-deploy.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | trigger: none 7 | pr: none 8 | 9 | variables: 10 | # Azure Resource Manager connection created during pipeline creation 11 | azureSubscription: fe7b93fe-e836-4a55-804c-883dbea6af24' 12 | 13 | # Web app name 14 | webAppName: 'vott' 15 | 16 | # Agent VM image name 17 | vmImageName: 'ubuntu-latest' 18 | 19 | stages: 20 | - stage: Build 21 | displayName: Build stage 22 | jobs: 23 | - job: Build 24 | displayName: Build 25 | pool: 26 | vmImage: $(vmImageName) 27 | 28 | steps: 29 | - task: NodeTool@0 30 | inputs: 31 | versionSpec: '10.x' 32 | displayName: 'Install Node.js' 33 | 34 | - script: | 35 | npm install 36 | npm run build --if-present 37 | # npm run test --if-present 38 | workingDirectory: $(System.DefaultWorkingDirectory)/server 39 | displayName: 'npm install, build and test' 40 | 41 | - task: ArchiveFiles@2 42 | displayName: 'Archive files' 43 | inputs: 44 | rootFolderOrFile: '$(System.DefaultWorkingDirectory)/server' 45 | includeRootFolder: false 46 | archiveType: zip 47 | archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip 48 | replaceExistingArchive: true 49 | 50 | - upload: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip 51 | artifact: drop 52 | 53 | - stage: Deploy 54 | displayName: Deploy stage 55 | dependsOn: Build 56 | condition: succeeded() 57 | jobs: 58 | - deployment: Deploy 59 | displayName: Deploy 60 | environment: 'development' 61 | pool: 62 | vmImage: $(vmImageName) 63 | strategy: 64 | runOnce: 65 | deploy: 66 | steps: 67 | - task: AzureWebApp@1 68 | displayName: 'Azure Web App Deploy: vott' 69 | inputs: 70 | azureSubscription: $(azureSubscription) 71 | appType: webAppLinux 72 | appName: $(webAppName) 73 | runtimeStack: 'NODE|10.10' 74 | package: $(Pipeline.Workspace)/drop/$(Build.BuildId).zip 75 | startUpCommand: 'npm run start' 76 | -------------------------------------------------------------------------------- /server/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | rootDir: "src", 5 | coverageDirectory: "../coverage", 6 | }; 7 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vott-server", 3 | "version": "1.0.0", 4 | "description": "Server to support VoTT with login", 5 | "main": "./lib/app.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "node ./lib/app.js", 9 | "test:unit": "jest --runInBand", 10 | "test": "npm run lint && npm run test:unit", 11 | "watch": "concurrently --kill-others \"tsc -w\" \"nodemon --inspect ./lib/app.js\"", 12 | "lint": "tslint -q -p . -c tslint.json", 13 | "lint:fix": "tslint --fix -p . -c tslint.json", 14 | "debug": "nodemon --inspect ./lib/app.js | bunyan" 15 | }, 16 | "author": "Microsoft", 17 | "license": "MIT", 18 | "dependencies": { 19 | "@microsoft/microsoft-graph-client": "^1.7.0", 20 | "body-parser": "^1.15.2", 21 | "bunyan": "*", 22 | "cookie-parser": "^1.4.3", 23 | "cookie-session": "^1.3.3", 24 | "cookies": "^0.7.3", 25 | "ejs": ">= 0.0.0", 26 | "ejs-locals": ">= 0.0.0", 27 | "express": "^4.17.1", 28 | "express-request-id": "^1.4.1", 29 | "method-override": "^3.0.0", 30 | "morgan": "^1.9.1", 31 | "node-fetch": "^2.6.0", 32 | "passport": "*", 33 | "passport-azure-ad": "^4.1.0", 34 | "simple-oauth2": "^2.2.1" 35 | }, 36 | "devDependencies": { 37 | "@types/bunyan": "^1.8.6", 38 | "@types/cookie-parser": "^1.4.2", 39 | "@types/cookie-session": "^2.0.37", 40 | "@types/cookies": "^0.7.2", 41 | "@types/dotenv": "^6.1.1", 42 | "@types/express": "^4.17.1", 43 | "@types/express-request-id": "^1.4.1", 44 | "@types/jest": "^24.0.17", 45 | "@types/method-override": "0.0.31", 46 | "@types/morgan": "^1.7.37", 47 | "@types/node-fetch": "^2.5.0", 48 | "@types/passport": "^1.0.1", 49 | "@types/passport-azure-ad": "^4.0.3", 50 | "@types/simple-oauth2": "^2.2.1", 51 | "concurrently": "^4.1.1", 52 | "dotenv": "^8.1.0", 53 | "jest": "^24.8.0", 54 | "nodemon": "^1.19.1", 55 | "ts-jest": "^24.0.2", 56 | "tslint": "^5.18.0", 57 | "typescript": "^3.6.2" 58 | }, 59 | "engines": { 60 | "node": ">= 10.0.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /server/public/views/account.ejs: -------------------------------------------------------------------------------- 1 | <% if (!user) { %> 2 |

Welcome! Please log in.

3 | Log In 4 | <% } else { %> 5 |

Profile ID: <%= user.oid %>

6 |

Email: <%= user.mail %>

7 |
<%- JSON.stringify(user, null, 2) %>
8 | <% } %> 9 | -------------------------------------------------------------------------------- /server/public/views/index.ejs: -------------------------------------------------------------------------------- 1 | <% if (!user) { %> 2 |

Welcome! Please log in.

3 | Log In
4 | Manage your permissions (organization)
5 | Manage account permissions (personal) 6 | <% } else { %> 7 |

Hello, <%= user.displayName %>

8 | Account Info
9 | Run tests
10 | End session
11 | Log Out
12 | Manage your permissions (organization)
13 | Manage account permissions (personal) 14 | <% } %> 15 | -------------------------------------------------------------------------------- /server/public/views/layout.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Passport-OpenID Example 5 | 6 | 7 | <% if (!user) { %> 8 |

9 | Home | 10 | Log In 11 |

12 | <% } else { %> 13 |

14 | Home | 15 | Account | 16 | Log Out 17 |

18 | <% } %> 19 | <%- body %> 20 | 21 | 22 | -------------------------------------------------------------------------------- /server/src/__tests__/api.test.ts: -------------------------------------------------------------------------------- 1 | 2 | // import { default as fetch } from 'node-fetch'; 3 | import * as config from '../config'; 4 | import { app, server } from '../app'; 5 | import { ServerResponse } from 'http'; 6 | 7 | 8 | beforeAll(async (done) => { 9 | if (!server.listening) { 10 | server.on('listening', done()) 11 | } else { 12 | done(); 13 | } 14 | }); 15 | 16 | afterAll(async (done) => { 17 | server.close(() => { 18 | console.log('done'); 19 | done(); 20 | }); 21 | }); 22 | 23 | describe('App Server', () => { 24 | 25 | afterAll(async (done) => { 26 | server.close(() => { 27 | console.log('done'); 28 | done(); 29 | }); 30 | }); 31 | 32 | test('initialized', async (done) => { 33 | expect(app.name).toBeDefined(); 34 | expect(server.listening).toBe(true); 35 | done(); 36 | }); 37 | 38 | test('loads app.html', async (done) => { 39 | const response = await fetch(config.baseUrl); 40 | expect(response.status).toBe(200); 41 | done(); 42 | }); 43 | 44 | test('redirects to login', async (done) => { 45 | const response = await fetch(config.baseUrl + '/api/v1.0/me'); 46 | expect(response.status).toBe(404); 47 | done(); 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /server/src/__tests__/extra.test.ts: -------------------------------------------------------------------------------- 1 | 2 | // import { default as fetch } from 'node-fetch'; 3 | import * as config from '../config'; 4 | 5 | describe('App Server', () => { 6 | 7 | 8 | test('should be closed', async (done) => { 9 | // do nothing 10 | done(); 11 | }); 12 | 13 | }); 14 | -------------------------------------------------------------------------------- /server/src/graph.ts: -------------------------------------------------------------------------------- 1 | import * as graphClient from '@microsoft/microsoft-graph-client'; 2 | 3 | export async function user(access_token: string) { 4 | const token = client(access_token); 5 | const result = await token.api('/me').get(); 6 | return result; 7 | } 8 | 9 | export async function getEvents(access_token: string) { 10 | const token = client(access_token); 11 | 12 | const events = await token.api('/me/events') 13 | .select('subject,organizer,start,end') 14 | .orderby('createdDateTime DESC') 15 | .get(); 16 | return events; 17 | } 18 | 19 | export function client(access_token: string): graphClient.Client { 20 | // Initialize Graph client 21 | const result = graphClient.Client.init({ 22 | // Use the provided access token to authenticate 23 | // requests 24 | authProvider: (done: (err: any, access_token: string) => void) => { 25 | done(null, access_token); 26 | }, 27 | }); 28 | 29 | return result; 30 | } 31 | -------------------------------------------------------------------------------- /server/src/oauth2.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as simple_oauth2 from 'simple-oauth2'; 3 | import * as config from './config'; 4 | 5 | export const oauth2 = simple_oauth2.create({ 6 | client: { 7 | id: config.creds.clientID, 8 | secret: config.creds.clientSecret, 9 | }, 10 | auth: { 11 | tokenHost: 'https://login.microsoftonline.com/common', 12 | authorizePath: '/oauth2/v2.0/authorize', 13 | tokenPath: '/oauth2/v2.0/token', 14 | }, 15 | }); 16 | 17 | export interface Token { 18 | refresh_token: string; 19 | access_token?: string; 20 | expires_at?: string | Date; 21 | } 22 | 23 | export function client(token: Token) { 24 | token.expires_at = token.expires_at || new Date(0); 25 | const result = oauth2.accessToken.create(token); 26 | return result; 27 | } 28 | -------------------------------------------------------------------------------- /server/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "linterOptions": { 7 | "exclude": [ 8 | "lib", 9 | "public", 10 | "src/routes", 11 | "jest.config.js" 12 | ] 13 | }, 14 | "jsRules": {}, 15 | "rules": { 16 | "no-console": false, 17 | "arrow-parens": false, 18 | "max-classes-per-file": false, 19 | "ordered-imports": false, 20 | "object-literal-sort-keys": false, 21 | "align": false, 22 | "interface-name": false, 23 | "quotemark": [ 24 | true, 25 | "single", 26 | "avoid-escape", 27 | "avoid-template" 28 | ], 29 | "max-line-length": { 30 | "severity": "warning", 31 | "options": [ 32 | 160, 33 | { 34 | "ignore-pattern": "^import |^export {(.*?)} | //" 35 | } 36 | ] 37 | }, 38 | "variable-name": { 39 | "options": [ 40 | "ban-keywords", 41 | "check-format", 42 | "allow-leading-underscore", 43 | "allow-pascal-case", 44 | "allow-snake-case" 45 | ] 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import App from "./App"; 3 | import { Provider } from "react-redux"; 4 | import createReduxStore from "./redux/store/store"; 5 | import initialState from "./redux/store/initialState"; 6 | import { IApplicationState } from "./models//applicationState"; 7 | import { mount } from "enzyme"; 8 | import { Router } from "react-router-dom"; 9 | import { KeyboardManager } from "./react/components/common/keyboardManager/keyboardManager"; 10 | import { ErrorHandler } from "./react/components/common/errorHandler/errorHandler"; 11 | 12 | describe("App Component", () => { 13 | const defaultState: IApplicationState = initialState; 14 | const store = createReduxStore(defaultState); 15 | const electronMock = { 16 | ipcRenderer: { 17 | send: jest.fn(), 18 | on: jest.fn(), 19 | }, 20 | }; 21 | 22 | beforeAll(() => { 23 | delete (window as any).require; 24 | }); 25 | 26 | function createComponent() { 27 | return mount( 28 | 29 | 30 | , 31 | ); 32 | } 33 | 34 | it("renders without crashing", () => { 35 | createComponent(); 36 | }); 37 | 38 | it("renders required top level components", () => { 39 | const wrapper = createComponent(); 40 | expect(wrapper.find(Router).exists()).toBe(true); 41 | expect(wrapper.find(KeyboardManager).exists()).toEqual(true); 42 | expect(wrapper.find(ErrorHandler).exists()).toEqual(true); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/assets/sass/theme.scss: -------------------------------------------------------------------------------- 1 | $color-yellow: #ffbb00; 2 | $color-green: #7cbb00; 3 | $color-blue: #00a1f1; 4 | $color-red: #f65314; 5 | 6 | $lighter-1: rgba(255,255,255,0.05); 7 | $lighter-2: rgba(255,255,255,0.10); 8 | $lighter-3: rgba(255,255,255,0.15); 9 | $lighter-4: rgba(255,255,255,0.20); 10 | $lighter-5: rgba(255,255,255,0.35); 11 | 12 | $darker-1: rgba(0,0,0,0.05); 13 | $darker-2: rgba(0,0,0,0.10); 14 | $darker-3: rgba(0,0,0,0.15); 15 | $darker-4: rgba(0,0,0,0.20); 16 | $darker-5: rgba(0,0,0,0.25); 17 | $darker-6: rgba(0,0,0,0.5); 18 | $darker-7: rgba(0,0,0,0.35); 19 | $darker-8: rgba(0,0,0,0.40); 20 | $darker-9: rgba(0,0,0,0.45); 21 | $darker-10: rgba(0,0,0,0.50); 22 | $darker-11: rgba(0,0,0,0.55); 23 | $darker-12: rgba(0,0,0,0.60); 24 | $darker-13: rgba(0,0,0,0.65); 25 | $darker-14: rgba(0,0,0,0.70); 26 | $darker-15: rgba(0,0,0,0.75); 27 | 28 | .bg-darker-1 { 29 | background-color: $darker-1; 30 | } 31 | .bg-darker-2 { 32 | background-color: $darker-2; 33 | } 34 | .bg-darker-3 { 35 | background-color: $darker-3; 36 | } 37 | .bg-darker-4 { 38 | background-color: $darker-4; 39 | } 40 | .bg-darker-5 { 41 | background-color: $darker-5; 42 | } 43 | 44 | .bg-lighter-1 { 45 | background-color: $lighter-1; 46 | } 47 | .bg-lighter-2 { 48 | background-color: $lighter-2; 49 | } 50 | .bg-lighter-3 { 51 | background-color: $lighter-3; 52 | } 53 | .bg-lighter-4 { 54 | background-color: $lighter-4; 55 | } 56 | .bg-lighter-5 { 57 | background-color: $lighter-5; 58 | } -------------------------------------------------------------------------------- /src/common/appInfo.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line:no-var-requires 2 | const packageJson = require("../../package.json"); 3 | 4 | /** 5 | * Defines the application information 6 | */ 7 | export interface IAppInfo { 8 | /** The app name */ 9 | name: string; 10 | /** The app version */ 11 | version: string; 12 | /** The app description */ 13 | description: string; 14 | } 15 | 16 | /** 17 | * Gets current application info 18 | */ 19 | export const appInfo = packageJson as IAppInfo; 20 | -------------------------------------------------------------------------------- /src/common/clipboard.test.ts: -------------------------------------------------------------------------------- 1 | import Clipboard from "./clipboard"; 2 | import MockFactory from "./mockFactory"; 3 | import { IProject } from "../models/applicationState"; 4 | import { IAssetMetadata } from "vott-react"; 5 | 6 | describe("Clipboard tests", () => { 7 | 8 | const mockObject = MockFactory.createTestProject(); 9 | 10 | beforeAll(() => { 11 | const clipboard = (navigator as any).clipboard; 12 | if (!(clipboard && clipboard.writeText && clipboard.readText)) { 13 | (navigator as any).clipboard = { 14 | writeText: jest.fn(() => Promise.resolve()), 15 | readText: jest.fn(() => Promise.resolve(JSON.stringify(mockObject))), 16 | }; 17 | } 18 | }); 19 | it("Writes text to the clipboard", async () => { 20 | const text = "test"; 21 | await Clipboard.writeText(text); 22 | expect((navigator as any).clipboard.writeText).toBeCalledWith(text); 23 | }); 24 | 25 | it("Writes object to the clipboard", async () => { 26 | await Clipboard.writeObject(mockObject); 27 | expect((navigator as any).clipboard.writeText).toBeCalledWith(JSON.stringify(mockObject)); 28 | }); 29 | 30 | it("Reads text from the clipboard", async () => { 31 | expect(await Clipboard.readText()).toEqual(JSON.stringify(mockObject)); 32 | }); 33 | 34 | it("Reads object from the clipboard", async () => { 35 | expect(await Clipboard.readObject()).toEqual(mockObject); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/common/clipboard.ts: -------------------------------------------------------------------------------- 1 | export default class Clipboard { 2 | public static async writeText(text: string): Promise { 3 | return (navigator as any).clipboard.writeText(text); 4 | } 5 | 6 | public static async writeObject(item: any): Promise { 7 | return Clipboard.writeText(JSON.stringify(item)); 8 | } 9 | 10 | public static async readText(): Promise { 11 | return (navigator as any).clipboard.readText(); 12 | } 13 | 14 | public static async readObject(): Promise { 15 | return Clipboard.readText().then((text) => Promise.resolve(JSON.parse(text))); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/common/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Constants used throughout application 3 | */ 4 | export const constants = { 5 | projectFileExtension: ".vott", 6 | assetMetadataFileExtension: "-asset.json", 7 | exportFileExtension: "-export.json", 8 | }; 9 | -------------------------------------------------------------------------------- /src/common/deferred.ts: -------------------------------------------------------------------------------- 1 | export interface IDeferred { 2 | resolve(result?: T): void; 3 | reject(err?: any): void; 4 | then(value: T): Promise; 5 | catch(err: any): Promise; 6 | } 7 | 8 | export class Deferred implements IDeferred { 9 | public promise: Promise; 10 | 11 | constructor() { 12 | this.promise = new Promise((resolve, reject) => { 13 | this.resolve = resolve; 14 | this.reject = reject; 15 | }); 16 | 17 | this.then = this.promise.then.bind(this.promise); 18 | this.catch = this.promise.catch.bind(this.promise); 19 | } 20 | // tslint:disable-next-line 21 | public resolve = (result?: T) => { }; 22 | // tslint:disable-next-line 23 | public reject = (err?: any) => { }; 24 | public then = (value: T) => { throw new Error("Not implemented yet"); }; 25 | public catch = (err: any) => { throw new Error("Not implemented yet"); }; 26 | } 27 | -------------------------------------------------------------------------------- /src/common/environment.ts: -------------------------------------------------------------------------------- 1 | export class Env { 2 | public static get() { 3 | return process.env.NODE_ENV; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/common/extensions/array.ts: -------------------------------------------------------------------------------- 1 | import Guard from "../guard"; 2 | 3 | /** 4 | * Processes items in the array within the specified batch size (default: 5) 5 | * @param this The array to process 6 | * @param action The action to perform on each item in the array 7 | * @param batchSize The batch size for actions to perform in parallel (default: 5) 8 | */ 9 | export async function forEachAsync( 10 | this: T[], 11 | action: (item: T) => Promise, 12 | batchSize: number = 5): Promise { 13 | Guard.null(this); 14 | Guard.null(action); 15 | Guard.expression(batchSize, (value) => value > 0); 16 | 17 | const all: T[] = [...this]; 18 | 19 | while (all.length > 0) { 20 | const batch: T[] = []; 21 | 22 | while (all.length > 0 && batch.length < batchSize) { 23 | batch.push(all.pop()); 24 | } 25 | 26 | const tasks = batch.map((item) => action(item)); 27 | await Promise.all(tasks); 28 | } 29 | } 30 | 31 | /** 32 | * Maps items in the array in async batches with the specified action 33 | * @param this The array to process 34 | * @param action The transformer action to perform on each item in the array 35 | * @param batchSize The batch size for actions to perform in parallel (default: 5); 36 | */ 37 | export async function mapAsync( 38 | this: T[], 39 | action: (item: T) => Promise, 40 | batchSize: number = 5): Promise { 41 | Guard.null(this); 42 | Guard.null(action); 43 | Guard.expression(batchSize, (value) => value > 0); 44 | 45 | let results: R[] = []; 46 | const all: T[] = [...this]; 47 | 48 | while (all.length > 0) { 49 | const batch: T[] = []; 50 | 51 | while (all.length > 0 && batch.length < batchSize) { 52 | batch.push(all.pop()); 53 | } 54 | 55 | const tasks = batch.map((item) => action(item)); 56 | const batchResults = await Promise.all(tasks); 57 | results = results.concat(batchResults); 58 | } 59 | 60 | return results; 61 | } 62 | -------------------------------------------------------------------------------- /src/common/extensions/map.test.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import MockFactory from "../mockFactory"; 3 | import { IAsset } from "../../models/applicationState"; 4 | import registerMixins from "../../registerMixins"; 5 | 6 | describe("Map Extensions", () => { 7 | const testArray = MockFactory.createTestAssets(100); 8 | 9 | beforeAll(registerMixins); 10 | 11 | describe("forEachAsync", () => { 12 | const map = testArray.map((asset) => [asset.id, asset]) as Array<[string, IAsset]>; 13 | const testMap = new Map(map); 14 | 15 | const output = []; 16 | 17 | const actionFunc = async (asset): Promise => { 18 | return new Promise((resolve) => { 19 | setImmediate(() => { 20 | output.push(asset); 21 | resolve(); 22 | }); 23 | }); 24 | }; 25 | 26 | const sortFunc = (a: IAsset, b: IAsset) => { 27 | return a.id > b.id ? 1 : (b.id > a.id ? -1 : 0); 28 | }; 29 | 30 | beforeEach(() => output.length = 0); 31 | 32 | it("processes items in batches of default size", async () => { 33 | await testMap.forEachAsync(actionFunc); 34 | expect(output.sort(sortFunc)).toEqual(testArray.sort(sortFunc)); 35 | }); 36 | 37 | it("processes items in batches of 25", async () => { 38 | await testMap.forEachAsync(actionFunc, 25); 39 | expect(output.sort(sortFunc)).toEqual(testArray.sort(sortFunc)); 40 | }); 41 | 42 | it("fails when called with invalid batch size", async () => { 43 | await expect(testMap.forEachAsync(() => null, 0)).rejects.not.toBeNull(); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/common/extensions/map.ts: -------------------------------------------------------------------------------- 1 | import Guard from "../guard"; 2 | 3 | /** 4 | * Processes items in the map within the specified batch size (default: 5) 5 | * @param this The map to process 6 | * @param action The action to perform on each item in the map 7 | * @param batchSize The batch size for actions to perform in parallel (default: 5) 8 | */ 9 | export async function forEachAsync( 10 | this: Map, 11 | action: (value: V, key: K) => Promise, 12 | batchSize: number = 5): Promise { 13 | Guard.null(this); 14 | Guard.null(action); 15 | Guard.expression(batchSize, (value) => value > 0); 16 | 17 | const all: Array<[K, V]> = [...this.entries()]; 18 | 19 | while (all.length > 0) { 20 | const batch: Array<[K, V]> = []; 21 | 22 | while (all.length > 0 && batch.length < batchSize) { 23 | batch.push(all.pop()); 24 | } 25 | 26 | const tasks = batch.map((item) => action(item[1], item[0])); 27 | await Promise.all(tasks); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/common/guard.test.ts: -------------------------------------------------------------------------------- 1 | import Guard from "./guard"; 2 | 3 | describe("Guard", () => { 4 | function methodWithRequiredName(name: string) { 5 | Guard.empty(name); 6 | } 7 | 8 | function methodWithRequiredNameWithParam(name: string) { 9 | Guard.empty(name, "name", "Name is required"); 10 | } 11 | 12 | function methodWithRequiredObject(options: any) { 13 | Guard.null(options); 14 | } 15 | 16 | function methodWithRequiredExpression(value: number) { 17 | Guard.expression(value, (num) => num > 0 && num < 100); 18 | } 19 | 20 | describe("empty", () => { 21 | it("throws error on null value", () => { 22 | expect(() => methodWithRequiredName(null)).toThrowError(); 23 | }); 24 | 25 | it("throws error on empty value", () => { 26 | expect(() => methodWithRequiredName("")).toThrowError(); 27 | }); 28 | 29 | it("throw error on whitespace", () => { 30 | expect(() => methodWithRequiredName(" ")).toThrowError(); 31 | }); 32 | 33 | it("does not throw error on valid value", () => { 34 | expect(() => methodWithRequiredName("valid")).not.toThrowError(); 35 | }); 36 | 37 | it("throws specific error message", () => { 38 | expect(() => methodWithRequiredNameWithParam(null)).toThrowError("Name is required"); 39 | }); 40 | }); 41 | 42 | describe("null", () => { 43 | it("throws error on null value", () => { 44 | expect(() => methodWithRequiredObject(null)).toThrowError(); 45 | }); 46 | 47 | it("does not throw error on valid value", () => { 48 | expect(() => methodWithRequiredObject({})).not.toThrowError(); 49 | }); 50 | }); 51 | 52 | describe("expression", () => { 53 | it("throws error on invalide value", () => { 54 | expect(() => methodWithRequiredExpression(0)).toThrowError(); 55 | }); 56 | 57 | it("does not throw error on valid value", () => { 58 | expect(() => methodWithRequiredExpression(1)).not.toThrowError(); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/common/guard.ts: -------------------------------------------------------------------------------- 1 | export default class Guard { 2 | /** 3 | * Validates the string express is not null or empty, otherwise throws an exception 4 | * @param value - The value to validate 5 | * @param paramName - The name of the parameter to validate 6 | * @param message - The error message to return on invalid value 7 | */ 8 | public static empty(value: string, paramName?: string, message?: string) { 9 | if ((!!value === false || value.trim().length === 0)) { 10 | message = message || (`'${paramName || "value"}' cannot be null or empty`); 11 | throw new Error(message); 12 | } 13 | } 14 | 15 | /** 16 | * Validates the value is not null, otherwise throw an exception 17 | * @param value - The value to validate 18 | * @param paramName - The name of the parameter to validate 19 | * @param message - The error message to return on invalid value 20 | */ 21 | public static null(value: any, paramName?: string, message?: string) { 22 | if ((!!value === false)) { 23 | message = message || (`'${paramName || "value"}' cannot be null or undefined`); 24 | throw new Error(message); 25 | } 26 | } 27 | 28 | /** 29 | * Validates the value meets the specified expectation, otherwise throws an exception 30 | * @param value - The value to validate 31 | * @param predicate - The predicate used for validation 32 | * @param paramName - The name of the parameter to validate 33 | * @param message - The error message to return on invalid value 34 | */ 35 | public static expression(value: T, predicate: (value: T) => boolean, paramName?: string, message?: string) { 36 | if (!!value === false || !predicate(value)) { 37 | message = message || (`'${paramName || "value"}' is not a valid value`); 38 | throw new Error(message); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/common/hostProcess.test.ts: -------------------------------------------------------------------------------- 1 | import getHostProcess, { HostProcessType } from "./hostProcess"; 2 | 3 | jest.mock("os"); 4 | import os from "os"; 5 | 6 | describe("Host Process", () => { 7 | let originalHostType: string = null; 8 | 9 | beforeAll(() => { 10 | originalHostType = process.env.HOST_TYPE; 11 | process.env.HOST_TYPE = ""; 12 | }); 13 | 14 | afterAll(() => { 15 | process.env.HOST_TYPE = originalHostType; 16 | }); 17 | 18 | it("sets host process type to electron when running as electron", () => { 19 | // tslint:disable-next-line:max-line-length 20 | const expectedRelease = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) vott-react-typescript/0.1.0 Chrome/66.0.3359.181 Electron/3.0.13 Safari/537.36"; 21 | const releaseMock = os.release as jest.Mock; 22 | releaseMock.mockImplementationOnce(() => expectedRelease); 23 | 24 | const hostProcess = getHostProcess(); 25 | 26 | expect(hostProcess.type).toEqual(HostProcessType.Electron); 27 | expect(hostProcess.release).toEqual(expectedRelease.toLowerCase()); 28 | }); 29 | 30 | it("sets host process type to browser when not electron", () => { 31 | // tslint:disable-next-line:max-line-length 32 | const expectedRelease = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"; 33 | const releaseMock = os.release as jest.Mock; 34 | releaseMock.mockImplementationOnce(() => expectedRelease); 35 | 36 | const hostProcess = getHostProcess(); 37 | 38 | expect(hostProcess.type).toEqual(HostProcessType.Browser); 39 | expect(hostProcess.release).toEqual(expectedRelease.toLowerCase()); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/common/hostProcess.ts: -------------------------------------------------------------------------------- 1 | import os from "os"; 2 | 3 | /** 4 | * @name - Host Process 5 | * @description - Describes the host process 6 | * @member type - The type of the host process (electron, browser, etc) 7 | * @member release - The release string of the host process 8 | */ 9 | export interface IHostProcess { 10 | type: HostProcessType; 11 | release: string; 12 | } 13 | 14 | /** 15 | * @enum ELECTRON - Electron Host Process Type 16 | * @enum BROWSER - Browser Host Process Type 17 | */ 18 | export enum HostProcessType { 19 | Electron = 1, // bits: 01 20 | Browser = 2, // bits: 10 21 | All = 3, // bits: 11 22 | } 23 | 24 | export enum PlatformType { 25 | Web = "web", 26 | Windows = "win32", 27 | Linux = "linux", 28 | MacOS = "darwin", 29 | } 30 | 31 | function getHostProcess(): IHostProcess { 32 | const osRelease = os.release().toLowerCase(); 33 | let hostProcessType: HostProcessType; 34 | if (osRelease.indexOf("electron") > -1 || process.env.HOST_TYPE === "electron") { 35 | hostProcessType = HostProcessType.Electron; 36 | } else { 37 | hostProcessType = HostProcessType.Browser; 38 | } 39 | 40 | return { 41 | release: osRelease, 42 | type: hostProcessType, 43 | }; 44 | } 45 | 46 | export function isElectron(): boolean { 47 | return getHostProcess().type === HostProcessType.Electron; 48 | } 49 | 50 | export function isBrowser(): boolean { 51 | return getHostProcess().type === HostProcessType.Browser; 52 | } 53 | 54 | export default getHostProcess; 55 | -------------------------------------------------------------------------------- /src/common/ipcRendererProxy.test.ts: -------------------------------------------------------------------------------- 1 | import { IpcRendererProxy } from "./ipcRendererProxy"; 2 | 3 | describe("IpcRendererProxy", () => { 4 | it("is defined", () => { 5 | expect(IpcRendererProxy).toBeDefined(); 6 | }); 7 | 8 | it("send method forwards request to electron ipcRenderer", () => { 9 | const commandName = "TEST_COMMAND"; 10 | const args = { 11 | a: 1, 12 | b: 2, 13 | }; 14 | 15 | const electronMock = { 16 | ipcRenderer: { 17 | send: jest.fn(), 18 | on: jest.fn(), 19 | }, 20 | }; 21 | 22 | (window as any).require = jest.fn(() => electronMock); 23 | expect(Object.keys(IpcRendererProxy.pending).length).toEqual(0); 24 | 25 | IpcRendererProxy.send(commandName, args); 26 | 27 | expect(electronMock.ipcRenderer.send).toBeCalledWith("ipc-main-proxy", { 28 | id: expect.any(String), 29 | type: commandName, 30 | args, 31 | }); 32 | 33 | expect(Object.keys(IpcRendererProxy.pending).length).toEqual(1); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/common/ipcRendererProxy.ts: -------------------------------------------------------------------------------- 1 | import * as shortid from "shortid"; 2 | import { IpcProxyMessage } from "../electron/common/ipcProxy"; 3 | import { Deferred } from "./deferred"; 4 | 5 | export class IpcRendererProxy { 6 | 7 | public static pending: { [id: string]: Deferred } = {}; 8 | 9 | public static initialize() { 10 | if (IpcRendererProxy.initialized) { 11 | return; 12 | } 13 | 14 | IpcRendererProxy.ipcRenderer = (window as any).require("electron").ipcRenderer; 15 | IpcRendererProxy.ipcRenderer.on("ipc-renderer-proxy", (sender, message: IpcProxyMessage) => { 16 | const deferred = IpcRendererProxy.pending[message.id]; 17 | 18 | if (!deferred) { 19 | throw new Error(`Cannot find deferred with id '${message.id}'`); 20 | } 21 | 22 | if (message.error) { 23 | deferred.reject(message.error); 24 | } else { 25 | deferred.resolve(message.result); 26 | } 27 | 28 | delete IpcRendererProxy.pending[message.id]; 29 | }); 30 | 31 | IpcRendererProxy.initialized = true; 32 | } 33 | 34 | public static send(type: string, args?: TArgs): Promise { 35 | IpcRendererProxy.initialize(); 36 | 37 | const id = shortid.generate(); 38 | const deferred = new Deferred(); 39 | IpcRendererProxy.pending[id] = deferred; 40 | 41 | const outgoingArgs: IpcProxyMessage = { 42 | id, 43 | type, 44 | args, 45 | }; 46 | 47 | IpcRendererProxy.ipcRenderer.send("ipc-main-proxy", outgoingArgs); 48 | 49 | return deferred.promise; 50 | } 51 | private static ipcRenderer; 52 | private static initialized: boolean = false; 53 | } 54 | -------------------------------------------------------------------------------- /src/electron/common/ipcMainProxy.test.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, IpcMain } from "electron"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | import LocalFileSystem from "../providers/storage/localFileSystem"; 5 | import { IpcMainProxy } from "./ipcMainProxy"; 6 | 7 | describe("IpcMainProxy", () => { 8 | let proxy: IpcMainProxy = null; 9 | 10 | beforeEach(() => { 11 | const ipcMainMock = {} as IpcMain; 12 | ipcMainMock.on = jest.fn(); 13 | const browserWindowMock = {} as BrowserWindow; 14 | 15 | proxy = new IpcMainProxy(ipcMainMock, browserWindowMock); 16 | }); 17 | 18 | it("is defined", () => { 19 | expect(proxy).toBeDefined(); 20 | expect(proxy).not.toBeNull(); 21 | }); 22 | 23 | it("registers a command handler for a single command", () => { 24 | const commandName = "COMMAND_ONE"; 25 | proxy.register(commandName, jest.fn()); 26 | 27 | expect(proxy.handlers[commandName]).toBeDefined(); 28 | expect(proxy.handlers[commandName]).not.toBeNull(); 29 | }); 30 | 31 | it("registers a suite of commands for a whole object", () => { 32 | const localFileSystem = new LocalFileSystem(null); 33 | proxy.registerProxy("LocalFileSystem", localFileSystem); 34 | 35 | expect(Object.keys(proxy.handlers).length).toBeGreaterThan(0); 36 | }); 37 | 38 | it("calls the methods correctly", async () => { 39 | const localFileSystem = new LocalFileSystem(null); 40 | proxy.registerProxy("LocalFileSystem", localFileSystem); 41 | 42 | const handler = proxy.handlers["LocalFileSystem:writeText"]; 43 | const filePath = path.join(__dirname, "test.json"); 44 | const args = [filePath, "test"]; 45 | await handler(null, args); 46 | 47 | expect(fs.existsSync(filePath)).toBeTruthy(); 48 | 49 | fs.unlinkSync(filePath); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/electron/common/ipcProxy.ts: -------------------------------------------------------------------------------- 1 | export interface IpcProxyMessage { 2 | id: string; 3 | type: string; 4 | args?: any; 5 | error?: string; 6 | result?: TResult; 7 | debug?: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/electron/start.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const port = process.env.PORT ? (process.env.PORT - 100) : 3000; 3 | 4 | process.env.ELECTRON_START_URL = `http://localhost:${port}`; 5 | 6 | const client = new net.Socket(); 7 | 8 | let startedElectron = false; 9 | const tryConnection = () => client.connect({ port: port }, () => { 10 | client.end(); 11 | if (!startedElectron) { 12 | console.log('starting electron'); 13 | startedElectron = true; 14 | const exec = require('child_process').exec; 15 | const electron = exec('npm run electron:run:dev', (error, stdout, stderr) => { 16 | console.log('Electron Process Terminated'); 17 | }); 18 | 19 | electron.stdout.on("data", (data) => { 20 | console.log(data); 21 | }); 22 | 23 | electron.on("message", (message, sendHandle) => { 24 | console.log(message); 25 | }); 26 | 27 | electron.on("error", (err) => { 28 | console.log(err); 29 | }); 30 | 31 | electron.on("exit", (code, signal) => { 32 | console.log(`Exit-Code: ${code}`); 33 | console.log(`Exit-Signal: ${signal}`); 34 | }); 35 | 36 | electron.on("close", (code, signal) => { 37 | console.log(`Close-Code: ${code}`); 38 | console.log(`Close-Signal: ${signal}`); 39 | }); 40 | 41 | electron.on("disconnect", () => { 42 | console.log('Electron Process Disconnect') 43 | }); 44 | } 45 | } 46 | ); 47 | 48 | tryConnection(); 49 | 50 | client.on('error', (error) => { 51 | setTimeout(tryConnection, 1000); 52 | }); -------------------------------------------------------------------------------- /src/history.ts: -------------------------------------------------------------------------------- 1 | import { createHashHistory } from "history"; 2 | export default createHashHistory(); 3 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | @import './assets/sass/theme.scss'; 2 | 3 | body { 4 | user-select: none; 5 | } 6 | 7 | #root { 8 | overflow: hidden; 9 | } 10 | 11 | input[type=file] { 12 | display: none; 13 | } 14 | 15 | /* Let's get this party started */ 16 | ::-webkit-scrollbar { 17 | width: 6px; 18 | } 19 | 20 | /* Track */ 21 | ::-webkit-scrollbar-track { 22 | box-shadow: inset 0 0 6px $darker-3; 23 | background-color: $lighter-1; 24 | border-radius: 10px; 25 | } 26 | 27 | /* Handle */ 28 | ::-webkit-scrollbar-thumb { 29 | border-radius: 10px; 30 | background: $lighter-4; 31 | box-shadow: 0 0 6px $darker-3; 32 | } 33 | ::-webkit-scrollbar-thumb:window-inactive { 34 | background: $lighter-4; 35 | } 36 | 37 | .form-group, .object-wrapper { 38 | margin-bottom: 1rem; 39 | 40 | .field-description { 41 | margin-bottom: 0.25rem; 42 | } 43 | 44 | &.is-invalid { 45 | .invalid-feedback { 46 | display: block; 47 | } 48 | 49 | .form-control { 50 | border: solid 1px #ee5f5b; 51 | } 52 | } 53 | 54 | &.is-valid { 55 | .valid-feedback { 56 | display: block; 57 | } 58 | 59 | .form-control { 60 | border: solid 1px #62c462; 61 | } 62 | } 63 | 64 | .rc-checkbox { 65 | margin-left: 0.5em; 66 | } 67 | 68 | .slider-value { 69 | font-weight: 500; 70 | font-size: 90%; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Provider } from "react-redux"; 3 | import ReactDOM from "react-dom"; 4 | import "../node_modules/bootstrap/dist/css/bootstrap.min.css"; 5 | import "../node_modules/@fortawesome/fontawesome-free/css/all.css"; 6 | import "./assets/css/bootstrap-theme-slate.css"; 7 | import "./index.scss"; 8 | import App from "./App"; 9 | import * as serviceWorker from "./serviceWorker"; 10 | import createReduxStore from "./redux/store/store"; 11 | import initialState from "./redux/store/initialState"; 12 | import { IApplicationState } from "./models/applicationState"; 13 | import registerProviders from "./registerProviders"; 14 | import registerMixins from "./registerMixins"; 15 | 16 | import { setUpAppInsights } from "./telemetry"; 17 | 18 | setUpAppInsights(); 19 | 20 | registerMixins(); 21 | registerProviders(); 22 | const defaultState: IApplicationState = initialState; 23 | const store = createReduxStore(defaultState, true); 24 | 25 | ReactDOM.render( 26 | 27 | 28 | 29 | , document.getElementById("root")); 30 | 31 | // If you want your app to work offline and load faster, you can change 32 | // unregister() to register() below. Note this comes with some pitfalls. 33 | // Learn more about service workers: http://bit.ly/CRA-PWA 34 | serviceWorker.unregister(); 35 | -------------------------------------------------------------------------------- /src/models/v1Models.ts: -------------------------------------------------------------------------------- 1 | import { ExportAssetState } from "../providers/export/exportProvider"; 2 | import { IPoint } from "./applicationState"; 3 | /** 4 | * @name - V1 Project 5 | * @description - Defines the structure of a version 1 Project 6 | * @member frames - Dictionary of all frame objects in the project (filename: list of regions in that file) 7 | * @member framerate - Rate at which a video is stepped through 8 | * @member inputTags - Comma delimited list of all tags in the project 9 | * @member suggestiontype - Setting to suggest regions for the next frame 10 | * @member scd - Boolean to describe whether scene-change detection is enabled 11 | * @member visitedFrames - List of frames in the project that have been visited--string for image, number for video 12 | * @member tag_colors - List of all tag colors corresponding to the tags in "tags" 13 | */ 14 | export interface IV1Project { 15 | frames: {[frameName: string]: IV1Region[]}; 16 | framerate: string; 17 | inputTags: string; 18 | suggestiontype: string; 19 | scd: boolean; 20 | visitedFrames: string[] | number[]; 21 | tag_colors: string[]; 22 | } 23 | 24 | /** 25 | * @name - V1 Region Object 26 | * @description - Defines the structure of a version 1 Region 27 | * @member x1 - Left-most x-value of the region 28 | * @member y1 - Top-most y-value of the region 29 | * @member x2 - Right-most x-value of the region 30 | * @member y2 - Bottom-most y-value of the region 31 | * @member width - Width of the frame that the region is in 32 | * @member height - Height of the frame that the region is in 33 | * @member box - Object holding x1, y1, x2, y2 34 | * @member points - List of IPoints describing the 4 corners of the region 35 | * @member UID - Unique, auto-generated ID 36 | * @member id - Index of the region in the list of all regions in project 37 | * @member type - shape of the region, "rect" is the only option in V1 38 | * @member tags - List of strings that are the tags associated with the region 39 | * @member name - Index of the region in the frame (starts at 1) 40 | */ 41 | export interface IV1Region { 42 | x1: number; 43 | y1: number; 44 | x2: number; 45 | y2: number; 46 | width: number; 47 | height: number; 48 | box: { [point: string]: number }; 49 | points: IPoint[]; 50 | UID: string; 51 | id: number; 52 | type: string; 53 | tags: string[]; 54 | name: number; 55 | } 56 | -------------------------------------------------------------------------------- /src/providers/activeLearning/electronProxyHandler.test.ts: -------------------------------------------------------------------------------- 1 | jest.mock("../storage/localFileSystemProxy"); 2 | import { LocalFileSystemProxy } from "../storage/localFileSystemProxy"; 3 | import { ElectronProxyHandler } from "./electronProxyHandler"; 4 | import * as tf from "@tensorflow/tfjs"; 5 | // tslint:disable-next-line:no-var-requires 6 | const modelJson = require("../../../cocoSSDModel/model.json"); 7 | 8 | describe("Load default model from filesystem with TF io.IOHandler", () => { 9 | it("Check file system proxy is correctly called", async () => { 10 | const storageProviderMock = LocalFileSystemProxy as jest.Mock; 11 | storageProviderMock.mockClear(); 12 | 13 | storageProviderMock.prototype.readText = jest.fn((fileName) => { 14 | return Promise.resolve(JSON.stringify(modelJson)); 15 | }); 16 | 17 | storageProviderMock.prototype.readBinary = jest.fn((fileName) => { 18 | return Promise.resolve([]); 19 | }); 20 | 21 | const handler = new ElectronProxyHandler("folder", false); 22 | try { 23 | const model = await tf.loadGraphModel(handler); 24 | } catch (_) { 25 | // fully loading TF model fails as it has to load also weights 26 | } 27 | 28 | expect(LocalFileSystemProxy.prototype.readText).toBeCalledWith("/model.json"); 29 | 30 | // Coco SSD Lite default embedded model has 5 weights matrix 31 | expect(LocalFileSystemProxy.prototype.readBinary).toBeCalledTimes(5); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/providers/export/azureCustomVision.ui.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiKey": { 3 | "ui:widget": "protectedInput" 4 | }, 5 | "description": { 6 | "ui:widget": "textarea" 7 | }, 8 | "projectId": { 9 | "ui:widget": "externalPicker", 10 | "ui:options": { 11 | "method": "GET", 12 | "url": "https://${props.formContext.providerOptions.region}.api.cognitive.microsoft.com/customvision/v3.3/Training/projects", 13 | "authHeaderName": "Training-key", 14 | "authHeaderValue": "${props.formContext.providerOptions.apiKey}", 15 | "keySelector": "${item.id}", 16 | "valueSelector": "${item.name}" 17 | } 18 | }, 19 | "domainId": { 20 | "ui:widget": "externalPicker", 21 | "ui:options": { 22 | "method": "GET", 23 | "url": "https://${props.formContext.providerOptions.region}.api.cognitive.microsoft.com/customvision/v3.3/Training/domains", 24 | "authHeaderName": "Training-key", 25 | "authHeaderValue": "${props.formContext.providerOptions.apiKey}", 26 | "keySelector": "${item.id}", 27 | "valueSelector": "${item.name}", 28 | "filter": { 29 | "left": "${item.type}", 30 | "operator": "eq", 31 | "right": "${props.formContext.providerOptions.projectType}" 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/providers/export/cntk.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "title": "${strings.export.providers.cntk.displayName}", 4 | "properties": { 5 | "assetState": { 6 | "type": "string", 7 | "title": "${strings.export.providers.common.properties.assetState.title}", 8 | "description": "${strings.export.providers.common.properties.assetState.description}", 9 | "enum": [ 10 | "all", 11 | "visited", 12 | "tagged" 13 | ], 14 | "default": "visited", 15 | "enumNames": [ 16 | "${strings.export.providers.common.properties.assetState.options.all}", 17 | "${strings.export.providers.common.properties.assetState.options.visited}", 18 | "${strings.export.providers.common.properties.assetState.options.tagged}" 19 | ] 20 | }, 21 | "testTrainSplit": { 22 | "title": "${strings.export.providers.common.properties.testTrainSplit.title}", 23 | "description": "${strings.export.providers.common.properties.testTrainSplit.description}", 24 | "type": "number", 25 | "default": 80 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/providers/export/cntk.ui.json: -------------------------------------------------------------------------------- 1 | { 2 | "testTrainSplit": { 3 | "ui:widget": "slider" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/providers/export/csv.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "title": "${strings.export.providers.csv.displayName}", 4 | "properties": { 5 | "assetState": { 6 | "type": "string", 7 | "title": "${strings.export.providers.common.properties.assetState.title}", 8 | "description": "${strings.export.providers.common.properties.assetState.description}", 9 | "enum": [ 10 | "all", 11 | "visited", 12 | "tagged" 13 | ], 14 | "default": "visited", 15 | "enumNames": [ 16 | "${strings.export.providers.common.properties.assetState.options.all}", 17 | "${strings.export.providers.common.properties.assetState.options.visited}", 18 | "${strings.export.providers.common.properties.assetState.options.tagged}" 19 | ] 20 | }, 21 | "includeImages": { 22 | "type": "boolean", 23 | "default": true, 24 | "title": "${strings.export.providers.common.properties.includeImages.title}", 25 | "description": "${strings.export.providers.common.properties.includeImages.description}" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/providers/export/csv.ui.json: -------------------------------------------------------------------------------- 1 | { 2 | "includeImages": { 3 | "ui:widget": "checkbox" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/providers/export/pascalVOC.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "title": "${strings.export.providers.pascalVoc.displayName}", 4 | "properties": { 5 | "assetState": { 6 | "type": "string", 7 | "title": "${strings.export.providers.common.properties.assetState.title}", 8 | "description": "${strings.export.providers.common.properties.assetState.description}", 9 | "enum": [ 10 | "all", 11 | "visited", 12 | "tagged" 13 | ], 14 | "default": "visited", 15 | "enumNames": [ 16 | "${strings.export.providers.common.properties.assetState.options.all}", 17 | "${strings.export.providers.common.properties.assetState.options.visited}", 18 | "${strings.export.providers.common.properties.assetState.options.tagged}" 19 | ] 20 | }, 21 | "testTrainSplit": { 22 | "title": "${strings.export.providers.common.properties.testTrainSplit.title}", 23 | "description": "${strings.export.providers.common.properties.testTrainSplit.description}", 24 | "type": "number", 25 | "default": 80 26 | }, 27 | "exportUnassigned": { 28 | "title": "${strings.export.providers.pascalVoc.exportUnassigned.title}", 29 | "description": "${strings.export.providers.pascalVoc.exportUnassigned.description}", 30 | "type": "boolean", 31 | "default": true 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/providers/export/pascalVOC.ui.json: -------------------------------------------------------------------------------- 1 | { 2 | "testTrainSplit": { 3 | "ui:widget": "slider" 4 | }, 5 | "exportUnassigned": { 6 | "ui:widget": "checkbox" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/providers/export/pascalVOC/pascalVOCTemplates.ts: -------------------------------------------------------------------------------- 1 | export const itemTemplate = "\ 2 | item {\n\ 3 | id: ${id}\n\ 4 | name: '${tag}'\n\ 5 | }\n"; 6 | 7 | export const annotationTemplate = "\ 8 | \n\ 9 | Annotation\n\ 10 | ${fileName}\n\ 11 | ${filePath}\n\ 12 | \n\ 13 | Unknown\n\ 14 | \n\ 15 | \n\ 16 | ${width}\n\ 17 | ${height}\n\ 18 | 3\n\ 19 | \n\ 20 | 0\n\ 21 | ${objects}\n\ 22 | \n"; 23 | 24 | export const objectTemplate = "\ 25 | \n\ 26 | ${name}\n\ 27 | Unspecified\n\ 28 | 0\n\ 29 | 0\n\ 30 | \n\ 31 | ${xmin}\n\ 32 | ${ymin}\n\ 33 | ${xmax}\n\ 34 | ${ymax}\n\ 35 | \n\ 36 | "; 37 | -------------------------------------------------------------------------------- /src/providers/export/tensorFlowRecords.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "title": "${strings.export.providers.tfRecords.displayName}", 4 | "properties": { 5 | "assetState": { 6 | "type": "string", 7 | "title": "${strings.export.providers.common.properties.assetState.title}", 8 | "description": "${strings.export.providers.common.properties.assetState.description}", 9 | "enum": [ 10 | "all", 11 | "visited", 12 | "tagged" 13 | ], 14 | "default": "visited", 15 | "enumNames": [ 16 | "${strings.export.providers.common.properties.assetState.options.all}", 17 | "${strings.export.providers.common.properties.assetState.options.visited}", 18 | "${strings.export.providers.common.properties.assetState.options.tagged}" 19 | ] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/providers/export/tensorFlowRecords.ui.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/providers/export/tensorFlowRecords/tensorFlowBuilder.test.ts: -------------------------------------------------------------------------------- 1 | import { TFRecordsBuilder, FeatureType } from "./tensorFlowBuilder"; 2 | 3 | describe("TFRecords Builder Functions", () => { 4 | describe("Check Adding Single Features", () => { 5 | let builder: TFRecordsBuilder; 6 | beforeEach(() => { 7 | builder = new TFRecordsBuilder(); 8 | }); 9 | 10 | it("Check addIntFeature", async () => { 11 | builder.addFeature("image/height", FeatureType.Int64, 123); 12 | 13 | expect(builder.build()).toEqual( 14 | new Buffer([10, 23, 10, 21, 10, 12, 105, 109, 97, 103, 101, 47, 104, 15 | 101, 105, 103, 104, 116, 18, 5, 26, 3, 10, 1, 123])); 16 | }); 17 | 18 | it("Check addFloatFeature", async () => { 19 | builder.addFeature("image/height", FeatureType.Float, 123.0); 20 | 21 | expect(builder.build()).toEqual( 22 | new Buffer([10, 26, 10, 24, 10, 12, 105, 109, 97, 103, 101, 47, 104, 23 | 101, 105, 103, 104, 116, 18, 8, 18, 6, 10, 4, 0, 0, 246, 66])); 24 | }); 25 | 26 | it("Check addStringFeature", async () => { 27 | builder.addFeature("image/height", FeatureType.String, "123"); 28 | 29 | expect(builder.build()).toEqual( 30 | new Buffer([10, 25, 10, 23, 10, 12, 105, 109, 97, 103, 101, 47, 104, 31 | 101, 105, 103, 104, 116, 18, 7, 10, 5, 10, 3, 49, 50, 51])); 32 | }); 33 | }); 34 | 35 | describe("Check single TFRecord generation with arrays", () => { 36 | let builder: TFRecordsBuilder; 37 | 38 | it("Check releaseTFRecord", async () => { 39 | builder = new TFRecordsBuilder(); 40 | 41 | builder.addArrayFeature("image/height", FeatureType.Int64, [1, 2]); 42 | builder.addArrayFeature("image/height", FeatureType.Float, [1.0, 2.0]); 43 | builder.addArrayFeature("image/height", FeatureType.String, ["1", "2"]); 44 | 45 | const buffer = builder.build(); 46 | expect(buffer.length).toEqual(28); 47 | 48 | const tfrecords = TFRecordsBuilder.buildTFRecords([buffer]); 49 | // 16 = 8bytes for Lenght + 4bytes for CRC(Length) + 4bytes CRC(buffer) 50 | const headersSize = 16; 51 | expect(tfrecords.length).toEqual(28 + headersSize); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/providers/export/tensorFlowRecords/tensorFlowHelpers.test.ts: -------------------------------------------------------------------------------- 1 | import { crc32c, maskCrc, getInt64Buffer, getInt32Buffer, textEncode, textDecode } from "./tensorFlowHelpers"; 2 | 3 | describe("TFRecords Helper Functions", () => { 4 | describe("Run getInt64Buffer method test", () => { 5 | it("Check getInt64Buffer for number 164865", async () => { 6 | expect(getInt64Buffer(164865)).toEqual(new Buffer([1, 132, 2, 0, 0, 0, 0, 0])); 7 | }); 8 | }); 9 | 10 | describe("Run getInt32Buffer method test", () => { 11 | it("Check getInt32Buffer for number 164865", async () => { 12 | expect(getInt32Buffer(164865)).toEqual(new Buffer([1, 132, 2, 0])); 13 | }); 14 | }); 15 | 16 | describe("Run crc32c method test", () => { 17 | it("Check crc32c for number 164865", async () => { 18 | expect(crc32c(new Buffer([1, 132, 2, 0, 0, 0, 0, 0]))).toEqual(1310106699); 19 | }); 20 | }); 21 | 22 | describe("Run maskCrc method test", () => { 23 | it("Check maskCrc for crc 1310106699", async () => { 24 | expect(maskCrc(1310106699)).toEqual(3944318725); 25 | }); 26 | }); 27 | 28 | describe("Run integration of getInt32Buffer(maskCrc(crc32c(getInt64Buffer())) methods test", () => { 29 | it("Check maskCrc for for number 164865", async () => { 30 | expect(getInt32Buffer(maskCrc(crc32c(getInt64Buffer(164865))))) 31 | .toEqual(new Buffer([5, 135, 25, 235])); 32 | }); 33 | }); 34 | 35 | describe("Run textEncode method test", () => { 36 | it("Check textEncode for string 'ABC123'", async () => { 37 | expect(textEncode("ABC123")).toEqual(new Uint8Array([65, 66, 67, 49, 50, 51])); 38 | }); 39 | }); 40 | 41 | describe("Run textDecode method test", () => { 42 | it("Check textDecode for array [65, 66, 67, 49, 50, 51]", async () => { 43 | expect(textDecode(new Uint8Array([65, 66, 67, 49, 50, 51]))).toEqual("ABC123"); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/providers/export/tensorFlowRecords/tensorFlowRecordsProtoBuf.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package tensorflow; 3 | 4 | // Containers to hold repeated fundamental values. 5 | message BytesList { 6 | repeated bytes value = 1; 7 | } 8 | message FloatList { 9 | repeated float value = 1 [packed = true]; 10 | } 11 | message Int64List { 12 | repeated int64 value = 1 [packed = true]; 13 | } 14 | 15 | // Containers for non-sequential data. 16 | message Feature { 17 | // Each feature can be exactly one kind. 18 | oneof kind { 19 | BytesList bytes_list = 1; 20 | FloatList float_list = 2; 21 | Int64List int64_list = 3; 22 | } 23 | }; 24 | 25 | message Features { 26 | // Map from feature name to feature. 27 | map feature = 1; 28 | }; 29 | 30 | // Containers for sequential data. 31 | // 32 | // A FeatureList contains lists of Features. These may hold zero or more 33 | // Feature values. 34 | // 35 | // FeatureLists are organized into categories by name. The FeatureLists message 36 | // contains the mapping from name to FeatureList. 37 | // 38 | message FeatureList { 39 | repeated Feature feature = 1; 40 | }; 41 | 42 | message FeatureLists { 43 | // Map from feature name to feature list. 44 | map feature_list = 1; 45 | }; 46 | 47 | // Containers for TFRecords Image data. 48 | // 49 | message TFRecordsImageMessage { 50 | Features context = 1; 51 | FeatureLists feature_lists = 2; 52 | }; 53 | 54 | -------------------------------------------------------------------------------- /src/providers/export/testAssetsSplitHelper.ts: -------------------------------------------------------------------------------- 1 | import { IAssetMetadata, ITag } from "../../models/applicationState"; 2 | 3 | /** 4 | * A helper function to split train and test assets 5 | * @param template String containing variables 6 | * @param params Params containing substitution values 7 | */ 8 | export function splitTestAsset(allAssets: IAssetMetadata[], tags: ITag[], testSplitRatio: number): string[] { 9 | if (testSplitRatio <= 0 || testSplitRatio > 1) { return []; } 10 | 11 | const testAssets: string[] = []; 12 | const tagsAssetDict: { [index: string]: { assetList: Set } } = {}; 13 | tags.forEach((tag) => tagsAssetDict[tag.name] = { assetList: new Set() }); 14 | allAssets.forEach((assetMetadata) => { 15 | assetMetadata.regions.forEach((region) => { 16 | region.tags.forEach((tagName) => { 17 | if (tagsAssetDict[tagName]) { 18 | tagsAssetDict[tagName].assetList.add(assetMetadata.asset.name); 19 | } 20 | }); 21 | }); 22 | }); 23 | 24 | for (const tagKey of Object.keys(tagsAssetDict)) { 25 | const assetList = tagsAssetDict[tagKey].assetList; 26 | const testCount = Math.ceil(assetList.size * testSplitRatio); 27 | testAssets.push(...Array.from(assetList).slice(0, testCount)); 28 | } 29 | return testAssets; 30 | } 31 | -------------------------------------------------------------------------------- /src/providers/export/vottJson.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "title": "${strings.export.providers.vottJson.displayName}", 4 | "properties": { 5 | "assetState": { 6 | "type": "string", 7 | "title": "${strings.export.providers.common.properties.assetState.title}", 8 | "description": "${strings.export.providers.common.properties.assetState.description}", 9 | "enum": [ 10 | "all", 11 | "visited", 12 | "tagged" 13 | ], 14 | "default": "visited", 15 | "enumNames": [ 16 | "${strings.export.providers.common.properties.assetState.options.all}", 17 | "${strings.export.providers.common.properties.assetState.options.visited}", 18 | "${strings.export.providers.common.properties.assetState.options.tagged}" 19 | ] 20 | }, 21 | "includeImages": { 22 | "type": "boolean", 23 | "default": true, 24 | "title": "${strings.export.providers.common.properties.includeImages.title}", 25 | "description": "${strings.export.providers.common.properties.includeImages.description}" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/providers/export/vottJson.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import { ExportProvider } from "./exportProvider"; 3 | import { IProject, IExportProviderOptions } from "../../models/applicationState"; 4 | import Guard from "../../common/guard"; 5 | import { constants } from "../../common/constants"; 6 | import HtmlFileReader from "../../common/htmlFileReader"; 7 | 8 | /** 9 | * VoTT Json Export Provider options 10 | */ 11 | export interface IVottJsonExportProviderOptions extends IExportProviderOptions { 12 | /** Whether or not to include binary assets in target connection */ 13 | includeImages: boolean; 14 | } 15 | 16 | /** 17 | * @name - Vott Json Export Provider 18 | * @description - Exports a project into a single JSON file that include all configured assets 19 | */ 20 | export class VottJsonExportProvider extends ExportProvider { 21 | constructor(project: IProject, options: IVottJsonExportProviderOptions) { 22 | super(project, options); 23 | Guard.null(options); 24 | } 25 | 26 | /** 27 | * Export project to VoTT JSON format 28 | */ 29 | public async export(): Promise { 30 | const results = await this.getAssetsForExport(); 31 | 32 | if (this.options.includeImages) { 33 | await results.forEachAsync(async (assetMetadata) => { 34 | const arrayBuffer = await HtmlFileReader.getAssetArray(assetMetadata.asset); 35 | const assetFilePath = `vott-json-export/${assetMetadata.asset.name}`; 36 | await this.storageProvider.writeBinary(assetFilePath, Buffer.from(arrayBuffer)); 37 | }); 38 | } 39 | 40 | const exportObject = { ...this.project }; 41 | exportObject.assets = _.keyBy(results, (assetMetadata) => assetMetadata.asset.id) as any; 42 | 43 | // We don't need these fields in the export JSON 44 | delete exportObject.sourceConnection; 45 | delete exportObject.targetConnection; 46 | delete exportObject.exportFormat; 47 | 48 | const fileName = `vott-json-export/${this.project.name.replace(/\s/g, "-")}${constants.exportFileExtension}`; 49 | await this.storageProvider.writeText(fileName, JSON.stringify(exportObject, null, 4)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/providers/export/vottJson.ui.json: -------------------------------------------------------------------------------- 1 | { 2 | "includeImages": { 3 | "ui:widget": "checkbox" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/providers/storage/assetProviderFactory.test.ts: -------------------------------------------------------------------------------- 1 | import { AssetProviderFactory, IAssetProvider } from "./assetProviderFactory"; 2 | import { IAsset } from "../../models/applicationState"; 3 | 4 | describe("Asset Provider Factory", () => { 5 | it("registers new storage providers", () => { 6 | expect(Object.keys(AssetProviderFactory.providers).length).toEqual(0); 7 | AssetProviderFactory.register("testProvider", () => new TestAssetProvider()); 8 | expect(Object.keys(AssetProviderFactory.providers).length).toEqual(1); 9 | }); 10 | 11 | it("creates a new instance of the provider", () => { 12 | AssetProviderFactory.register("testProvider", () => new TestAssetProvider()); 13 | const provider = AssetProviderFactory.create("testProvider"); 14 | 15 | expect(provider).not.toBeNull(); 16 | expect(provider).toBeInstanceOf(TestAssetProvider); 17 | }); 18 | 19 | it("throws error if provider is not found", () => { 20 | expect(() => AssetProviderFactory.create("unknown")).toThrowError(); 21 | }); 22 | }); 23 | 24 | class TestAssetProvider implements IAssetProvider { 25 | public initialize(): Promise { 26 | throw new Error("Method not implemented"); 27 | } 28 | public getAssets(): Promise { 29 | throw new Error("Method not implemented."); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/providers/storage/azureBlobStorage.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "${strings.connections.providers.azureBlob.title}", 3 | "description": "${strings.connections.providers.azureBlob.description}", 4 | "required": [ 5 | "accountName", 6 | "containerName", 7 | "sas" 8 | ], 9 | "type": "object", 10 | "properties": { 11 | "accountName": { 12 | "title": "${strings.connections.providers.azureBlob.accountName.title}", 13 | "type": "string" 14 | }, 15 | "containerName": { 16 | "title": "${strings.connections.providers.azureBlob.containerName.title}", 17 | "type": "string" 18 | }, 19 | "sas": { 20 | "title": "${strings.connections.providers.azureBlob.sas.title}", 21 | "description": "${strings.connections.providers.azureBlob.sas.description}", 22 | "type": "string" 23 | }, 24 | "createContainer": { 25 | "title": "${strings.connections.providers.azureBlob.createContainer.title}", 26 | "description": "${strings.connections.providers.azureBlob.createContainer.description}", 27 | "type": "boolean" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/providers/storage/azureBlobStorage.ui.json: -------------------------------------------------------------------------------- 1 | { 2 | "sas": { 3 | "ui:widget": "protectedInput" 4 | }, 5 | "createContainer": { 6 | "ui:widget": "checkbox" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/providers/storage/bingImageSearch.ui.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiKey": { 3 | "ui:widget": "protectedInput" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/providers/storage/localFileSystemProxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "${strings.connections.providers.local.title}", 3 | "required": [ 4 | "folderPath" 5 | ], 6 | "type": "object", 7 | "properties": { 8 | "folderPath": { 9 | "title": "${strings.connections.providers.local.folderPath}", 10 | "type": "string" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/providers/storage/localFileSystemProxy.ui.json: -------------------------------------------------------------------------------- 1 | { 2 | "folderPath": { 3 | "ui:widget": "localFolderPicker" 4 | } 5 | } -------------------------------------------------------------------------------- /src/providers/toolbar/toolbarItemFactory.test.ts: -------------------------------------------------------------------------------- 1 | import { ToolbarItemFactory } from "./toolbarItemFactory"; 2 | import { IToolbarItemMetadata, ToolbarItemType, ToolbarItem } from "../../react/components/toolbar/toolbarItem"; 3 | import registerToolbar, { ToolbarItemName, ToolbarItemGroup } from "../../registerToolbar"; 4 | 5 | class TestToolbarItem extends ToolbarItem { 6 | protected onItemClick() { 7 | throw new Error("Method not implemented."); 8 | } 9 | } 10 | 11 | describe("Toolbar Item Factory", () => { 12 | const testToolbarItemConfig: IToolbarItemMetadata = { 13 | name: ToolbarItemName.SelectCanvas, 14 | group: ToolbarItemGroup.Canvas, 15 | icon: "fa-test", 16 | tooltip: "Test Component", 17 | type: ToolbarItemType.Action, 18 | }; 19 | 20 | it("Register add a new component registration to the registry", () => { 21 | const existingItems = ToolbarItemFactory.getToolbarItems(); 22 | expect(existingItems.length).toEqual(0); 23 | 24 | ToolbarItemFactory.register(testToolbarItemConfig, TestToolbarItem); 25 | const newItems = ToolbarItemFactory.getToolbarItems(); 26 | expect(newItems.length).toEqual(1); 27 | expect(newItems[0].config).toEqual(testToolbarItemConfig); 28 | expect(newItems[0].component).toEqual(TestToolbarItem); 29 | }); 30 | 31 | it("Registering a toolbar item with invalid values throws an exception", () => { 32 | expect(() => ToolbarItemFactory.register(null, null)).toThrowError(); 33 | expect(() => ToolbarItemFactory.register(null, TestToolbarItem)).toThrowError(); 34 | expect(() => ToolbarItemFactory.register(testToolbarItemConfig, null)).toThrowError(); 35 | }); 36 | 37 | it("Calling 'getToolbarItems' returns a copy of the component registry", () => { 38 | ToolbarItemFactory.reset(); 39 | 40 | const itemsResult1 = ToolbarItemFactory.getToolbarItems(); 41 | registerToolbar(); 42 | const itemsResult2 = ToolbarItemFactory.getToolbarItems(); 43 | const itemsResult3 = ToolbarItemFactory.getToolbarItems(); 44 | 45 | expect(itemsResult1.length).toEqual(0); 46 | expect(itemsResult2.length).toBeGreaterThan(0); 47 | expect(itemsResult2.length).toEqual(itemsResult3.length); 48 | expect(itemsResult2).toEqual(itemsResult3); 49 | expect(itemsResult2).not.toBe(itemsResult3); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/providers/toolbar/toolbarItemFactory.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import { IToolbarItemMetadata, ToolbarItem } from "../../react/components/toolbar/toolbarItem"; 3 | import Guard from "../../common/guard"; 4 | 5 | /** 6 | * Interface for registering toolbar items 7 | */ 8 | export interface IToolbarItemRegistration { 9 | component: typeof ToolbarItem; 10 | config: IToolbarItemMetadata; 11 | } 12 | 13 | /** 14 | * @name - Toolbar Item Factory 15 | * @description - Creates instance of Toolbar Items based on specified options 16 | */ 17 | export class ToolbarItemFactory { 18 | /** 19 | * Register Toolbar Item for use in editor page 20 | * @param component - React component ToolbarItem 21 | * @param config - Configuration of ToolbarItem 22 | */ 23 | public static register(config: IToolbarItemMetadata, component: typeof ToolbarItem = ToolbarItem) { 24 | Guard.null(component); 25 | Guard.null(config); 26 | 27 | ToolbarItemFactory.componentRegistry.push({ component, config }); 28 | } 29 | 30 | /** 31 | * Get all registered Toolbar Items 32 | */ 33 | public static getToolbarItems() { 34 | return [...ToolbarItemFactory.componentRegistry]; 35 | } 36 | 37 | /** 38 | * Clear ToolbarItem Registry 39 | */ 40 | public static reset(): void { 41 | ToolbarItemFactory.componentRegistry = []; 42 | } 43 | 44 | private static componentRegistry: IToolbarItemRegistration[] = []; 45 | } 46 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/react/components/common/arrayField/arrayFieldTemplate.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ArrayFieldTemplateProps } from "react-jsonschema-form"; 3 | import { strings } from "../../../../common/strings"; 4 | 5 | export function ArrayFieldTemplate(props: ArrayFieldTemplateProps) { 6 | return ( 7 |
8 | {props.canAdd && 9 |
10 | 14 |
15 | } 16 | {props.items.map((item) => { 17 | return
18 | {item.children} 19 | {item.hasRemove && 20 |
21 | 28 |
29 | } 30 |
; 31 | })} 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/react/components/common/assetPreview/imageAsset.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { IAssetProps } from "./assetPreview"; 3 | 4 | /** 5 | * ImageAsset component used to render all image assets 6 | */ 7 | export class ImageAsset extends React.Component { 8 | private image: React.RefObject = React.createRef(); 9 | 10 | public render() { 11 | return ( 12 | ); 17 | } 18 | 19 | private onLoad = () => { 20 | if (this.props.onLoaded) { 21 | this.props.onLoaded(this.image.current); 22 | } 23 | if (this.props.onActivated) { 24 | this.props.onActivated(this.image.current); 25 | } 26 | if (this.props.onDeactivated) { 27 | this.props.onDeactivated(this.image.current); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/react/components/common/colorPicker.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { GithubPicker, CirclePicker } from "react-color"; 3 | 4 | export interface IColorPickerProps { 5 | show: boolean; 6 | color: string; 7 | colors: string[]; 8 | onEditColor: (color: string) => void; 9 | } 10 | 11 | export class ColorPicker extends React.Component { 12 | 13 | private pickerBackground = "#252526"; 14 | 15 | public render() { 16 | return ( 17 | this.props.show && 18 | this.GithubPicker() 19 | ); 20 | } 21 | 22 | private onChange = (color) => { 23 | this.props.onEditColor(color.hex); 24 | } 25 | 26 | private GithubPicker = () => { 27 | return ( 28 |
29 | 43 |
44 | ); 45 | } 46 | 47 | private CirclePicker = () => { 48 | return ( 49 |
50 | 57 |
58 | 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/react/components/common/common.scss: -------------------------------------------------------------------------------- 1 | .inline-block { 2 | display: inline-block; 3 | } 4 | 5 | @media (min-width: 1280px) { 6 | form { 7 | max-width: 1165px; 8 | } 9 | } 10 | 11 | .form-row { 12 | align-items: center; 13 | margin: 0; 14 | 15 | .object-wrapper { 16 | display: flex; 17 | flex-direction: row; 18 | flex-grow: 1; 19 | margin-bottom: 0; 20 | } 21 | 22 | .form-group { 23 | margin-right: 1em; 24 | flex-grow: 1; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/react/components/common/condensedList/condensedList.scss: -------------------------------------------------------------------------------- 1 | @import './../../../../assets/sass/theme.scss'; 2 | 3 | .condensed-list { 4 | display: flex; 5 | flex-grow: 1; 6 | flex-direction: column; 7 | 8 | &-header { 9 | font-size: 80%; 10 | color: #f1f1f1; 11 | margin: 0; 12 | text-transform: uppercase; 13 | 14 | i { 15 | margin-right: 0.5em; 16 | } 17 | } 18 | 19 | &-body { 20 | flex-grow: 1; 21 | display: flex; 22 | overflow: auto; 23 | flex-direction: column; 24 | position: relative; 25 | } 26 | } 27 | 28 | ul.condensed-list-items { 29 | margin: 0; 30 | padding: 0; 31 | 32 | li { 33 | list-style-type: none; 34 | font-size: 90%; 35 | color: #ccc; 36 | 37 | a { 38 | cursor: pointer; 39 | width: 100%; 40 | padding: 0.25em 1em; 41 | display: inline-block; 42 | text-decoration: none; 43 | 44 | &.active, &:hover { 45 | background-color: $lighter-2; 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/react/components/common/conditionalNavLink/conditionalNavLink.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ConditionalNavLink from "./conditionalNavLink"; 3 | import { mount, ReactWrapper } from "enzyme"; 4 | import { BrowserRouter as Router } from "react-router-dom"; 5 | 6 | describe("Conditional Nav Link", () => { 7 | function createLink(to: string, disabled: boolean, props: any): ReactWrapper { 8 | return mount( 9 | 10 | 14 | /* Example of child components */ 15 | 16 | , 17 | ); 18 | } 19 | 20 | it("Renders as a span element when disabled", () => { 21 | const props = { title: "Test Title" }; 22 | const disabled = true; 23 | 24 | const wrapper = createLink("/test", disabled, props); 25 | const disabledLink = wrapper.find("span").first(); 26 | expect(disabledLink).not.toBeNull(); 27 | expect(disabledLink.prop("title")).toEqual(props.title); 28 | const innerElem = disabledLink.find(".fa-user"); 29 | expect(innerElem.length).toEqual(1); 30 | }); 31 | 32 | it("Renders as a anchor element when enabled", () => { 33 | const props = { title: "Test Title" }; 34 | const disabled = false; 35 | const expectedHref = "/test"; 36 | 37 | const wrapper = createLink(expectedHref, disabled, props); 38 | const enabledLink = wrapper.find("a").first(); 39 | expect(enabledLink).not.toBeNull(); 40 | expect(enabledLink.prop("href")).toEqual(expectedHref); 41 | expect(enabledLink.prop("title")).toEqual(props.title); 42 | const innerElem = enabledLink.find(".fa-user"); 43 | expect(innerElem.length).toEqual(1); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/react/components/common/conditionalNavLink/conditionalNavLink.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | 4 | /** 5 | * Link able to be enabled/disabled 6 | * @param param0 - { 7 | * to: "link for item" 8 | * disabled: true if link is disabled 9 | * props: { 10 | * title: Title of item, 11 | * children: Child items to include in span 12 | * } 13 | * } 14 | */ 15 | export default function ConditionalNavLink({ to, disabled, ...props }) { 16 | if (disabled) { 17 | return ({props.children}); 18 | } else { 19 | return ({props.children}); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/react/components/common/connectionPicker/connectionPicker.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ConnectionPicker, ConnectionPickerWithRouter, IConnectionPickerProps } from "./connectionPicker"; 3 | import { BrowserRouter as Router } from "react-router-dom"; 4 | import MockFactory from "../../../../common/mockFactory"; 5 | import { mount, ReactWrapper } from "enzyme"; 6 | import { IConnection } from "../../../../models/applicationState"; 7 | 8 | describe("Connection Picker Component", () => { 9 | let wrapper: any = null; 10 | let connections: IConnection[] = null; 11 | let onChangeHandler: (value: any) => void; 12 | 13 | beforeEach(() => { 14 | connections = MockFactory.createTestConnections(); 15 | 16 | onChangeHandler = jest.fn(); 17 | 18 | wrapper = mount( 19 | 20 | 25 | , 26 | ); 27 | }); 28 | 29 | it("renders a default 'Select Connection' option", () => { 30 | const firstOption = wrapper.find("option").first(); 31 | expect(firstOption.text()).toEqual("Select Connection"); 32 | }); 33 | 34 | it("renders options from connection props", () => { 35 | expect(wrapper).not.toBeNull(); 36 | const optionElements = wrapper.find("option"); 37 | expect(optionElements.length).toEqual(connections.length + 1); 38 | expect(wrapper.prop("value")).not.toBeDefined(); 39 | }); 40 | 41 | it("raises onChange event when dropdown is modified", () => { 42 | const newConnection = connections[1]; 43 | 44 | wrapper.find("select").simulate("change", { target: { value: newConnection.id } }); 45 | expect(onChangeHandler).toBeCalledWith(newConnection); 46 | }); 47 | 48 | it("navigates to create connection page when clicking on Add Connection button", () => { 49 | const connectionPicker = wrapper.find(ConnectionPicker) as ReactWrapper; 50 | const pushSpy = jest.spyOn(connectionPicker.props().history, "push"); 51 | connectionPicker.find("button.add-connection").simulate("click"); 52 | expect(pushSpy).toBeCalledWith("/connections/create"); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/react/components/common/connectionProviderPicker/connectionProviderPicker.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import { StorageProviderFactory } from "../../../../providers/storage/storageProviderFactory"; 4 | import { AssetProviderFactory } from "../../../../providers/storage/assetProviderFactory"; 5 | 6 | /** 7 | * Properties for Connection Provider Picker 8 | * @member onChange - Function to call on change of selected value 9 | * @member id - ID of HTML select element 10 | * @member value - Selected value of picker 11 | */ 12 | export interface IConnectionProviderPickerProps { 13 | onChange: (value: string) => void; 14 | id: string; 15 | value: string; 16 | } 17 | 18 | /** 19 | * Creates HTML select object for selecting an asset or storage provider 20 | * @param props Properties for picker 21 | */ 22 | export default function ConnectionProviderPicker(props: IConnectionProviderPickerProps) { 23 | const storageProviders = _.values(StorageProviderFactory.providers); 24 | const assetProviders = _.values(AssetProviderFactory.providers); 25 | 26 | const allProviders = _([]) 27 | .concat(assetProviders) 28 | .concat(storageProviders) 29 | .uniqBy("name") 30 | .orderBy("displayName") 31 | .value(); 32 | 33 | function onChange(e) { 34 | props.onChange(e.target.value); 35 | } 36 | 37 | return ( 38 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/react/components/common/customField/customField.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FieldProps, WidgetProps } from "react-jsonschema-form"; 3 | import Guard from "../../../../common/guard"; 4 | 5 | /** 6 | * Custom field for react-jsonschema-form 7 | * @param Widget UI Widget for form 8 | * @param mapProps Function mapping props to an object 9 | */ 10 | export function CustomField(Widget: any, mapProps?: (props: FieldProps) => Props) { 11 | Guard.null(Widget); 12 | 13 | return function render(props: FieldProps) { 14 | const widgetProps = mapProps ? mapProps(props) : props; 15 | return (); 16 | }; 17 | } 18 | 19 | /** 20 | * Custom widget for react-jsonschema-form 21 | * @param Widget UI Widget for form 22 | * @param mapProps Function mapping component props to form widget props 23 | */ 24 | export function CustomWidget(Widget: any, mapProps?: (props: WidgetProps) => Props) { 25 | Guard.null(Widget); 26 | 27 | return function render(props: WidgetProps) { 28 | const widgetProps = mapProps ? mapProps(props) : props; 29 | return (); 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/react/components/common/customField/customFieldTemplate.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { FieldTemplateProps } from "react-jsonschema-form"; 3 | 4 | export default function CustomFieldTemplate(props: FieldTemplateProps) { 5 | const { id, label, required, description, rawErrors, schema, uiSchema, children } = props; 6 | const classNames = []; 7 | if (props.schema.type === "object") { 8 | classNames.push("object-wrapper"); 9 | } else { 10 | classNames.push("form-group"); 11 | } 12 | 13 | if (rawErrors && rawErrors.length > 0) { 14 | classNames.push("is-invalid"); 15 | } else { 16 | classNames.push("is-valid"); 17 | } 18 | 19 | return ( 20 |
21 | { /* Render label for non-objects except for when an object has defined a ui:field template */} 22 | {schema.type !== "array" && 23 | (schema.type !== "object" || (schema.type === "object" && uiSchema["ui:field"])) && 24 | 25 | } 26 | {schema.type === "array" && 27 | 28 |

{label}

29 | {description && {description}} 30 |
31 | } 32 | {children} 33 | {schema.type !== "array" && description && {description}} 34 | {rawErrors && rawErrors.length > 0 && 35 |
36 | {rawErrors.map((errorMessage, idx) =>
{label} {errorMessage}
)} 37 |
38 | } 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/react/components/common/exportProviderPicker/exportProviderPicker.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import { ExportProviderFactory } from "../../../../providers/export/exportProviderFactory"; 4 | 5 | /** 6 | * Properties for Export Provider Picker 7 | * @member onChange - Function to call on change of selected value 8 | * @member id - ID for HTML select element 9 | * @member value - Selected value in picker 10 | */ 11 | export interface IExportProviderPickerProps { 12 | onChange: (value: string) => void; 13 | id: string; 14 | value: string; 15 | } 16 | 17 | /** 18 | * Creates HTML select object for selecting an asset or storage provider 19 | * @param props Properties for picker 20 | */ 21 | export default function ExportProviderPicker(props: IExportProviderPickerProps) { 22 | const exportProviders = _.values(ExportProviderFactory.providers); 23 | 24 | const allProviders = _([]) 25 | .concat(exportProviders) 26 | .orderBy("displayName") 27 | .value(); 28 | 29 | function onChange(e) { 30 | props.onChange(e.target.value); 31 | } 32 | 33 | return ( 34 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/react/components/common/filePicker/filePicker.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { RefObject } from "react"; 2 | import { ReactWrapper, mount } from "enzyme"; 3 | import FilePicker from "./filePicker"; 4 | import HtmlFileReader from "../../../../common/htmlFileReader"; 5 | 6 | describe("File Picker Component", () => { 7 | let wrapper: ReactWrapper = null; 8 | const onChangeHandler = jest.fn(); 9 | const onErrorHandler = jest.fn(); 10 | 11 | function createComponent(): ReactWrapper { 12 | return mount( 13 | , 16 | ); 17 | } 18 | 19 | beforeEach(() => { 20 | wrapper = createComponent(); 21 | }); 22 | 23 | it("Renders a HTML input with type file element", () => { 24 | const input = wrapper.find("input").first(); 25 | expect(input).not.toBeNull(); 26 | expect(input.prop("type")).toEqual("file"); 27 | }); 28 | 29 | it("Calls the onChange handler on successfull file upload", (done) => { 30 | const expectedContent = "test file content"; 31 | HtmlFileReader.readAsText = jest.fn(() => Promise.resolve(expectedContent)); 32 | const event: any = { 33 | target: { 34 | files: ["text.txt"], 35 | }, 36 | }; 37 | 38 | wrapper.find("input").first().simulate("change", event); 39 | 40 | setImmediate(() => { 41 | expect(onChangeHandler).toBeCalledWith(expect.anything(), expectedContent); 42 | done(); 43 | }); 44 | }); 45 | 46 | it("Calls the onError handler on errored / cancelled file upload", (done) => { 47 | const event: any = { 48 | target: { 49 | files: [], 50 | }, 51 | }; 52 | 53 | wrapper.find("input").first().simulate("change", event); 54 | 55 | setImmediate(() => { 56 | expect(onErrorHandler).toBeCalledWith(expect.anything(), "No files were selected"); 57 | done(); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/react/components/common/filePicker/filePicker.tsx: -------------------------------------------------------------------------------- 1 | import React, { SyntheticEvent } from "react"; 2 | import shortid from "shortid"; 3 | import HtmlFileReader from "../../../../common/htmlFileReader"; 4 | import { IFileInfo } from "../../../../models/applicationState"; 5 | 6 | /** 7 | * Properties for File Picker 8 | * @member onChange - Function to call on change of file selection 9 | * @member onError - Function to call on file picking error 10 | */ 11 | export interface IFilePickerProps { 12 | onChange: (sender: SyntheticEvent, fileText: IFileInfo) => void; 13 | onError: (sender: SyntheticEvent, error: any) => void; 14 | } 15 | 16 | /** 17 | * @name - File Picker 18 | * @description - Pick file from local file system 19 | */ 20 | export default class FilePicker extends React.Component { 21 | private fileInput; 22 | 23 | constructor(props, context) { 24 | super(props, context); 25 | 26 | this.fileInput = React.createRef(); 27 | this.onFileUploaded = this.onFileUploaded.bind(this); 28 | } 29 | 30 | /** 31 | * Call click on current file input 32 | */ 33 | public upload = () => { 34 | this.fileInput.current.click(); 35 | } 36 | 37 | public render() { 38 | return ( 39 | 40 | ); 41 | } 42 | 43 | private onFileUploaded = (e) => { 44 | if (e.target.files.length === 0) { 45 | this.props.onError(e, "No files were selected"); 46 | } 47 | 48 | HtmlFileReader.readAsText(e.target.files[0]) 49 | .then((fileInfo) => this.props.onChange(e, fileInfo)) 50 | .catch((err) => this.props.onError(e, err)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/react/components/common/keyboardBinding/keyboardBinding.tsx: -------------------------------------------------------------------------------- 1 | import { KeyboardContext, IKeyboardContext, KeyEventType } from "../keyboardManager/keyboardManager"; 2 | import React from "react"; 3 | 4 | /** 5 | * Properties needed for a keyboard binding 6 | */ 7 | export interface IKeyboardBindingProps { 8 | /** Keys that the action is bound to */ 9 | accelerators: string[]; 10 | /** Friendly name for keyboard binding for display in help menu */ 11 | displayName: string; 12 | /** Action to trigger upon key event */ 13 | handler: (evt?: KeyboardEvent) => void; 14 | /** Type of key event (keypress, keyup, keydown) */ 15 | keyEventType?: KeyEventType; 16 | /** Icon to display in help menu */ 17 | icon?: string; 18 | } 19 | 20 | export class KeyboardBinding extends React.Component { 21 | public static contextType = KeyboardContext; 22 | public context!: IKeyboardContext; 23 | private deregisterBinding: () => void; 24 | 25 | public componentDidMount() { 26 | if (this.context && this.context.keyboard) { 27 | this.deregisterBinding = this.context.keyboard.registerBinding(this.props); 28 | } else { 29 | console.warn("Keyboard Mananger context cannot be found - Keyboard binding has NOT been set."); 30 | } 31 | } 32 | 33 | public componentWillUnmount() { 34 | if (this.deregisterBinding) { 35 | this.deregisterBinding(); 36 | } 37 | } 38 | 39 | public render() { 40 | return null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/react/components/common/localFolderPicker/localFolderPicker.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import LocalFolderPicker from "./localFolderPicker"; 3 | import { mount } from "enzyme"; 4 | 5 | jest.mock("../../../../providers/storage/localFileSystemProxy"); 6 | import { LocalFileSystemProxy } from "../../../../providers/storage/localFileSystemProxy"; 7 | 8 | describe("Local Folder Picker Component", () => { 9 | const onChangeHandler = jest.fn(); 10 | 11 | function createComponent(value: string, onChangeHandler: () => void) { 12 | return mount( 13 | , 16 | ); 17 | } 18 | 19 | it("renders correctly", () => { 20 | const wrapper = createComponent(null, onChangeHandler); 21 | const input = wrapper.find("input"); 22 | const button = wrapper.find("button"); 23 | 24 | expect(input.length).toEqual(1); 25 | expect(button.length).toEqual(1); 26 | }); 27 | 28 | it("sets input value from null props", () => { 29 | const wrapper = createComponent(null, onChangeHandler); 30 | const expectedValue = ""; 31 | const actualValue = wrapper.state()["value"]; 32 | expect(actualValue).toEqual(expectedValue); 33 | }); 34 | 35 | it("sets input value from set props", () => { 36 | const expectedValue = "C:\\Users\\User1\\Desktop"; 37 | const wrapper = createComponent(expectedValue, onChangeHandler); 38 | const actualValue = wrapper.state()["value"]; 39 | expect(actualValue).toEqual(expectedValue); 40 | }); 41 | 42 | it("Calls electron to select folder from open dialog", (done) => { 43 | const expectedValue = "C:\\Users\\User1\\test.txt"; 44 | const mocked = LocalFileSystemProxy as jest.Mocked; 45 | mocked.prototype.selectContainer = jest.fn(() => Promise.resolve(expectedValue)); 46 | 47 | const wrapper = createComponent(null, onChangeHandler); 48 | wrapper.find("button").simulate("click"); 49 | 50 | setImmediate(() => { 51 | expect(onChangeHandler).toBeCalledWith(expectedValue); 52 | done(); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/react/components/common/objectField/objectFieldTemplate.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { ObjectFieldTemplateProps } from "react-jsonschema-form"; 3 | 4 | export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { 5 | return ( 6 | 7 | {props.title} 8 | {props.description} 9 | {props.properties.map((item) => item.content)} 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/react/components/common/securityTokenPicker/securityTokenPicker.tsx: -------------------------------------------------------------------------------- 1 | import React, { SyntheticEvent } from "react"; 2 | import { ISecurityToken } from "../../../../models/applicationState"; 3 | 4 | /** 5 | * Security Token Picker Properties 6 | * @member id - The id to bind to the input element 7 | * @member value - The value to bind to the input element 8 | * @member securityTokens - The list of security tokens to display 9 | * @member onChange - The event handler to call when the input value changes 10 | */ 11 | export interface ISecurityTokenPickerProps { 12 | id?: string; 13 | value: string; 14 | securityTokens: ISecurityToken[]; 15 | onChange: (value: string) => void; 16 | } 17 | 18 | /** 19 | * Security Token Picker 20 | * @description - Used to display a list of security tokens 21 | */ 22 | export class SecurityTokenPicker extends React.Component { 23 | constructor(props) { 24 | super(props); 25 | 26 | this.onChange = this.onChange.bind(this); 27 | } 28 | 29 | public render() { 30 | return ( 31 | 38 | ); 39 | } 40 | 41 | private onChange(e: SyntheticEvent) { 42 | const inputElement = e.target as HTMLSelectElement; 43 | this.props.onChange(inputElement.value ? inputElement.value : undefined); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/react/components/common/slider/slider.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ISliderProps, Slider } from "./slider"; 3 | import { mount, ReactWrapper } from "enzyme"; 4 | import RcSlider from "rc-slider"; 5 | 6 | describe("Slider Component", () => { 7 | const onChangeHandler = jest.fn(); 8 | const defaultProps: ISliderProps = { 9 | value: 80, 10 | onChange: onChangeHandler, 11 | }; 12 | 13 | function createComponent(props?: ISliderProps): ReactWrapper { 14 | props = props || defaultProps; 15 | return mount(); 16 | } 17 | 18 | let wrapper: ReactWrapper; 19 | 20 | beforeEach(() => { 21 | wrapper = createComponent(); 22 | }); 23 | 24 | it("renders correctly", () => { 25 | expect(wrapper.find(".slider-value").text()).toEqual(defaultProps.value.toString()); 26 | expect(wrapper.find(RcSlider).exists()).toBe(true); 27 | }); 28 | 29 | it("raises onChange handler when value has changed", () => { 30 | const expectedValue = 60; 31 | const slider = wrapper.find(RcSlider) as ReactWrapper; 32 | slider.props().onChange(expectedValue); 33 | 34 | expect(onChangeHandler).toBeCalledWith(expectedValue); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/react/components/common/slider/slider.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RcSlider from "rc-slider"; 3 | import "rc-slider/assets/index.css"; 4 | 5 | export interface ISliderProps { 6 | value: number; 7 | min?: number; 8 | max?: number; 9 | onChange: (value) => void; 10 | disabled?: boolean; 11 | } 12 | 13 | /** 14 | * Slider component to select a value between a min / max range 15 | */ 16 | export class Slider extends React.Component { 17 | public render() { 18 | return ( 19 |
20 | {this.props.value} 21 | 22 |
23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/react/components/common/tagColors.json: -------------------------------------------------------------------------------- 1 | [ 2 | "#5db300", 3 | "#e81123", 4 | "#6917aa", 5 | "#015cda", 6 | "#4894fe", 7 | "#6b849c", 8 | "#70c400", 9 | "#f7929a", 10 | "#257ffe", 11 | "#ddabe9", 12 | "#ff8c00", 13 | "#386300", 14 | "#b40e1b", 15 | "#68217a", 16 | "#33cccc", 17 | "#016afe", 18 | "#595959", 19 | "#808080", 20 | "#990066", 21 | "#00bcf2" 22 | ] -------------------------------------------------------------------------------- /src/react/components/common/videoPlayer/customVideoPlayerButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { Player } from "video-react"; 3 | import { KeyboardBinding } from "../keyboardBinding/keyboardBinding"; 4 | import { KeyEventType } from "../keyboardManager/keyboardManager"; 5 | 6 | export interface ICustomVideoPlayerButtonProps { 7 | order: number; 8 | onClick: () => void; 9 | icon?: string; 10 | accelerators?: string[]; 11 | tooltip?: string; 12 | player?: Player; 13 | } 14 | 15 | export class CustomVideoPlayerButton extends React.Component { 16 | public render() { 17 | return ( 18 | 19 | {this.props.accelerators && 20 | 25 | } 26 | 33 | 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/react/components/pages/activeLearning/activeLearningForm.ui.json: -------------------------------------------------------------------------------- 1 | { 2 | "modelPath": { 3 | "ui:widget": "localFolderPicker" 4 | }, 5 | "predictTag": { 6 | "ui:widget": "checkbox" 7 | }, 8 | "autoDetect": { 9 | "ui:widget": "checkbox" 10 | }, 11 | "ui:order": [ 12 | "modelPathType", 13 | "*", 14 | "predictTag", 15 | "autoDetect" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/react/components/pages/appSettings/appSettingsForm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "securityTokens": { 5 | "title": "${strings.appSettings.securityTokens.title}", 6 | "description": "${strings.appSettings.securityTokens.description}", 7 | "type": "array", 8 | "items": { 9 | "type": "object", 10 | "required": [ 11 | "name", 12 | "key" 13 | ], 14 | "properties": { 15 | "name": { 16 | "type": "string", 17 | "title": "${strings.appSettings.securityToken.name.title}" 18 | }, 19 | "key": { 20 | "type": "string", 21 | "title": "${strings.appSettings.securityToken.key.title}" 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/react/components/pages/appSettings/appSettingsForm.ui.json: -------------------------------------------------------------------------------- 1 | { 2 | "securityTokens": { 3 | "ui:options": { 4 | "addable": true, 5 | "orderable": false, 6 | "removable": true 7 | }, 8 | "items": { 9 | "key": { 10 | "ui:field": "securityToken" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/react/components/pages/appSettings/appSettingsPage.scss: -------------------------------------------------------------------------------- 1 | .app-settings-page { 2 | display: flex; 3 | flex-direction: row; 4 | flex-grow: 1; 5 | min-height: fit-content; 6 | 7 | &-form { 8 | flex-grow: 1; 9 | } 10 | 11 | &-sidebar { 12 | flex-basis: 30vw; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/react/components/pages/connections/connectionForm.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": [ 3 | "name", 4 | "providerType" 5 | ], 6 | "type": "object", 7 | "properties": { 8 | "name": { 9 | "title": "${strings.common.displayName}", 10 | "type": "string" 11 | }, 12 | "description": { 13 | "title": "${strings.common.description}", 14 | "type": "string" 15 | }, 16 | "providerType": { 17 | "title": "${strings.common.provider}", 18 | "type": "string" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/react/components/pages/connections/connectionForm.ui.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": { 3 | "ui:widget": "textarea" 4 | }, 5 | "providerType": { 6 | "ui:widget": "connectionProviderPicker" 7 | } 8 | } -------------------------------------------------------------------------------- /src/react/components/pages/connections/connectionItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | 4 | export default function ConnectionItem({ item, onClick, onDelete }) { 5 | return ( 6 |
  • 7 | 8 | 9 | {item.name} 10 |
    11 |
    12 |
  • 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/react/components/pages/connections/connectionsPage.scss: -------------------------------------------------------------------------------- 1 | .app-connections-page { 2 | flex-grow: 1; 3 | display: flex; 4 | flex-direction: row; 5 | min-height: fit-content; 6 | 7 | &-list { 8 | flex-basis: 20vw; 9 | min-width: 250px; 10 | max-width: 300px; 11 | } 12 | 13 | &-detail { 14 | flex-grow: 1; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/react/components/pages/editorPage/editorToolbar.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../assets/sass/theme.scss'; 2 | 3 | .editor-page-content-main-header { 4 | padding: 5px; 5 | 6 | .btn-group { 7 | border-right: solid 1px $lighter-2; 8 | padding-right: 10px; 9 | } 10 | 11 | .toolbar-btn { 12 | width: 48px; 13 | height: 48px; 14 | outline: none; 15 | 16 | background-color: transparent; 17 | border: none; 18 | color: #CCC; 19 | 20 | &:hover, &.active { 21 | background-color: $lighter-2; 22 | border: solid 1px $lighter-3; 23 | } 24 | 25 | &:active { 26 | background-color: $darker-2; 27 | } 28 | 29 | .fas { 30 | font-size: 18px; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/react/components/pages/export/exportForm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": [ 4 | "providerType" 5 | ], 6 | "properties": { 7 | "providerType": { 8 | "title": "${strings.common.provider}", 9 | "type": "string" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/react/components/pages/export/exportForm.ui.json: -------------------------------------------------------------------------------- 1 | { 2 | "providerType": { 3 | "ui:widget": "exportProviderPicker" 4 | } 5 | } -------------------------------------------------------------------------------- /src/react/components/pages/homepage/homePage.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../assets/sass/theme.scss'; 2 | 3 | .app-homepage { 4 | flex-grow: 1; 5 | display: flex; 6 | flex-direction: row; 7 | min-height: fit-content; 8 | 9 | &-main { 10 | display: flex; 11 | flex-grow: 1; 12 | 13 | ul { 14 | display: flex; 15 | flex-direction: row; 16 | align-items: center; 17 | justify-content: center; 18 | margin: auto; 19 | flex-wrap: wrap; 20 | 21 | padding: 0; 22 | 23 | li { 24 | list-style-type: none; 25 | text-align: center; 26 | 27 | a { 28 | display: inline-block; 29 | 30 | &.active, &:hover { 31 | color: #fff; 32 | background-color: $lighter-2; 33 | text-decoration: none; 34 | } 35 | } 36 | } 37 | } 38 | } 39 | 40 | &-recent { 41 | flex-basis: 20vw; 42 | min-width: 250px; 43 | max-width: 300px; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/react/components/pages/homepage/recentProjectItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function RecentProjectItem({ item, onClick, onDelete }) { 4 | return ( 5 |
  • 6 | 7 | 8 | {item.name} 9 |
    10 |
    11 |
  • 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/react/components/pages/projectSettings/projectForm.ui.json: -------------------------------------------------------------------------------- 1 | { 2 | "securityToken": { 3 | "ui:field": "securityToken" 4 | }, 5 | "useSecurityToken": { 6 | "ui:widget": "checkbox" 7 | }, 8 | "description": { 9 | "ui:widget": "textarea" 10 | }, 11 | "sourceConnection": { 12 | "ui:field": "sourceConnection" 13 | }, 14 | "targetConnection": { 15 | "ui:field": "targetConnection" 16 | }, 17 | "tags": { 18 | "ui:field": "tagsInput" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/react/components/pages/projectSettings/projectSettingsPage.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../assets/sass/theme.scss"; 2 | 3 | .project-settings-page { 4 | flex-grow: 1; 5 | display: flex; 6 | flex-direction: row; 7 | min-height: fit-content; 8 | 9 | &-settings { 10 | flex-grow: 1; 11 | } 12 | 13 | &-metrics { 14 | display: flex; 15 | flex-direction: column; 16 | flex-grow: 1; 17 | min-width: 325px; 18 | max-width: 420px; 19 | color: #fff; 20 | 21 | .loading { 22 | position: absolute; 23 | left: 50%; 24 | top: 25%; 25 | } 26 | 27 | p { 28 | font-size: 90%; 29 | } 30 | } 31 | 32 | svg { 33 | margin-top: 1em; 34 | text { 35 | fill: #fff; 36 | font-weight: 500; 37 | } 38 | } 39 | 40 | .asset-chart { 41 | font-size: 90%; 42 | 43 | @media (max-width: 1920px) { 44 | display: flex; 45 | align-items: center; 46 | } 47 | 48 | .hint-content { 49 | display: flex; 50 | color: #fff; 51 | background: #000; 52 | align-items: center; 53 | padding: 5px; 54 | 55 | &-box { 56 | height: 10px; 57 | width: 10px; 58 | } 59 | } 60 | 61 | .rv-sunburst { 62 | margin: 0 auto; 63 | } 64 | 65 | .rv-hint { 66 | white-space: nowrap; 67 | } 68 | } 69 | 70 | .tag-chart { 71 | margin: 0 auto; 72 | 73 | line { 74 | stroke: $lighter-5; 75 | } 76 | } 77 | } 78 | 79 | .assetCount { 80 | display: flex; 81 | align-items: center; 82 | } 83 | -------------------------------------------------------------------------------- /src/react/components/shell/helpMenu.scss: -------------------------------------------------------------------------------- 1 | @import '../../../assets/sass/theme.scss'; 2 | 3 | .keybinding { 4 | font-weight: bolder; 5 | } 6 | 7 | .keybinding-icon { 8 | vertical-align: bottom; 9 | padding: 4px 15px; 10 | } 11 | 12 | .help-key.row { 13 | color: #ccc; 14 | padding: 2px; 15 | 16 | &:hover { 17 | color: #fff; 18 | background-color: $lighter-2; 19 | } 20 | } 21 | 22 | .help-menu-button { 23 | padding: 6px 10px; 24 | color: #ccc; 25 | display: inline-block; 26 | 27 | &:hover { 28 | color: #fff; 29 | background-color: $lighter-2; 30 | cursor: pointer; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/react/components/shell/mainContentRouter.test.tsx: -------------------------------------------------------------------------------- 1 | import { shallow, mount, ReactWrapper } from "enzyme"; 2 | 3 | import React from "react"; 4 | import { Route, StaticRouter as Router } from "react-router-dom"; 5 | 6 | import { Provider } from "react-redux"; 7 | import { AnyAction, Store } from "redux"; 8 | import createReduxStore from "../../../redux/store/store"; 9 | 10 | import MainContentRouter from "./mainContentRouter"; 11 | import HomePage, { IHomePageProps } from "./../pages/homepage/homePage"; 12 | import SettingsPage from "./../pages/appSettings/appSettingsPage"; 13 | import ConnectionsPage from "./../pages/connections/connectionsPage"; 14 | import { IApplicationState } from "./../../../models/applicationState"; 15 | 16 | describe("Main Content Router", () => { 17 | const badRoute: string = "/index.html"; 18 | 19 | function createComponent(routerContext, route, store, props: IHomePageProps): ReactWrapper { 20 | return mount( 21 | 22 | 23 | 24 | 25 | , 26 | ); 27 | } 28 | 29 | function createWrapper(route = badRoute, store = createStore(), props = null): ReactWrapper { 30 | const context = {}; 31 | return createComponent(context, route, store, props); 32 | } 33 | 34 | it("renders correct routes", () => { 35 | const wrapper = shallow(); 36 | const pathMap = wrapper.find(Route).reduce((pathMap, route) => { 37 | const routeProps = route.props(); 38 | pathMap[routeProps.path] = routeProps.component; 39 | return pathMap; 40 | }, {}); 41 | 42 | expect(pathMap["/"]).toBe(HomePage); 43 | expect(pathMap["/settings"]).toBe(SettingsPage); 44 | expect(pathMap["/connections"]).toBe(ConnectionsPage); 45 | }); 46 | 47 | it("renders a redirect when no route is matched", () => { 48 | const wrapper = createWrapper(); 49 | 50 | const homePage = wrapper.find(HomePage); 51 | expect(homePage.find(".app-homepage").exists()).toEqual(true); 52 | }); 53 | }); 54 | 55 | function createStore(state?: IApplicationState): Store { 56 | return createReduxStore(state); 57 | } 58 | -------------------------------------------------------------------------------- /src/react/components/shell/mainContentRouter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Switch, Route } from "react-router-dom"; 3 | import HomePage from "../pages/homepage/homePage"; 4 | import ActiveLearningPage from "../pages/activeLearning/activeLearningPage"; 5 | import AppSettingsPage from "../pages/appSettings/appSettingsPage"; 6 | import ConnectionPage from "../pages/connections/connectionsPage"; 7 | import EditorPage from "../pages/editorPage/editorPage"; 8 | import ExportPage from "../pages/export/exportPage"; 9 | import ProjectSettingsPage from "../pages/projectSettings/projectSettingsPage"; 10 | 11 | /** 12 | * @name - Main Content Router 13 | * @description - Controls main content pane based on route 14 | */ 15 | export default function MainContentRouter() { 16 | return ( 17 |
    18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
    31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/react/components/shell/sidebar.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Sidebar from "./sidebar"; 3 | import { BrowserRouter as Router } from "react-router-dom"; 4 | import { mount } from "enzyme"; 5 | import { IProject } from "../../../models/applicationState"; 6 | 7 | describe("Sidebar Component", () => { 8 | it("renders correctly", () => { 9 | const project: IProject = null; 10 | const wrapper = mount( 11 | 12 | 13 | , 14 | ); 15 | 16 | expect(wrapper).not.toBeNull(); 17 | 18 | const links = wrapper.find("ul li"); 19 | expect(links.length).toEqual(7); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/react/components/shell/statusBar.scss: -------------------------------------------------------------------------------- 1 | @import '../../../assets/sass/theme.scss'; 2 | 3 | .status-bar { 4 | font-size: 0.75em; 5 | color: #fff; 6 | display: flex; 7 | align-items: center; 8 | min-height: 24px; 9 | flex-direction: row; 10 | padding: 0.25em 0.8em; 11 | background-color: rgba($color-blue, 0.50); 12 | 13 | ul { 14 | display: inline-block; 15 | margin: 0; 16 | padding: 0; 17 | white-space: nowrap; 18 | } 19 | 20 | li { 21 | list-style: none; 22 | display: inline-block; 23 | margin: 0 0.75em; 24 | } 25 | 26 | li:first-child { 27 | margin: 0 0.25em; 28 | } 29 | 30 | span { 31 | padding-left: 0.4em; 32 | } 33 | 34 | &-main { 35 | flex-grow: 1; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/react/components/shell/statusBar.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { mount, ReactWrapper } from "enzyme"; 3 | import { StatusBar } from "./statusBar"; 4 | import { appInfo } from "../../../common/appInfo"; 5 | 6 | describe("StatusBar component", () => { 7 | let wrapper: ReactWrapper; 8 | 9 | function createComponent() { 10 | return mount( 11 | 12 |
    Child Component
    13 |
    , 14 | ); 15 | } 16 | 17 | beforeEach(() => { 18 | wrapper = createComponent(); 19 | }); 20 | 21 | it("renders app version", () => { 22 | const version = wrapper.find(".status-bar-version"); 23 | expect(version.exists()).toBe(true); 24 | expect(version.text()).toContain(appInfo.version); 25 | }); 26 | 27 | it("renders children", () => { 28 | const childrenContainer = wrapper.find(".status-bar-main"); 29 | expect(childrenContainer.find(".child-component").exists()).toBe(true); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/react/components/shell/statusBar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { appInfo } from "../../../common/appInfo"; 3 | import "./statusBar.scss"; 4 | 5 | export class StatusBar extends React.Component { 6 | public render() { 7 | return ( 8 |
    9 |
    {this.props.children}
    10 |
    11 |
      12 |
    • 13 | 14 | {appInfo.version} 15 |
    • 16 |
    17 |
    18 |
    19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/react/components/shell/statusBarMetrics.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import { mount, ReactWrapper } from "enzyme"; 4 | import { StatusBarMetrics, IStatusBarMetricsProps } from "./statusBarMetrics"; 5 | import MockFactory from "../../../common/mockFactory"; 6 | import { AssetState } from "../../../models/applicationState"; 7 | 8 | describe("StatusBarMetrics Component", () => { 9 | const testProject = MockFactory.createTestProject("TestProject"); 10 | const testAssets = MockFactory.createTestAssets(); 11 | testAssets[0].state = AssetState.Tagged; 12 | testAssets[1].state = AssetState.Tagged; 13 | testAssets[2].state = AssetState.Tagged; 14 | testAssets[3].state = AssetState.Visited; 15 | testAssets[4].state = AssetState.Visited; 16 | testProject.assets = _.keyBy(testAssets, (asset) => asset.id); 17 | 18 | function createComponent(props: IStatusBarMetricsProps) { 19 | return mount(); 20 | } 21 | 22 | it("Renders simple project metrics when a project has been loaded", () => { 23 | const wrapper = createComponent({ 24 | project: testProject, 25 | }); 26 | 27 | expect(wrapper.find(".metric-source-connection-name").text()).toEqual(testProject.sourceConnection.name); 28 | expect(wrapper.find(".metric-target-connection-name").text()).toEqual(testProject.targetConnection.name); 29 | expect(wrapper.find(".metric-visited-asset-count").text()).toEqual("5"); 30 | expect(wrapper.find(".metric-tagged-asset-count").text()).toEqual("3"); 31 | }); 32 | 33 | it("Does not render when a project hasn't been loaded", () => { 34 | const wrapper = createComponent({ 35 | project: null, 36 | }); 37 | 38 | expect(wrapper.find(".metric-source-connection-name").exists()).toBe(false); 39 | expect(wrapper.find(".metric-target-connection-name").exists()).toBe(false); 40 | expect(wrapper.find(".metric-visited-asset-count").exists()).toBe(false); 41 | expect(wrapper.find(".metric-tagged-asset-count").exists()).toBe(false); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/react/components/shell/statusBarMetrics.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import { IProject, AssetState } from "../../../models/applicationState"; 4 | import { strings, interpolate } from "../../../common/strings"; 5 | 6 | export interface IStatusBarMetricsProps { 7 | project: IProject; 8 | } 9 | 10 | export class StatusBarMetrics extends React.Component { 11 | public render() { 12 | const { project } = this.props; 13 | 14 | if (!project) { 15 | return null; 16 | } 17 | 18 | const projectAssets = _.values(project.assets); 19 | const visitedAssets = projectAssets 20 | .filter((asset) => asset.state === AssetState.Visited || asset.state === AssetState.Tagged); 21 | const taggedAssets = projectAssets 22 | .filter((asset) => asset.state === AssetState.Tagged); 23 | 24 | return ( 25 |
      26 |
    • 27 | 28 | {project.sourceConnection.name} 29 |
    • 30 |
    • 31 | 32 | {project.targetConnection.name} 33 |
    • 34 |
    • 35 | 36 | {taggedAssets.length} 37 |
    • 38 |
    • 39 | 40 | {visitedAssets.length} 41 |
    • 42 |
    43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/react/components/toolbar/exportProject.tsx: -------------------------------------------------------------------------------- 1 | import { ToolbarItem } from "./toolbarItem"; 2 | import { toast } from "react-toastify"; 3 | 4 | /** 5 | * @name - Export Project 6 | * @description - Toolbar item to export current project 7 | */ 8 | export class ExportProject extends ToolbarItem { 9 | protected onItemClick = async () => { 10 | const infoId = toast.info(`Started export for ${this.props.project.name}...`, { autoClose: false }); 11 | const results = await this.props.actions.exportProject(this.props.project); 12 | 13 | toast.dismiss(infoId); 14 | 15 | if (!results || (results && results.errors.length === 0)) { 16 | toast.success(`Export completed successfully!`); 17 | } else if (results && results.errors.length > 0) { 18 | toast.warn(`Successfully exported ${results.completed.length}/${results.count} assets`); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/react/components/toolbar/saveProject.tsx: -------------------------------------------------------------------------------- 1 | import { ToolbarItem } from "./toolbarItem"; 2 | import { toast } from "react-toastify"; 3 | 4 | /** 5 | * @name - Save Project 6 | * @description - Toolbar item to save current project 7 | */ 8 | export class SaveProject extends ToolbarItem { 9 | protected onItemClick = async () => { 10 | try { 11 | await this.props.actions.saveProject(this.props.project); 12 | toast.success(`${this.props.project.name} saved successfully!`); 13 | } catch (e) { 14 | toast.error(`Error saving ${this.props.project.name}`); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/redux/actions/actionTypes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Redux Action types 3 | */ 4 | export enum ActionTypes { 5 | // App 6 | TOGGLE_DEV_TOOLS_SUCCESS = "TOGGLE_DEV_TOOLS_SUCCESS", 7 | OPEN_LOCAL_FOLDER_SUCCESS = "OPEN_LOCAL_FOLDER_SUCCESS", 8 | REFRESH_APP_SUCCESS = "REFRESH_APP_SUCCESS", 9 | SAVE_APP_SETTINGS_SUCCESS = "SAVE_APP_SETTINGS_SUCCESS", 10 | ENSURE_SECURITY_TOKEN_SUCCESS = "ENSURE_SECURITY_TOKEN_SUCCESS", 11 | 12 | // Projects 13 | LOAD_PROJECT_SUCCESS = "LOAD_PROJECT_SUCCESS", 14 | SAVE_PROJECT_SUCCESS = "SAVE_PROJECT_SUCCESS", 15 | DELETE_PROJECT_SUCCESS = "DELETE_PROJECT_SUCCESS", 16 | CLOSE_PROJECT_SUCCESS = "CLOSE_PROJECT_SUCCESS", 17 | LOAD_PROJECT_ASSETS_SUCCESS = "LOAD_PROJECT_ASSETS_SUCCESS", 18 | EXPORT_PROJECT_SUCCESS = "EXPORT_PROJECT_SUCCESS", 19 | UPDATE_PROJECT_TAG_SUCCESS = "UPDATE_PROJECT_TAG_SUCCESS", 20 | DELETE_PROJECT_TAG_SUCCESS = "DELETE_PROJECT_TAG_SUCCESS", 21 | 22 | // Connections 23 | LOAD_CONNECTION_SUCCESS = "LOAD_CONNECTION_SUCCESS", 24 | SAVE_CONNECTION_SUCCESS = "SAVE_CONNECTION_SUCCESS", 25 | DELETE_CONNECTION_SUCCESS = "DELETE_CONNECTION_SUCCESS", 26 | 27 | // Assets 28 | SAVE_ASSET_METADATA_SUCCESS = "SAVE_ASSET_METADATA_SUCCESS", 29 | LOAD_ASSET_METADATA_SUCCESS = "LOAD_ASSET_METADATA_SUCCESS", 30 | 31 | ANY_OTHER_ACTION = "ANY_OTHER_ACTION_SUCCESS", 32 | 33 | SHOW_ERROR= "SHOW_ERROR", 34 | CLEAR_ERROR = "CLEAR_ERROR", 35 | } 36 | -------------------------------------------------------------------------------- /src/redux/actions/appErrorActions.test.ts: -------------------------------------------------------------------------------- 1 | import * as appErrorActions from "./appErrorActions"; 2 | import { ActionTypes } from "./actionTypes"; 3 | import { MockStoreEnhanced } from "redux-mock-store"; 4 | import thunk from "redux-thunk"; 5 | import createMockStore from "redux-mock-store"; 6 | import MockFactory from "../../common/mockFactory"; 7 | 8 | describe("App Error Actions", () => { 9 | let store: MockStoreEnhanced; 10 | 11 | beforeEach(() => { 12 | const middleware = [thunk]; 13 | store = createMockStore(middleware)(); 14 | }); 15 | 16 | it("Show error dispatches redux action", () => { 17 | const appError = MockFactory.createAppError(); 18 | 19 | appErrorActions.showError(appError)(store.dispatch); 20 | const actions = store.getActions(); 21 | 22 | expect(actions.length).toEqual(1); 23 | expect(actions[0]).toEqual({ 24 | type: ActionTypes.SHOW_ERROR, 25 | payload: appError, 26 | }); 27 | }); 28 | 29 | it("Clear error dispatches redux action", () => { 30 | appErrorActions.clearError()(store.dispatch); 31 | const actions = store.getActions(); 32 | 33 | expect(actions.length).toEqual(1); 34 | expect(actions[0]).toEqual({ 35 | type: ActionTypes.CLEAR_ERROR, 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/redux/actions/appErrorActions.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, Action } from "redux"; 2 | import { IAppError } from "../../models/applicationState"; 3 | import { createPayloadAction, IPayloadAction, createAction } from "./actionCreators"; 4 | import { ActionTypes } from "./actionTypes"; 5 | 6 | /** 7 | * Action to display alert when there's an error in the app 8 | * @member showError 9 | * @member clearError 10 | * @interface 11 | */ 12 | export default interface IAppErrorActions { 13 | showError(appError: IAppError): void; 14 | clearError(): void; 15 | } 16 | 17 | /** 18 | * show alert popup to indicate error 19 | * @param appError {IAppError} the error to display in alert 20 | * @returns {(dispatch: Dispatch) => void} 21 | */ 22 | export function showError(appError: IAppError): (dispatch: Dispatch) => void { 23 | return (dispatch: Dispatch) => { 24 | dispatch(showErrorAction(appError)); 25 | }; 26 | } 27 | 28 | /** 29 | * clear alert popup 30 | * @returns {(dispatch: Dispatch) => void} 31 | */ 32 | export function clearError(): (dispatch: Dispatch) => void { 33 | return (dispatch: Dispatch) => { 34 | dispatch(clearErrorAction()); 35 | }; 36 | } 37 | 38 | /** 39 | * Show error action type 40 | */ 41 | export interface IShowAppErrorAction extends IPayloadAction { 42 | type: ActionTypes.SHOW_ERROR; 43 | } 44 | 45 | /** 46 | * Clear error action type 47 | */ 48 | export interface IClearErrorAction extends Action { 49 | type: ActionTypes.CLEAR_ERROR; 50 | } 51 | 52 | /** 53 | * Instance of show error action 54 | */ 55 | export const showErrorAction = createPayloadAction(ActionTypes.SHOW_ERROR); 56 | 57 | /** 58 | * Instance of clear error action 59 | * @type {() => Action} 60 | */ 61 | export const clearErrorAction = createAction(ActionTypes.CLEAR_ERROR); 62 | -------------------------------------------------------------------------------- /src/redux/middleware/appInsights.test.ts: -------------------------------------------------------------------------------- 1 | import { trackReduxAction } from "../../telemetry"; 2 | import { createAppInsightsLogger } from "./appInsights"; 3 | jest.mock("../../telemetry"); 4 | 5 | describe("appInsights middleware", () => { 6 | const create = () => { 7 | const appInsightsLogger = createAppInsightsLogger(); 8 | 9 | const store = { 10 | getState: jest.fn(() => ({})), 11 | dispatch: jest.fn(), 12 | }; 13 | 14 | const next = jest.fn(); 15 | const invoke = (action) => appInsightsLogger(store)(next)(action); 16 | 17 | return { store, next, invoke}; 18 | }; 19 | 20 | it("calls trackReduxAction", () => { 21 | const { invoke } = create(); 22 | const action = { type: "TEST"}; 23 | invoke(action); 24 | 25 | expect(trackReduxAction).toHaveBeenCalledWith(action); 26 | }); 27 | 28 | it("passes through non-function action", () => { 29 | const { next, invoke } = create(); 30 | const action = { type: "TEST" }; 31 | invoke(action); 32 | expect(next).toHaveBeenCalledWith(action); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/redux/middleware/appInsights.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, Dispatch, Middleware, MiddlewareAPI } from "redux"; 2 | import { trackReduxAction } from "../../telemetry"; 3 | 4 | /** 5 | * return a middleware that send custom event to AppInsights tracking redux action 6 | */ 7 | export function createAppInsightsLogger(): Middleware { 8 | return (store: MiddlewareAPI>) => (next: Dispatch) => (action: any) => { 9 | trackReduxAction(action); 10 | return next(action); 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/redux/middleware/localStorage.ts: -------------------------------------------------------------------------------- 1 | import { Middleware, Dispatch, AnyAction, MiddlewareAPI } from "redux"; 2 | 3 | export interface ILocalStorageMiddlewareOptions { 4 | paths: string[]; 5 | } 6 | 7 | export function createLocalStorage(config: ILocalStorageMiddlewareOptions): Middleware { 8 | return (store: MiddlewareAPI>) => (next: Dispatch) => (action: any) => { 9 | const result = next(action); 10 | const state = store.getState(); 11 | 12 | config.paths.forEach((path) => { 13 | if (state[path]) { 14 | const json = JSON.stringify(state[path]); 15 | localStorage.setItem(path, json); 16 | } 17 | }); 18 | 19 | return result; 20 | }; 21 | } 22 | 23 | export function mergeInitialState(state: any, paths: string[]) { 24 | const initialState = { ...state }; 25 | paths.forEach((path) => { 26 | const json = localStorage.getItem(path); 27 | if (json) { 28 | initialState[path] = JSON.parse(json); 29 | } 30 | }); 31 | 32 | return initialState; 33 | } 34 | -------------------------------------------------------------------------------- /src/redux/reducers/appErrorReducer.test.ts: -------------------------------------------------------------------------------- 1 | import { reducer } from "./appErrorReducer"; 2 | import { IAppError, ErrorCode } from "../../models/applicationState"; 3 | import { clearErrorAction, showErrorAction } from "../actions/appErrorActions"; 4 | import { anyOtherAction } from "../actions/actionCreators"; 5 | import MockFactory from "../../common/mockFactory"; 6 | 7 | describe("AppError Reducer", () => { 8 | let state: IAppError; 9 | 10 | beforeEach( () => { 11 | state = MockFactory.createAppError(); 12 | }); 13 | 14 | it("ShowError discard previous state and return an appError", () => { 15 | const appError = MockFactory.createAppError( 16 | ErrorCode.Unknown, 17 | "Sample Error Title", 18 | "Sample Error Message", 19 | ); 20 | const action = showErrorAction(appError); 21 | 22 | const result = reducer(state, action); 23 | expect(result).not.toEqual(state); 24 | expect(result).toEqual(appError); 25 | }); 26 | 27 | it("ClearError return null", () => { 28 | const action = clearErrorAction(); 29 | 30 | const result = reducer(state, action); 31 | expect(result).toBe(null); 32 | }); 33 | 34 | it("Unknown action performs noop", () => { 35 | const action = anyOtherAction(); 36 | const result = reducer(state, action); 37 | expect(result).toBe(state); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/redux/reducers/appErrorReducer.ts: -------------------------------------------------------------------------------- 1 | import { ActionTypes } from "../actions/actionTypes"; 2 | import { AnyAction } from "../actions/actionCreators"; 3 | import { IAppError } from "../../models/applicationState"; 4 | 5 | /** 6 | * App Error Reducer 7 | * Actions handled: 8 | * SHOW_ERROR 9 | * CLEAR_ERROR 10 | * @param {IAppError} state 11 | * @param {AnyAction} action 12 | * @returns {any} 13 | */ 14 | export const reducer = (state: IAppError = null, action: AnyAction) => { 15 | switch (action.type) { 16 | case ActionTypes.SHOW_ERROR: 17 | return {...action.payload}; 18 | case ActionTypes.CLEAR_ERROR: 19 | return null; 20 | default: 21 | return state; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/redux/reducers/applicationReducer.test.ts: -------------------------------------------------------------------------------- 1 | import { reducer } from "./applicationReducer"; 2 | import { IAppSettings } from "../../models/applicationState"; 3 | import { toggleDevToolsAction, refreshApplicationAction, saveAppSettingsAction } from "../actions/applicationActions"; 4 | import { anyOtherAction } from "../actions/actionCreators"; 5 | 6 | describe("Application Reducer", () => { 7 | it("Toggle dev tools sets correct state", () => { 8 | const state: IAppSettings = { 9 | devToolsEnabled: false, 10 | securityTokens: [], 11 | }; 12 | 13 | const action = toggleDevToolsAction(true); 14 | 15 | const result = reducer(state, action); 16 | expect(result).not.toBe(state); 17 | expect(result.devToolsEnabled).toBe(action.payload); 18 | }); 19 | 20 | it("Refreshing app clones state", () => { 21 | const state: IAppSettings = { 22 | devToolsEnabled: false, 23 | securityTokens: [], 24 | }; 25 | 26 | const action = refreshApplicationAction(); 27 | const result = reducer(state, action); 28 | expect(result).not.toBe(state); 29 | }); 30 | 31 | it("Saves app settings state is persisted", () => { 32 | const state: IAppSettings = { 33 | devToolsEnabled: false, 34 | securityTokens: [], 35 | }; 36 | 37 | const payload: IAppSettings = { 38 | devToolsEnabled: false, 39 | securityTokens: [ 40 | { name: "A", key: "1" }, 41 | { name: "B", key: "2" }, 42 | { name: "C", key: "3" }, 43 | ], 44 | }; 45 | 46 | const action = saveAppSettingsAction(payload); 47 | const result = reducer(state, action); 48 | 49 | expect(result).not.toBe(state); 50 | expect(result).toEqual(payload); 51 | }); 52 | 53 | it("Unknown action performs noop", () => { 54 | const state: IAppSettings = { 55 | devToolsEnabled: false, 56 | securityTokens: [], 57 | }; 58 | 59 | const action = anyOtherAction(); 60 | const result = reducer(state, action); 61 | expect(result).toBe(state); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/redux/reducers/applicationReducer.ts: -------------------------------------------------------------------------------- 1 | import { ActionTypes } from "../actions/actionTypes"; 2 | import { IAppSettings } from "../../models/applicationState"; 3 | import { AnyAction } from "../actions/actionCreators"; 4 | 5 | /** 6 | * Reducer for application settings. Actions handled: 7 | * TOGGLE_DEV_TOOLS_SUCCESS 8 | * REFRESH_APP_SUCCESS 9 | * @param state - Current app settings 10 | * @param action - Action that was dispatched 11 | */ 12 | export const reducer = (state: IAppSettings = null, action: AnyAction): IAppSettings => { 13 | switch (action.type) { 14 | case ActionTypes.TOGGLE_DEV_TOOLS_SUCCESS: 15 | return { ...state, devToolsEnabled: action.payload }; 16 | case ActionTypes.REFRESH_APP_SUCCESS: 17 | return { ...state }; 18 | case ActionTypes.SAVE_APP_SETTINGS_SUCCESS: 19 | return { ...action.payload }; 20 | case ActionTypes.ENSURE_SECURITY_TOKEN_SUCCESS: 21 | return { ...action.payload }; 22 | default: 23 | return state; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/redux/reducers/connectionsReducer.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import { ActionTypes } from "../actions/actionTypes"; 3 | import { IConnection } from "../../models/applicationState"; 4 | import { AnyAction } from "../actions/actionCreators"; 5 | 6 | /** 7 | * Reducer for application connections. Actions handled: 8 | * SAVE_CONNECTION_SUCCESS 9 | * DELETE_CONNECTION_SUCCESS 10 | * LOAD_PROJECT_SUCCESS 11 | * @param state - Current array of connections 12 | * @param action - Action that was dispatched 13 | */ 14 | export const reducer = (state: IConnection[] = [], action: AnyAction): IConnection[] => { 15 | if (!state) { 16 | state = []; 17 | } 18 | 19 | switch (action.type) { 20 | case ActionTypes.SAVE_CONNECTION_SUCCESS: 21 | return [ 22 | { ...action.payload }, 23 | ...state.filter((connection) => connection.id !== action.payload.id), 24 | ]; 25 | case ActionTypes.DELETE_CONNECTION_SUCCESS: 26 | return [...state.filter((connection) => connection.id !== action.payload.id)]; 27 | case ActionTypes.LOAD_PROJECT_SUCCESS: 28 | const isSourceTargetEqual = action.payload.sourceConnection.id === action.payload.targetConnection.id; 29 | if (isSourceTargetEqual) { 30 | return [ 31 | { ...action.payload.sourceConnection }, 32 | ...state.filter((connection) => connection.id !== action.payload.sourceConnection.id), 33 | ]; 34 | } 35 | 36 | return [ 37 | { ...action.payload.sourceConnection }, 38 | { ...action.payload.targetConnection }, 39 | ...state.filter((connection) => { 40 | return connection.id !== action.payload.sourceConnection.id && 41 | connection.id !== action.payload.targetConnection.id; 42 | })]; 43 | default: 44 | return state; 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/redux/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import * as appSettings from "./applicationReducer"; 3 | import * as connections from "./connectionsReducer"; 4 | import * as currentProject from "./currentProjectReducer"; 5 | import * as recentProjects from "./recentProjectsReducer"; 6 | import * as appError from "./appErrorReducer"; 7 | 8 | /** 9 | * All application reducers 10 | * @member appSettings - Application Settings reducer 11 | * @member connections - Connections reducer 12 | * @member recentProjects - Recent Projects reducer 13 | * @member currentProject - Current Project reducer 14 | */ 15 | export default combineReducers({ 16 | appSettings: appSettings.reducer, 17 | connections: connections.reducer, 18 | recentProjects: recentProjects.reducer, 19 | currentProject: currentProject.reducer, 20 | appError: appError.reducer, 21 | }); 22 | -------------------------------------------------------------------------------- /src/redux/reducers/recentProjectsReducer.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import { ActionTypes } from "../actions/actionTypes"; 3 | import { IProject } from "../../models/applicationState"; 4 | import { AnyAction } from "../actions/actionCreators"; 5 | 6 | /** 7 | * Reducer for recent projects. Actions handled: 8 | * LOAD_PROJECT_SUCCESS 9 | * SAVE_PROJECT_SUCCESS 10 | * DELETE_PROJECT_SUCCESS 11 | * SAVE_CONNECTION_SUCCESS 12 | * @param state - Array of recent projects 13 | * @param action - Action that was dispatched 14 | */ 15 | export const reducer = (state: IProject[] = [], action: AnyAction): IProject[] => { 16 | if (!state) { 17 | state = []; 18 | } 19 | 20 | let newState: IProject[] = null; 21 | 22 | switch (action.type) { 23 | case ActionTypes.SAVE_PROJECT_SUCCESS: 24 | return [ 25 | { ...action.payload }, 26 | ...state.filter((project) => project.id !== action.payload.id), 27 | ]; 28 | case ActionTypes.DELETE_PROJECT_SUCCESS: 29 | return [...state.filter((project) => project.id !== action.payload.id)]; 30 | case ActionTypes.SAVE_CONNECTION_SUCCESS: 31 | newState = state.map((project) => { 32 | const updatedProject = { ...project }; 33 | if (project.sourceConnection.id === action.payload.id) { 34 | updatedProject.sourceConnection = { ...action.payload }; 35 | } 36 | if (project.targetConnection.id === action.payload.id) { 37 | updatedProject.targetConnection = { ...action.payload }; 38 | } 39 | return updatedProject; 40 | }); 41 | return newState; 42 | default: 43 | return state; 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/redux/store/initialState.ts: -------------------------------------------------------------------------------- 1 | import { IApplicationState } from "../../models/applicationState"; 2 | 3 | /** 4 | * Initial state of application 5 | * @member appSettings - Application settings 6 | * @member connections - Connections 7 | * @member recentProjects - Recent projects 8 | * @member currentProject - Current project 9 | */ 10 | const initialState: IApplicationState = { 11 | appSettings: { 12 | devToolsEnabled: false, 13 | securityTokens: [], 14 | }, 15 | connections: [], 16 | recentProjects: [], 17 | currentProject: null, 18 | appError: null, 19 | }; 20 | 21 | /** 22 | * Instance of initial application state 23 | */ 24 | export default initialState; 25 | -------------------------------------------------------------------------------- /src/redux/store/store.ts: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore, Store } from "redux"; 2 | import thunk from "redux-thunk"; 3 | import rootReducer from "../reducers"; 4 | import { IApplicationState } from "../../models/applicationState"; 5 | import { mergeInitialState } from "../middleware/localStorage"; 6 | import { createAppInsightsLogger } from "../middleware/appInsights"; 7 | import { Env } from "../../common/environment"; 8 | 9 | /** 10 | * Creates initial redux store from initial application state 11 | * @param initialState - Initial state of application 12 | * @param useLocalStorage - Whether or not to use localStorage middleware 13 | */ 14 | export default function createReduxStore( 15 | initialState?: IApplicationState, 16 | useLocalStorage: boolean = false): Store { 17 | const paths: string[] = ["appSettings", "connections", "recentProjects"]; 18 | 19 | let middlewares = [thunk, createAppInsightsLogger()]; 20 | 21 | if (useLocalStorage) { 22 | const localStorage = require("../middleware/localStorage"); 23 | const storage = localStorage.createLocalStorage({paths}); 24 | middlewares = [ 25 | ...middlewares, 26 | storage, 27 | ]; 28 | } 29 | 30 | if (Env.get() === "development") { 31 | const logger = require("redux-logger"); 32 | const reduxImmutableStateInvariant = require("redux-immutable-state-invariant"); 33 | middlewares = [ 34 | ...middlewares, 35 | reduxImmutableStateInvariant.default(), 36 | logger.createLogger(), 37 | ]; 38 | } 39 | 40 | return createStore( 41 | rootReducer, 42 | useLocalStorage ? mergeInitialState(initialState, paths) : initialState, 43 | applyMiddleware(...middlewares), 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/registerMixins.ts: -------------------------------------------------------------------------------- 1 | import { forEachAsync as arrayForEachAsync, mapAsync } from "./common/extensions/array"; 2 | import { forEachAsync as mapForEachAsync } from "./common/extensions/map"; 3 | 4 | declare global { 5 | // tslint:disable-next-line:interface-name 6 | interface Array { 7 | /** 8 | * Processes items in the array within the specified batch size (default: 5) 9 | * @param this The array to process 10 | * @param action The action to perform on each item in the array 11 | * @param batchSize The batch size for actions to perform in parallel (default: 5) 12 | */ 13 | forEachAsync(action: (item: T) => Promise, batchSize?: number): Promise; 14 | 15 | /** 16 | * Maps items in the array in async batches with the specified action 17 | * @param this The array to process 18 | * @param action The transformer action to perform on each item in the array 19 | * @param batchSize The batch size for actions to perform in parallel (default: 5); 20 | */ 21 | mapAsync(action: (item: T) => Promise, batchSize?: number): Promise; 22 | } 23 | 24 | // tslint:disable-next-line:interface-name 25 | interface Map { 26 | /** 27 | * Processes items in the map within the specified batch size (default: 5) 28 | * @param this The map to process 29 | * @param action The action to perform on each item in the map 30 | * @param batchSize The batch size for actions to perform in parallel (default: 5) 31 | */ 32 | forEachAsync(action: (value: V, key: K) => Promise, batchSize?: number): Promise; 33 | } 34 | } 35 | 36 | export default function registerMixins() { 37 | if (!Array.prototype.forEachAsync) { 38 | Array.prototype.forEachAsync = arrayForEachAsync; 39 | } 40 | 41 | if (!Array.prototype.mapAsync) { 42 | Array.prototype.mapAsync = mapAsync; 43 | } 44 | 45 | if (!Map.prototype.forEachAsync) { 46 | Map.prototype.forEachAsync = mapForEachAsync; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/registerProviders.test.ts: -------------------------------------------------------------------------------- 1 | import registerProviders from "./registerProviders"; 2 | import { StorageProviderFactory } from "./providers/storage/storageProviderFactory"; 3 | import { AssetProviderFactory } from "./providers/storage/assetProviderFactory"; 4 | 5 | jest.mock("./common/hostProcess"); 6 | import getHostProcess, { HostProcessType } from "./common/hostProcess"; 7 | 8 | const hostProcess = getHostProcess(); 9 | 10 | describe("Register Providers", () => { 11 | describe("Browser Registration", () => { 12 | it("Doesn't Register localFileSystemProxy", () => { 13 | const getHostProcessMock = getHostProcess as jest.Mock; 14 | getHostProcessMock.mockImplementation(() => { 15 | return { 16 | type: HostProcessType.Browser, 17 | release: "browser", 18 | }; 19 | }); 20 | 21 | registerProviders(); 22 | 23 | expect(StorageProviderFactory.providers["localFileSystemProxy"]).toBeUndefined(); 24 | expect(AssetProviderFactory.providers["localFileSystemProxy"]).toBeUndefined(); 25 | }); 26 | }); 27 | 28 | describe("Electron Registration", () => { 29 | it("Does Register localFileSystemProxy", () => { 30 | const getHostProcessMock = getHostProcess as jest.Mock; 31 | getHostProcessMock.mockImplementation(() => { 32 | return { 33 | type: HostProcessType.Electron, 34 | release: "electron", 35 | }; 36 | }); 37 | 38 | registerProviders(); 39 | 40 | expect(StorageProviderFactory.providers["localFileSystemProxy"]).toBeTruthy(); 41 | expect(AssetProviderFactory.providers["localFileSystemProxy"]).toBeTruthy(); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/services/connectionService.test.ts: -------------------------------------------------------------------------------- 1 | import ConnectionService, { IConnectionService } from "./connectionService"; 2 | import { AssetProviderFactory } from "../providers/storage/assetProviderFactory"; 3 | import MockFactory from "../common/mockFactory"; 4 | 5 | describe("Connection Service", () => { 6 | const connectionService: IConnectionService = new ConnectionService(); 7 | 8 | const storageProvider = MockFactory.createStorageProvider(); 9 | 10 | AssetProviderFactory.create = jest.fn(() => storageProvider); 11 | 12 | it("Saves connections", async () => { 13 | const connection = { 14 | ...MockFactory.createTestConnection(), 15 | id: undefined, 16 | }; 17 | 18 | const savedConnection = await connectionService.save(connection); 19 | expect(storageProvider.initialize).toBeCalled(); 20 | expect(savedConnection.id).not.toBeUndefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/services/connectionService.ts: -------------------------------------------------------------------------------- 1 | import shortid from "shortid"; 2 | import Guard from "../common/guard"; 3 | import { IConnection } from "../models/applicationState"; 4 | import { AssetProviderFactory } from "../providers/storage/assetProviderFactory"; 5 | 6 | /** 7 | * Functions required for a connection service 8 | * @member save - Save a connection 9 | */ 10 | export interface IConnectionService { 11 | save(connection: IConnection): Promise; 12 | } 13 | 14 | /** 15 | * @name - Connection Service 16 | * @description - Functions for dealing with project connections 17 | */ 18 | export default class ConnectionService implements IConnectionService { 19 | 20 | /** 21 | * Save a connection 22 | * @param connection - Connection to save 23 | */ 24 | public save(connection: IConnection) { 25 | Guard.null(connection); 26 | 27 | return new Promise(async (resolve, reject) => { 28 | try { 29 | if (!connection.id) { 30 | connection.id = shortid.generate(); 31 | } 32 | 33 | const assetProvider = AssetProviderFactory.createFromConnection(connection); 34 | if (assetProvider.initialize) { 35 | await assetProvider.initialize(); 36 | } 37 | 38 | resolve(connection); 39 | } catch (err) { 40 | reject(err); 41 | } 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import 'jest-enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | 5 | configure({ adapter: new Adapter() }); 6 | // Silence console.log and console.group statements in testing 7 | console.log = console.group = function() {}; 8 | const electronMock = { 9 | ipcRenderer: { 10 | send: jest.fn(), 11 | on: jest.fn(), 12 | }, 13 | }; 14 | 15 | window.require = jest.fn(() => electronMock); 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "allowJs": false, 5 | "skipLibCheck": false, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": false, 9 | "forceConsistentCasingInFileNames": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | "experimentalDecorators": true, 17 | "lib": [ 18 | "es2015", 19 | "dom" 20 | ], 21 | "typeRoots": [ 22 | "./typings", 23 | "./node_modules/@types" 24 | ], 25 | }, 26 | "include": [ 27 | "src" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "object-literal-sort-keys": false, 9 | "no-console": false, 10 | "no-shadowed-variable": false, 11 | "ordered-imports": false, 12 | "no-string-literal": false, 13 | "no-bitwise": false, 14 | "function-constructor": false 15 | }, 16 | "rulesDirectory": [] 17 | } -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/VoTT/47f5e0073092d706a216d44688565cd8d3e18dfc/typings.json -------------------------------------------------------------------------------- /typings/react-jsonschema-form/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-jsonschema-form/lib/utils" { 2 | export function getDefaultFormState(schema: any, formData: T): T; 3 | } 4 | --------------------------------------------------------------------------------