├── .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 | [](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 | 
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 | ";
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 |
--------------------------------------------------------------------------------