Comparar commits
7 Commits
| Autor | SHA1 | Data | |
|---|---|---|---|
| a3e51863df | |||
| a603a2743f | |||
| d48de94b8b | |||
| 27ce840067 | |||
| 719a89832c | |||
| fcc3523c95 | |||
| f731dfaece |
+11
@@ -268,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:
|
||||
|
||||
@@ -4,4 +4,4 @@ const common = require('./webpack.common.js')
|
||||
module.exports = merge(common, {
|
||||
mode: 'development',
|
||||
devtool: "inline-source-map",
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ Instruction on how to create new GitHub & Web Releases.
|
||||
|
||||
## Release Process
|
||||
|
||||

|
||||

|
||||
|
||||
### AzDO Tasks
|
||||
|
||||
gerado
+1000
-3
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
@@ -25,6 +25,7 @@
|
||||
"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",
|
||||
@@ -119,6 +120,7 @@
|
||||
"foreman": "^3.0.1",
|
||||
"jest-enzyme": "^7.0.1",
|
||||
"jquery": "^3.3.1",
|
||||
"mock-fs": "^4.13.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"popper.js": "^1.14.6",
|
||||
"redux-immutable-state-invariant": "^2.1.0",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -86,6 +86,11 @@ export const japanese: IAppStrings = {
|
||||
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,
|
||||
@@ -200,15 +205,54 @@ export const japanese: IAppStrings = {
|
||||
},
|
||||
bing: {
|
||||
title: "Bing 画像検索", // Bing Image Search,
|
||||
options: "Bing 画像検索のオプション", // Bing Image Search Options,
|
||||
apiKey: "APIキー", // API Key,
|
||||
query: "クエリ", // Query,
|
||||
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,
|
||||
all: "すべて", // All,
|
||||
square: "正方形", // Square,
|
||||
wide: "横長", // Wide,
|
||||
tall: "縦長", // Tall"
|
||||
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: {
|
||||
|
||||
@@ -86,6 +86,11 @@ export const korean: IAppStrings = {
|
||||
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,
|
||||
@@ -168,11 +173,11 @@ export const korean: IAppStrings = {
|
||||
deleteSuccess: "${connection.name}을 삭제했습니다.", // Successfully deleted ${connection.name}"
|
||||
},
|
||||
imageCorsWarning: "경고 : 웹 브라우저에서 VoTT를 사용하는 경우 CORS (Cross Origin Resource Sharing) " +
|
||||
"제한으로 인해 Bing Image Search의 일부 정보가 제대로 내보내지지 않을 수 있습니다.",
|
||||
"제한으로 인해 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}에서 찾을 수 있습니다.",
|
||||
"설정을 활성화 해야 합니다. 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.,
|
||||
@@ -201,15 +206,54 @@ export const korean: IAppStrings = {
|
||||
},
|
||||
bing: {
|
||||
title: "Bing 이미지 검색", // Bing Image Search,
|
||||
options: "Bing 이미지 검색 옵션", // Bing Image Search Options,
|
||||
apiKey: "API 키", // API Key,
|
||||
query: "쿼리", // Query,
|
||||
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,
|
||||
all: "모두", // All,
|
||||
square: "정사각형", // Square,
|
||||
wide: "넓은", // Wide,
|
||||
tall: "긴", // Tall"
|
||||
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: {
|
||||
|
||||
@@ -86,6 +86,11 @@ export const chinese: 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
|
||||
@@ -200,15 +205,54 @@ export const chinese: IAppStrings = {
|
||||
},
|
||||
bing: {
|
||||
title: "必应图片搜索", // Bing Image Search
|
||||
options: "必应图像搜索选项", // Bing Image Search Options
|
||||
apiKey: "API密钥", // API Key
|
||||
query: "查询", // Query
|
||||
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
|
||||
all: "所有", // All
|
||||
square: "正方形", // Square
|
||||
wide: "宽", // Wide
|
||||
tall: "高", // Tall
|
||||
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: {
|
||||
|
||||
@@ -86,6 +86,11 @@ export const chinesetw: 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
|
||||
@@ -203,15 +208,54 @@ export const chinesetw: IAppStrings = {
|
||||
},
|
||||
bing: {
|
||||
title: "Bing 影像搜尋", // Bing Image Search
|
||||
options: "Bing 影像搜尋選項", // Bing Image Search Options
|
||||
apiKey: "API密鑰", // API Key
|
||||
query: "查詢", // Query
|
||||
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
|
||||
all: "所有", // All
|
||||
square: "矩形", // Square
|
||||
wide: "寬", // Wide
|
||||
tall: "高", // Tall
|
||||
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: {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+53
-10
@@ -90,6 +90,10 @@ export interface IAppStrings {
|
||||
title: string;
|
||||
description: string;
|
||||
},
|
||||
useSecurityToken: {
|
||||
title: string;
|
||||
description: string;
|
||||
},
|
||||
save: string;
|
||||
sourceConnection: {
|
||||
title: string;
|
||||
@@ -196,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;
|
||||
@@ -456,7 +499,7 @@ 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
|
||||
// TODO: Need to comment out other languages which will not be used
|
||||
en: english,
|
||||
es: spanish,
|
||||
ja: japanese,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
Referência em uma Nova Issue
Bloquear um usuário