Comparar commits

...

7 Commits

Autor SHA1 Mensagem Data
Wallace Breza a3e51863df WIP: jimp 2020-11-12 14:11:19 -08:00
Tanner Barlow a603a2743f feat: Nested source directories (#1028) 2020-11-05 15:54:06 -08:00
Tanner Barlow d48de94b8b feat: Use relative paths for local file assets (#1027) 2020-11-04 15:25:00 -08:00
Wallace Breza 27ce840067 fix: Bing Image Search - Support to override endpoint url (#1025)
The Bing Search resource is undergoing a transition from a Cognitive Service to the Azure Marketplace. During this transition it is possible to have bing search resources that use different endpoint urls.

This fix introduces a new endpoint textbox that defaults to the new default resource url (https://api.bing.microsoft.com/). However, for users with older resources that have not specified the field it will fall back to the older cognitive services url ( https://api.cognitive.microsoft.com/bing).

In addition to the fix this also introduces addition filter options to filter image responses by image size and license type.
2020-11-04 15:16:36 -08:00
Phil 719a89832c Update Azure custom vision url (#1023) 2020-11-04 14:06:56 -05:00
Jeff Ding fcc3523c95 update release guide doc (#1019)
* added release process section into readme
* updated RELEASE_GUIDE.md accordingly
2020-11-03 14:57:45 -08:00
Wallace Breza f731dfaece feat: Make security tokens optional (#1022)
Security tokens are a great way to protect sensitive data from being stored in plain text within your project file, but at the same time can be more difficult to use when sharing projects with team members.

In this feature security tokens are still enabled by default but give the project author the ability to turn off security tokens which then stores all provider options in plain text.
2020-11-03 11:34:17 -08:00
47 arquivos alterados com 1852 adições e 162 exclusões
+11
Ver Arquivo
@@ -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
![alt text](./docs/images/release-process.png "Create 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
+3
Ver Arquivo
@@ -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:
+1 -1
Ver Arquivo
@@ -4,4 +4,4 @@ const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'development',
devtool: "inline-source-map",
})
})
+1 -1
Ver Arquivo
@@ -6,7 +6,7 @@ Instruction on how to create new GitHub & Web Releases.
## Release Process
![alt text](./docs/images/release-process.png "Create Release Process")
![alt text](./images/release-process.png "Create Release Process")
### AzDO Tasks
+1000 -3
Ver Arquivo
Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff
+2
Ver Arquivo
@@ -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",
+50 -7
Ver Arquivo
@@ -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: {
+50 -7
Ver Arquivo
@@ -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: {
+51 -7
Ver Arquivo
@@ -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: {
+53 -9
Ver Arquivo
@@ -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: {
+51 -7
Ver Arquivo
@@ -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: {
+51 -7
Ver Arquivo
@@ -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: {
+2
Ver Arquivo
@@ -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
Ver Arquivo
@@ -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,
+1 -1
Ver Arquivo
@@ -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);
}
+2 -1
Ver Arquivo
@@ -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);
}
+1 -1
Ver Arquivo
@@ -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();
}
+1 -1
Ver Arquivo
@@ -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`,
});
});
+1 -1
Ver Arquivo
@@ -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);
}
+2 -2
Ver Arquivo
@@ -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;
}
/**
+2 -2
Ver Arquivo
@@ -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) {
+62 -8
Ver Arquivo
@@ -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}"
]
}
}
}
}
+40 -5
Ver Arquivo
@@ -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);
+28 -3
Ver Arquivo
@@ -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");
});
});
});
+24 -4
Ver Arquivo
@@ -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();
}
+4 -6
Ver Arquivo
@@ -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");
}
+17
Ver Arquivo
@@ -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);
+29 -13
Ver Arquivo
@@ -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) {
+1
Ver Arquivo
@@ -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,
+43 -21
Ver Arquivo
@@ -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);
+10 -6
Ver Arquivo
@@ -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}`,