Comparar commits
65 Commits
| Autor | SHA1 | Data | |
|---|---|---|---|
| a3e51863df | |||
| a603a2743f | |||
| d48de94b8b | |||
| 27ce840067 | |||
| 719a89832c | |||
| fcc3523c95 | |||
| f731dfaece | |||
| 931115d59c | |||
| 2876444f21 | |||
| da28e80bc0 | |||
| 80add1ae25 | |||
| dba6dfebfa | |||
| a2689e290c | |||
| 7498260038 | |||
| 94daa110e9 | |||
| 9154f5178e | |||
| 94f09908a1 | |||
| 347f609096 | |||
| 2b68e213a1 | |||
| 0d5fdd00ca | |||
| a19d2b7688 | |||
| ed2d28fa99 | |||
| 51462af4d6 | |||
| c1afb7e355 | |||
| 54473577f8 | |||
| 90577ca8a0 | |||
| 11c962b8e8 | |||
| a4f9f652ac | |||
| 15881b7999 | |||
| 9d64f4aa0d | |||
| c0201ca51a | |||
| 5e25dc5406 | |||
| add4680e7b | |||
| 60ebb41540 | |||
| 745e854cc4 | |||
| 2234c8a0cc | |||
| 4d02db4215 | |||
| 90754dc74b | |||
| f29963c89e | |||
| acbbc86151 | |||
| 921dbac155 | |||
| a2ef52c7a4 | |||
| 25b4aa2dc8 | |||
| 0429590bec | |||
| 48805dcb85 | |||
| 0b06d6ac5b | |||
| 3998b6efc8 | |||
| 4a0dcb2905 | |||
| 354623ec21 | |||
| 8b34db5724 | |||
| bbd83a4df5 | |||
| 5b4610b3d9 | |||
| 996a555333 | |||
| 8439574dc5 | |||
| 4193bc0e6a | |||
| f394ea3d10 | |||
| 4f325dfe4b | |||
| 2ef4e1387f | |||
| 1001528a16 | |||
| 6974aef9d1 | |||
| 37234ec2e9 | |||
| d6a059447d | |||
| c10c971caf | |||
| 0fe63863b1 | |||
| 39521f2b61 |
@@ -0,0 +1,13 @@
|
||||
version: 1
|
||||
|
||||
update_configs:
|
||||
target_branch: "develop"
|
||||
|
||||
# Keep package.json (& lockfiles) up to date weekly
|
||||
- package_manager: "javascript"
|
||||
directory: "/"
|
||||
update_schedule: "weekly"
|
||||
|
||||
default_labels:
|
||||
- "dependencies"
|
||||
- "dependabot"
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
@@ -37,3 +38,8 @@ secrets.sh
|
||||
# complexity reports
|
||||
es6-src/
|
||||
report/
|
||||
|
||||
# VoTT Server
|
||||
server/lib
|
||||
server/node_modules
|
||||
server/coverage
|
||||
|
||||
@@ -2,6 +2,24 @@
|
||||
|
||||
<!-- cl-start -->
|
||||
|
||||
# [2.1.0](https://github.com/Microsoft/VoTT/compare/v2.0.0...v2.1.0) (04-29-2019)
|
||||
[GitHub Release](https://github.com/Microsoft/VoTT/releases/tag/v2.1.0)
|
||||
|
||||
- fix: Updates backwards compat & fixes cntk export image bug (#789)
|
||||
- fix: Updates export options for pascalVOC rename (#788)
|
||||
- fix: change method for alloc string to buffer (#777)
|
||||
- feat: Add CSV Exporter (#757)
|
||||
- fix: Fix display of tag color picker (#782)
|
||||
- feat: Active Learning Updates (#778)
|
||||
- doc: updates to readme and changelog (#781)
|
||||
- doc: Adds CODE_OF_CONDUCT.md (#779)
|
||||
- doc: Add bug & feature templates (#780)
|
||||
- fix: Refactored project tag/delete updates (#764)
|
||||
- fix: Enables selection of azure region for custom vision export (#765)
|
||||
- feat: CNTK Export Provider (#771)
|
||||
- feat: Save partial project progress during project creation (#769)
|
||||
- fix: Fixes ymax and rename Tensorflow nama everywhere (#763)
|
||||
|
||||
# [2.0.0](https://github.com/Microsoft/VoTT/compare/v2.0.0-preview.3...v2.0.0) (04-12-2019)
|
||||
[GitHub Release](https://github.com/Microsoft/VoTT/releases/tag/v2.0.0)
|
||||
|
||||
|
||||
+14
-2
@@ -28,6 +28,7 @@ VoTT helps facilitate an end-to-end machine learning pipeline:
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
|
||||
- [VoTT (Visual Object Tagging Tool)](#vott-visual-object-tagging-tool)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Getting Started](#getting-started)
|
||||
@@ -58,7 +59,7 @@ VoTT helps facilitate an end-to-end machine learning pipeline:
|
||||
|
||||
## Getting Started
|
||||
|
||||
VoTT can be installed as a native application or run from source. VoTT is also available as a [stand-alone Web application](https://vott.z5.web.core.windows.net) and can be used in any modern Web browser.
|
||||
VoTT can be installed as a native application or run from source. VoTT is also available as a [stand-alone Web application](https://vott.z22.web.core.windows.net) and can be used in any modern Web browser.
|
||||
|
||||
### Download and install a release package for your platform (recommended)
|
||||
|
||||
@@ -80,7 +81,7 @@ VoTT requires [NodeJS (>= 10.x, Dubnium) and NPM](https://github.com/nodejs/Rele
|
||||
|
||||
### Run as Web Application
|
||||
|
||||
Using a modern Web browser, VoTT can be loaded from: [https://vott.z5.web.core.windows.net](https://vott.z5.web.core.windows.net)
|
||||
Using a modern Web browser, VoTT can be loaded from: [https://vott.z22.web.core.windows.net](https://vott.z22.web.core.windows.net)
|
||||
|
||||
As noted above, the Web version of VoTT *cannot* access the local file system; all assets must be imported/exported through a Cloud project.
|
||||
|
||||
@@ -267,6 +268,17 @@ When the video playback bar is present, it allows the following shortcuts to sel
|
||||
* Multi-select - Hold down Shift while selecting regions
|
||||
* Exclusive Tracking mode - Ctrl + N to block frame UI allowing a user to create a region on top of existing regions
|
||||
|
||||
## Release Process
|
||||
|
||||

|
||||
|
||||
For more details on github/web releases and versions -- please review our [release process document](./docs/RELEASE_GUIDE.md)
|
||||
|
||||
To build VoTT executable using command:
|
||||
```
|
||||
npm run release
|
||||
```
|
||||
For details on packaging executable for the release -- please review our [PACKAGING.md](./docs/PACKAGING.md)
|
||||
|
||||
## Collaborators
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ pr:
|
||||
- dev* # kick off for pr targeting dev or prefix dev
|
||||
- master # trigger build for pr targeting master
|
||||
|
||||
variables:
|
||||
- group: CODE_COV
|
||||
|
||||
jobs:
|
||||
- job: Linux
|
||||
pool:
|
||||
@@ -17,7 +20,7 @@ jobs:
|
||||
|
||||
- job: MacOS
|
||||
pool:
|
||||
vmImage: macOS-10.13
|
||||
vmImage: macOS-10.15
|
||||
timeoutInMinutes: 60 # how long to run the job before automatically cancelling
|
||||
steps:
|
||||
- checkout: self # self represents the repo where the initial Pipelines YAML file was found
|
||||
@@ -26,7 +29,7 @@ jobs:
|
||||
|
||||
- job: Windows
|
||||
pool:
|
||||
vmImage: win1803
|
||||
vmImage: "windows-2019"
|
||||
timeoutInMinutes: 60 # how long to run the job before automatically cancelling
|
||||
steps:
|
||||
- checkout: self # self represents the repo where the initial Pipelines YAML file was found
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
name: "GitHub & Web Release [$(SourceBranchName)] - $(Date:yyyyMMdd)$(Rev:.r)"
|
||||
|
||||
trigger: none # manual queue only when we're ready to release
|
||||
pr: none # disable CI build for PR
|
||||
|
||||
variables:
|
||||
azureSubscription: "pj-little-sub"
|
||||
DEV_STORAGE_ACCOUNT: 'vottdev'
|
||||
PROD_STORAGE_ACCOUNT: 'vott'
|
||||
DEV_URL: "https://vottdev.z5.web.core.windows.net/"
|
||||
PROD_URL: "https://vott.z22.web.core.windows.net/"
|
||||
|
||||
stages:
|
||||
- stage: version_bump_commit
|
||||
jobs:
|
||||
- template: templates/npm-version-bump.yml
|
||||
parameters:
|
||||
versionType: $(NPM_VERSION_TYPE)
|
||||
|
||||
- stage: github_release
|
||||
dependsOn: version_bump_commit
|
||||
jobs:
|
||||
- template: templates/create-github-release.yml
|
||||
parameters:
|
||||
GitHubConnection: 'GitHub connection' # defaults for any parameters that aren't specified
|
||||
repositoryName: '$(Build.Repository.Name)' # microsoft/VoTT
|
||||
isPreRelease: $(IS_PRERELEASE) # set when queuing build
|
||||
isDraft: $(IS_DRAFT)
|
||||
|
||||
- stage: web_release
|
||||
dependsOn: version_bump_commit
|
||||
jobs:
|
||||
- template: templates/create-web-release.yml
|
||||
@@ -1,26 +0,0 @@
|
||||
# Node.js with React
|
||||
# Build a Node.js project that uses React.
|
||||
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: 'Use Node 10.x'
|
||||
inputs:
|
||||
versionSpec: 10.x
|
||||
|
||||
- bash: |
|
||||
set -ex
|
||||
|
||||
# clean install
|
||||
npm ci
|
||||
npm run release-ci
|
||||
|
||||
mkdir -p linux
|
||||
cp releases/vott*.snap linux/
|
||||
displayName: Build
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Artifact: linux'
|
||||
inputs:
|
||||
PathtoPublish: linux
|
||||
ArtifactName: linux
|
||||
@@ -1,22 +0,0 @@
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: 'Use Node 10.x'
|
||||
inputs:
|
||||
versionSpec: 10.x
|
||||
|
||||
- bash: |
|
||||
set -ex
|
||||
|
||||
# clean install
|
||||
npm ci
|
||||
npm run release-ci
|
||||
|
||||
mkdir -p mac
|
||||
cp releases/vott*.dmg mac/
|
||||
displayName: Build
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Artifact: mac'
|
||||
inputs:
|
||||
PathtoPublish: mac
|
||||
ArtifactName: mac
|
||||
@@ -1,69 +0,0 @@
|
||||
name: $(Date:yyyyMMdd).$(Hours)$(Minutes)$(Seconds)
|
||||
variables:
|
||||
AZURE_STORAGE_ACCOUNT: vottv2
|
||||
trigger:
|
||||
- dev*
|
||||
- master
|
||||
pr: none # disable CI build for PR
|
||||
|
||||
pool:
|
||||
vmImage: 'Ubuntu-16.04'
|
||||
|
||||
steps:
|
||||
- bash: |
|
||||
set -e
|
||||
|
||||
COMMIT_SHA=`echo ${BUILD_SOURCEVERSION} | cut -c1-8`
|
||||
echo "sha: " $COMMIT_SHA
|
||||
|
||||
# rewrite build number
|
||||
echo "##vso[build.updatebuildnumber]Report-${COMMIT_SHA}-${BUILD_BUILDNUMBER}"
|
||||
displayName: "Rewrite build number"
|
||||
|
||||
- bash: |
|
||||
set -e
|
||||
|
||||
ACCOUNT=$(AZURE_STORAGE_ACCOUNT)
|
||||
if [[ -z "${ACCOUNT}" ]]; then
|
||||
echo "Need to set AZURE_STORAGE_ACCOUNT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
KEY=$(AZURE_STORAGE_KEY)
|
||||
if [[ -z "${KEY}" ]]; then
|
||||
echo "Need to set AZURE_STORAGE_KEY"
|
||||
exit 1
|
||||
fi
|
||||
displayName: "Verify storage account cred exists"
|
||||
|
||||
- task: NodeTool@0
|
||||
displayName: "Use Node 10.x"
|
||||
inputs:
|
||||
versionSpec: 10.x
|
||||
|
||||
- task: Npm@1
|
||||
displayName: 'Run `npm ci`'
|
||||
inputs:
|
||||
command: custom
|
||||
verbose: false
|
||||
customCommand: ci
|
||||
|
||||
- task: AzureCLI@1
|
||||
displayName: "Pull down old report and add updates"
|
||||
inputs:
|
||||
azureSubscription: 'PELITTLE TEAM - CSE DWR (d36d0808-a967-4f73-9fdc-32ea232fc81d)'
|
||||
scriptLocation: inlineScript
|
||||
inlineScript: './scripts/update-report.sh'
|
||||
|
||||
- bash: |
|
||||
set -e
|
||||
|
||||
cat /tmp/download.log
|
||||
displayName: "print download log"
|
||||
condition: succeededOrFailed()
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Artifact: report'
|
||||
inputs:
|
||||
PathtoPublish: ./report
|
||||
ArtifactName: report
|
||||
@@ -1,5 +0,0 @@
|
||||
# Overview
|
||||
|
||||
Collection of release definitions used in this project.
|
||||
Since release does not yet support yaml, we're exporting them for
|
||||
record keeping.
|
||||
@@ -1,535 +0,0 @@
|
||||
{
|
||||
"source": 2,
|
||||
"revision": 6,
|
||||
"description": null,
|
||||
"createdBy": {
|
||||
"displayName": "My Ho 🥓🥐🍚",
|
||||
"url": "https://app.vssps.visualstudio.com/Af945f528-1155-48f8-b08b-b6529166b9f0/_apis/Identities/1077cc21-332d-415a-8e93-e6277da44f6d",
|
||||
"_links": {
|
||||
"avatar": {
|
||||
"href": "https://dev.azure.com/msft-vott/_apis/GraphProfile/MemberAvatars/aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1"
|
||||
}
|
||||
},
|
||||
"id": "1077cc21-332d-415a-8e93-e6277da44f6d",
|
||||
"uniqueName": "myho@microsoft.com",
|
||||
"imageUrl": "https://dev.azure.com/msft-vott/_apis/GraphProfile/MemberAvatars/aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1",
|
||||
"descriptor": "aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1"
|
||||
},
|
||||
"createdOn": "2019-03-22T18:43:08.407Z",
|
||||
"modifiedBy": {
|
||||
"displayName": "My Ho 🥓🥐🍚",
|
||||
"url": "https://app.vssps.visualstudio.com/Af945f528-1155-48f8-b08b-b6529166b9f0/_apis/Identities/1077cc21-332d-415a-8e93-e6277da44f6d",
|
||||
"_links": {
|
||||
"avatar": {
|
||||
"href": "https://dev.azure.com/msft-vott/_apis/GraphProfile/MemberAvatars/aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1"
|
||||
}
|
||||
},
|
||||
"id": "1077cc21-332d-415a-8e93-e6277da44f6d",
|
||||
"uniqueName": "myho@microsoft.com",
|
||||
"imageUrl": "https://dev.azure.com/msft-vott/_apis/GraphProfile/MemberAvatars/aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1",
|
||||
"descriptor": "aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1"
|
||||
},
|
||||
"modifiedOn": "2019-04-12T21:39:14.277Z",
|
||||
"isDeleted": false,
|
||||
"variables": {},
|
||||
"variableGroups": [],
|
||||
"environments": [
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Dev",
|
||||
"rank": 1,
|
||||
"owner": {
|
||||
"displayName": "My Ho 🥓🥐🍚",
|
||||
"url": "https://app.vssps.visualstudio.com/Af945f528-1155-48f8-b08b-b6529166b9f0/_apis/Identities/1077cc21-332d-415a-8e93-e6277da44f6d",
|
||||
"_links": {
|
||||
"avatar": {
|
||||
"href": "https://dev.azure.com/msft-vott/_apis/GraphProfile/MemberAvatars/aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1"
|
||||
}
|
||||
},
|
||||
"id": "1077cc21-332d-415a-8e93-e6277da44f6d",
|
||||
"uniqueName": "myho@microsoft.com",
|
||||
"imageUrl": "https://dev.azure.com/msft-vott/_apis/GraphProfile/MemberAvatars/aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1",
|
||||
"descriptor": "aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1"
|
||||
},
|
||||
"variables": {
|
||||
"DEV_STORAGE_ACCOUNT": {
|
||||
"value": "vottdev",
|
||||
"allowOverride": true
|
||||
}
|
||||
},
|
||||
"variableGroups": [],
|
||||
"preDeployApprovals": {
|
||||
"approvals": [
|
||||
{
|
||||
"rank": 1,
|
||||
"isAutomated": true,
|
||||
"isNotificationOn": false,
|
||||
"id": 10
|
||||
}
|
||||
],
|
||||
"approvalOptions": {
|
||||
"requiredApproverCount": null,
|
||||
"releaseCreatorCanBeApprover": false,
|
||||
"autoTriggeredAndPreviousEnvironmentApprovedCanBeSkipped": false,
|
||||
"enforceIdentityRevalidation": false,
|
||||
"timeoutInMinutes": 0,
|
||||
"executionOrder": 1
|
||||
}
|
||||
},
|
||||
"deployStep": {
|
||||
"id": 11
|
||||
},
|
||||
"postDeployApprovals": {
|
||||
"approvals": [
|
||||
{
|
||||
"rank": 1,
|
||||
"isAutomated": true,
|
||||
"isNotificationOn": false,
|
||||
"id": 12
|
||||
}
|
||||
],
|
||||
"approvalOptions": {
|
||||
"requiredApproverCount": null,
|
||||
"releaseCreatorCanBeApprover": false,
|
||||
"autoTriggeredAndPreviousEnvironmentApprovedCanBeSkipped": false,
|
||||
"enforceIdentityRevalidation": false,
|
||||
"timeoutInMinutes": 0,
|
||||
"executionOrder": 2
|
||||
}
|
||||
},
|
||||
"deployPhases": [
|
||||
{
|
||||
"deploymentInput": {
|
||||
"parallelExecution": {
|
||||
"parallelExecutionType": 0
|
||||
},
|
||||
"skipArtifactsDownload": false,
|
||||
"artifactsDownloadInput": {
|
||||
"downloadInputs": [
|
||||
{
|
||||
"alias": "_Build Web Artifact",
|
||||
"artifactType": "Build",
|
||||
"artifactDownloadMode": "All",
|
||||
"artifactItems": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"queueId": 3,
|
||||
"demands": [],
|
||||
"enableAccessToken": false,
|
||||
"timeoutInMinutes": 0,
|
||||
"jobCancelTimeoutInMinutes": 1,
|
||||
"condition": "succeeded()",
|
||||
"overrideInputs": {}
|
||||
},
|
||||
"rank": 1,
|
||||
"phaseType": 1,
|
||||
"name": "Deploy to dev*",
|
||||
"refName": null,
|
||||
"workflowTasks": [
|
||||
{
|
||||
"environment": {},
|
||||
"taskId": "eb72cb01-a7e5-427b-a8a1-1b31ccac8a43",
|
||||
"version": "2.*",
|
||||
"name": "AzureBlob File Copy to $(DEV_STORAGE_ACCOUNT)",
|
||||
"refName": "",
|
||||
"enabled": true,
|
||||
"alwaysRun": false,
|
||||
"continueOnError": false,
|
||||
"timeoutInMinutes": 0,
|
||||
"definitionType": "task",
|
||||
"overrideInputs": {},
|
||||
"condition": "succeeded()",
|
||||
"inputs": {
|
||||
"SourcePath": "$(System.DefaultWorkingDirectory)/_Build Web Artifact/build",
|
||||
"ConnectedServiceNameSelector": "ConnectedServiceNameARM",
|
||||
"ConnectedServiceName": "",
|
||||
"ConnectedServiceNameARM": "28e5058e-d34a-48f3-978b-4bb47ea9e227",
|
||||
"Destination": "AzureBlob",
|
||||
"StorageAccount": "",
|
||||
"StorageAccountRM": "$(DEV_STORAGE_ACCOUNT)",
|
||||
"ContainerName": "$web",
|
||||
"BlobPrefix": "",
|
||||
"EnvironmentName": "",
|
||||
"EnvironmentNameRM": "",
|
||||
"ResourceFilteringMethod": "machineNames",
|
||||
"MachineNames": "",
|
||||
"vmsAdminUserName": "",
|
||||
"vmsAdminPassword": "",
|
||||
"TargetPath": "",
|
||||
"AdditionalArgumentsForBlobCopy": "",
|
||||
"AdditionalArgumentsForVMCopy": "",
|
||||
"enableCopyPrerequisites": "false",
|
||||
"CopyFilesInParallel": "true",
|
||||
"CleanTargetBeforeCopy": "false",
|
||||
"skipCACheck": "true",
|
||||
"outputStorageUri": "",
|
||||
"outputStorageContainerSasToken": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"environmentOptions": {
|
||||
"emailNotificationType": "OnlyOnFailure",
|
||||
"emailRecipients": "release.environment.owner;release.creator",
|
||||
"skipArtifactsDownload": false,
|
||||
"timeoutInMinutes": 0,
|
||||
"enableAccessToken": false,
|
||||
"publishDeploymentStatus": true,
|
||||
"badgeEnabled": false,
|
||||
"autoLinkWorkItems": false,
|
||||
"pullRequestDeploymentEnabled": false
|
||||
},
|
||||
"demands": [],
|
||||
"conditions": [
|
||||
{
|
||||
"name": "ReleaseStarted",
|
||||
"conditionType": 1,
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"executionPolicy": {
|
||||
"concurrencyCount": 1,
|
||||
"queueDepthCount": 0
|
||||
},
|
||||
"schedules": [],
|
||||
"currentRelease": {
|
||||
"id": 215,
|
||||
"url": "https://vsrm.dev.azure.com/msft-vott/4e739af6-f85e-44b9-a3f3-75eb893a6223/_apis/Release/releases/215",
|
||||
"_links": {}
|
||||
},
|
||||
"retentionPolicy": {
|
||||
"daysToKeep": 30,
|
||||
"releasesToKeep": 3,
|
||||
"retainBuild": true
|
||||
},
|
||||
"processParameters": {
|
||||
"dataSourceBindings": [
|
||||
{
|
||||
"dataSourceName": "AzureRMWebAppNamesByAppType",
|
||||
"parameters": {
|
||||
"WebAppKind": "$(WebAppKind)"
|
||||
},
|
||||
"endpointId": "$(ConnectedServiceName)",
|
||||
"target": "WebAppName"
|
||||
}
|
||||
]
|
||||
},
|
||||
"properties": {},
|
||||
"preDeploymentGates": {
|
||||
"id": 0,
|
||||
"gatesOptions": null,
|
||||
"gates": []
|
||||
},
|
||||
"postDeploymentGates": {
|
||||
"id": 0,
|
||||
"gatesOptions": null,
|
||||
"gates": []
|
||||
},
|
||||
"environmentTriggers": [],
|
||||
"badgeUrl": "https://vsrm.dev.azure.com/msft-vott/_apis/public/Release/badge/4e739af6-f85e-44b9-a3f3-75eb893a6223/4/4"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "v2/master",
|
||||
"rank": 2,
|
||||
"owner": {
|
||||
"displayName": "My Ho 🥓🥐🍚",
|
||||
"url": "https://app.vssps.visualstudio.com/Af945f528-1155-48f8-b08b-b6529166b9f0/_apis/Identities/1077cc21-332d-415a-8e93-e6277da44f6d",
|
||||
"_links": {
|
||||
"avatar": {
|
||||
"href": "https://dev.azure.com/msft-vott/_apis/GraphProfile/MemberAvatars/aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1"
|
||||
}
|
||||
},
|
||||
"id": "1077cc21-332d-415a-8e93-e6277da44f6d",
|
||||
"uniqueName": "myho@microsoft.com",
|
||||
"imageUrl": "https://dev.azure.com/msft-vott/_apis/GraphProfile/MemberAvatars/aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1",
|
||||
"descriptor": "aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1"
|
||||
},
|
||||
"variables": {
|
||||
"PROD_STORAGE_ACCOUNT": {
|
||||
"value": "vott"
|
||||
}
|
||||
},
|
||||
"variableGroups": [],
|
||||
"preDeployApprovals": {
|
||||
"approvals": [
|
||||
{
|
||||
"rank": 1,
|
||||
"isAutomated": true,
|
||||
"isNotificationOn": false,
|
||||
"id": 19
|
||||
}
|
||||
],
|
||||
"approvalOptions": {
|
||||
"requiredApproverCount": null,
|
||||
"releaseCreatorCanBeApprover": false,
|
||||
"autoTriggeredAndPreviousEnvironmentApprovedCanBeSkipped": false,
|
||||
"enforceIdentityRevalidation": false,
|
||||
"timeoutInMinutes": 0,
|
||||
"executionOrder": 1
|
||||
}
|
||||
},
|
||||
"deployStep": {
|
||||
"id": 20
|
||||
},
|
||||
"postDeployApprovals": {
|
||||
"approvals": [
|
||||
{
|
||||
"rank": 1,
|
||||
"isAutomated": true,
|
||||
"isNotificationOn": false,
|
||||
"id": 21
|
||||
}
|
||||
],
|
||||
"approvalOptions": {
|
||||
"requiredApproverCount": null,
|
||||
"releaseCreatorCanBeApprover": false,
|
||||
"autoTriggeredAndPreviousEnvironmentApprovedCanBeSkipped": false,
|
||||
"enforceIdentityRevalidation": false,
|
||||
"timeoutInMinutes": 0,
|
||||
"executionOrder": 2
|
||||
}
|
||||
},
|
||||
"deployPhases": [
|
||||
{
|
||||
"deploymentInput": {
|
||||
"parallelExecution": {
|
||||
"parallelExecutionType": 0
|
||||
},
|
||||
"skipArtifactsDownload": false,
|
||||
"artifactsDownloadInput": {
|
||||
"downloadInputs": [
|
||||
{
|
||||
"alias": "_Build Web Artifact",
|
||||
"artifactType": "Build",
|
||||
"artifactDownloadMode": "All",
|
||||
"artifactItems": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"queueId": 3,
|
||||
"demands": [],
|
||||
"enableAccessToken": false,
|
||||
"timeoutInMinutes": 0,
|
||||
"jobCancelTimeoutInMinutes": 1,
|
||||
"condition": "and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/v2'), eq(variables['Build.SourceBranch'], 'refs/heads/master')))",
|
||||
"overrideInputs": {}
|
||||
},
|
||||
"rank": 1,
|
||||
"phaseType": 1,
|
||||
"name": "Deploy to V2/Master",
|
||||
"refName": null,
|
||||
"workflowTasks": [
|
||||
{
|
||||
"environment": {},
|
||||
"taskId": "eb72cb01-a7e5-427b-a8a1-1b31ccac8a43",
|
||||
"version": "2.*",
|
||||
"name": "AzureBlob File Copy to $(PROD_STORAGE_ACCOUNT)",
|
||||
"refName": "",
|
||||
"enabled": true,
|
||||
"alwaysRun": false,
|
||||
"continueOnError": false,
|
||||
"timeoutInMinutes": 0,
|
||||
"definitionType": "task",
|
||||
"overrideInputs": {},
|
||||
"condition": "succeeded()",
|
||||
"inputs": {
|
||||
"SourcePath": "$(System.DefaultWorkingDirectory)/_Build Web Artifact/build",
|
||||
"ConnectedServiceNameSelector": "ConnectedServiceNameARM",
|
||||
"ConnectedServiceName": "",
|
||||
"ConnectedServiceNameARM": "28e5058e-d34a-48f3-978b-4bb47ea9e227",
|
||||
"Destination": "AzureBlob",
|
||||
"StorageAccount": "",
|
||||
"StorageAccountRM": "$(PROD_STORAGE_ACCOUNT)",
|
||||
"ContainerName": "$web",
|
||||
"BlobPrefix": "",
|
||||
"EnvironmentName": "",
|
||||
"EnvironmentNameRM": "",
|
||||
"ResourceFilteringMethod": "machineNames",
|
||||
"MachineNames": "",
|
||||
"vmsAdminUserName": "",
|
||||
"vmsAdminPassword": "",
|
||||
"TargetPath": "",
|
||||
"AdditionalArgumentsForBlobCopy": "",
|
||||
"AdditionalArgumentsForVMCopy": "",
|
||||
"enableCopyPrerequisites": "false",
|
||||
"CopyFilesInParallel": "true",
|
||||
"CleanTargetBeforeCopy": "false",
|
||||
"skipCACheck": "true",
|
||||
"outputStorageUri": "",
|
||||
"outputStorageContainerSasToken": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"environmentOptions": {
|
||||
"emailNotificationType": "OnlyOnFailure",
|
||||
"emailRecipients": "release.environment.owner;release.creator",
|
||||
"skipArtifactsDownload": false,
|
||||
"timeoutInMinutes": 0,
|
||||
"enableAccessToken": false,
|
||||
"publishDeploymentStatus": true,
|
||||
"badgeEnabled": false,
|
||||
"autoLinkWorkItems": false,
|
||||
"pullRequestDeploymentEnabled": false
|
||||
},
|
||||
"demands": [],
|
||||
"conditions": [
|
||||
{
|
||||
"name": "Dev",
|
||||
"conditionType": 2,
|
||||
"value": "4"
|
||||
},
|
||||
{
|
||||
"name": "_Build Web Artifact",
|
||||
"conditionType": 4,
|
||||
"value": "{\"sourceBranch\":\"master\",\"tags\":[],\"useBuildDefinitionBranch\":false,\"createReleaseOnBuildTagging\":false,\"tagFilter\":null}"
|
||||
}
|
||||
],
|
||||
"executionPolicy": {
|
||||
"concurrencyCount": 1,
|
||||
"queueDepthCount": 0
|
||||
},
|
||||
"schedules": [],
|
||||
"currentRelease": {
|
||||
"id": 0,
|
||||
"url": "https://vsrm.dev.azure.com/msft-vott/4e739af6-f85e-44b9-a3f3-75eb893a6223/_apis/Release/releases/0",
|
||||
"_links": {}
|
||||
},
|
||||
"retentionPolicy": {
|
||||
"daysToKeep": 30,
|
||||
"releasesToKeep": 3,
|
||||
"retainBuild": true
|
||||
},
|
||||
"processParameters": {
|
||||
"dataSourceBindings": [
|
||||
{
|
||||
"dataSourceName": "AzureRMWebAppNamesByAppType",
|
||||
"parameters": {
|
||||
"WebAppKind": "$(WebAppKind)"
|
||||
},
|
||||
"endpointId": "$(ConnectedServiceName)",
|
||||
"target": "WebAppName"
|
||||
}
|
||||
]
|
||||
},
|
||||
"properties": {},
|
||||
"preDeploymentGates": {
|
||||
"id": 0,
|
||||
"gatesOptions": null,
|
||||
"gates": []
|
||||
},
|
||||
"postDeploymentGates": {
|
||||
"id": 0,
|
||||
"gatesOptions": null,
|
||||
"gates": []
|
||||
},
|
||||
"environmentTriggers": [],
|
||||
"badgeUrl": "https://vsrm.dev.azure.com/msft-vott/_apis/public/Release/badge/4e739af6-f85e-44b9-a3f3-75eb893a6223/4/7"
|
||||
}
|
||||
],
|
||||
"artifacts": [
|
||||
{
|
||||
"sourceId": "4e739af6-f85e-44b9-a3f3-75eb893a6223:36",
|
||||
"type": "Build",
|
||||
"alias": "_Build Web Artifact",
|
||||
"definitionReference": {
|
||||
"artifactSourceDefinitionUrl": {
|
||||
"id": "https://dev.azure.com/msft-vott/_permalink/_build/index?collectionId=2f887b33-5e65-447a-a189-049f04641d6d&projectId=4e739af6-f85e-44b9-a3f3-75eb893a6223&definitionId=36",
|
||||
"name": ""
|
||||
},
|
||||
"defaultVersionBranch": {
|
||||
"id": "",
|
||||
"name": ""
|
||||
},
|
||||
"defaultVersionSpecific": {
|
||||
"id": "",
|
||||
"name": ""
|
||||
},
|
||||
"defaultVersionTags": {
|
||||
"id": "",
|
||||
"name": ""
|
||||
},
|
||||
"defaultVersionType": {
|
||||
"id": "latestType",
|
||||
"name": "Latest"
|
||||
},
|
||||
"definition": {
|
||||
"id": "36",
|
||||
"name": "Build Web Artifact"
|
||||
},
|
||||
"definitions": {
|
||||
"id": "",
|
||||
"name": ""
|
||||
},
|
||||
"IsMultiDefinitionType": {
|
||||
"id": "False",
|
||||
"name": "False"
|
||||
},
|
||||
"project": {
|
||||
"id": "4e739af6-f85e-44b9-a3f3-75eb893a6223",
|
||||
"name": "VoTT"
|
||||
},
|
||||
"repository": {
|
||||
"id": "",
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"isPrimary": true,
|
||||
"isRetained": false
|
||||
}
|
||||
],
|
||||
"triggers": [
|
||||
{
|
||||
"artifactAlias": "_Build Web Artifact",
|
||||
"triggerConditions": [
|
||||
{
|
||||
"sourceBranch": "dev*",
|
||||
"tags": [],
|
||||
"useBuildDefinitionBranch": false,
|
||||
"createReleaseOnBuildTagging": false
|
||||
},
|
||||
{
|
||||
"sourceBranch": "v2",
|
||||
"tags": [],
|
||||
"useBuildDefinitionBranch": false,
|
||||
"createReleaseOnBuildTagging": false
|
||||
},
|
||||
{
|
||||
"sourceBranch": "master",
|
||||
"tags": [],
|
||||
"useBuildDefinitionBranch": false,
|
||||
"createReleaseOnBuildTagging": false
|
||||
}
|
||||
],
|
||||
"triggerType": 1
|
||||
}
|
||||
],
|
||||
"releaseNameFormat": "Release-$(rev:r)",
|
||||
"tags": [],
|
||||
"pipelineProcess": {
|
||||
"type": 1
|
||||
},
|
||||
"properties": {
|
||||
"DefinitionCreationSource": {
|
||||
"$type": "System.String",
|
||||
"$value": "ReleaseClone"
|
||||
}
|
||||
},
|
||||
"id": 4,
|
||||
"name": "VoTT - Deploy to Web",
|
||||
"path": "\\",
|
||||
"projectReference": null,
|
||||
"url": "https://vsrm.dev.azure.com/msft-vott/4e739af6-f85e-44b9-a3f3-75eb893a6223/_apis/Release/definitions/4",
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "https://vsrm.dev.azure.com/msft-vott/4e739af6-f85e-44b9-a3f3-75eb893a6223/_apis/Release/definitions/4"
|
||||
},
|
||||
"web": {
|
||||
"href": "https://dev.azure.com/msft-vott/4e739af6-f85e-44b9-a3f3-75eb893a6223/_release?definitionId=4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,329 +0,0 @@
|
||||
{
|
||||
"source": 2,
|
||||
"revision": 8,
|
||||
"description": "copy artifact to azure blob hosting plato report for VoTT",
|
||||
"createdBy": {
|
||||
"displayName": "My Ho 🥓🥐🍚",
|
||||
"url": "https://app.vssps.visualstudio.com/Af945f528-1155-48f8-b08b-b6529166b9f0/_apis/Identities/1077cc21-332d-415a-8e93-e6277da44f6d",
|
||||
"_links": {
|
||||
"avatar": {
|
||||
"href": "https://dev.azure.com/msft-vott/_apis/GraphProfile/MemberAvatars/aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1"
|
||||
}
|
||||
},
|
||||
"id": "1077cc21-332d-415a-8e93-e6277da44f6d",
|
||||
"uniqueName": "myho@microsoft.com",
|
||||
"imageUrl": "https://dev.azure.com/msft-vott/_apis/GraphProfile/MemberAvatars/aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1",
|
||||
"descriptor": "aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1"
|
||||
},
|
||||
"createdOn": "2019-03-25T22:52:29.780Z",
|
||||
"modifiedBy": {
|
||||
"displayName": "My Ho 🥓🥐🍚",
|
||||
"url": "https://app.vssps.visualstudio.com/Af945f528-1155-48f8-b08b-b6529166b9f0/_apis/Identities/1077cc21-332d-415a-8e93-e6277da44f6d",
|
||||
"_links": {
|
||||
"avatar": {
|
||||
"href": "https://dev.azure.com/msft-vott/_apis/GraphProfile/MemberAvatars/aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1"
|
||||
}
|
||||
},
|
||||
"id": "1077cc21-332d-415a-8e93-e6277da44f6d",
|
||||
"uniqueName": "myho@microsoft.com",
|
||||
"imageUrl": "https://dev.azure.com/msft-vott/_apis/GraphProfile/MemberAvatars/aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1",
|
||||
"descriptor": "aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1"
|
||||
},
|
||||
"modifiedOn": "2019-04-12T22:29:15.590Z",
|
||||
"isDeleted": false,
|
||||
"variables": {
|
||||
"STORAGE_ACCOUNT": {
|
||||
"value": "vottv2",
|
||||
"allowOverride": true
|
||||
}
|
||||
},
|
||||
"variableGroups": [],
|
||||
"environments": [
|
||||
{
|
||||
"id": 6,
|
||||
"name": "Upload Report",
|
||||
"rank": 1,
|
||||
"owner": {
|
||||
"displayName": "My Ho 🥓🥐🍚",
|
||||
"url": "https://app.vssps.visualstudio.com/Af945f528-1155-48f8-b08b-b6529166b9f0/_apis/Identities/1077cc21-332d-415a-8e93-e6277da44f6d",
|
||||
"_links": {
|
||||
"avatar": {
|
||||
"href": "https://dev.azure.com/msft-vott/_apis/GraphProfile/MemberAvatars/aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1"
|
||||
}
|
||||
},
|
||||
"id": "1077cc21-332d-415a-8e93-e6277da44f6d",
|
||||
"uniqueName": "myho@microsoft.com",
|
||||
"imageUrl": "https://dev.azure.com/msft-vott/_apis/GraphProfile/MemberAvatars/aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1",
|
||||
"descriptor": "aad.MTRlNGE0YjQtNDU5NS03OGI5LTk2MmItNTM0ZjNjYjUxMWI1"
|
||||
},
|
||||
"variables": {},
|
||||
"variableGroups": [],
|
||||
"preDeployApprovals": {
|
||||
"approvals": [
|
||||
{
|
||||
"rank": 1,
|
||||
"isAutomated": true,
|
||||
"isNotificationOn": false,
|
||||
"id": 16
|
||||
}
|
||||
],
|
||||
"approvalOptions": {
|
||||
"requiredApproverCount": null,
|
||||
"releaseCreatorCanBeApprover": false,
|
||||
"autoTriggeredAndPreviousEnvironmentApprovedCanBeSkipped": false,
|
||||
"enforceIdentityRevalidation": false,
|
||||
"timeoutInMinutes": 0,
|
||||
"executionOrder": 1
|
||||
}
|
||||
},
|
||||
"deployStep": {
|
||||
"id": 17
|
||||
},
|
||||
"postDeployApprovals": {
|
||||
"approvals": [
|
||||
{
|
||||
"rank": 1,
|
||||
"isAutomated": true,
|
||||
"isNotificationOn": false,
|
||||
"id": 18
|
||||
}
|
||||
],
|
||||
"approvalOptions": {
|
||||
"requiredApproverCount": null,
|
||||
"releaseCreatorCanBeApprover": false,
|
||||
"autoTriggeredAndPreviousEnvironmentApprovedCanBeSkipped": false,
|
||||
"enforceIdentityRevalidation": false,
|
||||
"timeoutInMinutes": 0,
|
||||
"executionOrder": 2
|
||||
}
|
||||
},
|
||||
"deployPhases": [
|
||||
{
|
||||
"deploymentInput": {
|
||||
"parallelExecution": {
|
||||
"parallelExecutionType": 0
|
||||
},
|
||||
"skipArtifactsDownload": false,
|
||||
"artifactsDownloadInput": {
|
||||
"downloadInputs": [
|
||||
{
|
||||
"alias": "_VoTT - Generate latest plato report",
|
||||
"artifactType": "Build",
|
||||
"artifactDownloadMode": "All",
|
||||
"artifactItems": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"queueId": 3,
|
||||
"demands": [],
|
||||
"enableAccessToken": false,
|
||||
"timeoutInMinutes": 0,
|
||||
"jobCancelTimeoutInMinutes": 1,
|
||||
"condition": "succeeded()",
|
||||
"overrideInputs": {}
|
||||
},
|
||||
"rank": 1,
|
||||
"phaseType": 1,
|
||||
"name": "Upload Report",
|
||||
"refName": null,
|
||||
"workflowTasks": [
|
||||
{
|
||||
"environment": {},
|
||||
"taskId": "eb72cb01-a7e5-427b-a8a1-1b31ccac8a43",
|
||||
"version": "2.*",
|
||||
"name": "AzureBlob File Copy To $(STORAGE_ACCOUNT)",
|
||||
"refName": "",
|
||||
"enabled": true,
|
||||
"alwaysRun": false,
|
||||
"continueOnError": false,
|
||||
"timeoutInMinutes": 0,
|
||||
"definitionType": "task",
|
||||
"overrideInputs": {},
|
||||
"condition": "succeeded()",
|
||||
"inputs": {
|
||||
"SourcePath": "$(System.DefaultWorkingDirectory)/_VoTT - Generate latest plato report/report",
|
||||
"ConnectedServiceNameSelector": "ConnectedServiceNameARM",
|
||||
"ConnectedServiceName": "",
|
||||
"ConnectedServiceNameARM": "28e5058e-d34a-48f3-978b-4bb47ea9e227",
|
||||
"Destination": "AzureBlob",
|
||||
"StorageAccount": "",
|
||||
"StorageAccountRM": "$(STORAGE_ACCOUNT)",
|
||||
"ContainerName": "$web",
|
||||
"BlobPrefix": "",
|
||||
"EnvironmentName": "",
|
||||
"EnvironmentNameRM": "",
|
||||
"ResourceFilteringMethod": "machineNames",
|
||||
"MachineNames": "",
|
||||
"vmsAdminUserName": "",
|
||||
"vmsAdminPassword": "",
|
||||
"TargetPath": "",
|
||||
"AdditionalArgumentsForBlobCopy": "",
|
||||
"AdditionalArgumentsForVMCopy": "",
|
||||
"enableCopyPrerequisites": "false",
|
||||
"CopyFilesInParallel": "true",
|
||||
"CleanTargetBeforeCopy": "false",
|
||||
"skipCACheck": "true",
|
||||
"outputStorageUri": "",
|
||||
"outputStorageContainerSasToken": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"environmentOptions": {
|
||||
"emailNotificationType": "OnlyOnFailure",
|
||||
"emailRecipients": "release.environment.owner;release.creator",
|
||||
"skipArtifactsDownload": false,
|
||||
"timeoutInMinutes": 0,
|
||||
"enableAccessToken": false,
|
||||
"publishDeploymentStatus": true,
|
||||
"badgeEnabled": false,
|
||||
"autoLinkWorkItems": false,
|
||||
"pullRequestDeploymentEnabled": false
|
||||
},
|
||||
"demands": [],
|
||||
"conditions": [
|
||||
{
|
||||
"name": "ReleaseStarted",
|
||||
"conditionType": 1,
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"executionPolicy": {
|
||||
"concurrencyCount": 1,
|
||||
"queueDepthCount": 0
|
||||
},
|
||||
"schedules": [],
|
||||
"currentRelease": {
|
||||
"id": 216,
|
||||
"url": "https://vsrm.dev.azure.com/msft-vott/4e739af6-f85e-44b9-a3f3-75eb893a6223/_apis/Release/releases/216",
|
||||
"_links": {}
|
||||
},
|
||||
"retentionPolicy": {
|
||||
"daysToKeep": 30,
|
||||
"releasesToKeep": 3,
|
||||
"retainBuild": true
|
||||
},
|
||||
"processParameters": {
|
||||
"dataSourceBindings": [
|
||||
{
|
||||
"dataSourceName": "AzureRMWebAppNamesByAppType",
|
||||
"parameters": {
|
||||
"WebAppKind": "$(WebAppKind)"
|
||||
},
|
||||
"endpointId": "$(ConnectedServiceName)",
|
||||
"target": "WebAppName"
|
||||
}
|
||||
]
|
||||
},
|
||||
"properties": {},
|
||||
"preDeploymentGates": {
|
||||
"id": 0,
|
||||
"gatesOptions": null,
|
||||
"gates": []
|
||||
},
|
||||
"postDeploymentGates": {
|
||||
"id": 0,
|
||||
"gatesOptions": null,
|
||||
"gates": []
|
||||
},
|
||||
"environmentTriggers": [],
|
||||
"badgeUrl": "https://vsrm.dev.azure.com/msft-vott/_apis/public/Release/badge/4e739af6-f85e-44b9-a3f3-75eb893a6223/6/6"
|
||||
}
|
||||
],
|
||||
"artifacts": [
|
||||
{
|
||||
"sourceId": "4e739af6-f85e-44b9-a3f3-75eb893a6223:41",
|
||||
"type": "Build",
|
||||
"alias": "_VoTT - Generate latest plato report",
|
||||
"definitionReference": {
|
||||
"artifactSourceDefinitionUrl": {
|
||||
"id": "https://dev.azure.com/msft-vott/_permalink/_build/index?collectionId=2f887b33-5e65-447a-a189-049f04641d6d&projectId=4e739af6-f85e-44b9-a3f3-75eb893a6223&definitionId=41",
|
||||
"name": ""
|
||||
},
|
||||
"defaultVersionBranch": {
|
||||
"id": "",
|
||||
"name": ""
|
||||
},
|
||||
"defaultVersionSpecific": {
|
||||
"id": "",
|
||||
"name": ""
|
||||
},
|
||||
"defaultVersionTags": {
|
||||
"id": "",
|
||||
"name": ""
|
||||
},
|
||||
"defaultVersionType": {
|
||||
"id": "latestType",
|
||||
"name": "Latest"
|
||||
},
|
||||
"definition": {
|
||||
"id": "41",
|
||||
"name": "VoTT - Generate latest plato report"
|
||||
},
|
||||
"definitions": {
|
||||
"id": "",
|
||||
"name": ""
|
||||
},
|
||||
"IsMultiDefinitionType": {
|
||||
"id": "False",
|
||||
"name": "False"
|
||||
},
|
||||
"project": {
|
||||
"id": "4e739af6-f85e-44b9-a3f3-75eb893a6223",
|
||||
"name": "VoTT"
|
||||
},
|
||||
"repository": {
|
||||
"id": "",
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"isPrimary": true,
|
||||
"isRetained": false
|
||||
}
|
||||
],
|
||||
"triggers": [
|
||||
{
|
||||
"artifactAlias": "_VoTT - Generate latest plato report",
|
||||
"triggerConditions": [
|
||||
{
|
||||
"sourceBranch": "dev*",
|
||||
"tags": [],
|
||||
"useBuildDefinitionBranch": false,
|
||||
"createReleaseOnBuildTagging": false
|
||||
},
|
||||
{
|
||||
"sourceBranch": "master",
|
||||
"tags": [],
|
||||
"useBuildDefinitionBranch": false,
|
||||
"createReleaseOnBuildTagging": false
|
||||
}
|
||||
],
|
||||
"triggerType": 1
|
||||
}
|
||||
],
|
||||
"releaseNameFormat": "Release-$(Date:MMddyy).$(Date:hhmm)-$(rev:r)",
|
||||
"tags": [],
|
||||
"pipelineProcess": {
|
||||
"type": 1
|
||||
},
|
||||
"properties": {
|
||||
"DefinitionCreationSource": {
|
||||
"$type": "System.String",
|
||||
"$value": "ReleaseClone"
|
||||
}
|
||||
},
|
||||
"id": 6,
|
||||
"name": "VoTT - Upload Plato Report",
|
||||
"path": "\\",
|
||||
"projectReference": null,
|
||||
"url": "https://vsrm.dev.azure.com/msft-vott/4e739af6-f85e-44b9-a3f3-75eb893a6223/_apis/Release/definitions/6",
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "https://vsrm.dev.azure.com/msft-vott/4e739af6-f85e-44b9-a3f3-75eb893a6223/_apis/Release/definitions/6"
|
||||
},
|
||||
"web": {
|
||||
"href": "https://dev.azure.com/msft-vott/4e739af6-f85e-44b9-a3f3-75eb893a6223/_release?definitionId=6"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
# https://docs.microsoft.com/en-us/azure/devops/pipelines/process/templates?view=azure-devops#job-templates-with-parameters
|
||||
jobs:
|
||||
- job: ${{ parameters.name }}
|
||||
pool: ${{ parameters.pool }}
|
||||
timeoutInMinutes: 15 # how long to run the job before automatically cancelling
|
||||
steps:
|
||||
- template: git-pull-current-branch.yml
|
||||
|
||||
- task: NodeTool@0
|
||||
displayName: 'Use Node 10.x'
|
||||
inputs:
|
||||
versionSpec: 10.x
|
||||
|
||||
- bash: |
|
||||
set -ex
|
||||
|
||||
# clean install
|
||||
npm ci
|
||||
npm run release-ci
|
||||
|
||||
OS=${{ parameters.os }}
|
||||
ARTIFACT_NAME=${{ parameters.artifact }}
|
||||
|
||||
mkdir -p ${OS}
|
||||
cp releases/${ARTIFACT_NAME} ${OS}/
|
||||
|
||||
displayName: Build
|
||||
|
||||
- publish: $(System.DefaultWorkingDirectory)/${{ parameters.os }}
|
||||
artifact: ${{ parameters.os }}
|
||||
@@ -0,0 +1,96 @@
|
||||
parameters:
|
||||
GitHubConnection: '' # defaults for any parameters that aren't specified
|
||||
repositoryName: ''
|
||||
isPreRelease: false
|
||||
isDraft: false
|
||||
|
||||
jobs:
|
||||
- template: build-artifact.yml
|
||||
parameters:
|
||||
name: LinuxArtifact
|
||||
pool:
|
||||
vmImage: ubuntu-16.04
|
||||
os: linux
|
||||
artifact: vott*.snap
|
||||
|
||||
- template: build-artifact.yml
|
||||
parameters:
|
||||
name: WindowsArtifact
|
||||
pool:
|
||||
vmImage: "windows-2019"
|
||||
os: windows
|
||||
artifact: vott*.exe
|
||||
|
||||
- template: build-artifact.yml
|
||||
parameters:
|
||||
name: MacOSArtifact
|
||||
pool:
|
||||
vmImage: macOS-10.15
|
||||
os: mac
|
||||
artifact: vott*.dmg
|
||||
|
||||
- job: Create_Github_Release
|
||||
timeoutInMinutes: 30 # timeout on job if deploy is not completed in 30 minutes
|
||||
dependsOn:
|
||||
- LinuxArtifact
|
||||
- WindowsArtifact
|
||||
- MacOSArtifact
|
||||
|
||||
pool:
|
||||
vmImage: ubuntu-16.04
|
||||
|
||||
steps:
|
||||
- download: current
|
||||
- template: git-pull-current-branch.yml
|
||||
|
||||
- bash: |
|
||||
set -e
|
||||
|
||||
echo
|
||||
echo "======> Set commit sha"
|
||||
COMMIT_SHA=$(git rev-parse --short HEAD)
|
||||
echo "COMMIT SHA: $COMMIT_SHA"
|
||||
echo "##vso[task.setvariable variable=COMMIT_SHA]$COMMIT_SHA"
|
||||
|
||||
###
|
||||
# These variables were set in the "Version Bump" stage. There are
|
||||
# currently no way to pass variables between stages, hence this workaround.
|
||||
###
|
||||
echo
|
||||
echo "======> Set version variables"
|
||||
CURRENT_VERSION=$(cat $(Pipeline.Workspace)/variables/CURRENT_VERSION)
|
||||
echo "##vso[task.setvariable variable=CURRENT_VERSION]$CURRENT_VERSION"
|
||||
|
||||
NEXT_VERSION=$(cat $(Pipeline.Workspace)/variables/NEXT_VERSION)
|
||||
echo "##vso[task.setvariable variable=NEXT_VERSION]$NEXT_VERSION"
|
||||
displayName: "Set variables for release task"
|
||||
|
||||
- task: GitHubRelease@1
|
||||
displayName: 'GitHub release (create)'
|
||||
inputs:
|
||||
gitHubConnection: ${{ parameters.GitHubConnection }}
|
||||
repositoryName: '$(Build.Repository.Name)'
|
||||
action: 'create'
|
||||
target: '$(Build.SourceBranch)'
|
||||
tagSource: 'userSpecifiedTag'
|
||||
tag: $(NEXT_VERSION)
|
||||
releaseNotesSource: 'inline'
|
||||
releaseNotesInline: |
|
||||
## Web Release
|
||||
### Dev
|
||||
$(DEV_URL)
|
||||
|
||||
### Prod
|
||||
$(PROD_URL)
|
||||
|
||||
## Docs
|
||||
https://github.com/Microsoft/VoTT/blob/$(Build.SourceBranch)/README.md
|
||||
assets: |
|
||||
../linux/*
|
||||
../windows/*
|
||||
../mac/*
|
||||
isDraft: ${{ parameters.isDraft }}
|
||||
isPreRelease: ${{ parameters.isPrelease }}
|
||||
changeLogCompareToRelease: 'lastNonDraftReleaseByTag'
|
||||
changeLogCompareToReleaseTag: $(CURRENT_VERSION)
|
||||
changeLogType: 'commitBased'
|
||||
@@ -0,0 +1,56 @@
|
||||
jobs:
|
||||
- job: "Web_Release"
|
||||
pool:
|
||||
vmImage: 'windows-latest'
|
||||
|
||||
steps:
|
||||
- template: git-pull-current-branch.yml
|
||||
|
||||
- task: NodeTool@0
|
||||
displayName: 'Use Node 10.x'
|
||||
inputs:
|
||||
versionSpec: 10.x
|
||||
|
||||
- task: Npm@1
|
||||
displayName: 'npm ci'
|
||||
inputs:
|
||||
command: custom
|
||||
workingDir: .
|
||||
verbose: false
|
||||
customCommand: ci
|
||||
|
||||
- task: Bash@3
|
||||
displayName: 'Create artifact'
|
||||
inputs:
|
||||
targetType: filePath
|
||||
filePath: './scripts/generate-web-artifact.sh'
|
||||
|
||||
- task: AzureFileCopy@3
|
||||
displayName: 'AzureBlob File Copy to $DEV_STORAGE_ACCOUNT'
|
||||
inputs:
|
||||
SourcePath: './build'
|
||||
azureSubscription: $(azureSubscription)
|
||||
destination: azureBlob
|
||||
storage: $(DEV_STORAGE_ACCOUNT)
|
||||
containerName: '$web'
|
||||
|
||||
- task: AzureFileCopy@3
|
||||
displayName: 'AzureBlob File Copy to $PROD_STORAGE_ACCOUNT'
|
||||
inputs:
|
||||
SourcePath: './build'
|
||||
azureSubscription: $(azureSubscription)
|
||||
destination: azureBlob
|
||||
storage: $(PROD_STORAGE_ACCOUNT)
|
||||
containerName: '$web'
|
||||
condition: eq(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
|
||||
- bash: |
|
||||
echo
|
||||
echo "Dev url: $DEV_URL"
|
||||
displayName: "Dev URL"
|
||||
|
||||
- bash: |
|
||||
echo
|
||||
echo "Prod url: $PROD_URL"
|
||||
displayName: "Prod URL"
|
||||
condition: eq(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
@@ -0,0 +1,8 @@
|
||||
steps:
|
||||
- task: Bash@3
|
||||
displayName: 'Pull latest from branch'
|
||||
inputs:
|
||||
targetType: filePath
|
||||
filePath: './scripts/git-pull-current-branch.sh'
|
||||
env:
|
||||
SOURCE_BRANCH: $(Build.SourceBranch)
|
||||
@@ -0,0 +1,60 @@
|
||||
parameters:
|
||||
versionType: ''
|
||||
|
||||
jobs:
|
||||
- job: "version_bump"
|
||||
|
||||
variables:
|
||||
- group: GitHub-Deploy-Creds
|
||||
|
||||
timeoutInMinutes: 30 # timeout on job if deploy is not completed in 30 minutes
|
||||
cancelTimeoutInMinutes: 1 # time limit to wait for job to cancel
|
||||
|
||||
pool:
|
||||
vmImage: macOS-latest # ssh key was generated on a Mac so using the same type of OS here
|
||||
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: 10.x
|
||||
displayName: 'Install Node.js'
|
||||
|
||||
# Download secure file
|
||||
# Download a secure file to the agent machine
|
||||
- task: DownloadSecureFile@1
|
||||
# name: sshKey # The name with which to reference the secure file's path on the agent, like $(mySecureFile.secureFilePath)
|
||||
inputs:
|
||||
secureFile: vott_id_rsa
|
||||
|
||||
# Install an SSH key prior to a build or deployment
|
||||
- task: InstallSSHKey@0 # https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/install-ssh-key?view=azure-devops
|
||||
inputs:
|
||||
knownHostsEntry: $(KNOWN_HOSTS_ENTRY)
|
||||
sshPublicKey: $(SSH_PUBLIC_KEY)
|
||||
#sshPassphrase: # Optional
|
||||
sshKeySecureFile: vott_id_rsa
|
||||
env:
|
||||
KNOWN_HOSTS_ENTRY: $(KNOWN_HOSTS_ENTRY)
|
||||
SSH_PUBLIC_KEY: $(SSH_PUBLIC_KEY) # map to the right format (camelCase) that Azure credentials understand
|
||||
|
||||
- task: Bash@3
|
||||
name: BumpNpmVersion
|
||||
displayName: Bump NPM Version
|
||||
inputs:
|
||||
targetType: filePath
|
||||
filePath: ./scripts/version-bump-commit.sh
|
||||
arguments:
|
||||
${{ parameters.versionType }}
|
||||
env:
|
||||
SOURCE_BRANCH: $(Build.SourceBranch)
|
||||
|
||||
- bash: |
|
||||
printenv | sort
|
||||
mkdir -p $(System.DefaultWorkingDirectory)/variables
|
||||
echo "$NEXT_VERSION" > $(System.DefaultWorkingDirectory)/variables/NEXT_VERSION
|
||||
echo "$CURRENT_VERSION" > $(System.DefaultWorkingDirectory)/variables/CURRENT_VERSION
|
||||
displayName: "Prep variables for publishing"
|
||||
|
||||
# Publish the variables folder as pipeline artifact
|
||||
- publish: $(System.DefaultWorkingDirectory)/variables
|
||||
artifact: variables
|
||||
@@ -0,0 +1,74 @@
|
||||
name: $(Date:yyyyMMdd).$(Hours)$(Minutes)$(Seconds)
|
||||
variables:
|
||||
AZURE_STORAGE_ACCOUNT: vottv2
|
||||
azureSubscription: 'pj-little-sub'
|
||||
REPORT_URL: 'https://vottv2.z22.web.core.windows.net/'
|
||||
|
||||
trigger:
|
||||
- dev*
|
||||
- master
|
||||
|
||||
pr: none # disable CI build for PR
|
||||
|
||||
pool:
|
||||
vmImage: 'Ubuntu-16.04'
|
||||
|
||||
steps:
|
||||
- bash: |
|
||||
set -e
|
||||
|
||||
COMMIT_SHA=`echo ${BUILD_SOURCEVERSION} | cut -c1-8`
|
||||
echo "sha: " $COMMIT_SHA
|
||||
|
||||
# rewrite build number
|
||||
echo "##vso[build.updatebuildnumber]Report-${COMMIT_SHA}-${BUILD_BUILDNUMBER}"
|
||||
displayName: "Rewrite build number"
|
||||
|
||||
- bash: |
|
||||
set -e
|
||||
|
||||
ACCOUNT=$(AZURE_STORAGE_ACCOUNT)
|
||||
if [[ -z "${ACCOUNT}" ]]; then
|
||||
echo "Need to set AZURE_STORAGE_ACCOUNT"
|
||||
exit 1
|
||||
fi
|
||||
displayName: "Verify storage account cred exists"
|
||||
|
||||
- task: NodeTool@0
|
||||
displayName: "Use Node 10.x"
|
||||
inputs:
|
||||
versionSpec: 10.x
|
||||
|
||||
- task: Npm@1
|
||||
displayName: 'Run `npm ci`'
|
||||
inputs:
|
||||
command: custom
|
||||
verbose: false
|
||||
customCommand: ci
|
||||
|
||||
- task: AzureCLI@1
|
||||
displayName: "Pull down old report and add updates"
|
||||
inputs:
|
||||
azureSubscription: $(azureSubscription)
|
||||
scriptLocation: inlineScript
|
||||
inlineScript: './scripts/update-report.sh'
|
||||
|
||||
- bash: |
|
||||
set -e
|
||||
|
||||
cat /tmp/download.log
|
||||
displayName: "print download log"
|
||||
condition: succeededOrFailed()
|
||||
|
||||
- task: AzureFileCopy@3
|
||||
displayName: 'AzureBlob File Copy to $(AZURE_STORAGE_ACCOUNT)'
|
||||
inputs:
|
||||
SourcePath: './report'
|
||||
azureSubscription: $(azureSubscription)
|
||||
destination: azureBlob
|
||||
storage: $(AZURE_STORAGE_ACCOUNT)
|
||||
containerName: '$web'
|
||||
|
||||
- bash: |
|
||||
echo "See report: $(REPORT_URL) "
|
||||
displayName: "Report URl"
|
||||
@@ -1,35 +0,0 @@
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- dev*
|
||||
- master
|
||||
pr: none # disable CI build for PR
|
||||
|
||||
pool:
|
||||
vmImage: 'Ubuntu-16.04'
|
||||
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: 'Use Node 10.x'
|
||||
inputs:
|
||||
versionSpec: 10.x
|
||||
|
||||
- task: Npm@1
|
||||
displayName: 'npm ci'
|
||||
inputs:
|
||||
command: custom
|
||||
workingDir: .
|
||||
verbose: false
|
||||
customCommand: ci
|
||||
|
||||
- task: Bash@3
|
||||
displayName: 'Create artifact'
|
||||
inputs:
|
||||
targetType: filePath
|
||||
filePath: './scripts/generate-web-artifact.sh'
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Artifact: build'
|
||||
inputs:
|
||||
PathtoPublish: ./build
|
||||
ArtifactName: build
|
||||
@@ -1,24 +0,0 @@
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: 'Use Node 10.x'
|
||||
inputs:
|
||||
versionSpec: 10.x
|
||||
|
||||
- bash: |
|
||||
set -ex
|
||||
|
||||
# clean install
|
||||
npm ci
|
||||
npm run release-ci
|
||||
|
||||
mkdir -p windows
|
||||
cp releases/vott*.exe windows/
|
||||
|
||||
displayName: Build
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Artifact: windows'
|
||||
inputs:
|
||||
PathtoPublish: windows
|
||||
ArtifactName: windows
|
||||
|
||||
@@ -4,4 +4,4 @@ const common = require('./webpack.common.js')
|
||||
module.exports = merge(common, {
|
||||
mode: 'development',
|
||||
devtool: "inline-source-map",
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Overview
|
||||
|
||||
[](https://dev.azure.com/msft-vott/VoTT/_build/latest?definitionId=55&branchName=master)
|
||||
|
||||
Instruction on how to create new GitHub & Web Releases.
|
||||
|
||||
## Release Process
|
||||
|
||||

|
||||
|
||||
### AzDO Tasks
|
||||
|
||||
[GitHub Release Task](https://github.com/microsoft/azure-pipelines-tasks/tree/master/Tasks/GitHubReleaseV1)
|
||||
|
||||
[Azure File Copy](https://github.com/microsoft/azure-pipelines-tasks/tree/master/Tasks/AzureFileCopyV3)
|
||||
|
||||
## Versioning
|
||||
|
||||
Follow [NPM Semantic Versioning](https://docs.npmjs.com/about-semantic-versioning#incrementing-semantic-versions-in-published-packages)
|
||||
|
||||
| Code status | Stage | Rule | Example version |
|
||||
| ----------------------------------------- | ------------- | ------------------------------------------------------------------ | --------------- |
|
||||
| First release | New product | Start with 1.0.0 | 1.0.0 |
|
||||
| Backward compatible bug fixes | Patch release | Increment the third digit | 1.0.1 |
|
||||
| Backward compatible new features | Minor release | Increment the middle digit and reset last digit to zero | 1.1.0 |
|
||||
| Changes that break backward compatibility | Major release | Increment the first digit and reset middle and last digits to zero | 2.0.0 |
|
||||
|
||||
### Commands
|
||||
|
||||
The pipeline use [npm-version](https://docs.npmjs.com/cli/version) to update version
|
||||
|
||||
#### Pre
|
||||
|
||||
All version with `pre`, ie. `preminor` will bump the appropriate didgit & append `-0` to the new version
|
||||
|
||||
Examples:
|
||||
|
||||
`npm version prepatch`
|
||||
|
||||
1. v2.3.0 --> v2.3.1-0
|
||||
1. v2.3.1-0 --> v2.3.2-0
|
||||
|
||||
`npm version preminor`
|
||||
|
||||
1. v2.3.0 --> v2.4.0-0
|
||||
1. v2.4.0-0 --> v2.5.0-0
|
||||
|
||||
##### Exception
|
||||
|
||||
`prerelease` behave similar to prepatch, but would increment the last digit.
|
||||
|
||||
Examples:
|
||||
|
||||
`npm version prerelease`
|
||||
|
||||
v2.3.0 --> v2.3.1-0
|
||||
|
||||
v2.3.0-0 --> v2.3.0-1
|
||||
|
||||
#### Major
|
||||
|
||||
v2.x.x --> v3.0.0
|
||||
|
||||
#### Minor
|
||||
|
||||
v2.2.0 --> v2.3.0
|
||||
Arquivo binário não exibido.
|
Depois Largura: | Altura: | Tamanho: 87 KiB |
gerado
+2552
-641
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+6
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vott",
|
||||
"version": "2.0.0",
|
||||
"version": "2.2.0",
|
||||
"author": {
|
||||
"name": "Microsoft",
|
||||
"url": "https://github.com/Microsoft/VoTT"
|
||||
@@ -23,7 +23,9 @@
|
||||
"buffer-reverse": "^1.0.1",
|
||||
"crypto-js": "^3.1.9-1",
|
||||
"dotenv": "^7.0.0",
|
||||
"express-request-id": "^1.4.1",
|
||||
"google-protobuf": "^3.6.1",
|
||||
"jimp": "^0.16.1",
|
||||
"jpeg-js": "^0.3.4",
|
||||
"json2csv": "^4.5.0",
|
||||
"lodash": "^4.17.11",
|
||||
@@ -112,13 +114,14 @@
|
||||
"@types/redux-mock-store": "^1.0.0",
|
||||
"cross-env": "^5.2.0",
|
||||
"electron": "^3.0.13",
|
||||
"electron-builder": "^20.38.3",
|
||||
"electron-builder": "^22.6.0",
|
||||
"enzyme": "^3.7.0",
|
||||
"enzyme-adapter-react-16": "^1.7.0",
|
||||
"foreman": "^3.0.1",
|
||||
"jest-enzyme": "^7.0.1",
|
||||
"jquery": "^3.3.1",
|
||||
"node-sass": "^4.10.0",
|
||||
"mock-fs": "^4.13.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"popper.js": "^1.14.6",
|
||||
"redux-immutable-state-invariant": "^2.1.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
trigger:
|
||||
- refs/tags/* # trigger on any tag
|
||||
pr: none # disable CI build for PR
|
||||
|
||||
jobs:
|
||||
- job: Linux
|
||||
condition: succeeded()
|
||||
pool:
|
||||
vmImage: ubuntu-16.04
|
||||
timeoutInMinutes: 60 # how long to run the job before automatically cancelling
|
||||
steps:
|
||||
- checkout: self # self represents the repo where the initial Pipelines YAML file was found
|
||||
fetchDepth: 1 # the depth of commits to ask Git to fetch
|
||||
- template: azure-pipelines/linux/artifact-build-linux.yml
|
||||
|
||||
- job: MacOS
|
||||
condition: succeeded()
|
||||
pool:
|
||||
vmImage: macOS-10.13
|
||||
timeoutInMinutes: 60 # how long to run the job before automatically cancelling
|
||||
steps:
|
||||
- checkout: self # self represents the repo where the initial Pipelines YAML file was found
|
||||
fetchDepth: 1 # the depth of commits to ask Git to fetch
|
||||
- template: azure-pipelines/mac/artifact-build-mac.yml
|
||||
|
||||
- job: Windows
|
||||
condition: succeeded()
|
||||
pool:
|
||||
vmImage: vs2017-win2016
|
||||
timeoutInMinutes: 60 # how long to run the job before automatically cancelling
|
||||
steps:
|
||||
- checkout: self # self represents the repo where the initial Pipelines YAML file was found
|
||||
fetchDepth: 1 # the depth of commits to ask Git to fetch
|
||||
- template: azure-pipelines/windows/artifact-build-windows.yml
|
||||
@@ -49,8 +49,15 @@ echo "commit=${COMMIT_SHA}"
|
||||
# these are just needed for the reports; just install ad-hoc and don't save to package.json
|
||||
npm install es6-plato eslint-plugin-react command-line-args --no-save
|
||||
|
||||
echo
|
||||
echo "------> Finish installing dependencies"
|
||||
|
||||
echo
|
||||
echo "------> Transpile TS to ES6"
|
||||
# we can't do complexity analysis on TypeScript directly; transpile to ES6
|
||||
rm -rf ${ES6_SRC}
|
||||
tsc --noEmit false --outDir ${ES6_SRC}
|
||||
|
||||
echo
|
||||
echo "------> Running complexity analasis ..."
|
||||
node ${BASEDIR}/complexity-analysis.js --src ${ES6_SRC} --output ${REPORT_DIR} --version ${VERSION} --commit ${COMMIT_SHA}
|
||||
|
||||
Arquivo executável
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Get full branch name excluding refs/head from the env var SOURCE_BRANCH
|
||||
SOURCE_BRANCH_NAME=${SOURCE_BRANCH/refs\/heads\/}
|
||||
|
||||
echo "SOURCE_BRANCH: ${SOURCE_BRANCH_NAME}"
|
||||
git pull origin ${SOURCE_BRANCH_NAME}
|
||||
git checkout ${SOURCE_BRANCH_NAME}
|
||||
echo "Checked out branch: ${SOURCE_BRANCH_NAME}"
|
||||
Arquivo executável
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
NPM_VERSION_TYPE=${1:-"prepatch --preid=preview"}
|
||||
echo "Next version type: ----->$NPM_VERSION_TYPE<-----"
|
||||
|
||||
PACKAGE_VERSION=$(node -pe "require('./package.json').version")
|
||||
CURRENT_VERSION="v$PACKAGE_VERSION"
|
||||
|
||||
# Get full branch name excluding refs/head from the env var SOURCE_BRANCH
|
||||
SOURCE_BRANCH_NAME=${SOURCE_BRANCH/refs\/heads\/}
|
||||
|
||||
# Configure git to commit as VoTT Service Account
|
||||
echo "Configuring git to use deploy key..."
|
||||
git config --local user.email "vott@microsoft.com"
|
||||
git config --local user.name "Vott"
|
||||
|
||||
echo "SOURCE_BRANCH: ${SOURCE_BRANCH_NAME}"
|
||||
git pull origin ${SOURCE_BRANCH_NAME}
|
||||
git checkout ${SOURCE_BRANCH_NAME}
|
||||
echo "Checked out branch: ${SOURCE_BRANCH_NAME}"
|
||||
|
||||
## format: v2.2.0
|
||||
NEXT_VERSION=`npm version ${NPM_VERSION_TYPE} -m "release: Update ${NPM_VERSION_TYPE} version to %s ***NO_CI***"`
|
||||
echo "Set next version to: ${NEXT_VERSION}"
|
||||
|
||||
# There is currently no way to pass variables between stages, hence this workaround
|
||||
echo
|
||||
echo "##vso[task.setvariable variable=NEXT_VERSION]$NEXT_VERSION"
|
||||
echo "##vso[task.setvariable variable=CURRENT_VERSION]$CURRENT_VERSION"
|
||||
|
||||
#### Push new tag
|
||||
SHA=`git rev-parse HEAD`
|
||||
|
||||
export GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
||||
git remote add authOrigin git@github.com:microsoft/VoTT.git
|
||||
git push authOrigin ${SOURCE_BRANCH_NAME} --tags
|
||||
|
||||
echo
|
||||
echo "Pushed new tag: ${NEXT_VERSION} @ SHA: ${SHA:0:8}"
|
||||
@@ -0,0 +1,5 @@
|
||||
APP_ID=xyz
|
||||
APP_SECRET=asdf
|
||||
COOKIE_SECRETS="[ { key: '12345678901234567890123456789012', iv: '123456789012' }, { key: 'abcdefghijklmnopqrstuvwxyzabcdef', iv: 'abcdefghijkl' }, ])"
|
||||
ALLOW_HTTP=true
|
||||
BASE_URL=http://localhost:3000/
|
||||
externo
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch via NPM",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run-script",
|
||||
"debug"
|
||||
],
|
||||
"port": 9229
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"program": "${workspaceFolder}/lib/app.js", //"${workspaceFolder}\\lib\\app.js",
|
||||
"args": [
|
||||
"|",
|
||||
"bunyan"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/**/*.js"
|
||||
],
|
||||
"console": "internalConsole",
|
||||
"outputCapture": "std",
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
externo
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build",
|
||||
"problemMatcher": [
|
||||
"$tsc"
|
||||
],
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"type": "typescript",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"option": "watch",
|
||||
"problemMatcher": [
|
||||
"$tsc-watch"
|
||||
],
|
||||
"group": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
# Node.js
|
||||
# Build a general Node.js project with npm.
|
||||
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
|
||||
|
||||
trigger: none
|
||||
pr: none
|
||||
|
||||
variables:
|
||||
# Azure Resource Manager connection created during pipeline creation
|
||||
azureSubscription: fe7b93fe-e836-4a55-804c-883dbea6af24'
|
||||
|
||||
# Web app name
|
||||
webAppName: 'vott'
|
||||
|
||||
# Agent VM image name
|
||||
vmImageName: 'ubuntu-latest'
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: Build stage
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: Build
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
displayName: 'Install Node.js'
|
||||
|
||||
- script: |
|
||||
npm install
|
||||
npm run build --if-present
|
||||
# npm run test --if-present
|
||||
workingDirectory: $(System.DefaultWorkingDirectory)/server
|
||||
displayName: 'npm install, build and test'
|
||||
|
||||
- task: ArchiveFiles@2
|
||||
displayName: 'Archive files'
|
||||
inputs:
|
||||
rootFolderOrFile: '$(System.DefaultWorkingDirectory)/server'
|
||||
includeRootFolder: false
|
||||
archiveType: zip
|
||||
archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
replaceExistingArchive: true
|
||||
|
||||
- upload: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
artifact: drop
|
||||
|
||||
- stage: Deploy
|
||||
displayName: Deploy stage
|
||||
dependsOn: Build
|
||||
condition: succeeded()
|
||||
jobs:
|
||||
- deployment: Deploy
|
||||
displayName: Deploy
|
||||
environment: 'development'
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureWebApp@1
|
||||
displayName: 'Azure Web App Deploy: vott'
|
||||
inputs:
|
||||
azureSubscription: $(azureSubscription)
|
||||
appType: webAppLinux
|
||||
appName: $(webAppName)
|
||||
runtimeStack: 'NODE|10.10'
|
||||
package: $(Pipeline.Workspace)/drop/$(Build.BuildId).zip
|
||||
startUpCommand: 'npm run start'
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
rootDir: "src",
|
||||
coverageDirectory: "../coverage",
|
||||
};
|
||||
gerado
+7320
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "vott-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Server to support VoTT with login",
|
||||
"main": "./lib/app.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node ./lib/app.js",
|
||||
"test:unit": "jest --runInBand",
|
||||
"test": "npm run lint && npm run test:unit",
|
||||
"watch": "concurrently --kill-others \"tsc -w\" \"nodemon --inspect ./lib/app.js\"",
|
||||
"lint": "tslint -q -p . -c tslint.json",
|
||||
"lint:fix": "tslint --fix -p . -c tslint.json",
|
||||
"debug": "nodemon --inspect ./lib/app.js | bunyan"
|
||||
},
|
||||
"author": "Microsoft",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@microsoft/microsoft-graph-client": "^1.7.0",
|
||||
"body-parser": "^1.15.2",
|
||||
"bunyan": "*",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"cookie-session": "^1.3.3",
|
||||
"cookies": "^0.7.3",
|
||||
"ejs": ">= 0.0.0",
|
||||
"ejs-locals": ">= 0.0.0",
|
||||
"express": "^4.17.1",
|
||||
"express-request-id": "^1.4.1",
|
||||
"method-override": "^3.0.0",
|
||||
"morgan": "^1.9.1",
|
||||
"node-fetch": "^2.6.0",
|
||||
"passport": "*",
|
||||
"passport-azure-ad": "^4.1.0",
|
||||
"simple-oauth2": "^2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bunyan": "^1.8.6",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/cookie-session": "^2.0.37",
|
||||
"@types/cookies": "^0.7.2",
|
||||
"@types/dotenv": "^6.1.1",
|
||||
"@types/express": "^4.17.1",
|
||||
"@types/express-request-id": "^1.4.1",
|
||||
"@types/jest": "^24.0.17",
|
||||
"@types/method-override": "0.0.31",
|
||||
"@types/morgan": "^1.7.37",
|
||||
"@types/node-fetch": "^2.5.0",
|
||||
"@types/passport": "^1.0.1",
|
||||
"@types/passport-azure-ad": "^4.0.3",
|
||||
"@types/simple-oauth2": "^2.2.1",
|
||||
"concurrently": "^4.1.1",
|
||||
"dotenv": "^8.1.0",
|
||||
"jest": "^24.8.0",
|
||||
"nodemon": "^1.19.1",
|
||||
"ts-jest": "^24.0.2",
|
||||
"tslint": "^5.18.0",
|
||||
"typescript": "^3.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Simple Integration Tests</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Simple integration tests</h2>
|
||||
<p>You must be logged in to use tests</p>
|
||||
<button onclick="test_get_me()">GET /api/v1.0/me</button><br />
|
||||
<button onclick="test_get_profile()">GET /api/v1.0/profile</button><br />
|
||||
<button onclick="test_put_profile()">PUT /api/v1.0/profile { "foo": "bar" }</button><br />
|
||||
<br />
|
||||
<button onclick="test_get_connection()">GET /api/v1.0/cloudconnections/connection1</button><br />
|
||||
<button onclick="test_put_connection()">PUT /api/v1.0/cloudconnections/connection1 { "foo": "bar" }</button><br />
|
||||
<button onclick="test_put_connection_alt()">PUT /api/v1.0/cloudconnections/connection1 { "foo": "baz" }</button><br />
|
||||
<button onclick="test_patch_connection()">PATCH /api/v1.0/cloudconnections/connection1 { "updated": ${now} }</button><br />
|
||||
<button onclick="test_delete_connection()">DELETE /api/v1.0/cloudconnections/connection1</button><br />
|
||||
|
||||
<pre id='result'></pre>
|
||||
|
||||
<script>
|
||||
function fetchOptions(method, thing) {
|
||||
return { method, body: JSON.stringify(thing), headers: { 'Content-Type': 'application/json' } };
|
||||
}
|
||||
|
||||
async function displayResponse(response) {
|
||||
let json = null;
|
||||
try { json = await response.json().catch(); } catch (err) { }
|
||||
let result = (response.ok ? '*success*' : '*failed*') + '\n' + (json ? JSON.stringify(json, undefined, 2) : '');
|
||||
document.getElementById("result").innerHTML = result;
|
||||
}
|
||||
|
||||
async function test_get_me(e) {
|
||||
let response = await fetch('/api/v1.0/me');
|
||||
displayResponse(response);
|
||||
}
|
||||
async function test_get_profile(e) {
|
||||
let response = await fetch('/api/v1.0/profile');
|
||||
displayResponse(response);
|
||||
}
|
||||
async function test_put_profile(e) {
|
||||
let response = await fetch('/api/v1.0/profile', fetchOptions("PUT", { foo: "bar" }));
|
||||
displayResponse(response);
|
||||
}
|
||||
async function test_get_connection(e) {
|
||||
let response = await fetch('/api/v1.0/cloudconnections/connection1');
|
||||
displayResponse(response);
|
||||
}
|
||||
async function test_put_connection(e) {
|
||||
let response = await fetch('/api/v1.0/cloudconnections/connection1', fetchOptions("PUT", { foo: "bar" }));
|
||||
displayResponse(response);
|
||||
}
|
||||
async function test_put_connection_alt(e) {
|
||||
let response = await fetch('/api/v1.0/cloudconnections/connection1', fetchOptions("PUT", { foo: "baz" }));
|
||||
displayResponse(response);
|
||||
}
|
||||
async function test_patch_connection(e) {
|
||||
let now = (new Date(Date.now())).toISOString();
|
||||
let response = await fetch('/api/v1.0/cloudconnections/connection1', fetchOptions("PATCH", { updated: now }));
|
||||
displayResponse(response);
|
||||
}
|
||||
async function test_delete_connection(e) {
|
||||
let response = await fetch('/api/v1.0/cloudconnections/connection1', fetchOptions("DELETE", undefined));
|
||||
displayResponse(response);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
<% if (!user) { %>
|
||||
<h2>Welcome! Please log in.</h2>
|
||||
<a href="/login">Log In</a>
|
||||
<% } else { %>
|
||||
<p>Profile ID: <%= user.oid %></p>
|
||||
<p>Email: <%= user.mail %></p>
|
||||
<pre><%- JSON.stringify(user, null, 2) %></pre>
|
||||
<% } %>
|
||||
@@ -0,0 +1,14 @@
|
||||
<% if (!user) { %>
|
||||
<h2>Welcome! Please log in.</h2>
|
||||
<a href="/login">Log In</a></br>
|
||||
<a href="https://myapps.microsoft.com">Manage your permissions (organization)</a></br>
|
||||
<a href="https://account.live.com/consent/Manage">Manage account permissions (personal)</a>
|
||||
<% } else { %>
|
||||
<h2>Hello, <%= user.displayName %></h2>
|
||||
<a href="/account">Account Info</a></br>
|
||||
<a href="/public/test.html">Run tests</a></br>
|
||||
<a href="/endsession">End session</a></br>
|
||||
<a href="/logout">Log Out</a></br>
|
||||
<a href="https://myapps.microsoft.com">Manage your permissions (organization)</a></br>
|
||||
<a href="https://account.live.com/consent/Manage">Manage account permissions (personal)</a>
|
||||
<% } %>
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Passport-OpenID Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<% if (!user) { %>
|
||||
<p>
|
||||
<a href="/">Home</a> |
|
||||
<a href="/login">Log In</a>
|
||||
</p>
|
||||
<% } else { %>
|
||||
<p>
|
||||
<a href="/">Home</a> |
|
||||
<a href="/account">Account</a> |
|
||||
<a href="/logout">Log Out</a>
|
||||
</p>
|
||||
<% } %>
|
||||
<%- body %>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
// import { default as fetch } from 'node-fetch';
|
||||
import * as config from '../config';
|
||||
import { app, server } from '../app';
|
||||
import { ServerResponse } from 'http';
|
||||
|
||||
|
||||
beforeAll(async (done) => {
|
||||
if (!server.listening) {
|
||||
server.on('listening', done())
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async (done) => {
|
||||
server.close(() => {
|
||||
console.log('done');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('App Server', () => {
|
||||
|
||||
afterAll(async (done) => {
|
||||
server.close(() => {
|
||||
console.log('done');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('initialized', async (done) => {
|
||||
expect(app.name).toBeDefined();
|
||||
expect(server.listening).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
test('loads app.html', async (done) => {
|
||||
const response = await fetch(config.baseUrl);
|
||||
expect(response.status).toBe(200);
|
||||
done();
|
||||
});
|
||||
|
||||
test('redirects to login', async (done) => {
|
||||
const response = await fetch(config.baseUrl + '/api/v1.0/me');
|
||||
expect(response.status).toBe(404);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
// import { default as fetch } from 'node-fetch';
|
||||
import * as config from '../config';
|
||||
|
||||
describe('App Server', () => {
|
||||
|
||||
|
||||
test('should be closed', async (done) => {
|
||||
// do nothing
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,364 @@
|
||||
import * as bodyParser from 'body-parser';
|
||||
import * as morgan from 'morgan';
|
||||
import * as bunyan from 'bunyan';
|
||||
import * as cookieParser from 'cookie-parser';
|
||||
import * as express from 'express';
|
||||
import cookieSession = require('cookie-session');
|
||||
import * as methodOverride from 'method-override';
|
||||
import * as passport from 'passport';
|
||||
import * as passportAzureAD from 'passport-azure-ad';
|
||||
import * as config from './config';
|
||||
import * as path from 'path';
|
||||
import * as express_request_id from 'express-request-id';
|
||||
import * as simple_oath2 from 'simple-oauth2';
|
||||
import * as graph from './graph';
|
||||
import * as oauth2 from './oauth2';
|
||||
|
||||
export const log = bunyan.createLogger({
|
||||
name: 'BUNYAN-LOGGER',
|
||||
src: true,
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Passport Setup Follows
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Setup schemas for Passport's User object and the the session persistence format.
|
||||
|
||||
// Augument Passport's request.user with the Azure AD oauthToken
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface User {
|
||||
oid: string;
|
||||
oauthToken: oauth2.Token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type FullUserSchema = Express.User; // This now includes the above declaration for User.
|
||||
|
||||
interface MinimizedUserSchema {
|
||||
oid: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
passport.serializeUser((user: FullUserSchema, done) => {
|
||||
const stored: MinimizedUserSchema = { oid: user.oid, refresh_token: user.oauthToken.refresh_token };
|
||||
done(null, stored);
|
||||
});
|
||||
|
||||
passport.deserializeUser(async (stored: MinimizedUserSchema, done) => {
|
||||
if (!stored || !stored.refresh_token) { return done(Error('no user profile')); }
|
||||
let oauthClient = await oauth2.client(stored);
|
||||
if (oauthClient.expired()) {
|
||||
oauthClient = await oauthClient.refresh(); // must reassign here.
|
||||
}
|
||||
const profile = await graph.user(oauthClient.token.access_token).catch(reason => { log.error('could not retrieve profile', reason); });
|
||||
if (!profile) { return done(Error('no user profile')); }
|
||||
const result = { ...profile, oid: stored.oid, oauthToken: oauthClient.token };
|
||||
return done(null, result);
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Define the AzureAD OIDCStrategy Strategy
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const OIDCStrategyTemplate = {} as passportAzureAD.IOIDCStrategyOptionWithoutRequest;
|
||||
|
||||
const azureStrategyOptions: passportAzureAD.IOIDCStrategyOptionWithRequest = {
|
||||
identityMetadata: config.creds.identityMetadata,
|
||||
clientID: config.creds.clientID,
|
||||
responseType: config.creds.responseType as typeof OIDCStrategyTemplate.responseType,
|
||||
responseMode: config.creds.responseMode as typeof OIDCStrategyTemplate.responseMode,
|
||||
redirectUrl: config.creds.redirectUrl,
|
||||
allowHttpForRedirectUrl: config.creds.allowHttpForRedirectUrl,
|
||||
clientSecret: config.creds.clientSecret,
|
||||
validateIssuer: config.creds.validateIssuer,
|
||||
isB2C: config.creds.isB2C,
|
||||
issuer: config.creds.issuer,
|
||||
passReqToCallback: true,
|
||||
scope: config.creds.scope,
|
||||
loggingLevel: config.creds.logLevel as typeof OIDCStrategyTemplate.loggingLevel,
|
||||
nonceLifetime: config.creds.nonceLifetime,
|
||||
nonceMaxAmount: config.creds.nonceMaxAmount,
|
||||
useCookieInsteadOfSession: config.creds.useCookieInsteadOfSession,
|
||||
cookieEncryptionKeys: config.creds.cookieEncryptionKeys,
|
||||
clockSkew: config.creds.clockSkew,
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Use the Azure OIDCStrategy within Passport.
|
||||
//
|
||||
// Strategies in passport require a `verify` function, which accepts credentials
|
||||
// (in this case, the `oid` claim in id_token), and invoke a callback to find
|
||||
// the corresponding user object.
|
||||
//
|
||||
// The following are the accepted prototypes for the `verify` function
|
||||
// (1) function(iss, sub, done)
|
||||
// (2) function(iss, sub, profile, done)
|
||||
// (3) function(iss, sub, profile, access_token, refresh_token, done)
|
||||
// (4) function(iss, sub, profile, access_token, refresh_token, params, done)
|
||||
// (5) function(iss, sub, profile, jwtClaims, access_token, refresh_token, params, done)
|
||||
// (6) prototype (1)-(5) with an additional `req` parameter as the first parameter
|
||||
//
|
||||
// To do prototype (6), passReqToCallback must be set to true in the config.
|
||||
// -----------------------------------------------------------------------------
|
||||
passport.use(new passportAzureAD.OIDCStrategy(azureStrategyOptions, processAzureStrategy));
|
||||
|
||||
async function processAzureStrategy(req: express.Request,
|
||||
iss: string, sub: string, profile: passportAzureAD.IProfile, jwtClaims: any,
|
||||
access_token: string, refresh_token: string, oauthToken: any,
|
||||
done: passportAzureAD.VerifyCallback) {
|
||||
|
||||
if (!profile.oid) {
|
||||
return done(new Error('No oid found'), null);
|
||||
}
|
||||
// asynchronous verification, for effect...
|
||||
process.nextTick(async () => {
|
||||
|
||||
const fullProfile = await graph.user(access_token);
|
||||
if (!fullProfile) {
|
||||
return done(Error('no profile'));
|
||||
}
|
||||
|
||||
const oauth = oauth2.client(oauthToken);
|
||||
const result = { ...fullProfile, oid: profile.oid, oauthToken: oauth.token };
|
||||
|
||||
return done(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Config the express app and all the required middleware
|
||||
// -----------------------------------------------------------------------------
|
||||
export const app = express();
|
||||
|
||||
app.use(morgan(config.httpLogFormat));
|
||||
app.set('trust proxy', true);
|
||||
app.set('views', path.join(__dirname, '../public/views'));
|
||||
app.set('view engine', 'ejs');
|
||||
app.use(express_request_id());
|
||||
app.use(methodOverride());
|
||||
app.use(cookieParser());
|
||||
app.use(cookieSession({ keys: config.creds.cookieEncryptionKeys.map(value => value.key), secure: false, maxAge: 1000 * 60 * 60 * 24 * 365 }));
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Define a couple of authencation request handlers.
|
||||
// -----------------------------------------------------------------------------
|
||||
function ensureAuthenticated(req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
if (req.isAuthenticated()) { return next(); }
|
||||
res.redirect('/login');
|
||||
}
|
||||
|
||||
function ensureAuthenticatedApi(req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
if (req.isAuthenticated()) { return next(); }
|
||||
res.sendStatus(401).end();
|
||||
return next();
|
||||
}
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
const user = { ...req.user, oauthToken: '[removed]'};
|
||||
res.render('index', { user });
|
||||
});
|
||||
|
||||
app.get('/account', ensureAuthenticated, (req, res, next) => {
|
||||
const user = { ...req.user, oauthToken: '[removed]'};
|
||||
res.render('account', { user });
|
||||
});
|
||||
|
||||
app.get('/login', (req, res, next) => {
|
||||
passport.authenticate('azuread-openidconnect',
|
||||
{
|
||||
response: res, // required
|
||||
customState: 'my_state', // optional. Provide a value if you want to provide custom state value.
|
||||
failureRedirect: '/',
|
||||
} as passport.AuthenticateOptions,
|
||||
)(req, res, next);
|
||||
},
|
||||
(req, res) => {
|
||||
log.info('login was called');
|
||||
res.redirect('/');
|
||||
});
|
||||
|
||||
// 'GET returnURL'
|
||||
// `passport.authenticate` will try to authenticate the content returned in
|
||||
// query (such as authorization code). If authentication fails, user will be
|
||||
// redirected to '/' (home page); otherwise, it passes to the next middleware.
|
||||
app.get('/auth/openid/return', (req, res, next) => {
|
||||
passport.authenticate('azuread-openidconnect',
|
||||
{
|
||||
response: res, // required
|
||||
failureRedirect: '/',
|
||||
} as passport.AuthenticateOptions,
|
||||
)(req, res, next);
|
||||
},
|
||||
(req, res, next) => {
|
||||
log.info('received a return from AzureAD.');
|
||||
res.redirect('/');
|
||||
});
|
||||
|
||||
// 'POST returnURL'
|
||||
// `passport.authenticate` will try to authenticate the content returned in
|
||||
// body (such as authorization code). If authentication fails, user will be
|
||||
// redirected to '/' (home page); otherwise, it passes to the next middleware.
|
||||
app.post('/auth/openid/return',
|
||||
(req, res, next) => {
|
||||
passport.authenticate('azuread-openidconnect',
|
||||
{
|
||||
response: res, // required
|
||||
failureRedirect: '/',
|
||||
} as passport.AuthenticateOptions,
|
||||
)(req, res, next);
|
||||
},
|
||||
(req, res, next) => {
|
||||
log.info('received a return from AzureAD.');
|
||||
res.redirect('/');
|
||||
});
|
||||
|
||||
// 'endsession' route, logout from passport, and destroy the session with AAD.
|
||||
app.get('/endsession', (req, res) => {
|
||||
req.session = null;
|
||||
req.logOut();
|
||||
res.redirect('/');
|
||||
});
|
||||
|
||||
// 'logout' route, logout from passport, and destroy the session with AAD.
|
||||
app.get('/logout', (req, res) => {
|
||||
req.session = null;
|
||||
// req.session.destroy((err) => {
|
||||
req.logOut();
|
||||
res.redirect(config.destroySessionUrl);
|
||||
});
|
||||
|
||||
app.get('/api/v1.0/me', ensureAuthenticatedApi,
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
let oauth = oauth2.client(req.user.oauthToken);
|
||||
if (oauth.expired()) { oauth = await oauth.refresh(); }
|
||||
const result = await graph.user(oauth.token.access_token);
|
||||
res.json(result);
|
||||
res.end();
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(error.statusCode).json(error).end();
|
||||
return next();
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/v1.0/profile', ensureAuthenticatedApi,
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
let oauth = oauth2.client(req.user.oauthToken);
|
||||
if (oauth.expired()) { oauth = await oauth.refresh(); }
|
||||
const result = await graph.client(oauth.token.access_token).api('/me/extensions/com.code-with.vott').get();
|
||||
res.json(result);
|
||||
res.end();
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(error.statusCode).json(error).end();
|
||||
return next();
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/v1.0/profile', ensureAuthenticatedApi,
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
let oauth = oauth2.client(req.user.oauthToken);
|
||||
if (oauth.expired()) { oauth = await oauth.refresh(); }
|
||||
const body = { ...req.body, extensionName: 'com.code-with.vott' };
|
||||
let result = null;
|
||||
try { // Handle bad graph open extension semantics
|
||||
result = await graph.client(oauth.token.access_token).api('/me/extensions/').post(body);
|
||||
} catch (error) {
|
||||
// if it already exists and we are replacing. Delete and try again.
|
||||
result = await graph.client(oauth.token.access_token).api('/me/extensions/com.code-with.vott').delete();
|
||||
result = await graph.client(oauth.token.access_token).api('/me/extensions').post(body);
|
||||
}
|
||||
res.json(result);
|
||||
res.end();
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(error.statusCode).json(error).end();
|
||||
return next();
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/v1.0/cloudconnections/:id', ensureAuthenticatedApi,
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const id = req.params.id; // careful should only be a domain name pattern.
|
||||
let oauth = oauth2.client(req.user.oauthToken);
|
||||
if (oauth.expired()) { oauth = await oauth.refresh(); }
|
||||
const result = await graph.client(oauth.token.access_token).api(`/me/extensions/com.code-with.vott.${id}`).get();
|
||||
res.json(result);
|
||||
res.end();
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(error.statusCode).json(error).end();
|
||||
return next();
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/v1.0/cloudconnections/:id', ensureAuthenticatedApi,
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const id = req.params.id; // careful should only be a domain name pattern.
|
||||
let oauth = oauth2.client(req.user.oauthToken);
|
||||
if (oauth.expired()) { oauth = await oauth.refresh(); }
|
||||
const body = { ...req.body, extensionName: `com.code-with.vott.${id}` };
|
||||
let result = null;
|
||||
try { // Handle bad graph open extension semantics
|
||||
result = await graph.client(oauth.token.access_token).api('/me/extensions/').post(body);
|
||||
} catch (error) {
|
||||
// if it already exists and we are replacing. Delete and try again.
|
||||
result = await graph.client(oauth.token.access_token).api(`/me/extensions/com.code-with.vott.${id}`).delete();
|
||||
result = await graph.client(oauth.token.access_token).api('/me/extensions').post(body);
|
||||
}
|
||||
res.json(result);
|
||||
res.end();
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(error.statusCode).json(error).end();
|
||||
return next();
|
||||
}
|
||||
});
|
||||
|
||||
app.patch('/api/v1.0/cloudconnections/:id', ensureAuthenticatedApi,
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const id = req.params.id; // careful should only be a domain name pattern.
|
||||
let oauth = oauth2.client(req.user.oauthToken);
|
||||
if (oauth.expired()) { oauth = await oauth.refresh(); }
|
||||
const body = { ...req.body, extensionName: `com.code-with.vott.${id}` };
|
||||
const result = await graph.client(oauth.token.access_token).api(`/me/extensions/com.code-with.vott.${id}`).patch(body);
|
||||
res.json(result);
|
||||
res.end();
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(error.statusCode).json(error).end();
|
||||
return next();
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/v1.0/cloudconnections/:id', ensureAuthenticatedApi,
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const id = req.params.id; // careful should only be a domain name pattern.
|
||||
let oauth = oauth2.client(req.user.oauthToken);
|
||||
if (oauth.expired()) { oauth = await oauth.refresh(); }
|
||||
const result = await graph.client(oauth.token.access_token).api(`/me/extensions/com.code-with.vott.${id}`).delete();
|
||||
res.end();
|
||||
return next();
|
||||
} catch (error) {
|
||||
res.status(error.statusCode).json(error).end();
|
||||
return next();
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/public', express.static(path.join(__dirname, '../public')));
|
||||
|
||||
export const server = app.listen(config.port);
|
||||
@@ -0,0 +1,94 @@
|
||||
// tslint:disable-next-line: no-var-requires
|
||||
require('dotenv').config();
|
||||
|
||||
export const baseUrl = process.env.BASE_URL || 'http://localhost:3000/';
|
||||
export const redirectPath = 'auth/openid/return';
|
||||
export const port = process.env.PORT || '3000';
|
||||
export const loggingLevel = process.env.LOGGING_LEVEL || 'info';
|
||||
export const httpLogFormat = process.env.HTTP_LOG_FORMAT || 'dev';
|
||||
|
||||
console.log('config values', process.env.APP_ID, process.env.APP_SECRET, baseUrl, redirectPath, port, loggingLevel, httpLogFormat);
|
||||
|
||||
export const creds = {
|
||||
// Required
|
||||
identityMetadata: 'https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration',
|
||||
// 'https://login.microsoftonline.com/<tenant_name>.onmicrosoft.com/v2.0/.well-known/openid-configuration',
|
||||
// or equivalently: 'https://login.microsoftonline.com/<tenant_guid>/v2.0/.well-known/openid-configuration'
|
||||
//
|
||||
// or you can use the common endpoint
|
||||
// 'https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration'
|
||||
// To use the common endpoint, you have to either turn `validateIssuer` off, or provide the `issuer` value.
|
||||
|
||||
// Required, the client ID of your app in AAD
|
||||
clientID: process.env.APP_ID,
|
||||
|
||||
// Required, must be 'code', 'code id_token', 'id_token code' or 'id_token'
|
||||
// If you want to get access_token, you must use 'code', 'code id_token' or 'id_token code'
|
||||
responseType: 'code id_token',
|
||||
|
||||
// Required
|
||||
responseMode: 'form_post',
|
||||
|
||||
// Required, the reply URL registered in AAD for your app
|
||||
redirectUrl: baseUrl + redirectPath,
|
||||
|
||||
// Required if we use http for redirectUrl
|
||||
allowHttpForRedirectUrl: process.env.ALLOW_HTTP ? process.env.ALLOW_HTTP === 'true' : true,
|
||||
|
||||
// Required if `responseType` is 'code', 'id_token code' or 'code id_token'.
|
||||
// If app key contains '\', replace it with '\\'.
|
||||
clientSecret: process.env.APP_SECRET,
|
||||
|
||||
// Required to set to false if you don't want to validate issuer
|
||||
validateIssuer: false,
|
||||
|
||||
// Required if you want to provide the issuer(s) you want to validate instead of using the issuer from metadata
|
||||
// issuer could be a string or an array of strings of the following form: 'https://sts.windows.net/<tenant_guid>/v2.0'
|
||||
issuer: null as string,
|
||||
|
||||
// !Bug - must be false in this sample
|
||||
// Required to set to true if the `verify` function has 'req' as the first parameter
|
||||
passReqToCallback: true,
|
||||
|
||||
// Recommended to set to true. By default we save state in express session, if this option is set to true, then
|
||||
// we encrypt state and save it in cookie instead. This option together with { session: false } allows your app
|
||||
// to be completely express session free.
|
||||
useCookieInsteadOfSession: true,
|
||||
|
||||
logLevel: loggingLevel,
|
||||
|
||||
// Required if `useCookieInsteadOfSession` is set to true. You can provide multiple set of key/iv pairs for key
|
||||
// rollover purpose. We always use the first set of key/iv pair to encrypt cookie, but we will try every set of
|
||||
// key/iv pair to decrypt cookie. Key can be any string of length 32, and iv can be any string of length 12.
|
||||
cookieEncryptionKeys: (process.env.COOKIES_SECRETS ? JSON.parse(process.env.COOKIES_SECRETS) :
|
||||
[
|
||||
{ key: '12345678901234567890123456789012', iv: '123456789012' },
|
||||
{ key: 'abcdefghijklmnopqrstuvwxyzabcdef', iv: 'abcdefghijkl' },
|
||||
]) as Array<{ key: string; iv: string; }>,
|
||||
|
||||
// The additional scopes we want besides 'openid'.
|
||||
// 'profile' scope is required, the rest scopes are optional.
|
||||
// (1) if you want to receive refresh_token, use 'offline_access' scope
|
||||
// (2) if you want to get access_token for graph api, use the graph api url like 'https://graph.microsoft.com/mail.read'
|
||||
scope: ['profile', /* 'offline_access', */ 'https://graph.microsoft.com/user.readwrite'],
|
||||
|
||||
// Optional. The lifetime of nonce in session or cookie, the default value is 3600 (seconds).
|
||||
nonceLifetime: null as number,
|
||||
|
||||
// Optional. The max amount of nonce saved in session or cookie, the default value is 10.
|
||||
nonceMaxAmount: 5,
|
||||
|
||||
// Optional. The clock skew allowed in token validation, the default value is 300 seconds.
|
||||
clockSkew: null as number,
|
||||
|
||||
// Optional. Is B2C
|
||||
isB2C: false,
|
||||
};
|
||||
|
||||
// The url you need to go to destroy the session with AAD
|
||||
export let destroySessionUrl = 'https://login.microsoftonline.com/common/oauth2/logout?post_logout_redirect_uri=http://localhost:3000';
|
||||
|
||||
if (!creds.clientID || !creds.clientSecret) {
|
||||
console.log('issue with config');
|
||||
throw Error('Missing configuration. You need a .env file or environment variables for APP_ID and APP_SECRET');
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as graphClient from '@microsoft/microsoft-graph-client';
|
||||
|
||||
export async function user(access_token: string) {
|
||||
const token = client(access_token);
|
||||
const result = await token.api('/me').get();
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getEvents(access_token: string) {
|
||||
const token = client(access_token);
|
||||
|
||||
const events = await token.api('/me/events')
|
||||
.select('subject,organizer,start,end')
|
||||
.orderby('createdDateTime DESC')
|
||||
.get();
|
||||
return events;
|
||||
}
|
||||
|
||||
export function client(access_token: string): graphClient.Client {
|
||||
// Initialize Graph client
|
||||
const result = graphClient.Client.init({
|
||||
// Use the provided access token to authenticate
|
||||
// requests
|
||||
authProvider: (done: (err: any, access_token: string) => void) => {
|
||||
done(null, access_token);
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as express from 'express';
|
||||
import * as simple_oauth2 from 'simple-oauth2';
|
||||
import * as config from './config';
|
||||
|
||||
export const oauth2 = simple_oauth2.create({
|
||||
client: {
|
||||
id: config.creds.clientID,
|
||||
secret: config.creds.clientSecret,
|
||||
},
|
||||
auth: {
|
||||
tokenHost: 'https://login.microsoftonline.com/common',
|
||||
authorizePath: '/oauth2/v2.0/authorize',
|
||||
tokenPath: '/oauth2/v2.0/token',
|
||||
},
|
||||
});
|
||||
|
||||
export interface Token {
|
||||
refresh_token: string;
|
||||
access_token?: string;
|
||||
expires_at?: string | Date;
|
||||
}
|
||||
|
||||
export function client(token: Token) {
|
||||
token.expires_at = token.expires_at || new Date(0);
|
||||
const result = oauth2.accessToken.create(token);
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"target": "ES2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
"allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
"sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./lib", /* Redirect output structure to the directory. */
|
||||
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
/* Strict Type-Checking Options */
|
||||
// "strict": true, /* Enable all strict type-checking options. */
|
||||
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
// "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"coverage",
|
||||
"data_models",
|
||||
"public",
|
||||
"lib",
|
||||
"temp",
|
||||
"jest.config.js",
|
||||
"src/__tests__"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"defaultSeverity": "error",
|
||||
"extends": [
|
||||
"tslint:recommended"
|
||||
],
|
||||
"linterOptions": {
|
||||
"exclude": [
|
||||
"lib",
|
||||
"public",
|
||||
"src/routes",
|
||||
"jest.config.js"
|
||||
]
|
||||
},
|
||||
"jsRules": {},
|
||||
"rules": {
|
||||
"no-console": false,
|
||||
"arrow-parens": false,
|
||||
"max-classes-per-file": false,
|
||||
"ordered-imports": false,
|
||||
"object-literal-sort-keys": false,
|
||||
"align": false,
|
||||
"interface-name": false,
|
||||
"quotemark": [
|
||||
true,
|
||||
"single",
|
||||
"avoid-escape",
|
||||
"avoid-template"
|
||||
],
|
||||
"max-line-length": {
|
||||
"severity": "warning",
|
||||
"options": [
|
||||
160,
|
||||
{
|
||||
"ignore-pattern": "^import |^export {(.*?)} | //"
|
||||
}
|
||||
]
|
||||
},
|
||||
"variable-name": {
|
||||
"options": [
|
||||
"ban-keywords",
|
||||
"check-format",
|
||||
"allow-leading-underscore",
|
||||
"allow-pascal-case",
|
||||
"allow-snake-case"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,16 @@ import { ErrorHandler } from "./react/components/common/errorHandler/errorHandle
|
||||
describe("App Component", () => {
|
||||
const defaultState: IApplicationState = initialState;
|
||||
const store = createReduxStore(defaultState);
|
||||
const electronMock = {
|
||||
ipcRenderer: {
|
||||
send: jest.fn(),
|
||||
on: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
delete (window as any).require;
|
||||
});
|
||||
|
||||
function createComponent() {
|
||||
return mount(
|
||||
|
||||
@@ -84,6 +84,10 @@ export const english: IAppStrings = {
|
||||
title: "Security Token",
|
||||
description: "Used to encrypt sensitive data within project files",
|
||||
},
|
||||
useSecurityToken: {
|
||||
title: "Use Security Token",
|
||||
description: "When enabled will encrypt sensitive data within provider configuration",
|
||||
},
|
||||
save: "Save Project",
|
||||
sourceConnection: {
|
||||
title: "Source Connection",
|
||||
@@ -194,15 +198,54 @@ export const english: IAppStrings = {
|
||||
},
|
||||
bing: {
|
||||
title: "Bing Image Search",
|
||||
options: "Bing Image Search Options",
|
||||
apiKey: "API Key",
|
||||
query: "Query",
|
||||
options: {
|
||||
title: "Bing Image Search Options",
|
||||
},
|
||||
endpoint: {
|
||||
title: "Endpoint",
|
||||
description: "The endpoint listed within the Bing Search Azure resource",
|
||||
},
|
||||
apiKey: {
|
||||
title: "API Key",
|
||||
description: "An API key listed within the Bing Search Azure resource",
|
||||
},
|
||||
query: {
|
||||
title: "Query",
|
||||
description: "The search query used to populate your connection",
|
||||
},
|
||||
aspectRatio: {
|
||||
title: "Aspect Ratio",
|
||||
all: "All",
|
||||
square: "Square",
|
||||
wide: "Wide",
|
||||
tall: "Tall",
|
||||
description: "Filters the results by the specified aspect ratio",
|
||||
options: {
|
||||
all: "All",
|
||||
square: "Square",
|
||||
wide: "Wide",
|
||||
tall: "Tall",
|
||||
},
|
||||
},
|
||||
licenseType: {
|
||||
title: "License Type",
|
||||
description: "Filters the results by the specified license type",
|
||||
options: {
|
||||
all: "All (does not filter any images)",
|
||||
any: "Any images with any license type",
|
||||
public: "Public domain",
|
||||
share: "Free to share and use",
|
||||
shareCommercially: "Free to share and use commercially",
|
||||
modify: "Free to modify, share and use",
|
||||
modifyCommercially: "Free to modify, share and use commercially",
|
||||
},
|
||||
},
|
||||
size: {
|
||||
title: "Size",
|
||||
description: "Filters the results by the specified size",
|
||||
options: {
|
||||
all: "All",
|
||||
small: "Small (Less than 200x200)",
|
||||
medium: "Medium (Less than 500x500)",
|
||||
large: "Large (Greater than 500x500)",
|
||||
wallpaper: "Wallpaper (Extra large images)",
|
||||
},
|
||||
},
|
||||
},
|
||||
local: {
|
||||
|
||||
@@ -85,6 +85,10 @@ export const spanish: IAppStrings = {
|
||||
title: "Token de seguridad",
|
||||
description: "Se utiliza para cifrar datos confidenciales dentro de archivos de proyecto",
|
||||
},
|
||||
useSecurityToken: {
|
||||
title: "Usar Token de Seguridad",
|
||||
description: "Si está habilitado, los datos confidenciales se cifrarán",
|
||||
},
|
||||
save: "Guardar el Proyecto",
|
||||
sourceConnection: {
|
||||
title: "Conexión de Origen",
|
||||
@@ -196,15 +200,54 @@ export const spanish: IAppStrings = {
|
||||
},
|
||||
bing: {
|
||||
title: "Búsqueda de Imágenes Bing",
|
||||
options: "Opciones de Búsqueda de Imágenes Bing",
|
||||
apiKey: "Clave API",
|
||||
query: "Consulta",
|
||||
options: {
|
||||
title: "Opciones de Búsqueda de Imágenes Bing",
|
||||
},
|
||||
endpoint: {
|
||||
title: "Extremo",
|
||||
description: "El punto de conexión que aparece en el recurso de Bing Search Azure",
|
||||
},
|
||||
apiKey: {
|
||||
title: "Clave API",
|
||||
description: "Una clave de API que aparece en el recurso de Bing Search Azure",
|
||||
},
|
||||
query: {
|
||||
title: "Consulta",
|
||||
description: "La consulta de búsqueda utilizada para rellenar la conexión",
|
||||
},
|
||||
aspectRatio: {
|
||||
title: "Relación de Aspecto",
|
||||
all: "Todos",
|
||||
square: "Cuadrado",
|
||||
wide: "Ancho",
|
||||
tall: "Alto",
|
||||
description: "Filtra los resultados por la relación de aspecto especificada",
|
||||
options: {
|
||||
all: "Todos",
|
||||
square: "Cuadrado",
|
||||
wide: "Ancho",
|
||||
tall: "Alto",
|
||||
},
|
||||
},
|
||||
licenseType: {
|
||||
title: "Tipo de licencia",
|
||||
description: "Filtra los resultados según el tipo de licencia especificado",
|
||||
options: {
|
||||
all: "Todos (no filtra ninguna imagen)",
|
||||
any: "Cualquier imagen con cualquier tipo de licencia",
|
||||
public: "Dominio público",
|
||||
share: "Libre para compartir y usar",
|
||||
shareCommercially: "Libre para compartir y usar comercialmente",
|
||||
modify: "Libre de modificar, compartir y usar",
|
||||
modifyCommercially: "Libre de modificar, compartir y ues comercialmente",
|
||||
},
|
||||
},
|
||||
size: {
|
||||
title: "Tamaño",
|
||||
description: "Filtra los resultados según el tamaño especificado",
|
||||
options: {
|
||||
all: "Todo",
|
||||
small: "Pequeño (Menos de 200x200)",
|
||||
medium: "Medio (Menos de 500x500)",
|
||||
large: "Grande (mayor de 500x500)",
|
||||
wallpaper: "Fondo de pantalla (imágenes extra grandes)",
|
||||
},
|
||||
},
|
||||
},
|
||||
local: {
|
||||
|
||||
@@ -0,0 +1,555 @@
|
||||
import { IAppStrings } from "../strings";
|
||||
|
||||
/**
|
||||
* App Strings for Japanese language from Google Translate
|
||||
*/
|
||||
export const japanese: IAppStrings = {
|
||||
appName: "ビジュアル オブジェクトタグ付けツール", // Visual Object Tagging Tool,
|
||||
common: {
|
||||
displayName: "表示名", // Display Name,
|
||||
description: "説明", // Description,
|
||||
submit: "送信", // Submit,
|
||||
cancel: "キャンセル", // Cancel,
|
||||
save: "保存", // Save,
|
||||
delete: "削除", // Delete,
|
||||
provider: "プロバイダー", // Provider,
|
||||
homePage: "ホーム ページ", // Home Page"
|
||||
},
|
||||
titleBar: {
|
||||
help: "ヘルプ", // Help,
|
||||
minimize: "最小化", // Minimize,
|
||||
maximize: "最大化", // Maximize,
|
||||
restore: "戻す", // Restore,
|
||||
close: "閉じる", // Close"
|
||||
},
|
||||
homePage: {
|
||||
newProject: "新規プロジェクト", // New Project,
|
||||
openLocalProject: {
|
||||
title: "ローカルプロジェクトを開く", // Open Local Project"
|
||||
},
|
||||
openCloudProject: {
|
||||
title: "クラウドプロジェクトを開く", // Open Cloud Project,
|
||||
selectConnection: "接続を選択", // Select a Connection"
|
||||
},
|
||||
recentProjects: "最近のプロジェクト", // Recent Projects,
|
||||
deleteProject: {
|
||||
title: "プロジェクトを削除", // Delete Project,
|
||||
confirmation: "プロジェクトを削除してもいいですか", // Are you sure you want to delete project"
|
||||
},
|
||||
importProject: {
|
||||
title: "プロジェクトをインポート", // Import Project,
|
||||
confirmation: "プロジェクト ${project.file.name} プロジェクト設定を v2 形式に変換してもいいですか",
|
||||
// Are you sure you want to conver project ${project.file.name} project settings to v2 format?
|
||||
// We recommend you backup the project file first."
|
||||
},
|
||||
messages: {
|
||||
deleteSuccess: "${project.name} を削除しました", // Successfully deleted ${project.name}"
|
||||
},
|
||||
},
|
||||
appSettings: {
|
||||
title: "アプリケーション設定", // Application Settings,
|
||||
storageTitle: "ストレージ設定", // Storage Settings,
|
||||
uiHelp: "設定の保存場所", // Where your settings are stored,
|
||||
save: "設定の保存", // Save Settings,
|
||||
securityToken: {
|
||||
name: {
|
||||
title: "名前", // Name"
|
||||
},
|
||||
key: {
|
||||
title: "キー", // Key"
|
||||
},
|
||||
},
|
||||
securityTokens: {
|
||||
title: "セキュリティ トークン", // Security Tokens,
|
||||
description: "セキュリティ トークンは、プロジェクト構成内の機密データを暗号化するために使用されます",
|
||||
// Security tokens are used to encrypt sensitive data within your project configuration"
|
||||
},
|
||||
version: {
|
||||
description: "バージョン:", // Version"
|
||||
},
|
||||
commit: "SHA をコミット", // Commit SHA,
|
||||
devTools: {
|
||||
description: "問題の診断に役立つアプリケーション開発者ツールを開く", // Open application developer tools to help diagnose issues,
|
||||
button: "開発者ツールを開く", // Toggle Developer Tools"
|
||||
},
|
||||
reload: {
|
||||
description: "現在の変更をすべて破棄して、アプリをリロード", // Reload the app discarding all current changes,
|
||||
button: "アプリケーションをリフレッシュ", // Refresh Application"
|
||||
},
|
||||
messages: {
|
||||
saveSuccess: "アプリケーション設定を正常に保存しました", // Successfully saved application settings"
|
||||
},
|
||||
},
|
||||
projectSettings: {
|
||||
title: "プロジェクト設定", // Project Settings,
|
||||
securityToken: {
|
||||
title: "セキュリティ トークン", // Security Token,
|
||||
description: "プロジェクト ファイル内の機密データを暗号化するために使用されます", // Used to encrypt sensitive data within project file"
|
||||
},
|
||||
useSecurityToken: {
|
||||
title: "セキュリティ トークン", // Use Security Token
|
||||
description: "有効にすると、プロバイダー構成内の機密データが暗号化されます。",
|
||||
// When enabled will encrypt sensitive data within provider configuration
|
||||
},
|
||||
save: "プロジェクトを保存", // Save Project,
|
||||
sourceConnection: {
|
||||
title: "ソース接続", // Source Connection,
|
||||
description: "アセットのロード元", // Where to load assets fro"
|
||||
},
|
||||
targetConnection: {
|
||||
title: "ターゲット接続", // Target Connection,
|
||||
description: "プロジェクトとエクスポートされたデータの保存場所", // Where to save the project and exported dat"
|
||||
},
|
||||
videoSettings: {
|
||||
title: "ビデオ設定", // Video Settings,
|
||||
description: "タグ付けにおけるフレームの抽出割合", // The rate at which frames are extracted for tagging.,
|
||||
frameExtractionRate: "フレーム抽出率(ビデオ 1 秒あたりのフレーム数)", // Frame Extraction Rate (frames per a video second)"
|
||||
},
|
||||
addConnection: "接続を追加", // Add Connection,
|
||||
messages: {
|
||||
saveSuccess: "${project.name} プロジェクト設定を正常に保存しました", // Successfully saved ${project.name} project settings"
|
||||
},
|
||||
},
|
||||
projectMetrics: {
|
||||
title: "プロジェクト メトリック", // Project Metrics,
|
||||
assetsSectionTitle: "アセット", // Assets,
|
||||
totalAssetCount: "すべてのアセット", // Total Assets,
|
||||
visitedAssets: "訪問済みアセット(${count})", // Visited Assets (${count}),
|
||||
taggedAssets: "タグ付きアセット(${count})", // Tagged Assets (${count}),
|
||||
nonTaggedAssets: "タグ付けされていないアセット(${count})", // Not Tagged Assets (${count}),
|
||||
nonVisitedAssets: "未訪問ないアセット(${count})", // Not Visited Assets (${count}),
|
||||
tagsSectionTitle: "タグ", // Tags & Labels,
|
||||
totalRegionCount: "タグ付けされたすべての領域", // Total Tagged Regions,
|
||||
totalTagCount: "すべてのタグ", // Total Tags,
|
||||
avgTagCountPerAsset: "アセットごとの平均タグ", // Average tags per asset"
|
||||
},
|
||||
tags: {
|
||||
title: "タグ", // Tags,
|
||||
placeholder: "新しいタグを追加", // Add new tag,
|
||||
editor: "タグ エディター", // Tags Editor,
|
||||
modal: {
|
||||
name: "タグ名", // Tag Name,
|
||||
color: "タグの色", // Tag Color"
|
||||
},
|
||||
colors: {
|
||||
white: "白", // White,
|
||||
gray: "グレー", // Gray,
|
||||
red: "赤", // Red,
|
||||
maroon: "マルーン", // Maroon,
|
||||
yellow: "黄", // Yellow,
|
||||
olive: "オリーブ", // Olive,
|
||||
lime: "ライム", // Lime,
|
||||
green: "緑", // Green,
|
||||
aqua: "アクア", // Aqua,
|
||||
teal: "ティール", // Teal,
|
||||
blue: "青", // Blue,
|
||||
navy: "濃紺", // Navy,
|
||||
fuschia: "赤紫", // Fuschia,
|
||||
purple: "紫", // Purple"
|
||||
},
|
||||
warnings: {
|
||||
existingName: "タグ名が既に存在します。別の名前を選んでください", // Tag name already exists. Choose another name,
|
||||
emptyName: "空のタグ名を持つことはできません", // Cannot have an empty tag name,
|
||||
unknownTagName: "不明", // Unknown"
|
||||
},
|
||||
toolbar: {
|
||||
add: "新しいタグを追加", // Add new tag,
|
||||
search: "タグを検索", // Search tags,
|
||||
edit: "タグを編集", // Edit tag,
|
||||
lock: "タグをロック", // Lock tag,
|
||||
moveUp: "タグを上に移動", // Move tag up,
|
||||
moveDown: "タグを下に移動", // Move tag down,
|
||||
delete: "タグを削除", // Delete tag"
|
||||
},
|
||||
},
|
||||
connections: {
|
||||
title: "接続", // Connections,
|
||||
details: "接続の詳細", // Connection Details,
|
||||
settings: "接続設定", // Connection Settings,
|
||||
instructions: "編集する接続を選択してください", // Please select a connection to edit,
|
||||
save: "接続を保存", // Save Connection,
|
||||
messages: {
|
||||
saveSuccess: "${connection.name} を保存しました", // Successfully saved ${connection.name},
|
||||
deleteSuccess: "${connection.name} を削除しました", // Successfully deleted ${connection.name}"
|
||||
},
|
||||
imageCorsWarning: "警告:Web ブラウザーで VoTT を使用する場合、CORS(クロス オリジン リソース共有)の制限により、" +
|
||||
"Bing 画像検索の一部のアセットが正しくエクスポートされない場合があります。",
|
||||
// Warning: When using VoTT in a Web browser, some assets from Bing Image Search may no export
|
||||
// correctly due to CORS (Cross Origin Resource Sharing) restrictions.",
|
||||
blobCorsWarning: "警告:ソースまたはターゲット接続として使用するには、Azure Blob Storage アカウントで CORS(クロス オリジン リソース共有)を有効にする必要があります。 ",
|
||||
// Warning: CORS (Cross Domain Resource Sharing) must be enabled on the Azure Blob Storage account, in order
|
||||
// to use i as a source or target connection. More information on enabling CORS can be found in the {0}",
|
||||
azDocLinkText: "Azure ドキュメント", // Azure Documentation.,
|
||||
providers: {
|
||||
azureBlob: {
|
||||
title: "Azure Blob Storage", // Azure Blob Storage,
|
||||
description: "",
|
||||
accountName: {
|
||||
title: "アカウント名", // Account Name,
|
||||
description: "",
|
||||
},
|
||||
containerName: {
|
||||
title: "コンテナー名", // Container Name,
|
||||
description: "",
|
||||
},
|
||||
sas: {
|
||||
title: "SAS", // SAS,
|
||||
description: "Blob Storage アカウントの認証に使用される共有アクセス署名",
|
||||
// Shared access signature used to authenticate to the blob storage account"
|
||||
},
|
||||
createContainer: {
|
||||
title: "コンテナーを作成", // Create Container,
|
||||
description: "Blob Storage コンテナーがまだ存在しない場合は作成します",
|
||||
// Creates the blob container if it does not already exist"
|
||||
},
|
||||
},
|
||||
bing: {
|
||||
title: "Bing 画像検索", // Bing Image Search,
|
||||
options: {
|
||||
title: "Bing 画像検索のオプション",
|
||||
}, // Bing Image Search Options,
|
||||
endpoint: {
|
||||
title: "エンドポイント", // Endpoint
|
||||
description: "Bing検索 Azure リソース内に一覧表示されるエンドポイント",
|
||||
},
|
||||
apiKey: {
|
||||
title: "APIキー", // API Key
|
||||
description: "Bing検索 Azure リソース内に表示される API キー",
|
||||
},
|
||||
query: {
|
||||
title: "クエリ", // Query
|
||||
description: "接続の設定に使用する検索クエリ",
|
||||
},
|
||||
aspectRatio: {
|
||||
title: "アスペクト比", // Aspect Ratio,
|
||||
description: "指定した縦横比で結果をフィルター処理します。",
|
||||
options: {
|
||||
all: "すべて", // All,
|
||||
square: "正方形", // Square,
|
||||
wide: "横長", // Wide,
|
||||
tall: "縦長", // Tall"
|
||||
},
|
||||
},
|
||||
licenseType: {
|
||||
title: "ライセンスの種類",
|
||||
description: "指定したライセンスの種類で結果をフィルター処理します。",
|
||||
options: {
|
||||
all: "すべて (画像をフィルター処理しません)",
|
||||
any: "任意のライセンスタイプの画像",
|
||||
public: "パブリック ドメイン",
|
||||
share: "無料で共有・使用",
|
||||
shareCommercially: "無料で共有し、商業的に使用する",
|
||||
modify: "変更、共有、使用が無料",
|
||||
modifyCommercially: "無料で変更、共有、および商用で使用",
|
||||
},
|
||||
},
|
||||
size: {
|
||||
title: "サイズ",
|
||||
description: "結果を指定したサイズでフィルター処理します。",
|
||||
options: {
|
||||
all: "すべての",
|
||||
small: "小 (200x200 未満)",
|
||||
medium: "中 (500x500 未満)",
|
||||
large: "大 (500x500 より大きい)",
|
||||
wallpaper: "壁紙(特大画像)",
|
||||
},
|
||||
},
|
||||
},
|
||||
local: {
|
||||
title: "ローカル ファイル システム", // Local File System,
|
||||
folderPath: "フォルダー パス", // Folder Path,
|
||||
selectFolder: "フォルダーを選択", // Select Folder,
|
||||
chooseFolder: "フォルダーを選択", // Choose Folder"
|
||||
},
|
||||
},
|
||||
},
|
||||
editorPage: {
|
||||
width: "幅", // Width,
|
||||
height: "高さ", // Height,
|
||||
tagged: "タグ付き", // Tagged,
|
||||
visited: "訪問済み", // Visited,
|
||||
toolbar: {
|
||||
select: "選択(V)", // Select (V),
|
||||
pan: "パン", // Pan,
|
||||
drawRectangle: "長方形を描く", // Draw Rectangle,
|
||||
drawPolygon: "ポリゴンを描く", // Draw Polygon,
|
||||
copyRectangle: "長方形をコピー", // Copy Rectangle,
|
||||
copy: "領域をコピー", // Copy Regions,
|
||||
cut: "領域をカット", // Cut Regions,
|
||||
paste: "領域を貼り付け", // Paste Regions,
|
||||
removeAllRegions: "すべてのリージョンを削除", // Remove All Regions,
|
||||
previousAsset: "前のアセット", // Previous Asset,
|
||||
nextAsset: "次のアセット", // Next Asset,
|
||||
saveProject: "プロジェクトを保存", // Save Project,
|
||||
exportProject: "プロジェクトをエクスポート", // Export Project,
|
||||
activeLearning: "アクティブ ラーニング", // Active Learning"
|
||||
},
|
||||
videoPlayer: {
|
||||
previousTaggedFrame: {
|
||||
tooltip: "前のタグ付きフレーム", // Previous Tagged Frame"
|
||||
},
|
||||
nextTaggedFrame: {
|
||||
tooltip: "次のタグ付きフレーム", // Next Tagged Frame"
|
||||
},
|
||||
previousExpectedFrame: {
|
||||
tooltip: "前のフレーム", // Previous Frame"
|
||||
},
|
||||
nextExpectedFrame: {
|
||||
tooltip: "次のフレーム", // Next Frame"
|
||||
},
|
||||
},
|
||||
help: {
|
||||
title: "ヘルプ メニューの切り替え", // Toggle Help Menu,
|
||||
escape: "ヘルプメニューを抜ける", // Escape Help Menu"
|
||||
},
|
||||
assetError: "アセットを読み込めません", // Unable to load asset,
|
||||
tags: {
|
||||
hotKey: {
|
||||
apply: "ホット キーでタグを適用", // Apply Tag with Hot Key,
|
||||
lock: "ホット キーでタグをロック", // Lock Tag with Hot Key"
|
||||
},
|
||||
rename: {
|
||||
title: "タグの名前を変更", // Rename Tag,
|
||||
confirmation: "このタグの名前を変更してもいいですか",
|
||||
// Are you sure you want to rename this tag? It will be renamed throughout all assets"
|
||||
},
|
||||
delete: {
|
||||
title: "タグを削除", // Delete Tag,
|
||||
confirmation: "このタグを削除してもいいですか。このタグはすべてのアセットで削除され、このタグのみのあらゆる領域も削除されます",
|
||||
// Are you sure you want to delete this tag? It will be deleted throughout all assets
|
||||
// and any regions where this is the only tag will also be deleted"
|
||||
},
|
||||
},
|
||||
canvas: {
|
||||
removeAllRegions: {
|
||||
title: "すべてのリージョンを削除", // Remove All Regions,
|
||||
confirmation: "すべてのリージョンを削除してもいいですか", // Are you sure you want to remove all regions"
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
enforceTaggedRegions: {
|
||||
title: "無効な領域が検出されました", // Invalid region(s) detected,
|
||||
description: "1 つ以上の領域にタグが付けられていません。次のアセットに進む前に、すべての領域をタグ付けしてください。",
|
||||
// 1 or more regions have not been tagged. Ensure all regions ar tagged before continuing to next asset"
|
||||
},
|
||||
},
|
||||
},
|
||||
export: {
|
||||
title: "エクスポート", // Export,
|
||||
settings: "エクスポート設定", // Export Settings,
|
||||
saveSettings: "エクスポート設定を保存", // Save Export Settings,
|
||||
providers: {
|
||||
common: {
|
||||
properties: {
|
||||
assetState: {
|
||||
title: "アセットの状態", // Asset State,
|
||||
description: "エクスポートに含めるアセット", // Which assets to include in the export,
|
||||
options: {
|
||||
all: "すべてのアセット", // All Assets,
|
||||
visited: "訪問済みのアセットのみ", // Only Visited Assets,
|
||||
tagged: "タグ付きアセットのみ", // Only tagged Assets"
|
||||
},
|
||||
},
|
||||
testTrainSplit: {
|
||||
title: "テスト/トレーニング分割", // Test / Train Split,
|
||||
description: "エクスポートされたデータに使用するテスト/トレーニングの分割",
|
||||
// The test train split to use for exported data"
|
||||
},
|
||||
includeImages: {
|
||||
title: "画像を含める", // Include Images,
|
||||
description: "ターゲット接続にバイナリ画像アセットを含めるかどうか",
|
||||
// Whether or not to include binary image assets in target connection"
|
||||
},
|
||||
},
|
||||
},
|
||||
vottJson: {
|
||||
displayName: "VoTT JSON", // VoTT JSO"
|
||||
},
|
||||
azureCV: {
|
||||
displayName: "Azure Custom Vision サービス", // Azure Custom Vision Service,
|
||||
regions: {
|
||||
australiaEast: "オーストラリア東部", // Australia East,
|
||||
centralIndia: "インド中部", // Central India,
|
||||
eastUs: "米国東部", // East US,
|
||||
eastUs2: "米国東部 2", // East US 2,
|
||||
japanEast: "東日本", // Japan East,
|
||||
northCentralUs: "米国中北部", // North Central US,
|
||||
northEurope: "北ヨーロッパ", // North Europe,
|
||||
southCentralUs: "アメリカ中南部", // South Central US,
|
||||
southeastAsia: "東南アジア", // Southeast Asia,
|
||||
ukSouth: "英国南部", // UK South,
|
||||
westUs2: "米国西部 2", // West US 2,
|
||||
westEurope: "西ヨーロッパ", // West Europe"
|
||||
},
|
||||
properties: {
|
||||
apiKey: {
|
||||
title: "API キー", // API Key"
|
||||
},
|
||||
region: {
|
||||
title: "領域", // Region,
|
||||
description: "サービスがデプロイされている Azure リージョン", // The Azure region where your service is deployed"
|
||||
},
|
||||
classificationType: {
|
||||
title: "分類タイプ", // Classification Type,
|
||||
options: {
|
||||
multiLabel: "画像ごとに複数のタグ", // Multiple tags per image,
|
||||
multiClass: "画像ごとに単一のタグ", // Single tag per image"
|
||||
},
|
||||
},
|
||||
name: {
|
||||
title: "プロジェクト名", // Project Name"
|
||||
},
|
||||
description: {
|
||||
title: "プロジェクトの説明", // Project Description"
|
||||
},
|
||||
domainId: {
|
||||
title: "ドメイン", // Domain"
|
||||
},
|
||||
newOrExisting: {
|
||||
title: "新規または既存プロジェクト", // New or Existing Project,
|
||||
options: {
|
||||
new: "新規プロジェクト", // New Project,
|
||||
existing: "既存プロジェクト", // Existing Project"
|
||||
},
|
||||
},
|
||||
projectId: {
|
||||
title: "プロジェクト名", // Project Name"
|
||||
},
|
||||
projectType: {
|
||||
title: "プロジェクトの種類", // Project Type,
|
||||
options: {
|
||||
classification: "分類", // Classification,
|
||||
objectDetection: "物体検出", // Object Detection"
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tfRecords: {
|
||||
displayName: "TensorFlow レコード", // Tensorflow Record"
|
||||
},
|
||||
pascalVoc: {
|
||||
displayName: "Pascal VOC", // Pascal VOC,
|
||||
exportUnassigned: {
|
||||
title: "未割り当てをエクスポート", // Export Unassigned,
|
||||
description: "エクスポートされたデータに未割り当てのタグを含めるかどうか",
|
||||
// Whether or not to include unassigned tags in exported data"
|
||||
},
|
||||
},
|
||||
cntk: {
|
||||
displayName: "Microsoft Cognitive Toolkit(CNTK)", // Microsoft Cognitive Toolkit (CNTK)"
|
||||
},
|
||||
csv: {
|
||||
displayName: "コンマ区切り値(CSV)", // Comma Separated Values (CSV)"
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
saveSuccess: "エクスポート設定を保存しました", // Successfully saved export settings"
|
||||
},
|
||||
},
|
||||
activeLearning: {
|
||||
title: "アクティブ ラーニング", // Active Learning,
|
||||
form: {
|
||||
properties: {
|
||||
modelPathType: {
|
||||
title: "モデル プロバイダー", // Model Provider,
|
||||
description: "トレーニング モデルのロード元", // Where to load the training model from,
|
||||
options: {
|
||||
preTrained: "事前トレーニング済みの Coco SSD", // Pre-trained Coco SSD,
|
||||
customFilePath: "カスタム(ファイル パス)", // Custom (File path),
|
||||
customWebUrl: "カスタム(URL)", // Custom (Url)"
|
||||
},
|
||||
},
|
||||
autoDetect: {
|
||||
title: "自動検出", // Auto Detect,
|
||||
description: "アセット間を移動するときに自動的に予測を行うかどうか",
|
||||
// Whether or not to automatically make predictions as you navigate between assets"
|
||||
},
|
||||
modelPath: {
|
||||
title: "モデル パス", // Model path,
|
||||
description: "ローカル ファイル システムからモデルを選択します", // Select a model from your local file system"
|
||||
},
|
||||
modelUrl: {
|
||||
title: "モデルURL", // Model URL,
|
||||
description: "公開 Web URL からモデルを読み込む", // Load your model from a public web URL"
|
||||
},
|
||||
predictTag: {
|
||||
title: "予測タグ", // Predict Tag,
|
||||
description: "予測にタグを自動的に含めるかどうか", // Whether or not to automatically include tags in predictions"
|
||||
},
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
loadingModel: "アクティブ ラーニング モデルを読み込んでいます...", // Loading active learning model...,
|
||||
errorLoadModel: "アクティブ ラーニング モデルの読み込みエラー", // Error loading active learning model,
|
||||
saveSuccess: "アクティブ ラーニング設定を保存しました", // Successfully saved active learning settings"
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
settings: "プロファイル設定", // Profile Settings"
|
||||
},
|
||||
errors: {
|
||||
unknown: {
|
||||
title: "不明なエラー", // Unknown Error,
|
||||
message: "アプリで不明なエラーが発生しました。", // The app encountered an unknown error. Please try again"
|
||||
},
|
||||
projectUploadError: {
|
||||
title: "ファイルのアップロード エラー", // Error Uploading File,
|
||||
message: "ファイルのアップロード中にエラーが発生しました。",
|
||||
// There was an error uploading the file. Please verify the file is of the correct format and try again."
|
||||
},
|
||||
genericRenderError: {
|
||||
title: "アプリケーションの読み込みエラー", // Error Loading Application,
|
||||
message: "アプリケーションのレンダリング中にエラーが発生しました。",
|
||||
// An error occured while rendering the application. Please try again"
|
||||
},
|
||||
projectInvalidSecurityToken: {
|
||||
title: "プロジェクト ファイルの読み込みエラー", // Error loading project file,
|
||||
message: "プロジェクトが参照するセキュリティ トークンが無効です。",
|
||||
// The security token referenced by the project is invalid.
|
||||
// Verify that the security token for the project has been set correctly within your application settings"
|
||||
},
|
||||
projectInvalidJson: {
|
||||
title: "プロジェクト ファイルの解析エラー", // Error parsing project file,
|
||||
message: "選択したプロジェクト ファイルに有効なJSONが含まれていません。",
|
||||
// The selected project files does not contain valid JSON Please check the file any try again."
|
||||
},
|
||||
projectDeleteError: {
|
||||
title: "プロジェクトの削除エラー", // Error deleting project,
|
||||
message: "プロジェクトの削除中にエラーが発生しました。",
|
||||
// An error occured while deleting the project.
|
||||
// Validate the project file an security token exist and try again"
|
||||
},
|
||||
securityTokenNotFound: {
|
||||
title: "プロジェクト ファイルの読み込みエラー", // Error loading project file,
|
||||
message: "プロジェクトが参照するセキュリティ トークンが現在のアプリケーション設定に見つかりません。",
|
||||
// The security token referenced by the project cannot be found in your current application settings.
|
||||
// Verify the security token exists and try to reload the project."
|
||||
},
|
||||
canvasError: {
|
||||
title: "キャンバスの読み込みエラー", // Error loading canvas,
|
||||
message: "キャンバスのロード中にエラーが発生しました。プロジェクトのアセットを確認して、再試行してください。",
|
||||
// There was an error loading the canvas, check the project's assets and try again."
|
||||
},
|
||||
importError: {
|
||||
title: "V1 プロジェクトのインポート エラー", // Error importing V1 project,
|
||||
message: "V1 プロジェクトのインポート中にエラーが発生しました。",
|
||||
// There was an error importing the V1 project. Check the project file and try again"
|
||||
},
|
||||
pasteRegionTooBigError: {
|
||||
title: "領域の貼り付けエラー", // Error pasting region,
|
||||
message: "このアセットに対して領域が大きすぎます。別のリージョンをコピーしてください",
|
||||
// Region too big for this asset. Try copying another region"
|
||||
},
|
||||
exportFormatNotFound: {
|
||||
title: "プロジェクトのエクスポート エラー", // Error exporting project,
|
||||
message: "プロジェクトにエクスポート形式がありません。",
|
||||
// Project is missing export format. Please select an export format in the export setting page."
|
||||
},
|
||||
activeLearningPredictionError: {
|
||||
title: "アクティブ ラーニングのエラー", // Active Learning Error,
|
||||
message: "現在のアセットの領域を予測中にエラーが発生しました。",
|
||||
// An error occurred while predicting regions in the current asset.
|
||||
// Please verify your active learning configuration and try again"
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,556 @@
|
||||
import { IAppStrings } from "../strings";
|
||||
|
||||
/**
|
||||
* App Strings for Korean language from Google Translate
|
||||
*/
|
||||
export const korean: IAppStrings = {
|
||||
appName: "비주얼 객체 태깅 도구", // Visual Object Tagging Tool,
|
||||
common: {
|
||||
displayName: "프로젝트 이름", // Display Name,
|
||||
description: "설명", // Description,
|
||||
submit: "제출", // Submit,
|
||||
cancel: "취소", // Cancel,
|
||||
save: "저장", // Save,
|
||||
delete: "삭제", // Delete,
|
||||
provider: "공급자", // Provider,
|
||||
homePage: "홈페이지", // Home Page"
|
||||
},
|
||||
titleBar: {
|
||||
help: "도움", // Help,
|
||||
minimize: "최소화", // Minimize,
|
||||
maximize: "최대화", // Maximize,
|
||||
restore: "되돌리기", // Restore,
|
||||
close: "닫기", // Close"
|
||||
},
|
||||
homePage: {
|
||||
newProject: "새로운 프로젝트", // New Project,
|
||||
openLocalProject: {
|
||||
title: "로컬 프로젝트 열기", // Open Local Project"
|
||||
},
|
||||
openCloudProject: {
|
||||
title: "클라우드 프로젝트 열기", // Open Cloud Project,
|
||||
selectConnection: "Connection 선택", // Select a Connection
|
||||
},
|
||||
recentProjects: "최근 프로젝트", // Recent Projects,
|
||||
deleteProject: {
|
||||
title: "프로젝트 삭제", // Delete Project,
|
||||
confirmation: "프로젝트를 삭제 하시겠습니까?", // Are you sure you want to delete project
|
||||
},
|
||||
importProject: {
|
||||
title: "프로젝트 가져 오기", // Import Project,
|
||||
confirmation: "${project.file.name} 프로젝트 설정을 v2 형식으로 수정 하시겠습니까? 수정하시기 전에 프로젝트 파일을 백업해두시기 바랍니다.",
|
||||
// Are you sure you want to conver project ${project.file.name} project settings to v2 format?
|
||||
// We recommend you backup the project file first."
|
||||
},
|
||||
messages: {
|
||||
deleteSuccess: "${project.name}을 삭제했습니다", // Successfully deleted ${project.name}"
|
||||
},
|
||||
},
|
||||
appSettings: {
|
||||
title: "애플리케이션 설정", // Application Settings,
|
||||
storageTitle: "저장소 설정", // Storage Settings,
|
||||
uiHelp: "설정이 저장된 위치", // Where your settings are stored,
|
||||
save: "설정 저장", // Save Settings,
|
||||
securityToken: {
|
||||
name: {
|
||||
title: "이름", // Name
|
||||
},
|
||||
key: {
|
||||
title: "키", // Key
|
||||
},
|
||||
},
|
||||
securityTokens: {
|
||||
title: "보안 토큰", // Security Tokens,
|
||||
description: "보안 토큰은 프로젝트 구성 내에서 중요한 데이터를 암호화하는 데 사용됩니다",
|
||||
// Security tokens are used to encryp sensitive data within your project configuration"
|
||||
},
|
||||
version: {
|
||||
description: "버전:", // Version"
|
||||
},
|
||||
commit: "커밋 SHA", // Commit SHA,
|
||||
devTools: {
|
||||
description: "이슈 진단을 돕기 위한 개발자 도구 열기", // Open application developer tools to help diagnose issues,
|
||||
button: "개발자 도구 전환", // Toggle Developer Tools
|
||||
},
|
||||
reload: {
|
||||
description: "모든 변경사항을 버리고 애플리케이션을 재시작 합니다", // Reload the app discarding all current changes,
|
||||
button: "애플리케이션 새로고침", // Refresh Application
|
||||
},
|
||||
messages: {
|
||||
saveSuccess: "애플리케이션 설정이 성공적으로 저장되었습니다", // Successfully saved application settings
|
||||
},
|
||||
},
|
||||
projectSettings: {
|
||||
title: "프로젝트 설정", // Project Settings,
|
||||
securityToken: {
|
||||
title: "보안 토큰", // Security Token,
|
||||
description: "프로젝트 파일 내에서 중요한 데이터를 암호화하는 데 사용", // Used to encrypt sensitive data within project file
|
||||
},
|
||||
useSecurityToken: {
|
||||
title: "보안 토큰 사용", // Use Security Token
|
||||
description: "활성화되면 공급자 구성 내에서 중요한 데이터를 암호화합니다.",
|
||||
// When enabled will encrypt sensitive data within provider configuration
|
||||
},
|
||||
save: "프로젝트 저장", // Save Project,
|
||||
sourceConnection: {
|
||||
title: "소스 연결", // Source Connection,
|
||||
description: "Asset 저장 경로", // Where to load assets from
|
||||
},
|
||||
targetConnection: {
|
||||
title: "대상 연결", // Target Connection,
|
||||
description: "프로젝트 및 내 보낸 데이터를 저장할 위치", // Where to save the project and exported data
|
||||
},
|
||||
videoSettings: {
|
||||
title: "비디오 설정", // Video Settings,
|
||||
description: "태그 지정을 위해 프레임을 추출하는 비율", // The rate at which frames are extracted for tagging
|
||||
frameExtractionRate: "프레임 추출 속도 (비디오 초당 프레임)", // Frame Extraction Rate (frames per a video second)
|
||||
},
|
||||
addConnection: "연결 추가", // Add Connection,
|
||||
messages: {
|
||||
saveSuccess: "${project.name} 프로젝트 설정을 성공적으로 저장했습니다", // Successfully saved ${project.name} project settings
|
||||
},
|
||||
},
|
||||
projectMetrics: {
|
||||
title: "프로젝트 매트릭", // Project Metrics,
|
||||
assetsSectionTitle: "Asset", // Assets,
|
||||
totalAssetCount: "총 Asset", // Total Assets,
|
||||
visitedAssets: "검토한 Asset (${count})", // Visited Assets (${count}),
|
||||
taggedAssets: "태그된 Asset (${count})", // Tagged Assets (${count}),
|
||||
nonTaggedAssets: "태그가 없는 Asset (${count})", // Not Tagged Assets (${count}),
|
||||
nonVisitedAssets: "검토하지 않은 Asset (${count})", // Not Visited Assets (${count}),
|
||||
tagsSectionTitle: "태그", // Tags & Labels,
|
||||
totalRegionCount: "태그된 지역 수", // Total Tagged Regions,
|
||||
totalTagCount: "태그 숫자", // Total Tags,
|
||||
avgTagCountPerAsset: "Asset 당 평균 태그 숫자", // Average tags per asset"
|
||||
},
|
||||
tags: {
|
||||
title: "태그", // Tags,
|
||||
placeholder: "새 태그 추가", // Add new tag,
|
||||
editor: "태그 편집기", // Tags Editor,
|
||||
modal: {
|
||||
name: "태그 이름", // Tag Name,
|
||||
color: "태그 색상", // Tag Color"
|
||||
},
|
||||
colors: {
|
||||
white: "하얀색", // White,
|
||||
gray: "회색", // Gray,
|
||||
red: "빨간색", // Red,
|
||||
maroon: "밤색", // Maroon,
|
||||
yellow: "노랑색", // Yellow,
|
||||
olive: "올리브색", // Olive,
|
||||
lime: "라임색", // Lime,
|
||||
green: "초록색", // Green,
|
||||
aqua: "아쿠아", // Aqua,
|
||||
teal: "물오리", // Teal,
|
||||
blue: "파랑색", // Blue,
|
||||
navy: "군청색", // Navy,
|
||||
fuschia: "푸시아", // Fuschia,
|
||||
purple: "보라색", // Purple"
|
||||
},
|
||||
warnings: {
|
||||
existingName: "태그 이름이 이미 존재합니다. 다른 이름을 입력하십시오", // Tag name already exists. Choose another name,
|
||||
emptyName: "빈 태그 이름을 가질 수 없습니다", // Cannot have an empty tag name,
|
||||
unknownTagName: "알 수 없는 태그 이름", // Unknown"
|
||||
},
|
||||
toolbar: {
|
||||
add: "새 태그 추가", // Add new tag,
|
||||
search: "태크 검색", // Search tags,
|
||||
edit: "태그 편집", // Edit tag,
|
||||
lock: "태그 잠금", // Lock tag,
|
||||
moveUp: "태그를 위로 이동", // Move tag up,
|
||||
moveDown: "태그를 아래로 이동", // Move tag down,
|
||||
delete: "태그 삭제", // Delete tag"
|
||||
},
|
||||
},
|
||||
connections: {
|
||||
title: "연결 설정", // Connections,
|
||||
details: "설명", // Connection Details,
|
||||
settings: "설정", // Connection Settings,
|
||||
instructions: "편집 할 연결 정보를 선택하십시오", // Please select a connection to edit,
|
||||
save: "저장", // Save Connection,
|
||||
messages: {
|
||||
saveSuccess: "${connection.name}을 성공적으로 저장했습니다", // Successfully saved ${connection.name},
|
||||
deleteSuccess: "${connection.name}을 삭제했습니다.", // Successfully deleted ${connection.name}"
|
||||
},
|
||||
imageCorsWarning: "경고 : 웹 브라우저에서 VoTT를 사용하는 경우 CORS (Cross Origin Resource Sharing) " +
|
||||
"제한으로 인해 Bing Image Search의 일부 정보가 제대로 내보내지지 않을 수 있습니다.",
|
||||
// Warning: When using VoTT in a Web browser, some assets from Bing Image Search may no export
|
||||
// correctly due to CORS (Cross Origin Resource Sharing) restrictions.",
|
||||
blobCorsWarning: "경고 : 소스 또는 대상 연결로 사용하려면, Azure Blob Storage 계정에서 CORS(Cross Domain Resource Sharing) " +
|
||||
"설정을 활성화 해야 합니다. CORS 설정에 대한 자세한 정보는 {0}에서 찾을 수 있습니다.",
|
||||
// Warning: CORS (Cross Domain Resource Sharing) must be enabled on the Azure Blob Storage account, in order
|
||||
// to use i as a source or target connection. More information on enabling CORS can be found in the {0}",
|
||||
azDocLinkText: "Azure 설명서.", // Azure Documentation.,
|
||||
providers: {
|
||||
azureBlob: {
|
||||
title: "Azure Blob 저장소", // Azure Blob Storage,
|
||||
description: "",
|
||||
accountName: {
|
||||
title: "계정 이름", // Account Name,
|
||||
description: "",
|
||||
},
|
||||
containerName: {
|
||||
title: "컨테이너 이름", // Container Name,
|
||||
description: "",
|
||||
},
|
||||
sas: {
|
||||
title: "SAS", // SAS,
|
||||
description: "Blob Storage 계정을 인증하는 데 사용되는 공유 액세스 서명",
|
||||
// Shared access signature used to authenticate to the blob storage account"
|
||||
},
|
||||
createContainer: {
|
||||
title: "컨테이너 만들기", // Create Container,
|
||||
description: "Blob 컨테이너가 없으면 새로 생성합니다.",
|
||||
// Creates the blob container if it does not already exist"
|
||||
},
|
||||
},
|
||||
bing: {
|
||||
title: "Bing 이미지 검색", // Bing Image Search,
|
||||
options: {
|
||||
title: "Bing 이미지 검색 옵션",
|
||||
}, // Bing Image Search Options,
|
||||
endpoint: {
|
||||
title: "끝점",
|
||||
description: "Bing 검색 Azure 리소스 내에 나열된 끝점",
|
||||
},
|
||||
apiKey: {
|
||||
title: "API 키",
|
||||
description: "Bing 검색 Azure 리소스 내에 나열된 API 키",
|
||||
}, // API Key,
|
||||
query: {
|
||||
title: "쿼리",
|
||||
description: "연결을 채우는 데 사용되는 검색 쿼리",
|
||||
}, // Query,
|
||||
aspectRatio: {
|
||||
title: "종횡비", // Aspect Ratio,
|
||||
description: "지정된 종횡비로 결과를 필터링합니다.",
|
||||
options: {
|
||||
all: "모두", // All,
|
||||
square: "정사각형", // Square,
|
||||
wide: "넓은", // Wide,
|
||||
tall: "긴", // Tall"
|
||||
},
|
||||
},
|
||||
licenseType: {
|
||||
title: "라이센스 유형",
|
||||
description: "지정된 라이센스 유형으로 결과 필터링",
|
||||
options: {
|
||||
all: "모든 (이미지를 필터링하지 않음)",
|
||||
any: "라이센스 유형이 있는 모든 이미지",
|
||||
public: "퍼블릭 도메인",
|
||||
share: "무료 공유 및 사용",
|
||||
shareCommercially: "상업적으로 자유롭게 공유하고 사용할 수 있습니다.",
|
||||
modify: "자유롭게 수정, 공유 및 사용",
|
||||
modifyCommercially: "상업적으로 자유롭게 수정, 공유 및 사용",
|
||||
},
|
||||
},
|
||||
size: {
|
||||
title: "크기",
|
||||
description: "지정된 크기로 결과를 필터링합니다.",
|
||||
options: {
|
||||
all: "모든",
|
||||
small: "스몰(200x200 미만)",
|
||||
medium: "중간(500x500 미만)",
|
||||
large: "대형(500x500 이상)",
|
||||
wallpaper: "배경 화면 (초대형 이미지)",
|
||||
},
|
||||
},
|
||||
},
|
||||
local: {
|
||||
title: "로컬 파일 시스템", // Local File System,
|
||||
folderPath: "경로", // Folder Path,
|
||||
selectFolder: "폴더 선택", // Select Folder,
|
||||
chooseFolder: "선택", // Choose Folder"
|
||||
},
|
||||
},
|
||||
},
|
||||
editorPage: {
|
||||
width: "너비", // Width,
|
||||
height: "높이", // Height,
|
||||
tagged: "태그", // Tagged,
|
||||
visited: "방문", // Visited,
|
||||
toolbar: {
|
||||
select: "선택 (V)", // Select (V),
|
||||
pan: "팬", // Pan,
|
||||
drawRectangle: "사각형 그리기", // Draw Rectangle,
|
||||
drawPolygon: "다각형 그리기", // Draw Polygon,
|
||||
copyRectangle: "사각형 복사", // Copy Rectangle,
|
||||
copy: "영역 복사", // Copy Regions,
|
||||
cut: "영역 잘라내기", // Cut Regions,
|
||||
paste: "영역 붙여 넣기", // Paste Regions,
|
||||
removeAllRegions: "모든 지역 제거", // Remove All Regions,
|
||||
previousAsset: "이전 Asset", // Previous Asset,
|
||||
nextAsset: "다음 Asset", // Next Asset,
|
||||
saveProject: "프로젝트 저장", // Save Project,
|
||||
exportProject: "프로젝트 내보내기", // Export Project,
|
||||
activeLearning: "Active Learning", // Active Learning"
|
||||
},
|
||||
videoPlayer: {
|
||||
previousTaggedFrame: {
|
||||
tooltip: "이전 태그 된 프레임", // Previous Tagged Frame"
|
||||
},
|
||||
nextTaggedFrame: {
|
||||
tooltip: "다음 태그 된 프레임", // Next Tagged Frame"
|
||||
},
|
||||
previousExpectedFrame: {
|
||||
tooltip: "이전 프레임", // Previous Frame"
|
||||
},
|
||||
nextExpectedFrame: {
|
||||
tooltip: "다음 프레임", // Next Frame"
|
||||
},
|
||||
},
|
||||
help: {
|
||||
title: "도움말", // Toggle Help Menu,
|
||||
escape: "나가기", // Escape Help Menu"
|
||||
},
|
||||
assetError: "Asset을 불러올 수 없습니다", // Unable to load asset,
|
||||
tags: {
|
||||
hotKey: {
|
||||
apply: "단축키로 태그 적용", // Apply Tag with Hot Key,
|
||||
lock: "단축키가 있는 태그 잠금", // Lock Tag with Hot Key"
|
||||
},
|
||||
rename: {
|
||||
title: "태그 이름 바꾸기", // Rename Tag,
|
||||
confirmation: "이 태그의 이름을 바꾸시겠습니까? 모든 Asset에서 이름이 변경됩니다",
|
||||
// Are you sure you want to rename this tag? It will be renamed throughout all assets"
|
||||
},
|
||||
delete: {
|
||||
title: "태그 삭제", // Delete Tag,
|
||||
confirmation: "이 태그를 삭제 하시겠습니까? 모든 Asset 및 태그가 유일한 지역 인 모든 지역에서 삭제됩니다.",
|
||||
// Are you sure you want to delete this tag? It will be deleted throughout all assets
|
||||
// and any regions where this is the only tag will also be deleted"
|
||||
},
|
||||
},
|
||||
canvas: {
|
||||
removeAllRegions: {
|
||||
title: "모든 지역 제거", // Remove All Regions,
|
||||
confirmation: "모든 지역을 삭제 하시겠습니까?", // Are you sure you want to remove all regions"
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
enforceTaggedRegions: {
|
||||
title: "유효하지 않은 지역이 감지되었습니다.", // Invalid region(s) detected,
|
||||
description: "1 개 이상의 지역이 태그되어야 합니다. 다음 작업을 계속 진행하기 위해 모든 지역에 태그가 지정되어 있는지 확인하십시오.",
|
||||
// 1 or more regions have not been tagged. Ensure all regions ar tagged before continuing to next asset"
|
||||
},
|
||||
},
|
||||
},
|
||||
export: {
|
||||
title: "내보내기", // Export,
|
||||
settings: "내보내기 설정", // Export Settings,
|
||||
saveSettings: "내보내기 설정 저장", // Save Export Settings,
|
||||
providers: {
|
||||
common: {
|
||||
properties: {
|
||||
assetState: {
|
||||
title: "Asset 상태", // Asset State,
|
||||
description: "내보내기에 포함 할 Asset", // Which assets to include in the export,
|
||||
options: {
|
||||
all: "모든 Asset", // All Assets,
|
||||
visited: "방문한 Asset만", // Only Visited Assets,
|
||||
tagged: "태그된 Asset만", // Only tagged Assets"
|
||||
},
|
||||
},
|
||||
testTrainSplit: {
|
||||
title: "테스트용 / 학습용 분할", // Test / Train Split,
|
||||
description: "내보내는 데이터에 테스트용 / 학습용 분할",
|
||||
// The test train split to use for exported data"
|
||||
},
|
||||
includeImages: {
|
||||
title: "이미지 포함", // Include Images,
|
||||
description: "대상 연결에 이진 이미지 Asset을 포함할지 여부",
|
||||
// Whether or not to include binary image assets in target connection"
|
||||
},
|
||||
},
|
||||
},
|
||||
vottJson: {
|
||||
displayName: "VoTT JSON", // VoTT JSON
|
||||
},
|
||||
azureCV: {
|
||||
displayName: "Azure Custom Vision 서비스", // Azure Custom Vision Service,
|
||||
regions: {
|
||||
australiaEast: "호주 동부", // Australia East,
|
||||
centralIndia: "중앙 인도", // Central India,
|
||||
eastUs: "미국 동부", // East US,
|
||||
eastUs2: "미국 동부 2", // East US 2,
|
||||
japanEast: "일본 동부", // Japan East,
|
||||
northCentralUs: "미국 중북부", // North Central US,
|
||||
northEurope: "북유럽", // North Europe,
|
||||
southCentralUs: "미국 중남부", // South Central US,
|
||||
southeastAsia: "동남아시아", // Southeast Asia,
|
||||
ukSouth: "영국 남부", // UK South,
|
||||
westUs2: "미국 서부 2", // West US 2,
|
||||
westEurope: "서유럽", // West Europe"
|
||||
},
|
||||
properties: {
|
||||
apiKey: {
|
||||
title: "API 키", // API Key"
|
||||
},
|
||||
region: {
|
||||
title: "지역", // Region,
|
||||
description: "서비스가 배포 된 Azure 지역", // The Azure region where your service is deployed"
|
||||
},
|
||||
classificationType: {
|
||||
title: "분류 유형", // Classification Type,
|
||||
options: {
|
||||
multiLabel: "이미지 당 여러 태그", // Multiple tags per image,
|
||||
multiClass: "이미지 당 단일 태그", // Single tag per image"
|
||||
},
|
||||
},
|
||||
name: {
|
||||
title: "프로젝트 이름", // Project Name"
|
||||
},
|
||||
description: {
|
||||
title: "설명", // Project Description"
|
||||
},
|
||||
domainId: {
|
||||
title: "도메인", // Domain"
|
||||
},
|
||||
newOrExisting: {
|
||||
title: "신규 또는 기존 프로젝트", // New or Existing Project,
|
||||
options: {
|
||||
new: "새로운 프로젝트", // New Project,
|
||||
existing: "기존 프로젝트", // Existing Project"
|
||||
},
|
||||
},
|
||||
projectId: {
|
||||
title: "프로젝트 이름", // Project Name"
|
||||
},
|
||||
projectType: {
|
||||
title: "프로젝트 유형", // Project Type,
|
||||
options: {
|
||||
classification: "분류", // Classification,
|
||||
objectDetection: "물체 감지", // Object Detection"
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tfRecords: {
|
||||
displayName: "TensorFlow 기록", // Tensorflow Record"
|
||||
},
|
||||
pascalVoc: {
|
||||
displayName: "Pascal VOC", // Pascal VOC,
|
||||
exportUnassigned: {
|
||||
title: "할당되지 않은 태그 내보내기", // Export Unassigned,
|
||||
description: "내보내는 데이터에 할당되지 않은 태그를 포함할지 여부",
|
||||
// Whether or not to include unassigned tags in exported data"
|
||||
},
|
||||
},
|
||||
cntk: {
|
||||
displayName: "Microsoft Cognitive Toolkit(CNTK)", // Microsoft Cognitive Toolkit (CNTK)"
|
||||
},
|
||||
csv: {
|
||||
displayName: "쉼표로 구분 된 값(CSV)", // Comma Separated Values (CSV)"
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
saveSuccess: "내보내기 설정이 성공적으로 저장되었습니다", // Successfully saved export settings"
|
||||
},
|
||||
},
|
||||
activeLearning: {
|
||||
title: "Active Learning", // Active Learning,
|
||||
form: {
|
||||
properties: {
|
||||
modelPathType: {
|
||||
title: "모델 제공자", // Model Provider,
|
||||
description: "학습 모델을 불러올 위치", // Where to load the training model from,
|
||||
options: {
|
||||
preTrained: "미리 학습된 Coco SSD", // Pre-trained Coco SSD,
|
||||
customFilePath: "사용자 정의 (파일 경로)", // Custom (File path),
|
||||
customWebUrl: "사용자 정의 (URL)", // Custom (Url)"
|
||||
},
|
||||
},
|
||||
autoDetect: {
|
||||
title: "자동 감지", // Auto Detect,
|
||||
description: "Asset 간을 탐색 할 때 자동 예측 여부",
|
||||
// Whether or not to automatically make predictions as you navigate between assets"
|
||||
},
|
||||
modelPath: {
|
||||
title: "모델 경로", // Model path,
|
||||
description: "로컬 파일 시스템에서 모델을 선택하십시오.", // Select a model from your local file system"
|
||||
},
|
||||
modelUrl: {
|
||||
title: "모델 URL", // Model URL,
|
||||
description: "URL에서 모델 불러오기", // Load your model from a public web URL"
|
||||
},
|
||||
predictTag: {
|
||||
title: "태그 예측", // Predict Tag,
|
||||
description: "예측에 태그를 자동으로 포함할지 여부", // Whether or not to automatically include tags in predictions"
|
||||
},
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
loadingModel: "Active Learning 모델 불러오는 중 ...", // Loading active learning model...,
|
||||
errorLoadModel: "Active Learning 모델을 불러오는 중 오류가 발생했습니다", // Error loading active learning model,
|
||||
saveSuccess: "Active Learning 모델 설정을 성공적으로 저장했습니다", // Successfully saved active learning settings"
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
settings: "프로필 설정", // Profile Settings"
|
||||
},
|
||||
errors: {
|
||||
unknown: {
|
||||
title: "알 수 없는 오류", // Unknown Error,
|
||||
message: "애플리케이션에 알 수 없는 오류가 발생했습니다. 다시 시도하십시오.", // The app encountered an unknown error. Please try again"
|
||||
},
|
||||
projectUploadError: {
|
||||
title: "파일 업로드 오류", // Error Uploading File,
|
||||
message: "파일을 업로드하는 중에 오류가 발생했습니다. 파일이 올바른 형식인지 확인한 후 다시 시도하십시오.",
|
||||
// There was an error uploading the file. Please verify the file is of the correct format and try again."
|
||||
},
|
||||
genericRenderError: {
|
||||
title: "응용 프로그램 로딩 오류", // Error Loading Application,
|
||||
message: "응용 프로그램을 렌더링하는 중에 오류가 발생했습니다. 다시 시도하십시오",
|
||||
// An error occured while rendering the application. Please try again"
|
||||
},
|
||||
projectInvalidSecurityToken: {
|
||||
title: "프로젝트 파일을 로드하는 중 오류가 발생했습니다", // Error loading project file,
|
||||
message: "프로젝트에서 참조한 보안 토큰이 유효하지 않습니다.응용 프로그램 설정 내에서 프로젝트의 보안 토큰이 올바르게 설정되었는지 확인하십시오",
|
||||
// The security token referenced by the project is invalid.
|
||||
// Verify that the security token for the project has been set correctly within your application settings"
|
||||
},
|
||||
projectInvalidJson: {
|
||||
title: "프로젝트 파일 파싱 오류", // Error parsing project file,
|
||||
message: "선택한 프로젝트 파일에 유효한 JSON이 포함되어 있지 않습니다. 파일을 다시 확인하십시오.",
|
||||
// The selected project files does not contain valid JSON Please check the file any try again."
|
||||
},
|
||||
projectDeleteError: {
|
||||
title: "프로젝트 삭제 오류", // Error deleting project,
|
||||
message: "프로젝트를 삭제하는 중에 오류가 발생했습니다. 프로젝트 파일에 보안 토큰이 존재하는지 확인한 후 다시 시도하십시오.",
|
||||
// An error occured while deleting the project.
|
||||
// Validate the project file an security token exist and try again"
|
||||
},
|
||||
securityTokenNotFound: {
|
||||
title: "프로젝트 파일을 로드하는 중 오류가 발생했습니다", // Error loading project file,
|
||||
message: "프로젝트가 참조하는 보안 토큰을 현재 애플리케이션 설정에서 찾을 수 없습니다. 보안 토큰이 있는지 확인하고 프로젝트를 다시로드하십시오.",
|
||||
// The security token referenced by the project cannot be found in your current application settings.
|
||||
// Verify the security token exists and try to reload the project."
|
||||
},
|
||||
canvasError: {
|
||||
title: "캔버스 불러 오기 오류", // Error loading canvas,
|
||||
message: "캔버스를 로드하는 중에 오류가 발생했습니다. 프로젝트 Asset을 확인한 후 다시 시도하십시오.",
|
||||
// There was an error loading the canvas, check the project's assets and try again."
|
||||
},
|
||||
importError: {
|
||||
title: "V1 프로젝트 가져 오기 오류", // Error importing V1 project,
|
||||
message: "V1 프로젝트를 가져 오는 중에 오류가 발생했습니다. 프로젝트 파일을 확인하고 다시 시도하십시오.",
|
||||
// There was an error importing the V1 project. Check the project file and try again"
|
||||
},
|
||||
pasteRegionTooBigError: {
|
||||
title: "지역 붙여 넣기 오류", // Error pasting region,
|
||||
message: "이 Asset에 비해 지역이 너무 큽니다. 다른 지역을 복사 해보십시오.",
|
||||
// Region too big for this asset. Try copying another region"
|
||||
},
|
||||
exportFormatNotFound: {
|
||||
title: "프로젝트 내보내기 오류", // Error exporting project,
|
||||
message: "프로젝트에 내보내기 형식이 없습니다. 내보내기 설정 페이지에서 내보내기 형식을 선택하십시오.",
|
||||
// Project is missing export format. Please select an export format in the export setting page."
|
||||
},
|
||||
activeLearningPredictionError: {
|
||||
title: "Active Learning 오류", // Active Learning Error,
|
||||
message: "현재 Asset의 지역을 예측하는 동안 오류가 발생했습니다. Active Learning 구성을 확인하고 다시 시도하십시오",
|
||||
// An error occurred while predicting regions in the current asset.
|
||||
// Please verify your active learning configuration and try again"
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,553 @@
|
||||
import { IAppStrings } from "../strings";
|
||||
|
||||
/**
|
||||
* App Strings for Simplified Chinese (zh-ch)
|
||||
*/
|
||||
export const chinese: IAppStrings = {
|
||||
appName: "视觉对象标记工具", // Visual Object Tagging Tool
|
||||
common: {
|
||||
displayName: "显示名称", // Display Name
|
||||
description: "描述", // Description
|
||||
submit: "提交", // Submit
|
||||
cancel: "取消", // Cancel
|
||||
save: "保存", // Save
|
||||
delete: "删除", // Delete
|
||||
provider: "提供者", // Provider
|
||||
homePage: "主页", // Home Page
|
||||
},
|
||||
titleBar: {
|
||||
help: "帮助", // Help
|
||||
minimize: "最小化", // Minimize
|
||||
maximize: "最大化", // Maximize
|
||||
restore: "复原", // Restore
|
||||
close: "关闭", // Close
|
||||
},
|
||||
homePage: {
|
||||
newProject: "新项目", // New Project
|
||||
openLocalProject: {
|
||||
title: "打开本地项目", // Open Local Project
|
||||
},
|
||||
openCloudProject: {
|
||||
title: "打开云端项目", // Open Cloud Project
|
||||
selectConnection: "选择一个连接", // Select a Connection
|
||||
},
|
||||
recentProjects: "最近的项目", // Recent Projects
|
||||
deleteProject: {
|
||||
title: "删除项目", // Delete Project
|
||||
confirmation: "确定要删除项目吗", // Are you sure you want to delete project
|
||||
},
|
||||
importProject: {
|
||||
title: "导入项目", // Import Project
|
||||
confirmation: "您确定要将项目${project.file.name}设置转换为v2格式吗?我们建议您首先备份项目文件。",
|
||||
// Are you sure you want to convert project ${project.file.name} project settings to v2 format?
|
||||
// We recommend you backup the project file first.
|
||||
},
|
||||
messages: {
|
||||
deleteSuccess: "已成功删除${project.name}", // Successfully deleted ${project.name}
|
||||
},
|
||||
},
|
||||
appSettings: {
|
||||
title: "应用程序设置", // Application Settings
|
||||
storageTitle: "储存设置", // Storage Settings
|
||||
uiHelp: "您的设置储存在哪里", // Where your settings are stored
|
||||
save: "保存设置", // Save Settings
|
||||
securityToken: {
|
||||
name: {
|
||||
title: "名称", // Name
|
||||
},
|
||||
key: {
|
||||
title: "密钥", // Key
|
||||
},
|
||||
},
|
||||
securityTokens: {
|
||||
title: "安全令牌", // Security Tokens
|
||||
description: "安全令牌用于加密项目配置中的敏感数据",
|
||||
// Security tokens are used to encrypt sensitive data within your project configuration
|
||||
},
|
||||
version: {
|
||||
description: "版本:", // Version:
|
||||
},
|
||||
commit: "提交SHA", // Commit SHA
|
||||
devTools: {
|
||||
description: "打开应用程序开发者工具以帮助诊断问题", // Open application developer tools to help diagnose issues
|
||||
button: "切换至开发者工具", // Toggle Developer Tools
|
||||
},
|
||||
reload: {
|
||||
description: "重新加载应用,放弃当前所有更改", // Reload the app discarding all current changes
|
||||
button: "刷新应用", // Refresh Application
|
||||
},
|
||||
messages: {
|
||||
saveSuccess: "成功保存应用程序设置", // Successfully saved application settings
|
||||
},
|
||||
},
|
||||
projectSettings: {
|
||||
title: "项目设定", // Project Settings
|
||||
securityToken: {
|
||||
title: "安全令牌", // Security Token
|
||||
description: "用于加密项目文件中的敏感数据", // Used to encrypt sensitive data within project files
|
||||
},
|
||||
useSecurityToken: {
|
||||
title: "使用安全令牌", // Use Security Token
|
||||
description: "启用后将在提供者配置内加密敏感数据",
|
||||
// When enabled will encrypt sensitive data within provider configuration
|
||||
},
|
||||
save: "保存项目", // Save Project
|
||||
sourceConnection: {
|
||||
title: "源连接", // Source Connection
|
||||
description: "从何处加载素材", // Where to load assets from
|
||||
},
|
||||
targetConnection: {
|
||||
title: "目标连接", // Target Connection
|
||||
description: "在哪里保存项目和导出数据", // Where to save the project and exported data
|
||||
},
|
||||
videoSettings: {
|
||||
title: "视频设定", // Video Settings
|
||||
description: "提取帧以进行标记的速率", // The rate at which frames are extracted for tagging.
|
||||
frameExtractionRate: "帧提取率(每视频每秒的帧数)", // Frame Extraction Rate (frames per a video second)
|
||||
},
|
||||
addConnection: "添加连接", // Add Connection
|
||||
messages: {
|
||||
saveSuccess: "成功保存${project.name}项目设置", // Successfully saved ${project.name} project settings
|
||||
},
|
||||
},
|
||||
projectMetrics: {
|
||||
title: "项目指标", // Project Metrics
|
||||
assetsSectionTitle: "素材", // Assets
|
||||
totalAssetCount: "素材总数", // Total Assets
|
||||
visitedAssets: "已访问素材(${count})", // Visited Assets (${count})
|
||||
taggedAssets: "已标记素材(${count})", // Tagged Assets (${count})
|
||||
nonTaggedAssets: "未标记素材(${count})", // Not Tagged Assets (${count})
|
||||
nonVisitedAssets: "未访问素材(${count})", // Not Visited Assets (${count})
|
||||
tagsSectionTitle: "标记与标签", // Tags & Labels
|
||||
totalRegionCount: "标记区域总数", // Total Tagged Regions
|
||||
totalTagCount: "标签总数", // Total Tags
|
||||
avgTagCountPerAsset: "每个素材的平均标签数", // Average tags per asset
|
||||
},
|
||||
tags: {
|
||||
title: "标签", // Tags
|
||||
placeholder: "添加标签", // Add new tag
|
||||
editor: "编辑标签", // Tags Editor
|
||||
modal: {
|
||||
name: "标签名称", // Tag Name
|
||||
color: "标签颜色", // Tag Color
|
||||
},
|
||||
colors: {
|
||||
white: "白色", // White
|
||||
gray: "灰色", // Gray
|
||||
red: "红色", // Red
|
||||
maroon: "栗色", // Maroon
|
||||
yellow: "黄色", // Yellow
|
||||
olive: "橄榄色", // Olive
|
||||
lime: "青色", // Lime
|
||||
green: "绿色", // Green
|
||||
aqua: "浅绿色", // Aqua
|
||||
teal: "蓝绿色", // Teal
|
||||
blue: "蓝色", // Blue
|
||||
navy: "海军蓝色", // Navy
|
||||
fuschia: "紫红色", // Fuschia
|
||||
purple: "紫色", // Purple
|
||||
},
|
||||
warnings: {
|
||||
existingName: "标签名称已存在。请选择另一个名字", // Tag name already exists. Choose another name
|
||||
emptyName: "标签名称不能为空", // Cannot have an empty tag name
|
||||
unknownTagName: "未知", // Unknown
|
||||
},
|
||||
toolbar: {
|
||||
add: "添加标签", // Add new tag
|
||||
search: "搜索标签", // Search tags
|
||||
edit: "编辑标签", // Edit tag
|
||||
lock: "锁定标签", // Lock tag
|
||||
moveUp: "向上移动标签", // Move tag up
|
||||
moveDown: "向下移动标签", // Move tag down
|
||||
delete: "删除标签", // Delete tag
|
||||
},
|
||||
},
|
||||
connections: {
|
||||
title: "连接数", // Connections
|
||||
details: "连接详细信息", // Connection Details
|
||||
settings: "连接设定", // Connection Settings
|
||||
instructions: "请选择一个连接进行编辑", // Please select a connection to edit
|
||||
save: "保存连接", // Save Connection
|
||||
messages: {
|
||||
saveSuccess: "已成功保存${connection.name}", // Successfully saved ${connection.name}
|
||||
deleteSuccess: "已成功删除${connection.name}", // Successfully deleted ${connection.name}
|
||||
},
|
||||
imageCorsWarning: "警告:在Web浏览器中使用VoTT时,由于CORS(跨源资源共享)限制,来自Bing Image Search的某些素材可能无法正确导出。",
|
||||
// Warning: When using VoTT in a Web browser, some assets from Bing Image Search may not export correctly
|
||||
// due to CORS (Cross Origin Resource Sharing) restrictions.
|
||||
blobCorsWarning: "警告:必须在Azure Blob存储帐户上启用CORS(跨域资源共享),才能将其用作源或目标连接。 {0}中提供了有关启用CORS的更多信息。",
|
||||
// Warning: CORS (Cross Domain Resource Sharing) must be enabled on the Azure Blob Storage account,
|
||||
// in order to use it as a source or target connection.
|
||||
// More information on enabling CORS can be found in the {0}
|
||||
azDocLinkText: "Azure文档", // Azure Documentation.
|
||||
providers: {
|
||||
azureBlob: {
|
||||
title: "Azure Blob存储", // Azure Blob Storage
|
||||
description: "",
|
||||
accountName: {
|
||||
title: "用户名", // Account Name
|
||||
description: "",
|
||||
},
|
||||
containerName: {
|
||||
title: "容器名称", // Container Name
|
||||
description: "",
|
||||
},
|
||||
sas: {
|
||||
title: "SAS", // SAS
|
||||
description: "用于验证Blob存储帐户的共享访问签名",
|
||||
// Shared access signature used to authenticate to the blob storage account
|
||||
},
|
||||
createContainer: {
|
||||
title: "创建容器", // Create Container
|
||||
description: "创建blob容器(如果尚不存在)",
|
||||
// Creates the blob container if it does not already exist
|
||||
},
|
||||
},
|
||||
bing: {
|
||||
title: "必应图片搜索", // Bing Image Search
|
||||
options: {
|
||||
title: "必应图像搜索选项",
|
||||
}, // Bing Image Search Options
|
||||
endpoint: {
|
||||
title: "端点",
|
||||
description: "必应搜索 Azure 资源中列出的终结点",
|
||||
},
|
||||
apiKey: {
|
||||
title: "API密钥",
|
||||
description: "必应搜索 Azure 资源中列出的 API 密钥",
|
||||
}, // API Key
|
||||
query: {
|
||||
title: "查询",
|
||||
description: "用于填充连接的搜索查询",
|
||||
}, // Query
|
||||
aspectRatio: {
|
||||
title: "长宽比", // Aspect Ratio
|
||||
description: "按指定的纵横比筛选结果",
|
||||
options: {
|
||||
all: "所有", // All
|
||||
square: "正方形", // Square
|
||||
wide: "宽", // Wide
|
||||
tall: "高", // Tall
|
||||
},
|
||||
},
|
||||
licenseType: {
|
||||
title: "许可证类型",
|
||||
description: "按指定的许可证类型筛选结果",
|
||||
options: {
|
||||
all: "全部(不过滤任何图像)",
|
||||
any: "任何许可证类型的图像",
|
||||
public: "公有领域",
|
||||
share: "免费分享和使用",
|
||||
shareCommercially: "免费共享和使用商业",
|
||||
modify: "免费修改、共享和使用",
|
||||
modifyCommercially: "可自由修改、共享和在商业上使用",
|
||||
},
|
||||
},
|
||||
size: {
|
||||
title: "大小",
|
||||
description: "按指定大小筛选结果",
|
||||
options: {
|
||||
all: "所有",
|
||||
small: "小(小于200x200)",
|
||||
medium: "中等(小于 500x500)",
|
||||
large: "大(大于 500x500)",
|
||||
wallpaper: "壁纸(超大图像)",
|
||||
},
|
||||
},
|
||||
},
|
||||
local: {
|
||||
title: "本地文件系统", // Local File System
|
||||
folderPath: "文件夹路径", // Folder Path
|
||||
selectFolder: "选择文件夹", // Select Folder
|
||||
chooseFolder: "选择文件夹", // Choose Folder
|
||||
},
|
||||
},
|
||||
},
|
||||
editorPage: {
|
||||
width: "宽度", // Width
|
||||
height: "高度", // Height
|
||||
tagged: "已标记", // Tagged
|
||||
visited: "已访问", // Visited
|
||||
toolbar: {
|
||||
select: "选择[V]", // Select (V)
|
||||
pan: "泛", // Pan
|
||||
drawRectangle: "绘制矩形", // Draw Rectangle
|
||||
drawPolygon: "绘制多边形", // Draw Polygon
|
||||
copyRectangle: "复制矩形", // Copy Rectangle
|
||||
copy: "复制区域", // Copy Regions
|
||||
cut: "剪切区域", // Cut Regions
|
||||
paste: "粘贴区域", // Paste Regions
|
||||
removeAllRegions: "删除所有区域", // Remove All Regions
|
||||
previousAsset: "以前的素材", // Previous Asset
|
||||
nextAsset: "下一项素材", // Next Asset
|
||||
saveProject: "保存项目", // Save Project
|
||||
exportProject: "导出项目", // Export Project
|
||||
activeLearning: "主动学习", // Active Learning
|
||||
},
|
||||
videoPlayer: {
|
||||
previousTaggedFrame: {
|
||||
tooltip: "上一个已标记的帧", // Previous Tagged Frame
|
||||
},
|
||||
nextTaggedFrame: {
|
||||
tooltip: "下一个已标记的帧", // Next Tagged Frame
|
||||
},
|
||||
previousExpectedFrame: {
|
||||
tooltip: "上一帧", // Previous Frame
|
||||
},
|
||||
nextExpectedFrame: {
|
||||
tooltip: "下一帧", // Next Frame
|
||||
},
|
||||
},
|
||||
help: {
|
||||
title: "切换帮助菜单", // Toggle Help Menu
|
||||
escape: "退出帮助菜单", // Escape Help Menu
|
||||
},
|
||||
assetError: "无法加载素材", // Unable to load asset
|
||||
tags: {
|
||||
hotKey: {
|
||||
apply: "使用快捷键应用标签", // Apply Tag with Hot Key
|
||||
lock: "使用快捷键锁定标签", // Lock Tag with Hot Key
|
||||
},
|
||||
rename: {
|
||||
title: "重新命名标签", // Rename Tag
|
||||
confirmation: "您确定要重新命名此标签吗?它将在所有素材中被重新命名",
|
||||
// Are you sure you want to rename this tag? It will be renamed throughout all assets
|
||||
},
|
||||
delete: {
|
||||
title: "删除标签", // Delete Tag
|
||||
confirmation: "您确定要删除此标签吗?它将在所有素材中被删除,并且仅使用此标签标记的任何区域也将被删除",
|
||||
// Are you sure you want to delete this tag? It will be deleted throughout all assets
|
||||
// and any regions where this is the only tag will also be deleted
|
||||
},
|
||||
},
|
||||
canvas: {
|
||||
removeAllRegions: {
|
||||
title: "删除所有区域", // Remove All Regions
|
||||
confirmation: "您确定要删除所有区域吗?", // Are you sure you want to remove all regions?
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
enforceTaggedRegions: {
|
||||
title: "检测到无效的区域", // Invalid region(s) detected
|
||||
description: "1个或多个区域尚未被标记。在继续下一个素材之前,请确保所有区域均已标记。",
|
||||
// 1 or more regions have not been tagged.
|
||||
// Ensure all regions are tagged before continuing to next asset.
|
||||
},
|
||||
},
|
||||
},
|
||||
export: {
|
||||
title: "导出", // Export
|
||||
settings: "导出设置", // Export Settings
|
||||
saveSettings: "保存导出设置", // Save Export Settings
|
||||
providers: {
|
||||
common: {
|
||||
properties: {
|
||||
assetState: {
|
||||
title: "素材状态", // Asset State
|
||||
description: "导出中包括哪些素材", // Which assets to include in the export
|
||||
options: {
|
||||
all: "所有素材", // All Assets
|
||||
visited: "仅已访问素材", // Only Visited Assets
|
||||
tagged: "仅已标记素材", // Only tagged Assets
|
||||
},
|
||||
},
|
||||
testTrainSplit: {
|
||||
title: "测试/训练用数据分离", // Test / Train Split
|
||||
description: "导出时分离测试/训练用数据", // The test train split to use for exported data
|
||||
},
|
||||
includeImages: {
|
||||
title: "包含图片", // Include Images
|
||||
description: "是否在目标连接中包括二值化图像素材",
|
||||
// Whether or not to include binary image assets in target connection
|
||||
},
|
||||
},
|
||||
},
|
||||
vottJson: {
|
||||
displayName: "VoTT JSON", // VoTT JSON
|
||||
},
|
||||
azureCV: {
|
||||
displayName: "Azure自定义视觉服务", // Azure Custom Vision Service
|
||||
regions: {
|
||||
australiaEast: "澳大利亚东部", // Australia East
|
||||
centralIndia: "印度中部", // Central India
|
||||
eastUs: "美国东部", // East US
|
||||
eastUs2: "美国东部2", // East US 2
|
||||
japanEast: "日本东部", // Japan East
|
||||
northCentralUs: "美国中北部", // North Central US
|
||||
northEurope: "欧州北部", // North Europe
|
||||
southCentralUs: "美国中南部", // South Central US
|
||||
southeastAsia: "东南亚", // Southeast Asia
|
||||
ukSouth: "英国南部", // UK South
|
||||
westUs2: "美国西部2", // West US 2
|
||||
westEurope: "欧州西部", // West Europe
|
||||
},
|
||||
properties: {
|
||||
apiKey: {
|
||||
title: "API密钥", // API Key
|
||||
},
|
||||
region: {
|
||||
title: "区域", // Region
|
||||
description: "部署服务的Azure区域", // The Azure region where your service is deployed
|
||||
},
|
||||
classificationType: {
|
||||
title: "分类类型", // Classification Type
|
||||
options: {
|
||||
multiLabel: "每个图像多个标签", // Multiple tags per image
|
||||
multiClass: "每个图像一个标签", // Single tag per image
|
||||
},
|
||||
},
|
||||
name: {
|
||||
title: "项目名", // Project Name
|
||||
},
|
||||
description: {
|
||||
title: "项目简介", // Project Description
|
||||
},
|
||||
domainId: {
|
||||
title: "域", // Domain
|
||||
},
|
||||
newOrExisting: {
|
||||
title: "新项目或现有项目", // New or Existing Project
|
||||
options: {
|
||||
new: "新项目", // New Project
|
||||
existing: "现有项目", // Existing Project
|
||||
},
|
||||
},
|
||||
projectId: {
|
||||
title: "项目名", // Project Name
|
||||
},
|
||||
projectType: {
|
||||
title: "项目类型", // Project Type
|
||||
options: {
|
||||
classification: "分类", // Classification
|
||||
objectDetection: "物体识别", // Object Detection
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tfRecords: {
|
||||
displayName: "Tensorflow记录", // Tensorflow Records
|
||||
},
|
||||
pascalVoc: {
|
||||
displayName: "Pascal VOC", // Pascal VOC
|
||||
exportUnassigned: {
|
||||
title: "导出未分配", // Export Unassigned
|
||||
description: "是否在导出的数据中包括未被分配的标签", // Whether or not to include unassigned tags in exported data
|
||||
},
|
||||
},
|
||||
cntk: {
|
||||
displayName: "Microsoft Cognitive Toolkit(CNTK)", // Microsoft Cognitive Toolkit (CNTK)
|
||||
},
|
||||
csv: {
|
||||
displayName: "逗号分隔值 (CSV)", // Comma Separated Values (CSV)
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
saveSuccess: "成功保存导出设置", // Successfully saved export settings
|
||||
},
|
||||
},
|
||||
activeLearning: {
|
||||
title: "主动学习", // Active Learning
|
||||
form: {
|
||||
properties: {
|
||||
modelPathType: {
|
||||
title: "模型提供者", // Model Provider
|
||||
description: "从何处加载训练模型", // Where to load the training model from
|
||||
options: {
|
||||
preTrained: "预先训练 Coco SSD", // Pre-trained Coco SSD
|
||||
customFilePath: "自定义(文件路径)", // Custom (File path)
|
||||
customWebUrl: "自定义 (URL)", // Custom (Url)
|
||||
},
|
||||
},
|
||||
autoDetect: {
|
||||
title: "自动识别", // Auto Detect
|
||||
description: "在素材之间导航时是否自动进行预测",
|
||||
// Whether or not to automatically make predictions as you navigate between assets
|
||||
},
|
||||
modelPath: {
|
||||
title: "模型路径", // Model path
|
||||
description: "从本地文件系统中选择模型", // Select a model from your local file system
|
||||
},
|
||||
modelUrl: {
|
||||
title: "模型 URL", // Model URL
|
||||
description: "从公共网址加载模型", // Load your model from a public web URL
|
||||
},
|
||||
predictTag: {
|
||||
title: "预测标签", // Predict Tag
|
||||
description: "是否在预测中自动包含标签", // Whether or not to automatically include tags in predictions
|
||||
},
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
loadingModel: "正在加载主动学习模型...", // Loading active learning model...
|
||||
errorLoadModel: "加载主动学习模型时出错", // Error loading active learning model
|
||||
saveSuccess: "成功保存了主动学习设置", // Successfully saved active learning settings
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
settings: "个人资料设置", // Profile Settings
|
||||
},
|
||||
errors: {
|
||||
unknown: {
|
||||
title: "未知错误", // Unknown Error
|
||||
message: "该应用程序遇到未知错误。请重试。", // The app encountered an unknown error. Please try again.
|
||||
},
|
||||
projectUploadError: {
|
||||
title: "上传文件时出错", // Error Uploading File
|
||||
message: "上传文件时出错。请确认文件格式正确,然后重试。",
|
||||
// There was an error uploading the file. Please verify the file is of the correct format and try again.
|
||||
},
|
||||
genericRenderError: {
|
||||
title: "加载应用程序时出错", // Error Loading Application
|
||||
message: "呈现应用程序时发生错误。请重试",
|
||||
// An error occured while rendering the application. Please try again
|
||||
},
|
||||
projectInvalidSecurityToken: {
|
||||
title: "加载项目文件时出错", // Error loading project file
|
||||
message: "项目引用的安全令牌无效。请验证是否在您的应用程序设置中正确设置了项目的安全令牌",
|
||||
// The security token referenced by the project is invalid.
|
||||
// Verify that the security token for the project has been set correctly within your application settings
|
||||
},
|
||||
projectInvalidJson: {
|
||||
title: "解析项目文件时出错", // Error parsing project file
|
||||
message: "所选的项目文件不包含有效的JSON。请检查该文件, 然后重试。",
|
||||
// The selected project files does not contain valid JSON. Please check the file any try again.
|
||||
},
|
||||
projectDeleteError: {
|
||||
title: "删除项目时出错", // Error deleting project
|
||||
message: "删除项目时发生错误。验证项目文件和安全令牌是否存在,然后重试",
|
||||
// An error occured while deleting the project.
|
||||
// Validate the project file and security token exist and try again
|
||||
},
|
||||
securityTokenNotFound: {
|
||||
title: "加载项目文件时出错", // Error loading project file
|
||||
message: "在当前的应用程序设置中找不到该项目引用的安全令牌。验证安全令牌是否存在,然后尝试重新加载项目。",
|
||||
// The security token referenced by the project cannot be found in your current application settings.
|
||||
// Verify the security token exists and try to reload the project.
|
||||
},
|
||||
canvasError: {
|
||||
title: "加载画布时出错", // Error loading canvas
|
||||
message: "加载画布时发生错误,请检查项目的素材,然后重试。",
|
||||
// There was an error loading the canvas, check the project's assets and try again.
|
||||
},
|
||||
importError: {
|
||||
title: "导入V1项目时出错", // Error importing V1 project
|
||||
message: "导入V1项目时出错。检查项目文件,然后重试",
|
||||
// There was an error importing the V1 project. Check the project file and try again
|
||||
},
|
||||
pasteRegionTooBigError: {
|
||||
title: "粘贴区域时出错", // Error pasting region
|
||||
message: "区域对于该素材过大。请尝试复制其他区域", // Region too big for this asset. Try copying another region
|
||||
},
|
||||
exportFormatNotFound: {
|
||||
title: "导出项目时出错", // Error exporting project
|
||||
message: "项目缺少导出格式。请在导出设置页面中选择一种导出格式。",
|
||||
// Project is missing export format. Please select an export format in the export setting page.
|
||||
},
|
||||
activeLearningPredictionError: {
|
||||
title: "主动学习错误", // Active Learning Error
|
||||
message: "预测当前素材中的区域时发生错误。请验证您的主动学习配置,然后重试",
|
||||
// An error occurred while predicting regions in the current asset.
|
||||
// Please verify your active learning configuration and try again
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,558 @@
|
||||
import { IAppStrings } from "../strings";
|
||||
|
||||
/**
|
||||
* App Strings for Traditional Chinese (zh-tw)
|
||||
*/
|
||||
export const chinesetw: IAppStrings = {
|
||||
appName: "VOTT視覺物件標記工具", // Visual Object Tagging Tool
|
||||
common: {
|
||||
displayName: "顯示名稱", // Display Name
|
||||
description: "說明", // Description
|
||||
submit: "送出", // Submit
|
||||
cancel: "取消", // Cancel
|
||||
save: "儲存", // Save
|
||||
delete: "刪除", // Delete
|
||||
provider: "提供者", // Provider
|
||||
homePage: "首頁", // Home Page
|
||||
},
|
||||
titleBar: {
|
||||
help: "說明", // Help
|
||||
minimize: "最小化", // Minimize
|
||||
maximize: "最大化", // Maximize
|
||||
restore: "還原", // Restore
|
||||
close: "關閉", // Close
|
||||
},
|
||||
homePage: {
|
||||
newProject: "新專案", // New Project
|
||||
openLocalProject: {
|
||||
title: "打開本機專案", // Open Local Project
|
||||
},
|
||||
openCloudProject: {
|
||||
title: "打開雲端專案", // Open Cloud Project
|
||||
selectConnection: "選擇連線", // Select a Connection
|
||||
},
|
||||
recentProjects: "最近的專案", // Recent Projects
|
||||
deleteProject: {
|
||||
title: "刪除專案", // Delete Project
|
||||
confirmation: "確定要刪除專案?", // Are you sure you want to delete project
|
||||
},
|
||||
importProject: {
|
||||
title: "匯入專案", // Import Project
|
||||
confirmation: "您確定要將專案${project.file.name}的設定轉換為v2格式嗎?我們建議您首先備份專案文件。",
|
||||
// Are you sure you want to convert project ${project.file.name} project settings to v2 format?
|
||||
// We recommend you backup the project file first.
|
||||
},
|
||||
messages: {
|
||||
deleteSuccess: "已成功刪除${project.name}專案", // Successfully deleted ${project.name}
|
||||
},
|
||||
},
|
||||
appSettings: {
|
||||
title: "應用程式設定", // Application Settings
|
||||
storageTitle: "儲存空間設定", // Storage Settings
|
||||
uiHelp: "您的設定存放在哪裡", // Where your settings are stored
|
||||
save: "保存設定", // Save Settings
|
||||
securityToken: {
|
||||
name: {
|
||||
title: "名稱", // Name
|
||||
},
|
||||
key: {
|
||||
title: "鍵", // Key
|
||||
},
|
||||
},
|
||||
securityTokens: {
|
||||
title: "安全性權杖", // Security Tokens
|
||||
description: "安全性權杖用於加密專案組態中的敏感資料",
|
||||
// Security tokens are used to encrypt sensitive data within your project configuration
|
||||
},
|
||||
version: {
|
||||
description: "版本:", // Version:
|
||||
},
|
||||
commit: "提交SHA", // Commit SHA
|
||||
devTools: {
|
||||
description: "打開應用程式開發工具以幫助診斷問題", // Open application developer tools to help diagnose issues
|
||||
button: "切換開發工具", // Toggle Developer Tools
|
||||
},
|
||||
reload: {
|
||||
description: "重新載入應用程式,放棄所有目前做的修改", // Reload the app discarding all current changes
|
||||
button: "重新整理應用程式", // Refresh Application
|
||||
},
|
||||
messages: {
|
||||
saveSuccess: "已成功保存應用程式設定", // Successfully saved application settings
|
||||
},
|
||||
},
|
||||
projectSettings: {
|
||||
title: "專案設定", // Project Settings
|
||||
securityToken: {
|
||||
title: "安全性權杖", // Security Token
|
||||
description: "用於加密專案檔案中的敏感資料", // Used to encrypt sensitive data within project files
|
||||
},
|
||||
useSecurityToken: {
|
||||
title: "使用安全令牌", // Use Security Token
|
||||
description: "啟用後將在提供者配置內加密敏感數據",
|
||||
// When enabled will encrypt sensitive data within provider configuration
|
||||
},
|
||||
save: "保存專案", // Save Project
|
||||
sourceConnection: {
|
||||
title: "來源連線", // Source Connection
|
||||
description: "從何處載入資料", // Where to load assets from
|
||||
},
|
||||
targetConnection: {
|
||||
title: "目標連線", // Target Connection
|
||||
description: "在哪裡保存專案和匯出的資料", // Where to save the project and exported data
|
||||
},
|
||||
videoSettings: {
|
||||
title: "影片設定", // Video Settings
|
||||
description: "設定影片標記的速率", // The rate at which frames are extracted for tagging.
|
||||
frameExtractionRate: "影像取樣率(影像每秒的畫面數)", // Frame Extraction Rate (frames per a video second)
|
||||
},
|
||||
addConnection: "新增連線", // Add Connection
|
||||
messages: {
|
||||
saveSuccess: "已成功保存${project.name}專案設定", // Successfully saved ${project.name} project settings
|
||||
},
|
||||
},
|
||||
projectMetrics: {
|
||||
title: "專案相關指標", // Project Metrics
|
||||
assetsSectionTitle: "圖像數據", // Assets
|
||||
// As for this VOTT tool, translate "Assets" to "Image data" in Traditional Chinese,
|
||||
// as "Asset" can be confusing if directly translated.
|
||||
totalAssetCount: "圖像數據總數", // Total Assets
|
||||
visitedAssets: "已檢視的圖像數據(${count})", // Visited Assets (${count})
|
||||
taggedAssets: "已標記的圖像數據(${count})", // Tagged Assets (${count})
|
||||
nonTaggedAssets: "未標記的圖像數據(${count})", // Not Tagged Assets (${count})
|
||||
nonVisitedAssets: "未檢視的圖像數據(${count})", // Not Visited Assets (${count})
|
||||
tagsSectionTitle: "標記和標籤",
|
||||
// Tags & Labels, so it can actually be same translation to Tags and Labels in Traditional Chinese,
|
||||
// to differentiate, having slightly different translation for both keywords.
|
||||
totalRegionCount: "已標記區域總數", // Total Tagged Regions
|
||||
totalTagCount: "標記總數", // Total Tags
|
||||
avgTagCountPerAsset: "每個圖像數據的平均標記數", // Average tags per asset
|
||||
},
|
||||
tags: {
|
||||
title: "標記", // Tags
|
||||
placeholder: "新增標記", // Add new tag
|
||||
editor: "標記編輯器", // Tags Editor
|
||||
modal: {
|
||||
name: "標記名稱", // Tag Name
|
||||
color: "標記顏色", // Tag Color
|
||||
},
|
||||
colors: {
|
||||
white: "白色", // White
|
||||
gray: "灰色", // Gray
|
||||
red: "紅色", // Red
|
||||
maroon: "栗色", // Maroon
|
||||
yellow: "黃色", // Yellow
|
||||
olive: "橄欖", // Olive
|
||||
lime: "酸橙", // Lime
|
||||
green: "綠色", // Green
|
||||
aqua: "水色", // Aqua
|
||||
teal: "藍綠色", // Teal
|
||||
blue: "藍色", // Blue
|
||||
navy: "海軍", // Navy
|
||||
fuschia: "紫紅色", // Fuschia
|
||||
purple: "紫色", // Purple
|
||||
},
|
||||
warnings: {
|
||||
existingName: "標記名稱已存在。請選擇其他名字", // Tag name already exists. Choose another name
|
||||
emptyName: "標記名稱不能為空白", // Cannot have an empty tag name
|
||||
unknownTagName: "未命名", // Unknown
|
||||
},
|
||||
toolbar: {
|
||||
add: "新增標記", // Add new tag
|
||||
search: "尋找標記", // Search tags
|
||||
edit: "編輯標記", // Edit tag
|
||||
lock: "鎖定標記", // Lock tag
|
||||
moveUp: "向上移動標記", // Move tag up
|
||||
moveDown: "向下移動標記", // Move tag down
|
||||
delete: "刪除標記", // Delete tag
|
||||
},
|
||||
},
|
||||
connections: {
|
||||
title: "連線", // Connections
|
||||
details: "連線細節", // Connection Details
|
||||
settings: "連線設定", // Connection Settings
|
||||
instructions: "請選擇一個連線進行編輯", // Please select a connection to edit
|
||||
save: "保存連線", // Save Connection
|
||||
messages: {
|
||||
saveSuccess: "已成功儲存${connection.name}", // Successfully saved ${connection.name}
|
||||
deleteSuccess: "已成功刪除${connection.name}", // Successfully deleted ${connection.name}
|
||||
},
|
||||
imageCorsWarning: "警告:在Web瀏覽器中使用VoTT時,由於CORS(跨源資源共享)限制,來自Bing Image Search的某些圖像數據可能無法正確匯出。",
|
||||
// Warning: When using VoTT in a Web browser, some assets from Bing Image Search may not export correctly
|
||||
// due to CORS (Cross Origin Resource Sharing) restrictions.
|
||||
blobCorsWarning: "警告:必須在Azure Blob儲存體帳戶上啟用CORS(跨域資源共享),才能將其用作來源或目標連接。 {0}中提供了有關啟用CORS的更多資訊。",
|
||||
// Warning: CORS (Cross Domain Resource Sharing) must be enabled on the Azure Blob Storage account,
|
||||
// in order to use it as a source or target connection.
|
||||
// More information on enabling CORS can be found in the {0}
|
||||
azDocLinkText: "Azure說明文件", // Azure Documentation.
|
||||
providers: {
|
||||
azureBlob: {
|
||||
title: "Azure Blob 儲存體", // Azure Blob Storage
|
||||
description: "",
|
||||
accountName: {
|
||||
title: "帳戶名", // Account Name
|
||||
description: "",
|
||||
},
|
||||
containerName: {
|
||||
title: "容器名稱", // Container Name
|
||||
description: "",
|
||||
},
|
||||
sas: {
|
||||
title: "SAS", // SAS
|
||||
description: "用於驗證Blob儲存體帳戶的共用存取簽章",
|
||||
// Shared access signature used to authenticate to the blob storage account
|
||||
},
|
||||
createContainer: {
|
||||
title: "新增容器", // Create Container
|
||||
description: "新增blob容器(如果還不存在時)", // Creates the blob container if it does not already exist
|
||||
},
|
||||
},
|
||||
bing: {
|
||||
title: "Bing 影像搜尋", // Bing Image Search
|
||||
options: {
|
||||
title: "Bing 影像搜尋選項",
|
||||
}, // Bing Image Search Options
|
||||
endpoint: {
|
||||
title: "Endpoint",
|
||||
description: "必應搜索 Azure 資源中列出的終結點",
|
||||
},
|
||||
apiKey: {
|
||||
title: "API密鑰",
|
||||
description: "必應搜索 Azure 資源中列出的 API 金鑰",
|
||||
}, // API Key
|
||||
query: {
|
||||
title: "查詢",
|
||||
description: "用於填充連接的搜索查詢",
|
||||
}, // Query
|
||||
aspectRatio: {
|
||||
title: "長寬比", // Aspect Ratio
|
||||
description: "按指定的縱橫比篩選結果",
|
||||
options: {
|
||||
all: "所有", // All
|
||||
square: "矩形", // Square
|
||||
wide: "寬", // Wide
|
||||
tall: "高", // Tall
|
||||
},
|
||||
},
|
||||
licenseType: {
|
||||
title: "許可證類型",
|
||||
description: "按指定的許可證類型篩選結果",
|
||||
options: {
|
||||
all: "全部(不過濾任何影像)",
|
||||
any: "任何許可證類型的圖像",
|
||||
public: "公有領域",
|
||||
share: "免費分享和使用",
|
||||
shareCommercially: "免費共用和使用商業",
|
||||
modify: "免費修改、共用和使用",
|
||||
modifyCommercially: "可自由修改、共用和在商業上使用",
|
||||
},
|
||||
},
|
||||
size: {
|
||||
title: "大小",
|
||||
description: "按指定大小篩選結果",
|
||||
options: {
|
||||
all: "所有",
|
||||
small: "小(小於200x200)",
|
||||
medium: "中等(小於 500x500)",
|
||||
large: "大(大於 500x500)",
|
||||
wallpaper: "桌布(超大影像)",
|
||||
},
|
||||
},
|
||||
},
|
||||
local: {
|
||||
title: "本機檔案系統", // Local File System
|
||||
folderPath: "資料夾路徑", // Folder Path
|
||||
selectFolder: "選擇資料夾", // Select Folder
|
||||
chooseFolder: "選取資料夾", // Choose Folder
|
||||
},
|
||||
},
|
||||
},
|
||||
editorPage: {
|
||||
width: "寬度", // Width
|
||||
height: "高度", // Height
|
||||
tagged: "已標記", // Tagged
|
||||
visited: "已檢視", // Visited
|
||||
toolbar: {
|
||||
select: "選擇 (V)", // Select (V)
|
||||
pan: "全景", // Pan
|
||||
drawRectangle: "繪製矩形", // Draw Rectangle
|
||||
drawPolygon: "繪製多邊形", // Draw Polygon
|
||||
copyRectangle: "複製矩形", // Copy Rectangle
|
||||
copy: "複製區域", // Copy Regions
|
||||
cut: "剪下區域", // Cut Regions
|
||||
paste: "貼上區域", // Paste Regions
|
||||
removeAllRegions: "刪除所有區域", // Remove All Regions
|
||||
previousAsset: "以前的圖像數據", // Previous Asset
|
||||
nextAsset: "下一個圖像數據", // Next Asset
|
||||
saveProject: "儲存專案", // Save Project
|
||||
exportProject: "匯出專案", // Export Project
|
||||
activeLearning: "主動學習", // Active Learning
|
||||
},
|
||||
videoPlayer: {
|
||||
previousTaggedFrame: {
|
||||
tooltip: "上一個標記的畫面", // Previous Tagged Frame
|
||||
},
|
||||
nextTaggedFrame: {
|
||||
tooltip: "下一個標記的畫面", // Next Tagged Frame
|
||||
},
|
||||
previousExpectedFrame: {
|
||||
tooltip: "上一個畫面", // Previous Frame
|
||||
},
|
||||
nextExpectedFrame: {
|
||||
tooltip: "下一個畫面", // Next Frame
|
||||
},
|
||||
},
|
||||
help: {
|
||||
title: "切換輔助說明選單", // Toggle Help Menu
|
||||
escape: "離開輔助說明選單", // Escape Help Menu
|
||||
},
|
||||
assetError: "無法載入圖像數據", // Unable to load asset
|
||||
tags: {
|
||||
hotKey: {
|
||||
apply: "使用快捷鍵來套用標記", // Apply Tag with Hot Key
|
||||
lock: "用快捷鍵來鎖定標記", // Lock Tag with Hot Key
|
||||
},
|
||||
rename: {
|
||||
title: "重新命名標記", // Rename Tag
|
||||
confirmation: "您確定要重新命名此標記嗎?它將在所有圖像數據中被重新命名",
|
||||
// Are you sure you want to rename this tag? It will be renamed throughout all assets
|
||||
},
|
||||
delete: {
|
||||
title: "刪除標記", // Delete Tag
|
||||
confirmation: "您確定要刪除此標記嗎?它將在所有圖像數據中被刪除,並且只有使用此標記的任何區域也將被刪除",
|
||||
// Are you sure you want to delete this tag? It will be deleted throughout all assets
|
||||
// and any regions where this is the only tag will also be deleted
|
||||
},
|
||||
},
|
||||
canvas: {
|
||||
removeAllRegions: {
|
||||
title: "刪除所有區域", // Remove All Regions
|
||||
confirmation: "您確定要刪除所有區域嗎?", // Are you sure you want to remove all regions?
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
enforceTaggedRegions: {
|
||||
title: "檢測到無效的區域", // Invalid region(s) detected
|
||||
description: "一個或多個區域尚未被標記。在繼續下一個圖像數據之前,請確保所有區域均已標記。",
|
||||
// 1 or more regions have not been tagged.
|
||||
// Ensure all regions are tagged before continuing to next asset.
|
||||
},
|
||||
},
|
||||
},
|
||||
export: {
|
||||
title: "匯出", // Export
|
||||
settings: "匯出設定", // Export Settings
|
||||
saveSettings: "儲存匯出設定", // Save Export Settings
|
||||
providers: {
|
||||
common: {
|
||||
properties: {
|
||||
assetState: {
|
||||
title: "圖像數據狀態", // Asset State
|
||||
description: "匯出項目中包括哪些圖像數據", // Which assets to include in the export
|
||||
options: {
|
||||
all: "所有圖像數據", // All Assets
|
||||
visited: "只有已檢視的圖像數據", // Only Visited Assets
|
||||
tagged: "只有已標記的圖像數據", // Only tagged Assets
|
||||
},
|
||||
},
|
||||
testTrainSplit: {
|
||||
title: "測試/訓練分割", // Test / Train Split
|
||||
description: "測試訓練分割以用於匯出數據", // The test train split to use for exported data
|
||||
},
|
||||
includeImages: {
|
||||
title: "包含圖像", // Include Images
|
||||
description: "是否在目標連接中包括二進位圖像數據",
|
||||
// Whether or not to include binary image assets in target connection
|
||||
},
|
||||
},
|
||||
},
|
||||
vottJson: {
|
||||
displayName: "VoTT JSON", // VoTT JSON
|
||||
},
|
||||
azureCV: {
|
||||
displayName: "Azure自訂視覺服務", // Azure Custom Vision Service
|
||||
regions: {
|
||||
// reference to https://azure.microsoft.com/zh-tw/global-infrastructure/geographies/
|
||||
// for official translation
|
||||
australiaEast: "澳大利亞東部", // Australia East
|
||||
centralIndia: "印度中部", // Central India
|
||||
eastUs: "美國東部", // East US
|
||||
eastUs2: "美國東部 2", // East US 2
|
||||
japanEast: "日本東部", // Japan East
|
||||
northCentralUs: "美國中北部", // North Central US
|
||||
northEurope: "北歐", // North Europe
|
||||
southCentralUs: "美國中南部", // South Central US
|
||||
southeastAsia: "東南亞", // Southeast Asia
|
||||
ukSouth: "英國南部", // UK South
|
||||
westUs2: "美國西部 2", // West US 2
|
||||
westEurope: "西歐", // West Europe
|
||||
},
|
||||
properties: {
|
||||
apiKey: {
|
||||
title: "API密鑰", // API Key
|
||||
},
|
||||
region: {
|
||||
title: "區域", // Region
|
||||
description: "部署服務的Azure區域", // The Azure region where your service is deployed
|
||||
},
|
||||
classificationType: {
|
||||
title: "分類類型", // Classification Type
|
||||
options: {
|
||||
multiLabel: "每個圖像多個標記", // Multiple tags per image
|
||||
multiClass: "每個圖像一個標記", // Single tag per image
|
||||
},
|
||||
},
|
||||
name: {
|
||||
title: "專案名", // Project Name
|
||||
},
|
||||
description: {
|
||||
title: "專案簡介", // Project Description
|
||||
},
|
||||
domainId: {
|
||||
title: "領域", // Domain
|
||||
},
|
||||
newOrExisting: {
|
||||
title: "新增專案或既有專案", // New or Existing Project
|
||||
options: {
|
||||
new: "新增專案", // New Project
|
||||
existing: "既有專案", // Existing Project
|
||||
},
|
||||
},
|
||||
projectId: {
|
||||
title: "專案名稱", // Project Name
|
||||
},
|
||||
projectType: {
|
||||
title: "專案類型", // Project Type
|
||||
options: {
|
||||
classification: "分類", // Classification
|
||||
objectDetection: "物件偵測", // Object Detection
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tfRecords: {
|
||||
displayName: "Tensorflow記錄", // Tensorflow Records
|
||||
},
|
||||
pascalVoc: {
|
||||
displayName: "Pascal VOC", // Pascal VOC
|
||||
exportUnassigned: {
|
||||
title: "匯出未指定的項目", // Export Unassigned
|
||||
description: "是否在已匯出的數據中包括未指定的標記", // Whether or not to include unassigned tags in exported data
|
||||
},
|
||||
},
|
||||
cntk: {
|
||||
displayName: "Microsoft Cognitive Toolkit(CNTK)", // Microsoft Cognitive Toolkit (CNTK)
|
||||
},
|
||||
csv: {
|
||||
displayName: "逗號分隔格式 (CSV)", // Comma Separated Values (CSV)
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
saveSuccess: "已成功儲存匯出設定", // Successfully saved export settings
|
||||
},
|
||||
},
|
||||
activeLearning: {
|
||||
title: "主動學習", // Active Learning
|
||||
form: {
|
||||
properties: {
|
||||
modelPathType: {
|
||||
title: "模型提供者", // Model Provider
|
||||
description: "從何處載入訓練模型", // Where to load the training model from
|
||||
options: {
|
||||
preTrained: "預先訓練Coco SSD", // Pre-trained Coco SSD
|
||||
customFilePath: "自訂(檔案路徑)", // Custom (File path)
|
||||
customWebUrl: "自訂 (URL)", // Custom (Url)
|
||||
},
|
||||
},
|
||||
autoDetect: {
|
||||
title: "自動偵測", // Auto Detect
|
||||
description: "在圖像數據之間瀏覽時是否自動進行預測",
|
||||
// Whether or not to automatically make predictions as you navigate between assets
|
||||
},
|
||||
modelPath: {
|
||||
title: "模型路徑", // Model path
|
||||
description: "從本機檔案系統中選擇模型", // Select a model from your local file system
|
||||
},
|
||||
modelUrl: {
|
||||
title: "模型網址", // Model URL
|
||||
description: "從公共網址載入模型", // Load your model from a public web URL
|
||||
},
|
||||
predictTag: {
|
||||
title: "預測標記", // Predict Tag
|
||||
description: "是否在預測中自動包含標記", // Whether or not to automatically include tags in predictions
|
||||
},
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
loadingModel: "正在載入主動學習模型...", // Loading active learning model...
|
||||
errorLoadModel: "載入主動學習模型時出現錯誤", // Error loading active learning model
|
||||
saveSuccess: "已成功儲存主動學習設定", // Successfully saved active learning settings
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
settings: "個人資料設定", // Profile Settings
|
||||
},
|
||||
errors: {
|
||||
unknown: {
|
||||
title: "未知錯誤", // Unknown Error
|
||||
message: "該應用程式遇到未知錯誤。請再試一遍。", // The app encountered an unknown error. Please try again.
|
||||
},
|
||||
projectUploadError: {
|
||||
title: "上傳檔案時出現錯誤", // Error Uploading File
|
||||
message: "上傳檔案時出現錯誤。請確認檔案格式正確,然後重試。",
|
||||
// There was an error uploading the file. Please verify the file is of the correct format and try again.
|
||||
},
|
||||
genericRenderError: {
|
||||
title: "載入應用程序時出現錯誤", // Error Loading Application
|
||||
message: "轉譯應用程序時發生錯誤。請再試一遍", // An error occured while rendering the application. Please try again
|
||||
},
|
||||
projectInvalidSecurityToken: {
|
||||
title: "載入專案文件時出現錯誤", // Error loading project file
|
||||
message: "專案使用的安全性認證無效。請驗證是否在您的應用程式設定中正確的設定了專案的安全性認證",
|
||||
// The security token referenced by the project is invalid. Verify that the security token
|
||||
// for the project has been set correctly within your application settings
|
||||
},
|
||||
projectInvalidJson: {
|
||||
title: "解析專案文件時出現錯誤", // Error parsing project file
|
||||
message: "所選擇的專案文件不包含有效的JSON格式。請確認該專案檔案並且重試。",
|
||||
// The selected project files does not contain valid JSON. Please check the file any try again.
|
||||
},
|
||||
projectDeleteError: {
|
||||
title: "刪除專案時出現錯誤", // Error deleting project
|
||||
message: "刪除專案時發生錯誤。請確認專案檔案和安全性認證是否存在,然後重試",
|
||||
// An error occured while deleting the project.
|
||||
// Validate the project file and security token exist and try again
|
||||
},
|
||||
securityTokenNotFound: {
|
||||
title: "載入專案檔案時出現錯誤", // Error loading project file
|
||||
message: "在當前的應用程式設定中找不到該專案所使用的安全性認證。請確認安全性認證是否存在,然後嘗試重新載入專案。",
|
||||
// The security token referenced by the project cannot be found in your current application settings.
|
||||
// Verify the security token exists and try to reload the project.
|
||||
},
|
||||
canvasError: {
|
||||
title: "載入畫面時出現錯誤", // Error loading canvas
|
||||
message: "載入畫面時發生錯誤,請檢查專案的圖像數據,然後重試。",
|
||||
// There was an error loading the canvas, check the project's assets and try again.
|
||||
},
|
||||
importError: {
|
||||
title: "匯入V1格式專案時出現錯誤", // Error importing V1 project
|
||||
message: "匯入V1格式專案時出現錯誤。請檢查專案檔案,然後重試",
|
||||
// There was an error importing the V1 project. Check the project file and try again
|
||||
},
|
||||
pasteRegionTooBigError: {
|
||||
title: "貼上區域時發生錯誤", // Error pasting region
|
||||
message: "此區域對於這一個圖像數據太大了。請嘗試複製其他的區域",
|
||||
// Region too big for this asset. Try copying another region
|
||||
},
|
||||
exportFormatNotFound: {
|
||||
title: "匯出專案時出現錯誤", // Error exporting project
|
||||
message: "專案設定中缺少匯出格式。請在匯出設定畫面中選擇一種匯出格式。",
|
||||
// Project is missing export format. Please select an export format in the export setting page.
|
||||
},
|
||||
activeLearningPredictionError: {
|
||||
title: "主動學習錯誤", // Active Learning Error
|
||||
message: "在預測當前圖像數據中的區域時發生錯誤。請確認您的主動學習相關設定,然後重試",
|
||||
// An error occurred while predicting regions in the current asset.
|
||||
// Please verify your active learning configuration and try again
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -277,6 +277,7 @@ export default class MockFactory {
|
||||
id: `project-${name}`,
|
||||
name: `Project ${name}`,
|
||||
version: appInfo.version,
|
||||
useSecurityToken: true,
|
||||
securityToken: `Security-Token-${name}`,
|
||||
assets: {},
|
||||
exportFormat: MockFactory.exportFormat(),
|
||||
@@ -397,6 +398,7 @@ export default class MockFactory {
|
||||
public static createLocalFileSystemOptions(): ILocalFileSystemProxyOptions {
|
||||
return {
|
||||
folderPath: "C:\\projects\\vott\\project",
|
||||
relativePath: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import { strings, addLocValues, IAppStrings, interpolate, interpolateJson } from "./strings";
|
||||
import { english } from "./localization/en-us";
|
||||
import { spanish } from "./localization/es-cl";
|
||||
import { japanese } from "./localization/ja";
|
||||
import { chinesetw } from "./localization/zh-tw";
|
||||
import { korean } from "./localization/ko-kr";
|
||||
import { chinese } from "./localization/zh-ch";
|
||||
|
||||
const languages = ["en", "es"];
|
||||
const languages = [
|
||||
"en",
|
||||
"es",
|
||||
"ja",
|
||||
"tw",
|
||||
"ko",
|
||||
"ch",
|
||||
];
|
||||
|
||||
describe("Localization tests", () => {
|
||||
|
||||
@@ -10,6 +21,10 @@ describe("Localization tests", () => {
|
||||
return {
|
||||
en: english,
|
||||
es: spanish,
|
||||
ja: japanese,
|
||||
tw: chinesetw,
|
||||
ko: korean,
|
||||
ch: chinese,
|
||||
}[language];
|
||||
}
|
||||
|
||||
|
||||
+61
-9
@@ -1,6 +1,10 @@
|
||||
import LocalizedStrings, { LocalizedStringsMethods } from "react-localization";
|
||||
import { english } from "./localization/en-us";
|
||||
import { spanish } from "./localization/es-cl";
|
||||
import { japanese } from "./localization/ja";
|
||||
import { chinesetw } from "./localization/zh-tw";
|
||||
import { korean } from "./localization/ko-kr";
|
||||
import { chinese } from "./localization/zh-ch";
|
||||
|
||||
/**
|
||||
* Interface for all required strings in application
|
||||
@@ -86,6 +90,10 @@ export interface IAppStrings {
|
||||
title: string;
|
||||
description: string;
|
||||
},
|
||||
useSecurityToken: {
|
||||
title: string;
|
||||
description: string;
|
||||
},
|
||||
save: string;
|
||||
sourceConnection: {
|
||||
title: string;
|
||||
@@ -192,17 +200,56 @@ export interface IAppStrings {
|
||||
}
|
||||
},
|
||||
bing: {
|
||||
title: string;
|
||||
options: string;
|
||||
apiKey: string;
|
||||
query: string;
|
||||
title: string,
|
||||
endpoint: {
|
||||
title: string,
|
||||
description?: string,
|
||||
},
|
||||
apiKey: {
|
||||
title: string,
|
||||
description?: string,
|
||||
},
|
||||
query: {
|
||||
title: string,
|
||||
description?: string,
|
||||
},
|
||||
options: {
|
||||
title: string,
|
||||
},
|
||||
aspectRatio: {
|
||||
title: string;
|
||||
all: string;
|
||||
square: string;
|
||||
wide: string;
|
||||
tall: string;
|
||||
}
|
||||
description?: string,
|
||||
options: {
|
||||
all: string;
|
||||
square: string;
|
||||
wide: string;
|
||||
tall: string;
|
||||
}
|
||||
},
|
||||
size: {
|
||||
title: string,
|
||||
description?: string,
|
||||
options: {
|
||||
all: string,
|
||||
small: string,
|
||||
medium: string,
|
||||
large: string,
|
||||
wallpaper: string,
|
||||
},
|
||||
},
|
||||
licenseType: {
|
||||
title: string,
|
||||
description?: string,
|
||||
options: {
|
||||
all: string,
|
||||
any: string,
|
||||
public: string,
|
||||
share: string,
|
||||
shareCommercially: string,
|
||||
modify: string,
|
||||
modifyCommercially: string,
|
||||
},
|
||||
},
|
||||
},
|
||||
local: {
|
||||
title: string;
|
||||
@@ -452,8 +499,13 @@ interface IErrorMetadata {
|
||||
interface IStrings extends LocalizedStringsMethods, IAppStrings { }
|
||||
|
||||
export const strings: IStrings = new LocalizedStrings({
|
||||
// TODO: Need to comment out other languages which will not be used
|
||||
en: english,
|
||||
es: spanish,
|
||||
ja: japanese,
|
||||
tw: chinesetw,
|
||||
ko: korean,
|
||||
ch: chinese,
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -59,7 +59,7 @@ export function encodeFileURI(path: string, additionalEncodings?: boolean): stri
|
||||
const encodings = {
|
||||
"\#": "%23",
|
||||
"\?": "%3F",
|
||||
};
|
||||
};
|
||||
const encodedURI = `file:${encodeURI(normalizeSlashes(path))}`;
|
||||
if (additionalEncodings) {
|
||||
return encodedURI.replace(matchString, (match) => encodings[match]);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import path, { relative, sep } from "path";
|
||||
import shortid from "shortid";
|
||||
import LocalFileSystem from "./localFileSystem";
|
||||
import mockFs from "mock-fs";
|
||||
import { AssetService } from "../../../services/assetService";
|
||||
import registerMixins from "../../../registerMixins";
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
dialog: {
|
||||
@@ -10,13 +13,57 @@ jest.mock("electron", () => ({
|
||||
}));
|
||||
import { dialog } from "electron";
|
||||
|
||||
registerMixins();
|
||||
|
||||
describe("LocalFileSystem Storage Provider", () => {
|
||||
let localFileSystem: LocalFileSystem = null;
|
||||
const sourcePath = path.join("path", "to", "my", "source");
|
||||
const sourceFilePaths = [
|
||||
path.join(sourcePath, "file1.jpg"),
|
||||
path.join(sourcePath, "file2.jpg"),
|
||||
path.join(sourcePath, "subDir1", "file3.jpg"),
|
||||
path.join(sourcePath, "subDir1", "file4.jpg"),
|
||||
path.join(sourcePath, "subDir2", "file5.jpg"),
|
||||
path.join(sourcePath, "subDir2", "file6.jpg"),
|
||||
path.join(sourcePath, "subDir2", "subSubDir2", "file7.jpg"),
|
||||
path.join(sourcePath, "subDir2", "subSubDir2", "file8.jpg"),
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
localFileSystem = new LocalFileSystem(null);
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
mockFs({
|
||||
path: {
|
||||
to: {
|
||||
my: {
|
||||
source: {
|
||||
"file1.jpg": "contents",
|
||||
"file2.jpg": "contents",
|
||||
"subDir1": {
|
||||
"file3.jpg": "contents",
|
||||
"file4.jpg": "contents",
|
||||
},
|
||||
"subDir2": {
|
||||
"file5.jpg": "contents",
|
||||
"file6.jpg": "contents",
|
||||
"subSubDir2": {
|
||||
"file7.jpg": "contents",
|
||||
"file8.jpg": "contents",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("writes, reads and deletes a file as text", async () => {
|
||||
const filePath = path.join(process.cwd(), "test-output", `${shortid.generate()}.json`);
|
||||
const contents = {
|
||||
@@ -89,4 +136,22 @@ describe("LocalFileSystem Storage Provider", () => {
|
||||
it("deleting file that doesn't exist resolves successfully", async () => {
|
||||
await expect(localFileSystem.deleteFile("/path/to/fake/file.txt")).resolves.not.toBeNull();
|
||||
});
|
||||
|
||||
it("getAssets gets all files recursively using path relative to the source", async () => {
|
||||
AssetService.createAssetFromFilePath = jest.fn(() => []);
|
||||
await localFileSystem.getAssets(sourcePath, true);
|
||||
const calls: any[] = (AssetService.createAssetFromFilePath as any).mock.calls;
|
||||
expect(calls).toHaveLength(8);
|
||||
expect(calls).toEqual(sourceFilePaths.map((path) => [
|
||||
path, undefined, path.replace(`${sourcePath}${sep}`, ""),
|
||||
]));
|
||||
});
|
||||
|
||||
it("getAssets gets all files recursively using absolute path", async () => {
|
||||
AssetService.createAssetFromFilePath = jest.fn(() => []);
|
||||
await localFileSystem.getAssets(sourcePath, false);
|
||||
const calls: any[] = (AssetService.createAssetFromFilePath as any).mock.calls;
|
||||
expect(calls).toHaveLength(8);
|
||||
expect(calls).toEqual(sourceFilePaths.map((path) => [path, undefined, path]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,9 +3,10 @@ import fs from "fs";
|
||||
import path from "path";
|
||||
import rimraf from "rimraf";
|
||||
import { IStorageProvider } from "../../../providers/storage/storageProviderFactory";
|
||||
import { IAsset, AssetType, StorageType } from "../../../models/applicationState";
|
||||
import { IAsset, AssetType, StorageType, IConnection } from "../../../models/applicationState";
|
||||
import { AssetService } from "../../../services/assetService";
|
||||
import { strings } from "../../../common/strings";
|
||||
import { ILocalFileSystemProxyOptions } from "../../../providers/storage/localFileSystemProxy";
|
||||
|
||||
export default class LocalFileSystem implements IStorageProvider {
|
||||
public storageType: StorageType.Local;
|
||||
@@ -92,8 +93,16 @@ export default class LocalFileSystem implements IStorageProvider {
|
||||
});
|
||||
}
|
||||
|
||||
public listFiles(folderPath: string): Promise<string[]> {
|
||||
return this.listItems(path.normalize(folderPath), (stats) => !stats.isDirectory());
|
||||
public async listFiles(folderPath: string): Promise<string[]> {
|
||||
const normalizedPath = path.normalize(folderPath);
|
||||
console.log(`Listing files from ${normalizedPath}`);
|
||||
const files = await this.listItems(normalizedPath, (stats) => !stats.isDirectory());
|
||||
const directories = await this.listItems(normalizedPath, (stats) => stats.isDirectory());
|
||||
await directories.forEachAsync(async (directory) => {
|
||||
const directoryFiles = await this.listFiles(directory);
|
||||
directoryFiles.forEach((file) => files.push(file));
|
||||
});
|
||||
return files;
|
||||
}
|
||||
|
||||
public listContainers(folderPath: string): Promise<string[]> {
|
||||
@@ -136,9 +145,12 @@ export default class LocalFileSystem implements IStorageProvider {
|
||||
});
|
||||
}
|
||||
|
||||
public async getAssets(folderPath?: string): Promise<IAsset[]> {
|
||||
return (await this.listFiles(path.normalize(folderPath)))
|
||||
.map((filePath) => AssetService.createAssetFromFilePath(filePath))
|
||||
public async getAssets(sourceConnectionFolderPath?: string, relativePath: boolean = false): Promise<IAsset[]> {
|
||||
const files = await this.listFiles(path.normalize(sourceConnectionFolderPath));
|
||||
return files.map((filePath) => AssetService.createAssetFromFilePath(
|
||||
filePath,
|
||||
undefined,
|
||||
relativePath ? path.relative(sourceConnectionFolderPath, filePath) : filePath))
|
||||
.filter((asset) => asset.type !== AssetType.Unknown);
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,8 @@ export interface IProject {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
securityToken: string;
|
||||
useSecurityToken: boolean;
|
||||
securityToken?: string;
|
||||
description?: string;
|
||||
tags: ITag[];
|
||||
sourceConnection: IConnection;
|
||||
|
||||
@@ -18,7 +18,7 @@ describe("Load default model from filesystem with TF io.IOHandler", () => {
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
|
||||
const handler = new ElectronProxyHandler("folder");
|
||||
const handler = new ElectronProxyHandler("folder", false);
|
||||
try {
|
||||
const model = await tf.loadGraphModel(handler);
|
||||
} catch (_) {
|
||||
|
||||
@@ -4,8 +4,8 @@ import { LocalFileSystemProxy, ILocalFileSystemProxyOptions } from "../../provid
|
||||
export class ElectronProxyHandler implements tfc.io.IOHandler {
|
||||
protected readonly provider: LocalFileSystemProxy;
|
||||
|
||||
constructor(folderPath: string) {
|
||||
const options: ILocalFileSystemProxyOptions = { folderPath };
|
||||
constructor(folderPath: string, relativePath: boolean) {
|
||||
const options: ILocalFileSystemProxyOptions = { folderPath, relativePath };
|
||||
this.provider = new LocalFileSystemProxy(options);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ export class ObjectDetection {
|
||||
const response = await axios.get(modelFolderPath + "/classes.json");
|
||||
this.jsonClasses = JSON.parse(JSON.stringify(response.data));
|
||||
} else {
|
||||
const handler = new ElectronProxyHandler(modelFolderPath);
|
||||
const handler = new ElectronProxyHandler(modelFolderPath, false);
|
||||
this.model = await tf.loadGraphModel(handler);
|
||||
this.jsonClasses = await handler.loadClasses();
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("Azure Custom Vision Export Provider", () => {
|
||||
|
||||
expect(customVisionMock).toBeCalledWith({
|
||||
apiKey: providerOptions.apiKey,
|
||||
baseUrl: `https://${providerOptions.region}.api.cognitive.microsoft.com/customvision/v2.2/Training`,
|
||||
baseUrl: `https://${providerOptions.region}.api.cognitive.microsoft.com/customvision/v3.3/Training`,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ export class AzureCustomVisionProvider extends ExportProvider<IAzureCustomVision
|
||||
|
||||
const cusomVisionServiceOptions: IAzureCustomVisionServiceOptions = {
|
||||
apiKey: options.apiKey,
|
||||
baseUrl: `https://${options.region}.api.cognitive.microsoft.com/customvision/v2.2/Training`,
|
||||
baseUrl: `https://${options.region}.api.cognitive.microsoft.com/customvision/v3.3/Training`,
|
||||
};
|
||||
this.customVisionService = new AzureCustomVisionService(cusomVisionServiceOptions);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"ui:widget": "externalPicker",
|
||||
"ui:options": {
|
||||
"method": "GET",
|
||||
"url": "https://${props.formContext.providerOptions.region}.api.cognitive.microsoft.com/customvision/v2.2/Training/projects",
|
||||
"url": "https://${props.formContext.providerOptions.region}.api.cognitive.microsoft.com/customvision/v3.3/Training/projects",
|
||||
"authHeaderName": "Training-key",
|
||||
"authHeaderValue": "${props.formContext.providerOptions.apiKey}",
|
||||
"keySelector": "${item.id}",
|
||||
@@ -20,7 +20,7 @@
|
||||
"ui:widget": "externalPicker",
|
||||
"ui:options": {
|
||||
"method": "GET",
|
||||
"url": "https://${props.formContext.providerOptions.region}.api.cognitive.microsoft.com/customvision/v2.2/Training/domains",
|
||||
"url": "https://${props.formContext.providerOptions.region}.api.cognitive.microsoft.com/customvision/v3.3/Training/domains",
|
||||
"authHeaderName": "Training-key",
|
||||
"authHeaderValue": "${props.formContext.providerOptions.apiKey}",
|
||||
"keySelector": "${item.id}",
|
||||
|
||||
@@ -13,7 +13,7 @@ describe("Azure Custom Vision Service", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
customVisionOptions = {
|
||||
baseUrl: "https://southcentralus.api.cognitive.microsoft.com/customvision/v2.2/Training",
|
||||
baseUrl: "https://southcentralus.api.cognitive.microsoft.com/customvision/v3.3/Training",
|
||||
apiKey: "ABC123",
|
||||
};
|
||||
customVisionService = new AzureCustomVisionService(customVisionOptions);
|
||||
|
||||
@@ -115,9 +115,37 @@ describe("CNTK Export Provider", () => {
|
||||
|
||||
const assetsToExport = await getAssetsSpy.mock.results[0].value;
|
||||
const testSplit = (100 - (defaultOptions.testTrainSplit || 80)) / 100;
|
||||
const testCount = Math.ceil(assetsToExport.length * testSplit);
|
||||
const testArray = assetsToExport.slice(0, testCount);
|
||||
const trainArray = assetsToExport.slice(testCount, assetsToExport.length);
|
||||
|
||||
const trainArray = [];
|
||||
const testArray = [];
|
||||
const tagsAssestList: {
|
||||
[index: string]: {
|
||||
assetSet: Set<string>,
|
||||
testArray: string[],
|
||||
trainArray: string[],
|
||||
},
|
||||
} = {};
|
||||
testProject.tags.forEach((tag) =>
|
||||
tagsAssestList[tag.name] = {
|
||||
assetSet: new Set(), testArray: [],
|
||||
trainArray: [],
|
||||
});
|
||||
assetsToExport.forEach((assetMetadata) => {
|
||||
assetMetadata.regions.forEach((region) => {
|
||||
region.tags.forEach((tagName) => {
|
||||
if (tagsAssestList[tagName]) {
|
||||
tagsAssestList[tagName].assetSet.add(assetMetadata.asset.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
for (const tagKey of Object.keys(tagsAssestList)) {
|
||||
const assetSet = tagsAssestList[tagKey].assetSet;
|
||||
const testCount = Math.ceil(assetSet.size * testSplit);
|
||||
testArray.push(...Array.from(assetSet).slice(0, testCount));
|
||||
trainArray.push(...Array.from(assetSet).slice(testCount, assetSet.size));
|
||||
}
|
||||
|
||||
const storageProviderMock = LocalFileSystemProxy as any;
|
||||
const writeBinaryCalls = storageProviderMock.mock.instances[0].writeBinary.mock.calls;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ExportProvider, IExportResults } from "./exportProvider";
|
||||
import { IAssetMetadata, IExportProviderOptions, IProject } from "../../models/applicationState";
|
||||
import HtmlFileReader from "../../common/htmlFileReader";
|
||||
import Guard from "../../common/guard";
|
||||
import { splitTestAsset } from "./testAssetsSplitHelper";
|
||||
|
||||
enum ExportSplit {
|
||||
Test,
|
||||
@@ -33,13 +34,17 @@ export class CntkExportProvider extends ExportProvider<ICntkExportProviderOption
|
||||
public async export(): Promise<IExportResults> {
|
||||
await this.createFolderStructure();
|
||||
const assetsToExport = await this.getAssetsForExport();
|
||||
const testAssets: string[] = [];
|
||||
|
||||
const testSplit = (100 - (this.options.testTrainSplit || 80)) / 100;
|
||||
const testCount = Math.ceil(assetsToExport.length * testSplit);
|
||||
const testArray = assetsToExport.slice(0, testCount);
|
||||
if (testSplit > 0 && testSplit <= 1) {
|
||||
const splittedAssets = splitTestAsset(assetsToExport, this.project.tags, testSplit);
|
||||
testAssets.push(...splittedAssets);
|
||||
}
|
||||
|
||||
const results = await assetsToExport.mapAsync(async (assetMetadata) => {
|
||||
try {
|
||||
const exportSplit = testArray.find((am) => am.asset.id === assetMetadata.asset.id)
|
||||
const exportSplit = testAssets.find((am) => am === assetMetadata.asset.id)
|
||||
? ExportSplit.Test
|
||||
: ExportSplit.Train;
|
||||
|
||||
|
||||
@@ -69,7 +69,9 @@ describe("PascalVOC Json Export Provider", () => {
|
||||
beforeEach(() => {
|
||||
const assetServiceMock = AssetService as jest.Mocked<typeof AssetService>;
|
||||
assetServiceMock.prototype.getAssetMetadata = jest.fn((asset) => {
|
||||
const mockTag = MockFactory.createTestTag();
|
||||
const mockTag1 = MockFactory.createTestTag("1");
|
||||
const mockTag2 = MockFactory.createTestTag("2");
|
||||
const mockTag = Number(asset.id.split("-")[1]) > 7 ? mockTag1 : mockTag2;
|
||||
const mockRegion1 = MockFactory.createTestRegion("region-1", [mockTag.name]);
|
||||
const mockRegion2 = MockFactory.createTestRegion("region-2", [mockTag.name]);
|
||||
|
||||
@@ -352,27 +354,70 @@ describe("PascalVOC Json Export Provider", () => {
|
||||
};
|
||||
|
||||
const testProject = { ...baseTestProject };
|
||||
const testAssets = MockFactory.createTestAssets(10, 0);
|
||||
const testAssets = MockFactory.createTestAssets(13, 0);
|
||||
testAssets.forEach((asset) => asset.state = AssetState.Tagged);
|
||||
testProject.assets = _.keyBy(testAssets, (asset) => asset.id);
|
||||
testProject.tags = [MockFactory.createTestTag("1")];
|
||||
testProject.tags = MockFactory.createTestTags(3);
|
||||
|
||||
const exportProvider = new PascalVOCExportProvider(testProject, options);
|
||||
const getAssetsSpy = jest.spyOn(exportProvider, "getAssetsForExport");
|
||||
|
||||
await exportProvider.export();
|
||||
|
||||
const storageProviderMock = LocalFileSystemProxy as any;
|
||||
const writeTextFileCalls = storageProviderMock.mock.instances[0].writeText.mock.calls as any[];
|
||||
|
||||
const valDataIndex = writeTextFileCalls
|
||||
const valDataIndex1 = writeTextFileCalls
|
||||
.findIndex((args) => args[0].endsWith("/ImageSets/Main/Tag 1_val.txt"));
|
||||
const trainDataIndex = writeTextFileCalls
|
||||
const trainDataIndex1 = writeTextFileCalls
|
||||
.findIndex((args) => args[0].endsWith("/ImageSets/Main/Tag 1_train.txt"));
|
||||
const valDataIndex2 = writeTextFileCalls
|
||||
.findIndex((args) => args[0].endsWith("/ImageSets/Main/Tag 2_val.txt"));
|
||||
const trainDataIndex2 = writeTextFileCalls
|
||||
.findIndex((args) => args[0].endsWith("/ImageSets/Main/Tag 2_train.txt"));
|
||||
|
||||
const expectedTrainCount = (testTrainSplit / 100) * testAssets.length;
|
||||
const expectedTestCount = ((100 - testTrainSplit) / 100) * testAssets.length;
|
||||
const assetsToExport = await getAssetsSpy.mock.results[0].value;
|
||||
const trainArray = [];
|
||||
const testArray = [];
|
||||
const tagsAssestList: {
|
||||
[index: string]: {
|
||||
assetSet: Set<string>,
|
||||
testArray: string[],
|
||||
trainArray: string[],
|
||||
},
|
||||
} = {};
|
||||
testProject.tags.forEach((tag) =>
|
||||
tagsAssestList[tag.name] = {
|
||||
assetSet: new Set(), testArray: [],
|
||||
trainArray: [],
|
||||
});
|
||||
assetsToExport.forEach((assetMetadata) => {
|
||||
assetMetadata.regions.forEach((region) => {
|
||||
region.tags.forEach((tagName) => {
|
||||
if (tagsAssestList[tagName]) {
|
||||
tagsAssestList[tagName].assetSet.add(assetMetadata.asset.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
expect(writeTextFileCalls[valDataIndex][1].split("\n")).toHaveLength(expectedTestCount);
|
||||
expect(writeTextFileCalls[trainDataIndex][1].split("\n")).toHaveLength(expectedTrainCount);
|
||||
for (const tagKey of Object.keys(tagsAssestList)) {
|
||||
const assetSet = tagsAssestList[tagKey].assetSet;
|
||||
const testCount = Math.ceil(((100 - testTrainSplit) / 100) * assetSet.size);
|
||||
tagsAssestList[tagKey].testArray = Array.from(assetSet).slice(0, testCount);
|
||||
tagsAssestList[tagKey].trainArray = Array.from(assetSet).slice(testCount, assetSet.size);
|
||||
testArray.push(...tagsAssestList[tagKey].testArray);
|
||||
trainArray.push(...tagsAssestList[tagKey].trainArray);
|
||||
}
|
||||
|
||||
expect(writeTextFileCalls[valDataIndex1][1].split(/\r?\n/).filter((line) =>
|
||||
line.endsWith(" 1"))).toHaveLength(tagsAssestList["Tag 1"].testArray.length);
|
||||
expect(writeTextFileCalls[trainDataIndex1][1].split(/\r?\n/).filter((line) =>
|
||||
line.endsWith(" 1"))).toHaveLength(tagsAssestList["Tag 1"].trainArray.length);
|
||||
expect(writeTextFileCalls[valDataIndex2][1].split(/\r?\n/).filter((line) =>
|
||||
line.endsWith(" 1"))).toHaveLength(tagsAssestList["Tag 2"].testArray.length);
|
||||
expect(writeTextFileCalls[trainDataIndex2][1].split(/\r?\n/).filter((line) =>
|
||||
line.endsWith(" 1"))).toHaveLength(tagsAssestList["Tag 2"].trainArray.length);
|
||||
}
|
||||
|
||||
it("Correctly generated files based on 50/50 test / train split", async () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import HtmlFileReader from "../../common/htmlFileReader";
|
||||
import { itemTemplate, annotationTemplate, objectTemplate } from "./pascalVOC/pascalVOCTemplates";
|
||||
import { interpolate } from "../../common/strings";
|
||||
import os from "os";
|
||||
import { splitTestAsset } from "./testAssetsSplitHelper";
|
||||
|
||||
interface IObjectInfo {
|
||||
name: string;
|
||||
@@ -253,40 +254,58 @@ export class PascalVOCExportProvider extends ExportProvider<IPascalVOCExportProv
|
||||
}
|
||||
});
|
||||
|
||||
// Save ImageSets
|
||||
await tags.forEachAsync(async (tag) => {
|
||||
const tagInstances = tagUsage.get(tag.name) || 0;
|
||||
if (!exportUnassignedTags && tagInstances === 0) {
|
||||
return;
|
||||
}
|
||||
if (testSplit > 0 && testSplit <= 1) {
|
||||
const tags = this.project.tags;
|
||||
const testAssets: string[] = splitTestAsset(allAssets, tags, testSplit);
|
||||
|
||||
const assetList = [];
|
||||
assetUsage.forEach((tags, assetName) => {
|
||||
if (tags.has(tag.name)) {
|
||||
assetList.push(`${assetName} 1`);
|
||||
} else {
|
||||
assetList.push(`${assetName} -1`);
|
||||
await tags.forEachAsync(async (tag) => {
|
||||
const tagInstances = tagUsage.get(tag.name) || 0;
|
||||
if (!exportUnassignedTags && tagInstances === 0) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (testSplit > 0 && testSplit <= 1) {
|
||||
// Split in Test and Train sets
|
||||
const totalAssets = assetUsage.size;
|
||||
const testCount = Math.ceil(totalAssets * testSplit);
|
||||
|
||||
const testArray = assetList.slice(0, testCount);
|
||||
const trainArray = assetList.slice(testCount, totalAssets);
|
||||
const testArray = [];
|
||||
const trainArray = [];
|
||||
assetUsage.forEach((tags, assetName) => {
|
||||
let assetString = "";
|
||||
if (tags.has(tag.name)) {
|
||||
assetString = `${assetName} 1`;
|
||||
} else {
|
||||
assetString = `${assetName} -1`;
|
||||
}
|
||||
if (testAssets.find((am) => am === assetName)) {
|
||||
testArray.push(assetString);
|
||||
} else {
|
||||
trainArray.push(assetString);
|
||||
}
|
||||
});
|
||||
|
||||
const testImageSetFileName = `${imageSetsMainFolderName}/${tag.name}_val.txt`;
|
||||
await this.storageProvider.writeText(testImageSetFileName, testArray.join(os.EOL));
|
||||
|
||||
const trainImageSetFileName = `${imageSetsMainFolderName}/${tag.name}_train.txt`;
|
||||
await this.storageProvider.writeText(trainImageSetFileName, trainArray.join(os.EOL));
|
||||
});
|
||||
} else {
|
||||
|
||||
// Save ImageSets
|
||||
await tags.forEachAsync(async (tag) => {
|
||||
const tagInstances = tagUsage.get(tag.name) || 0;
|
||||
if (!exportUnassignedTags && tagInstances === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assetList = [];
|
||||
assetUsage.forEach((tags, assetName) => {
|
||||
if (tags.has(tag.name)) {
|
||||
assetList.push(`${assetName} 1`);
|
||||
} else {
|
||||
assetList.push(`${assetName} -1`);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
const imageSetFileName = `${imageSetsMainFolderName}/${tag.name}.txt`;
|
||||
await this.storageProvider.writeText(imageSetFileName, assetList.join(os.EOL));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import _ from "lodash";
|
||||
import {
|
||||
IAssetMetadata, AssetState, IRegion,
|
||||
RegionType, IPoint, IExportProviderOptions,
|
||||
} from "../../models/applicationState";
|
||||
import MockFactory from "../../common/mockFactory";
|
||||
import { splitTestAsset } from "./testAssetsSplitHelper";
|
||||
import { appInfo } from "../../common/appInfo";
|
||||
|
||||
describe("splitTestAsset Helper tests", () => {
|
||||
|
||||
describe("Test Train Splits", () => {
|
||||
async function testTestTrainSplit(testTrainSplit: number): Promise<void> {
|
||||
const assetArray = MockFactory.createTestAssets(13, 0);
|
||||
const tags = MockFactory.createTestTags(2);
|
||||
assetArray.forEach((asset) => asset.state = AssetState.Tagged);
|
||||
|
||||
const testSplit = (100 - testTrainSplit) / 100;
|
||||
const testCount = Math.ceil(testSplit * assetArray.length);
|
||||
|
||||
const assetMetadatas = assetArray.map((asset, i) =>
|
||||
MockFactory.createTestAssetMetadata(asset,
|
||||
i < (assetArray.length - testCount) ?
|
||||
[MockFactory.createTestRegion("Region" + i, [tags[0].name])] :
|
||||
[MockFactory.createTestRegion("Region" + i, [tags[1].name])]));
|
||||
const testAssetsNames = splitTestAsset(assetMetadatas, tags, testSplit);
|
||||
|
||||
const trainAssetsArray = assetMetadatas.filter((assetMetadata) =>
|
||||
testAssetsNames.indexOf(assetMetadata.asset.name) < 0);
|
||||
const testAssetsArray = assetMetadatas.filter((assetMetadata) =>
|
||||
testAssetsNames.indexOf(assetMetadata.asset.name) >= 0);
|
||||
|
||||
const expectedTestCount = Math.ceil(testSplit * testCount) +
|
||||
Math.ceil(testSplit * (assetArray.length - testCount));
|
||||
expect(testAssetsNames).toHaveLength(expectedTestCount);
|
||||
expect(trainAssetsArray.length + testAssetsArray.length).toEqual(assetMetadatas.length);
|
||||
expect(testAssetsArray).toHaveLength(expectedTestCount);
|
||||
|
||||
expect(testAssetsArray.filter((assetMetadata) => assetMetadata.regions[0].tags[0] === tags[0].name).length)
|
||||
.toBeGreaterThan(0);
|
||||
expect(testAssetsArray.filter((assetMetadata) => assetMetadata.regions[0].tags[0] === tags[1].name).length)
|
||||
.toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
it("Correctly generated files based on 50/50 test / train split", async () => {
|
||||
await testTestTrainSplit(50);
|
||||
});
|
||||
|
||||
it("Correctly generated files based on 60/40 test / train split", async () => {
|
||||
await testTestTrainSplit(60);
|
||||
});
|
||||
|
||||
it("Correctly generated files based on 80/20 test / train split", async () => {
|
||||
await testTestTrainSplit(80);
|
||||
});
|
||||
|
||||
it("Correctly generated files based on 90/10 test / train split", async () => {
|
||||
await testTestTrainSplit(90);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { IAssetMetadata, ITag } from "../../models/applicationState";
|
||||
|
||||
/**
|
||||
* A helper function to split train and test assets
|
||||
* @param template String containing variables
|
||||
* @param params Params containing substitution values
|
||||
*/
|
||||
export function splitTestAsset(allAssets: IAssetMetadata[], tags: ITag[], testSplitRatio: number): string[] {
|
||||
if (testSplitRatio <= 0 || testSplitRatio > 1) { return []; }
|
||||
|
||||
const testAssets: string[] = [];
|
||||
const tagsAssetDict: { [index: string]: { assetList: Set<string> } } = {};
|
||||
tags.forEach((tag) => tagsAssetDict[tag.name] = { assetList: new Set() });
|
||||
allAssets.forEach((assetMetadata) => {
|
||||
assetMetadata.regions.forEach((region) => {
|
||||
region.tags.forEach((tagName) => {
|
||||
if (tagsAssetDict[tagName]) {
|
||||
tagsAssetDict[tagName].assetList.add(assetMetadata.asset.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
for (const tagKey of Object.keys(tagsAssetDict)) {
|
||||
const assetList = tagsAssetDict[tagKey].assetList;
|
||||
const testCount = Math.ceil(assetList.size * testSplitRatio);
|
||||
testAssets.push(...Array.from(assetList).slice(0, testCount));
|
||||
}
|
||||
return testAssets;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ class TestAssetProvider implements IAssetProvider {
|
||||
public initialize(): Promise<void> {
|
||||
throw new Error("Method not implemented");
|
||||
}
|
||||
public getAssets(containerName?: string): Promise<IAsset[]> {
|
||||
public getAssets(): Promise<IAsset[]> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import getHostProcess, { HostProcessType } from "../../common/hostProcess";
|
||||
export interface IAssetProvider {
|
||||
initialize?(): Promise<void>;
|
||||
getAssets(containerName?: string): Promise<IAsset[]>;
|
||||
addDefaultPropsToNewConnection?(connection: IConnection): IConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -191,8 +191,8 @@ export class AzureBlobStorage implements IStorageProvider {
|
||||
* @param containerName - Container from which to retrieve assets. Defaults to
|
||||
* container specified in Azure Cloud Storage options
|
||||
*/
|
||||
public async getAssets(containerName?: string): Promise<IAsset[]> {
|
||||
containerName = (containerName) ? containerName : this.options.containerName;
|
||||
public async getAssets(): Promise<IAsset[]> {
|
||||
const { containerName } = this.options;
|
||||
const files = await this.listFiles(containerName);
|
||||
const result: IAsset[] = [];
|
||||
for (const file of files) {
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
{
|
||||
"type": "object",
|
||||
"title": "${strings.connections.providers.bing.options}",
|
||||
"title": "${strings.connections.providers.bing.options.title}",
|
||||
"required": ["apiKey","query"],
|
||||
"properties": {
|
||||
"endpoint": {
|
||||
"type": "string",
|
||||
"title": "Endpoint",
|
||||
"description": "The endpoint from your Bing Search Azure resource",
|
||||
"default": "https://api.bing.microsoft.com/",
|
||||
"pattern": "^https?\\\\://[a-zA-Z0-9\\\\-\\\\.]+\\\\.[a-zA-Z]{2,3}(/\\\\S*)?$"
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "string",
|
||||
"title": "${strings.connections.providers.bing.apiKey}"
|
||||
"title": "${strings.connections.providers.bing.apiKey.title}",
|
||||
"description": "${strings.connections.providers.bing.apiKey.description}"
|
||||
},
|
||||
"query": {
|
||||
"type": "string",
|
||||
"title": "${strings.connections.providers.bing.query}"
|
||||
"title": "${strings.connections.providers.bing.query.title}",
|
||||
"description": "${strings.connections.providers.bing.query.description}"
|
||||
},
|
||||
"aspectRatio": {
|
||||
"type": "string",
|
||||
"title": "${strings.connections.providers.bing.aspectRatio.title}",
|
||||
"description": "${strings.connections.providers.bing.aspectRatio.description}",
|
||||
"enum": [
|
||||
"all",
|
||||
"square",
|
||||
@@ -22,11 +32,55 @@
|
||||
],
|
||||
"default": "all",
|
||||
"enumNames": [
|
||||
"${strings.connections.providers.bing.aspectRatio.all}",
|
||||
"${strings.connections.providers.bing.aspectRatio.square}",
|
||||
"${strings.connections.providers.bing.aspectRatio.wide}",
|
||||
"${strings.connections.providers.bing.aspectRatio.tall}"
|
||||
"${strings.connections.providers.bing.aspectRatio.options.all}",
|
||||
"${strings.connections.providers.bing.aspectRatio.options.square}",
|
||||
"${strings.connections.providers.bing.aspectRatio.options.wide}",
|
||||
"${strings.connections.providers.bing.aspectRatio.options.tall}"
|
||||
]
|
||||
},
|
||||
"size": {
|
||||
"type": "string",
|
||||
"title": "${strings.connections.providers.bing.size.title}",
|
||||
"description": "${strings.connections.providers.bing.size.description}",
|
||||
"enum": [
|
||||
"All",
|
||||
"Small",
|
||||
"Medium",
|
||||
"Large",
|
||||
"Wallpaper"
|
||||
],
|
||||
"default": "All",
|
||||
"enumNames": [
|
||||
"${strings.connections.providers.bing.size.options.all}",
|
||||
"${strings.connections.providers.bing.size.options.small}",
|
||||
"${strings.connections.providers.bing.size.options.medium}",
|
||||
"${strings.connections.providers.bing.size.options.large}",
|
||||
"${strings.connections.providers.bing.size.options.wallpaper}"
|
||||
]
|
||||
},
|
||||
"licenseType": {
|
||||
"type": "string",
|
||||
"title": "${strings.connections.providers.bing.licenseType.title}",
|
||||
"description": "${strings.connections.providers.bing.licenseType.description}",
|
||||
"enum": [
|
||||
"All",
|
||||
"Any",
|
||||
"Public",
|
||||
"Share",
|
||||
"ShareCommercially",
|
||||
"Modify",
|
||||
"ModifyCommercially"
|
||||
],
|
||||
"default": "All",
|
||||
"enumNames": [
|
||||
"${strings.connections.providers.bing.licenseType.options.all}",
|
||||
"${strings.connections.providers.bing.licenseType.options.any}",
|
||||
"${strings.connections.providers.bing.licenseType.options.public}",
|
||||
"${strings.connections.providers.bing.licenseType.options.share}",
|
||||
"${strings.connections.providers.bing.licenseType.options.shareCommercially}",
|
||||
"${strings.connections.providers.bing.licenseType.options.modify}",
|
||||
"${strings.connections.providers.bing.licenseType.options.modifyCommercially}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import axios from "axios";
|
||||
import { BingImageSearch, IBingImageSearchOptions, BingImageSearchAspectRatio } from "./bingImageSearch";
|
||||
import {
|
||||
BingImageSearch,
|
||||
IBingImageSearchOptions,
|
||||
BingImageSearchAspectRatio,
|
||||
BingImageSearchSize,
|
||||
BingImageSearchLicenseType,
|
||||
} from "./bingImageSearch";
|
||||
import { IAsset, AssetType, AssetState } from "../../models/applicationState";
|
||||
import MD5 from "md5.js";
|
||||
|
||||
describe("Bing Image Search", () => {
|
||||
const options: IBingImageSearchOptions = {
|
||||
const defaultOptions: IBingImageSearchOptions = {
|
||||
apiKey: "ABC123",
|
||||
query: "Waterfalls",
|
||||
aspectRatio: BingImageSearchAspectRatio.All,
|
||||
size: BingImageSearchSize.All,
|
||||
licenseType: BingImageSearchLicenseType.All,
|
||||
};
|
||||
const provider = new BingImageSearch(options);
|
||||
|
||||
const assets = [
|
||||
{ contentUrl: "http://images.com/image1.jpg" },
|
||||
@@ -26,9 +33,36 @@ describe("Bing Image Search", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("calls the Bing image search API", async () => {
|
||||
it("calls the Bing image search API with default API url", async () => {
|
||||
const provider = new BingImageSearch(defaultOptions);
|
||||
// tslint:disable-next-line:max-line-length
|
||||
const expectedUrl = `https://api.cognitive.microsoft.com/bing/v7.0/images/search?q=${options.query}&aspect=${options.aspectRatio}`;
|
||||
const expectedUrl = `${BingImageSearch.DefaultApiUrl}/v7.0/images/search?q=${defaultOptions.query}&aspect=${defaultOptions.aspectRatio}&license=${defaultOptions.licenseType}&size=${defaultOptions.size}`;
|
||||
const expectedHeaders = {
|
||||
headers: {
|
||||
"Ocp-Apim-Subscription-Key": defaultOptions.apiKey,
|
||||
"Accept": "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
await provider.getAssets();
|
||||
expect(axios.get).toBeCalledWith(expectedUrl, expectedHeaders);
|
||||
});
|
||||
|
||||
it("calls the Bing image search API with custom configuration", async () => {
|
||||
const options: IBingImageSearchOptions = {
|
||||
...defaultOptions,
|
||||
apiKey: "XYZ123",
|
||||
query: "Custom",
|
||||
endpoint: "https://api.bing.microsoft.com",
|
||||
aspectRatio: BingImageSearchAspectRatio.Square,
|
||||
licenseType: BingImageSearchLicenseType.Public,
|
||||
size: BingImageSearchSize.Large,
|
||||
};
|
||||
|
||||
const provider = new BingImageSearch(options);
|
||||
|
||||
// tslint:disable-next-line:max-line-length
|
||||
const expectedUrl = `${options.endpoint}/v7.0/images/search?q=${options.query}&aspect=${options.aspectRatio}&license=${options.licenseType}&size=${options.size}`;
|
||||
const expectedHeaders = {
|
||||
headers: {
|
||||
"Ocp-Apim-Subscription-Key": options.apiKey,
|
||||
@@ -51,6 +85,7 @@ describe("Bing Image Search", () => {
|
||||
size: null,
|
||||
};
|
||||
|
||||
const provider = new BingImageSearch(defaultOptions);
|
||||
const assets = await provider.getAssets();
|
||||
expect(assets.length).toEqual(assets.length);
|
||||
expect(assets[0]).toEqual(expectedAsset);
|
||||
|
||||
@@ -7,14 +7,18 @@ import { createQueryString } from "../../common/utils";
|
||||
|
||||
/**
|
||||
* Options for Bing Image Search
|
||||
* @member endpoint - The endpoint to use for the Bing Search API
|
||||
* @member apiKey - Bing Search API Key (Cognitive Services)
|
||||
* @member query - Query for Bing Search
|
||||
* @member aspectRatio - Aspect Ratio for desired images
|
||||
*/
|
||||
export interface IBingImageSearchOptions {
|
||||
endpoint?: string;
|
||||
apiKey: string;
|
||||
query: string;
|
||||
aspectRatio: BingImageSearchAspectRatio;
|
||||
size?: BingImageSearchSize;
|
||||
licenseType?: BingImageSearchLicenseType;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,11 +31,29 @@ export enum BingImageSearchAspectRatio {
|
||||
All = "All",
|
||||
}
|
||||
|
||||
export enum BingImageSearchLicenseType {
|
||||
All = "All",
|
||||
Any = "Any",
|
||||
Public = "Public",
|
||||
Share = "Share",
|
||||
ShareCommercially = "ShareCommercially",
|
||||
Modify = "Modify",
|
||||
ModifyCommercially = "ModifyCommercially",
|
||||
}
|
||||
|
||||
export enum BingImageSearchSize {
|
||||
All = "All",
|
||||
Small = "Small",
|
||||
Medium = "Medium",
|
||||
Large = "Large",
|
||||
Wallpaper = "Wallpaper",
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset Provider for Bing Image Search
|
||||
*/
|
||||
export class BingImageSearch implements IAssetProvider {
|
||||
private static SEARCH_URL = "https://api.cognitive.microsoft.com/bing/v7.0/images/search";
|
||||
public static DefaultApiUrl = "https://api.cognitive.microsoft.com/bing";
|
||||
|
||||
constructor(private options: IBingImageSearchOptions) {
|
||||
Guard.null(options);
|
||||
@@ -44,11 +66,14 @@ export class BingImageSearch implements IAssetProvider {
|
||||
const query = {
|
||||
q: this.options.query,
|
||||
aspect: this.options.aspectRatio,
|
||||
license: this.options.licenseType || BingImageSearchLicenseType.All,
|
||||
size: this.options.size || BingImageSearchSize.All,
|
||||
};
|
||||
|
||||
const url = `${BingImageSearch.SEARCH_URL}?${createQueryString(query)}`;
|
||||
const baseUrl = this.options.endpoint || BingImageSearch.DefaultApiUrl;
|
||||
const apiUrl = `${baseUrl}/v7.0/images/search?${createQueryString(query)}`;
|
||||
|
||||
const response = await axios.get(url, {
|
||||
const response = await axios.get(apiUrl, {
|
||||
headers: {
|
||||
"Ocp-Apim-Subscription-Key": this.options.apiKey,
|
||||
"Accept": "application/json",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { IpcRendererProxy } from "../../common/ipcRendererProxy";
|
||||
import { LocalFileSystemProxy, ILocalFileSystemProxyOptions } from "./localFileSystemProxy";
|
||||
import { StorageProviderFactory } from "./storageProviderFactory";
|
||||
import registerProviders from "../../registerProviders";
|
||||
import MockFactory from "../../common/mockFactory";
|
||||
|
||||
describe("LocalFileSystem Proxy Storage Provider", () => {
|
||||
it("Provider is registered with the StorageProviderFactory", () => {
|
||||
@@ -19,6 +20,7 @@ describe("LocalFileSystem Proxy Storage Provider", () => {
|
||||
let provider: LocalFileSystemProxy = null;
|
||||
const options: ILocalFileSystemProxyOptions = {
|
||||
folderPath: "/test",
|
||||
relativePath: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -122,5 +124,40 @@ describe("LocalFileSystem Proxy Storage Provider", () => {
|
||||
expect(IpcRendererProxy.send).toBeCalledWith("LocalFileSystem:listContainers", [expectedContainerPath]);
|
||||
expect(actualFolders).toEqual(expectedFolders);
|
||||
});
|
||||
|
||||
it("sends relative path argument according to options", async () => {
|
||||
const sendFunction = jest.fn();
|
||||
IpcRendererProxy.send = sendFunction;
|
||||
await provider.getAssets();
|
||||
const { folderPath, relativePath } = options;
|
||||
expect(IpcRendererProxy.send).toBeCalledWith("LocalFileSystem:getAssets", [folderPath, relativePath]);
|
||||
sendFunction.mockReset();
|
||||
|
||||
const newFolderPath = "myFolder";
|
||||
const newRelativePath = true;
|
||||
|
||||
const relativeProvider = new LocalFileSystemProxy({
|
||||
folderPath: newFolderPath,
|
||||
relativePath: newRelativePath,
|
||||
});
|
||||
await relativeProvider.getAssets();
|
||||
expect(IpcRendererProxy.send).toBeCalledWith("LocalFileSystem:getAssets", [newFolderPath, newRelativePath]);
|
||||
});
|
||||
|
||||
it("adds default props to a new connection", () => {
|
||||
const connection = MockFactory.createTestConnection();
|
||||
delete connection.providerOptions["relativePath"];
|
||||
expect(connection).not.toHaveProperty("providerOptions.relativePath");
|
||||
delete connection.id;
|
||||
expect(provider.addDefaultPropsToNewConnection(connection))
|
||||
.toHaveProperty("providerOptions.relativePath", true);
|
||||
});
|
||||
|
||||
it("does not add default props to existing connection", () => {
|
||||
const connection = MockFactory.createTestConnection();
|
||||
delete connection.providerOptions["relativePath"];
|
||||
expect(provider.addDefaultPropsToNewConnection(connection))
|
||||
.not.toHaveProperty("providerOptions.relativePath");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IpcRendererProxy } from "../../common/ipcRendererProxy";
|
||||
import { IStorageProvider } from "./storageProviderFactory";
|
||||
import { IAssetProvider } from "./assetProviderFactory";
|
||||
import { IAsset, StorageType } from "../../models/applicationState";
|
||||
import { IAsset, IConnection, StorageType } from "../../models/applicationState";
|
||||
|
||||
const PROXY_NAME = "LocalFileSystem";
|
||||
|
||||
@@ -11,6 +11,7 @@ const PROXY_NAME = "LocalFileSystem";
|
||||
*/
|
||||
export interface ILocalFileSystemProxyOptions {
|
||||
folderPath: string;
|
||||
relativePath: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,6 +27,7 @@ export class LocalFileSystemProxy implements IStorageProvider, IAssetProvider {
|
||||
if (!this.options) {
|
||||
this.options = {
|
||||
folderPath: null,
|
||||
relativePath: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -125,8 +127,26 @@ export class LocalFileSystemProxy implements IStorageProvider, IAssetProvider {
|
||||
* Retrieve assets from directory
|
||||
* @param folderName - Directory containing assets
|
||||
*/
|
||||
public getAssets(folderName?: string): Promise<IAsset[]> {
|
||||
const folderPath = [this.options.folderPath, folderName].join("/");
|
||||
return IpcRendererProxy.send(`${PROXY_NAME}:getAssets`, [folderPath]);
|
||||
public getAssets(): Promise<IAsset[]> {
|
||||
const { folderPath, relativePath } = this.options;
|
||||
return IpcRendererProxy.send(`${PROXY_NAME}:getAssets`, [folderPath, relativePath]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds default properties to new connections
|
||||
*
|
||||
* Currently adds `relativePath: true` to the providerOptions. Pre-existing connections
|
||||
* will only use absolute path
|
||||
*
|
||||
* @param connection Connection
|
||||
*/
|
||||
public addDefaultPropsToNewConnection(connection: IConnection): IConnection {
|
||||
return connection.id ? connection : {
|
||||
...connection,
|
||||
providerOptions: {
|
||||
...connection.providerOptions,
|
||||
relativePath: true,
|
||||
} as any,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class TestStorageProvider implements IStorageProvider {
|
||||
public deleteContainer(folderPath: string): Promise<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
public getAssets(containerName?: string): Promise<IAsset[]> {
|
||||
public getAssets(): Promise<IAsset[]> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export class ConnectionPicker extends React.Component<IConnectionPickerProps, IC
|
||||
<option
|
||||
className="connection-option"
|
||||
key={connection.id}
|
||||
value={connection.id}>{connection.name}
|
||||
value={connection.id}>{this.getConnectionText(connection)}
|
||||
</option>)
|
||||
}
|
||||
</select>
|
||||
@@ -71,6 +71,18 @@ export class ConnectionPicker extends React.Component<IConnectionPickerProps, IC
|
||||
);
|
||||
}
|
||||
|
||||
private getConnectionText = (connection: IConnection): string => {
|
||||
const options = connection.providerOptions;
|
||||
|
||||
if (options["folderPath"]) {
|
||||
return `${connection.name} (${options["folderPath"]})`;
|
||||
} else if (options["accountName"]) {
|
||||
return `${connection.name} (Azure:${options["accountName"]}\\${options["containerName"]})`;
|
||||
} else {
|
||||
return connection.name;
|
||||
}
|
||||
}
|
||||
|
||||
private onChange = (e) => {
|
||||
const selectedConnection = this.props.connections
|
||||
.find((connection) => connection.id === e.target.value) || {};
|
||||
|
||||
@@ -11,6 +11,7 @@ import ConnectionForm from "./connectionForm";
|
||||
import ConnectionItem from "./connectionItem";
|
||||
import "./connectionsPage.scss";
|
||||
import { toast } from "react-toastify";
|
||||
import { AssetProviderFactory } from "../../../../providers/storage/assetProviderFactory";
|
||||
|
||||
/**
|
||||
* Properties for Connection Page
|
||||
@@ -134,12 +135,20 @@ export default class ConnectionPage extends React.Component<IConnectionPageProps
|
||||
}
|
||||
|
||||
private onFormSubmit = async (connection: IConnection) => {
|
||||
connection = this.addDefaultPropsIfNewConnection(connection);
|
||||
await this.props.actions.saveConnection(connection);
|
||||
toast.success(interpolate(strings.connections.messages.saveSuccess, { connection }));
|
||||
|
||||
this.props.history.goBack();
|
||||
}
|
||||
|
||||
private addDefaultPropsIfNewConnection(connection: IConnection): IConnection {
|
||||
const assetProvider = AssetProviderFactory.createFromConnection(connection);
|
||||
return !connection.id && assetProvider.addDefaultPropsToNewConnection
|
||||
? assetProvider.addDefaultPropsToNewConnection(connection)
|
||||
: connection;
|
||||
}
|
||||
|
||||
private onFormCancel() {
|
||||
this.props.history.goBack();
|
||||
}
|
||||
|
||||
@@ -6,11 +6,6 @@
|
||||
"type": "string",
|
||||
"pattern": "^[^\\\\\\\\/:*?\\\\\\\"<>|]*$"
|
||||
},
|
||||
"securityToken": {
|
||||
"title": "${strings.projectSettings.securityToken.title}",
|
||||
"description": "${strings.projectSettings.securityToken.description}",
|
||||
"type": "string"
|
||||
},
|
||||
"sourceConnection": {
|
||||
"title": "${strings.projectSettings.sourceConnection.title}",
|
||||
"description": "${strings.projectSettings.sourceConnection.description}",
|
||||
@@ -44,6 +39,41 @@
|
||||
"tags": {
|
||||
"title": "${strings.tags.title}",
|
||||
"type": "array"
|
||||
},
|
||||
"useSecurityToken": {
|
||||
"title": "${strings.projectSettings.useSecurityToken.title}",
|
||||
"description": "${strings.projectSettings.useSecurityToken.description}",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"useSecurityToken": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"useSecurityToken": {
|
||||
"enum": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"securityToken": {
|
||||
"title": "${strings.projectSettings.securityToken.title}",
|
||||
"description": "${strings.projectSettings.securityToken.description}",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"useSecurityToken": {
|
||||
"enum": [
|
||||
false
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -263,8 +263,8 @@ describe("Project Form Component", () => {
|
||||
onCancel: onCancelHandler,
|
||||
});
|
||||
const newTagName = "My new tag";
|
||||
wrapper.find("input").last().simulate("change", { target: { value: newTagName } });
|
||||
wrapper.find("input").last().simulate("keyDown", { keyCode: 13 });
|
||||
wrapper.find("input#tagInputField").last().simulate("change", { target: { value: newTagName } });
|
||||
wrapper.find("input#tagInputField").last().simulate("keyDown", { keyCode: 13 });
|
||||
|
||||
const tags = wrapper.state().formData.tags;
|
||||
expect(tags).toHaveLength(1);
|
||||
|
||||
@@ -5,10 +5,11 @@ import { addLocValues, strings } from "../../../../common/strings";
|
||||
import { IConnection, IProject, ITag, IAppSettings } from "../../../../models/applicationState";
|
||||
import { StorageProviderFactory } from "../../../../providers/storage/storageProviderFactory";
|
||||
import { ConnectionPickerWithRouter } from "../../common/connectionPicker/connectionPicker";
|
||||
import { CustomField } from "../../common/customField/customField";
|
||||
import { CustomField, CustomWidget } from "../../common/customField/customField";
|
||||
import CustomFieldTemplate from "../../common/customField/customFieldTemplate";
|
||||
import { ISecurityTokenPickerProps, SecurityTokenPicker } from "../../common/securityTokenPicker/securityTokenPicker";
|
||||
import "vott-react/dist/css/tagsInput.css";
|
||||
import Checkbox from "rc-checkbox";
|
||||
import { IConnectionProviderPickerProps } from "../../common/connectionProviderPicker/connectionProviderPicker";
|
||||
import LocalFolderPicker from "../../common/localFolderPicker/localFolderPicker";
|
||||
|
||||
@@ -54,6 +55,11 @@ export interface IProjectFormState {
|
||||
export default class ProjectForm extends React.Component<IProjectFormProps, IProjectFormState> {
|
||||
private widgets = {
|
||||
localFolderPicker: (LocalFolderPicker as any) as Widget,
|
||||
checkbox: CustomWidget(Checkbox, (props) => ({
|
||||
checked: props.value,
|
||||
onChange: (value) => props.onChange(value.target.checked),
|
||||
disabled: props.disabled,
|
||||
})),
|
||||
};
|
||||
|
||||
private tagsInput: React.RefObject<TagsInput>;
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"securityToken": {
|
||||
"ui:field": "securityToken"
|
||||
},
|
||||
"useSecurityToken": {
|
||||
"ui:widget": "checkbox"
|
||||
},
|
||||
"description": {
|
||||
"ui:widget": "textarea"
|
||||
},
|
||||
|
||||
@@ -60,6 +60,7 @@ describe("Project settings page", () => {
|
||||
const store = createReduxStore(MockFactory.initialState());
|
||||
const props = MockFactory.projectSettingsProps();
|
||||
const saveProjectSpy = jest.spyOn(props.projectActions, "saveProject");
|
||||
const ensureSecurityTokenSpy = jest.spyOn(props.applicationActions, "ensureSecurityToken");
|
||||
|
||||
projectServiceMock.prototype.save = jest.fn((project) => Promise.resolve(project));
|
||||
|
||||
@@ -68,6 +69,7 @@ describe("Project settings page", () => {
|
||||
await MockFactory.flushUi();
|
||||
|
||||
expect(saveProjectSpy).toBeCalled();
|
||||
expect(ensureSecurityTokenSpy).toBeCalled();
|
||||
});
|
||||
|
||||
it("Throws an error when a user tries to create a duplicate project", async () => {
|
||||
@@ -114,6 +116,7 @@ describe("Project settings page", () => {
|
||||
const store = createReduxStore(initialState);
|
||||
const props = MockFactory.projectSettingsProps();
|
||||
const saveProjectSpy = jest.spyOn(props.projectActions, "saveProject");
|
||||
const ensureSecurityTokenSpy = jest.spyOn(props.applicationActions, "ensureSecurityToken");
|
||||
const saveAppSettingsSpy = jest.spyOn(props.applicationActions, "saveAppSettings");
|
||||
|
||||
projectServiceMock.prototype.save = jest.fn((project) => Promise.resolve(project));
|
||||
@@ -133,6 +136,11 @@ describe("Project settings page", () => {
|
||||
securityToken: `${project.name} Token`,
|
||||
});
|
||||
|
||||
expect(ensureSecurityTokenSpy).toBeCalledWith({
|
||||
...project,
|
||||
securityToken: `${project.name} Token`,
|
||||
});
|
||||
|
||||
expect(localStorage.removeItem).toBeCalledWith("projectForm");
|
||||
});
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ function mapDispatchToProps(dispatch) {
|
||||
};
|
||||
}
|
||||
|
||||
const projectFormTempKey = "projectForm";
|
||||
const projectFormKey = "projectForm";
|
||||
|
||||
/**
|
||||
* @name - Project Settings Page
|
||||
@@ -64,14 +64,17 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
|
||||
// If we are creating a new project check to see if there is a partial
|
||||
// project already created in local storage
|
||||
if (this.props.match.url === "/projects/create") {
|
||||
const projectJson = localStorage.getItem(projectFormTempKey);
|
||||
const projectJson = localStorage.getItem(projectFormKey);
|
||||
if (projectJson) {
|
||||
this.setState({ project: JSON.parse(projectJson) });
|
||||
}
|
||||
} else if (!this.props.project && projectId) {
|
||||
const projectToLoad = this.props.recentProjects.find((project) => project.id === projectId);
|
||||
if (projectToLoad) {
|
||||
await this.props.applicationActions.ensureSecurityToken(projectToLoad);
|
||||
if (projectToLoad.useSecurityToken) {
|
||||
await this.props.applicationActions.ensureSecurityToken(projectToLoad);
|
||||
}
|
||||
|
||||
await this.props.projectActions.loadProject(projectToLoad);
|
||||
}
|
||||
}
|
||||
@@ -119,16 +122,19 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
|
||||
*/
|
||||
private onFormChange = (project: IProject) => {
|
||||
if (this.isPartialProject(project)) {
|
||||
localStorage.setItem(projectFormTempKey, JSON.stringify(project));
|
||||
localStorage.setItem(projectFormKey, JSON.stringify(project));
|
||||
}
|
||||
}
|
||||
|
||||
private onFormSubmit = async (project: IProject) => {
|
||||
const isNew = !(!!project.id);
|
||||
|
||||
await this.props.applicationActions.ensureSecurityToken(project);
|
||||
if (project.useSecurityToken) {
|
||||
await this.props.applicationActions.ensureSecurityToken(project);
|
||||
}
|
||||
|
||||
await this.props.projectActions.saveProject(project);
|
||||
localStorage.removeItem(projectFormTempKey);
|
||||
localStorage.removeItem(projectFormKey);
|
||||
|
||||
toast.success(interpolate(strings.projectSettings.messages.saveSuccess, { project }));
|
||||
|
||||
@@ -140,7 +146,7 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
|
||||
}
|
||||
|
||||
private onFormCancel = () => {
|
||||
localStorage.removeItem(projectFormTempKey);
|
||||
localStorage.removeItem(projectFormKey);
|
||||
this.props.history.goBack();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,9 @@ import {
|
||||
IProject,
|
||||
} from "../../models/applicationState";
|
||||
import { createAction, createPayloadAction, IPayloadAction } from "./actionCreators";
|
||||
import { ExportAssetState, IExportResults } from "../../providers/export/exportProvider";
|
||||
import { IExportResults } from "../../providers/export/exportProvider";
|
||||
import { appInfo } from "../../common/appInfo";
|
||||
import { strings } from "../../common/strings";
|
||||
import { IExportFormat } from "vott-react";
|
||||
import { IVottJsonExportProviderOptions } from "../../providers/export/vottJson";
|
||||
|
||||
/**
|
||||
* Actions to be performed in relation to projects
|
||||
@@ -48,7 +46,7 @@ export function loadProject(project: IProject):
|
||||
const projectToken = appState.appSettings.securityTokens
|
||||
.find((securityToken) => securityToken.name === project.securityToken);
|
||||
|
||||
if (!projectToken) {
|
||||
if (project.useSecurityToken && !projectToken) {
|
||||
throw new AppError(ErrorCode.SecurityTokenNotFound, "Security Token Not Found");
|
||||
}
|
||||
const loadedProject = await projectService.load(project, projectToken);
|
||||
@@ -76,7 +74,7 @@ export function saveProject(project: IProject)
|
||||
const projectToken = appState.appSettings.securityTokens
|
||||
.find((securityToken) => securityToken.name === project.securityToken);
|
||||
|
||||
if (!projectToken) {
|
||||
if (project.useSecurityToken && !projectToken) {
|
||||
throw new AppError(ErrorCode.SecurityTokenNotFound, "Security Token Not Found");
|
||||
}
|
||||
|
||||
@@ -104,7 +102,7 @@ export function deleteProject(project: IProject)
|
||||
const projectToken = appState.appSettings.securityTokens
|
||||
.find((securityToken) => securityToken.name === project.securityToken);
|
||||
|
||||
if (!projectToken) {
|
||||
if (project.useSecurityToken && !projectToken) {
|
||||
throw new AppError(ErrorCode.SecurityTokenNotFound, "Security Token Not Found");
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import HtmlFileReader from "../common/htmlFileReader";
|
||||
import { encodeFileURI } from "../common/utils";
|
||||
import _ from "lodash";
|
||||
import registerMixins from "../registerMixins";
|
||||
import MD5 from "md5.js";
|
||||
|
||||
describe("Asset Service", () => {
|
||||
describe("Static Methods", () => {
|
||||
@@ -24,6 +25,22 @@ describe("Asset Service", () => {
|
||||
expect(asset.format).toEqual("jpg");
|
||||
});
|
||||
|
||||
it("creates an asset using file path as identifier", () => {
|
||||
const path = "c:/dir1/dir2/asset1.jpg";
|
||||
const asset = AssetService.createAssetFromFilePath(path);
|
||||
const expectedIdenfifier = `file:${path}`;
|
||||
const expectedId = new MD5().update(expectedIdenfifier).digest("hex");
|
||||
expect(asset.id).toEqual(expectedId);
|
||||
});
|
||||
|
||||
it("creates an asset using passed in identifier", () => {
|
||||
const path = "C:\\dir1\\dir2\\asset1.jpg";
|
||||
const identifier = "asset1.jpg";
|
||||
const asset = AssetService.createAssetFromFilePath(path, undefined, identifier);
|
||||
const expectedId = new MD5().update(identifier).digest("hex");
|
||||
expect(asset.id).toEqual(expectedId);
|
||||
});
|
||||
|
||||
it("creates an asset from an encoded file", () => {
|
||||
const path = "C:\\dir1\\dir2\\asset%201.jpg";
|
||||
const asset = AssetService.createAssetFromFilePath(path);
|
||||
|
||||
@@ -14,6 +14,8 @@ import { TFRecordsReader } from "../providers/export/tensorFlowRecords/tensorFlo
|
||||
import { FeatureType } from "../providers/export/tensorFlowRecords/tensorFlowBuilder";
|
||||
import { appInfo } from "../common/appInfo";
|
||||
import { encodeFileURI } from "../common/utils";
|
||||
import Jimp from "jimp";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* @name - Asset Service
|
||||
@@ -23,13 +25,15 @@ export class AssetService {
|
||||
|
||||
/**
|
||||
* Create IAsset from filePath
|
||||
* @param filePath - filepath of asset
|
||||
* @param fileName - name of asset
|
||||
* @param assetFilePath - filepath of asset
|
||||
* @param assetFileName - name of asset
|
||||
*/
|
||||
public static createAssetFromFilePath(filePath: string, fileName?: string): IAsset {
|
||||
Guard.empty(filePath);
|
||||
|
||||
const normalizedPath = filePath.toLowerCase();
|
||||
public static createAssetFromFilePath(
|
||||
assetFilePath: string,
|
||||
assetFileName?: string,
|
||||
assetIdentifier?: string): IAsset {
|
||||
Guard.empty(assetFilePath);
|
||||
const normalizedPath = assetFilePath.toLowerCase();
|
||||
|
||||
// If the path is not already prefixed with a protocol
|
||||
// then assume it comes from the local file system
|
||||
@@ -37,17 +41,18 @@ export class AssetService {
|
||||
!normalizedPath.startsWith("https://") &&
|
||||
!normalizedPath.startsWith("file:")) {
|
||||
// First replace \ character with / the do the standard url encoding then encode unsupported characters
|
||||
filePath = encodeFileURI(filePath, true);
|
||||
assetFilePath = encodeFileURI(assetFilePath, true);
|
||||
}
|
||||
assetIdentifier = assetIdentifier || assetFilePath;
|
||||
|
||||
const md5Hash = new MD5().update(filePath).digest("hex");
|
||||
const pathParts = filePath.split(/[\\\/]/);
|
||||
const md5Hash = new MD5().update(assetIdentifier).digest("hex");
|
||||
const pathParts = assetFilePath.split(/[\\\/]/);
|
||||
// Example filename: video.mp4#t=5
|
||||
// fileNameParts[0] = "video"
|
||||
// fileNameParts[1] = "mp4"
|
||||
// fileNameParts[2] = "t=5"
|
||||
fileName = fileName || pathParts[pathParts.length - 1];
|
||||
const fileNameParts = fileName.split(".");
|
||||
assetFileName = assetFileName || pathParts[pathParts.length - 1];
|
||||
const fileNameParts = assetFileName.split(".");
|
||||
const extensionParts = fileNameParts[fileNameParts.length - 1].split(/[\?#]/);
|
||||
const assetFormat = extensionParts[0];
|
||||
|
||||
@@ -58,8 +63,8 @@ export class AssetService {
|
||||
format: assetFormat,
|
||||
state: AssetState.NotVisited,
|
||||
type: assetType,
|
||||
name: fileName,
|
||||
path: filePath,
|
||||
name: assetFileName,
|
||||
path: assetFilePath,
|
||||
size: null,
|
||||
};
|
||||
}
|
||||
@@ -185,6 +190,17 @@ export class AssetService {
|
||||
|
||||
const fileName = `${asset.id}${constants.assetMetadataFileExtension}`;
|
||||
try {
|
||||
console.log('Jimp start');
|
||||
const readBuffer = await this.storageProvider.readBinary(fileName);
|
||||
const jimp = await Jimp.read(readBuffer);
|
||||
const writeBuffer = await jimp.resize(256, 256)
|
||||
.quality(60)
|
||||
.greyscale()
|
||||
.getBufferAsync(Jimp.MIME_JPEG);
|
||||
|
||||
await this.storageProvider.writeBinary(`${path.dirname(fileName)}/test.jpg`, writeBuffer);
|
||||
console.log('jimp end');
|
||||
|
||||
const json = await this.storageProvider.readText(fileName);
|
||||
return JSON.parse(json) as IAssetMetadata;
|
||||
} catch (err) {
|
||||
|
||||
@@ -57,6 +57,7 @@ export default class ImportService implements IImportService {
|
||||
id: shortid.generate(),
|
||||
name: projectInfo.file.name.split(".")[0],
|
||||
version: packageJson.version,
|
||||
useSecurityToken: true,
|
||||
securityToken: `${projectInfo.file.name.split(".")[0]} Token`,
|
||||
description: "Converted V1 Project",
|
||||
tags: parsedTags,
|
||||
|
||||
@@ -9,13 +9,13 @@ import {
|
||||
import { constants } from "../common/constants";
|
||||
import { ExportProviderFactory } from "../providers/export/exportProviderFactory";
|
||||
import { generateKey } from "../common/crypto";
|
||||
import { encryptProject, decryptProject } from "../common/utils";
|
||||
import * as utils from "../common/utils";
|
||||
import { ExportAssetState } from "../providers/export/exportProvider";
|
||||
import { IVottJsonExportProviderOptions } from "../providers/export/vottJson";
|
||||
import { IPascalVOCExportProviderOptions } from "../providers/export/pascalVOC";
|
||||
|
||||
describe("Project Service", () => {
|
||||
let projectSerivce: IProjectService = null;
|
||||
let projectService: IProjectService = null;
|
||||
let testProject: IProject = null;
|
||||
let projectList: IProject[] = null;
|
||||
let securityToken: ISecurityToken = null;
|
||||
@@ -33,27 +33,41 @@ describe("Project Service", () => {
|
||||
StorageProviderFactory.create = jest.fn(() => storageProviderMock);
|
||||
ExportProviderFactory.create = jest.fn(() => exportProviderMock);
|
||||
|
||||
const encryptSpy = jest.spyOn(utils, "encryptProject");
|
||||
const decryptSpy = jest.spyOn(utils, "decryptProject");
|
||||
|
||||
beforeEach(() => {
|
||||
securityToken = {
|
||||
name: "TestToken",
|
||||
key: generateKey(),
|
||||
};
|
||||
testProject = MockFactory.createTestProject("TestProject");
|
||||
projectSerivce = new ProjectService();
|
||||
projectService = new ProjectService();
|
||||
|
||||
storageProviderMock.writeText.mockClear();
|
||||
storageProviderMock.deleteFile.mockClear();
|
||||
encryptSpy.mockClear();
|
||||
decryptSpy.mockClear();
|
||||
});
|
||||
|
||||
it("Load decrypts any project settings using the specified key", async () => {
|
||||
const encryptedProject = encryptProject(testProject, securityToken);
|
||||
const decryptedProject = await projectSerivce.load(encryptedProject, securityToken);
|
||||
const encryptedProject = utils.encryptProject(testProject, securityToken);
|
||||
const decryptedProject = await projectService.load(encryptedProject, securityToken);
|
||||
|
||||
expect(decryptedProject).toEqual(testProject);
|
||||
expect(decryptSpy).toBeCalledWith(encryptedProject, securityToken);
|
||||
});
|
||||
|
||||
it("Does not decrypt project when a security token is not in use", async () => {
|
||||
const project: IProject = { ...testProject, useSecurityToken: false };
|
||||
|
||||
const loadedProject = await projectService.load(project);
|
||||
expect(loadedProject).toEqual(project);
|
||||
expect(decryptSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("Saves calls project storage provider to write project", async () => {
|
||||
const result = await projectSerivce.save(testProject, securityToken);
|
||||
const result = await projectService.save(testProject, securityToken);
|
||||
|
||||
const encryptedProject: IProject = {
|
||||
...testProject,
|
||||
@@ -72,6 +86,7 @@ describe("Project Service", () => {
|
||||
};
|
||||
|
||||
expect(result).toEqual(encryptedProject);
|
||||
expect(encryptSpy).toBeCalledWith(testProject, securityToken);
|
||||
expect(StorageProviderFactory.create).toBeCalledWith(
|
||||
testProject.targetConnection.providerType,
|
||||
testProject.targetConnection.providerOptions,
|
||||
@@ -82,9 +97,16 @@ describe("Project Service", () => {
|
||||
expect.any(String));
|
||||
});
|
||||
|
||||
it("Does not encrypt project during save when a security token is not in use", async () => {
|
||||
const projectToSave: IProject = { ...testProject, useSecurityToken: false };
|
||||
await projectService.save(projectToSave);
|
||||
|
||||
expect(encryptSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("sets default export settings when not defined", async () => {
|
||||
testProject.exportFormat = null;
|
||||
const result = await projectSerivce.save(testProject, securityToken);
|
||||
const result = await projectService.save(testProject, securityToken);
|
||||
|
||||
const vottJsonExportProviderOptions: IVottJsonExportProviderOptions = {
|
||||
assetState: ExportAssetState.Visited,
|
||||
@@ -96,14 +118,14 @@ describe("Project Service", () => {
|
||||
providerOptions: vottJsonExportProviderOptions,
|
||||
};
|
||||
|
||||
const decryptedProject = decryptProject(result, securityToken);
|
||||
const decryptedProject = utils.decryptProject(result, securityToken);
|
||||
|
||||
expect(decryptedProject.exportFormat).toEqual(expectedExportFormat);
|
||||
});
|
||||
|
||||
it("sets default active learning setting when not defined", async () => {
|
||||
testProject.activeLearningSettings = null;
|
||||
const result = await projectSerivce.save(testProject, securityToken);
|
||||
const result = await projectService.save(testProject, securityToken);
|
||||
|
||||
const activeLearningSettings: IActiveLearningSettings = {
|
||||
autoDetect: false,
|
||||
@@ -116,7 +138,7 @@ describe("Project Service", () => {
|
||||
|
||||
it("initializes tags to empty array if not defined", async () => {
|
||||
testProject.tags = null;
|
||||
const result = await projectSerivce.save(testProject, securityToken);
|
||||
const result = await projectService.save(testProject, securityToken);
|
||||
|
||||
expect(result.tags).toEqual([]);
|
||||
});
|
||||
@@ -127,7 +149,7 @@ describe("Project Service", () => {
|
||||
providerOptions: null,
|
||||
};
|
||||
|
||||
await projectSerivce.save(testProject, securityToken);
|
||||
await projectService.save(testProject, securityToken);
|
||||
|
||||
expect(ExportProviderFactory.create).toBeCalledWith(
|
||||
testProject.exportFormat.providerType,
|
||||
@@ -140,7 +162,7 @@ describe("Project Service", () => {
|
||||
it("Save throws error if writing to storage provider fails", async () => {
|
||||
const expectedError = "Error writing to storage provider";
|
||||
storageProviderMock.writeText.mockImplementationOnce(() => Promise.reject(expectedError));
|
||||
await expect(projectSerivce.save(testProject, securityToken)).rejects.toEqual(expectedError);
|
||||
await expect(projectService.save(testProject, securityToken)).rejects.toEqual(expectedError);
|
||||
});
|
||||
|
||||
it("Save throws error if storage provider cannot be created", async () => {
|
||||
@@ -148,11 +170,11 @@ describe("Project Service", () => {
|
||||
const createMock = StorageProviderFactory.create as jest.Mock;
|
||||
createMock.mockImplementationOnce(() => { throw expectedError; });
|
||||
|
||||
await expect(projectSerivce.save(testProject, securityToken)).rejects.toEqual(expectedError);
|
||||
await expect(projectService.save(testProject, securityToken)).rejects.toEqual(expectedError);
|
||||
});
|
||||
|
||||
it("Delete calls project storage provider to delete project", async () => {
|
||||
await projectSerivce.delete(testProject);
|
||||
await projectService.delete(testProject);
|
||||
|
||||
expect(StorageProviderFactory.create).toBeCalledWith(
|
||||
testProject.targetConnection.providerType,
|
||||
@@ -167,7 +189,7 @@ describe("Project Service", () => {
|
||||
storageProviderMock.deleteFile
|
||||
.mockImplementationOnce(() => Promise.reject(expectedError));
|
||||
|
||||
await expect(projectSerivce.delete(testProject)).rejects.toEqual(expectedError);
|
||||
await expect(projectService.delete(testProject)).rejects.toEqual(expectedError);
|
||||
});
|
||||
|
||||
it("Delete call fails if storage provider cannot be created", async () => {
|
||||
@@ -175,20 +197,20 @@ describe("Project Service", () => {
|
||||
const createMock = StorageProviderFactory.create as jest.Mock;
|
||||
createMock.mockImplementationOnce(() => { throw expectedError; });
|
||||
|
||||
await expect(projectSerivce.delete(testProject)).rejects.toEqual(expectedError);
|
||||
await expect(projectService.delete(testProject)).rejects.toEqual(expectedError);
|
||||
});
|
||||
|
||||
it("isDuplicate returns false when called with a unique project", async () => {
|
||||
testProject = MockFactory.createTestProject("TestProject");
|
||||
projectList = MockFactory.createTestProjects();
|
||||
expect(projectSerivce.isDuplicate(testProject, projectList)).toEqual(false);
|
||||
expect(projectService.isDuplicate(testProject, projectList)).toEqual(false);
|
||||
});
|
||||
|
||||
it("isDuplicate returns true when called with a duplicate project", async () => {
|
||||
testProject = MockFactory.createTestProject("1");
|
||||
testProject.id = undefined;
|
||||
projectList = MockFactory.createTestProjects();
|
||||
expect(projectSerivce.isDuplicate(testProject, projectList)).toEqual(true);
|
||||
expect(projectService.isDuplicate(testProject, projectList)).toEqual(true);
|
||||
});
|
||||
|
||||
it("deletes all asset metadata files when project is deleted", async () => {
|
||||
@@ -199,7 +221,7 @@ describe("Project Service", () => {
|
||||
|
||||
testProject.assets = _.keyBy(assets, (asset) => asset.id);
|
||||
|
||||
await projectSerivce.delete(testProject);
|
||||
await projectService.delete(testProject);
|
||||
expect(storageProviderMock.deleteFile.mock.calls).toHaveLength(assets.length + 1);
|
||||
});
|
||||
|
||||
@@ -215,8 +237,8 @@ describe("Project Service", () => {
|
||||
} as IPascalVOCExportProviderOptions,
|
||||
};
|
||||
|
||||
const encryptedProject = encryptProject(testProject, securityToken);
|
||||
const decryptedProject = await projectSerivce.load(encryptedProject, securityToken);
|
||||
const encryptedProject = utils.encryptProject(testProject, securityToken);
|
||||
const decryptedProject = await projectService.load(encryptedProject, securityToken);
|
||||
|
||||
expect(decryptedProject.exportFormat.providerType).toEqual("pascalVOC");
|
||||
expect(decryptedProject.exportFormat.providerOptions).toEqual(testProject.exportFormat.providerOptions);
|
||||
|
||||
@@ -19,8 +19,8 @@ import { IExportFormat } from "vott-react";
|
||||
* @member delete - Delete a project
|
||||
*/
|
||||
export interface IProjectService {
|
||||
load(project: IProject, securityToken: ISecurityToken): Promise<IProject>;
|
||||
save(project: IProject, securityToken: ISecurityToken): Promise<IProject>;
|
||||
load(project: IProject, securityToken?: ISecurityToken): Promise<IProject>;
|
||||
save(project: IProject, securityToken?: ISecurityToken): Promise<IProject>;
|
||||
delete(project: IProject): Promise<void>;
|
||||
isDuplicate(project: IProject, projectList: IProject[]): boolean;
|
||||
}
|
||||
@@ -49,11 +49,13 @@ export default class ProjectService implements IProjectService {
|
||||
* @param project The project JSON to load
|
||||
* @param securityToken The security token used to decrypt sensitive project settings
|
||||
*/
|
||||
public load(project: IProject, securityToken: ISecurityToken): Promise<IProject> {
|
||||
public load(project: IProject, securityToken?: ISecurityToken): Promise<IProject> {
|
||||
Guard.null(project);
|
||||
|
||||
try {
|
||||
const loadedProject = decryptProject(project, securityToken);
|
||||
const loadedProject = project.useSecurityToken
|
||||
? decryptProject(project, securityToken)
|
||||
: { ...project };
|
||||
|
||||
// Ensure tags is always initialized to an array
|
||||
if (!loadedProject.tags) {
|
||||
@@ -84,7 +86,7 @@ export default class ProjectService implements IProjectService {
|
||||
* @param project - Project to save
|
||||
* @param securityToken - Security Token to encrypt
|
||||
*/
|
||||
public async save(project: IProject, securityToken: ISecurityToken): Promise<IProject> {
|
||||
public async save(project: IProject, securityToken?: ISecurityToken): Promise<IProject> {
|
||||
Guard.null(project);
|
||||
|
||||
if (!project.id) {
|
||||
@@ -110,7 +112,9 @@ export default class ProjectService implements IProjectService {
|
||||
|
||||
const storageProvider = StorageProviderFactory.createFromConnection(project.targetConnection);
|
||||
await this.saveExportSettings(project);
|
||||
project = encryptProject(project, securityToken);
|
||||
project = project.useSecurityToken
|
||||
? encryptProject(project, securityToken)
|
||||
: { ...project };
|
||||
|
||||
await storageProvider.writeText(
|
||||
`${project.name}${constants.projectFileExtension}`,
|
||||
|
||||
+9
-1
@@ -4,4 +4,12 @@ import Adapter from 'enzyme-adapter-react-16';
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
// Silence console.log and console.group statements in testing
|
||||
console.log = console.group = function() {};
|
||||
console.log = console.group = function() {};
|
||||
const electronMock = {
|
||||
ipcRenderer: {
|
||||
send: jest.fn(),
|
||||
on: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
window.require = jest.fn(() => electronMock);
|
||||
|
||||
Referência em uma Nova Issue
Bloquear um usuário