Local dmr (#612)
* working version1 * update tests * fix build break * update per comments1
This commit is contained in:
Родитель
b7826d54ba
Коммит
b794202329
|
@ -10,15 +10,11 @@ export const MESSAGE_CHANNELS = {
|
|||
AUTHENTICATION_GET_PROFILE_TOKEN: 'authentication_get_profile_token',
|
||||
AUTHENTICATION_LOGIN: 'authentication_login',
|
||||
AUTHENTICATION_LOGOUT: 'authentication_logout',
|
||||
DIRECTORY_GET_DIRECTORIES: 'directory_getDirectories',
|
||||
MODEL_REPOSITORY_GET_DEFINITION: 'model_definition',
|
||||
SETTING_HIGH_CONTRAST: 'setting_highContrast',
|
||||
};
|
||||
|
||||
export const API_INTERFACES = {
|
||||
AUTHENTICATION: 'api_authentication',
|
||||
DEVICE: 'api_device',
|
||||
DIRECTORY: 'api_directory',
|
||||
MODEL_DEFINITION: 'api_modelDefinition',
|
||||
SETTINGS: 'api_settings'
|
||||
};
|
||||
|
|
|
@ -4,12 +4,8 @@
|
|||
**********************************************************/
|
||||
import { contextBridge } from 'electron';
|
||||
import { generateSettingsInterface } from './factories/settingsInterfaceFactory';
|
||||
import { generateDirectoryInterface } from './factories/directoryInterfaceFactory';
|
||||
import { generateModelRepositoryInterface } from './factories/modelRepositoryInterfaceFactory';
|
||||
import { generateAuthenticationInterface } from './factories/authenticationInterfaceFactory';
|
||||
import { API_INTERFACES } from './constants';
|
||||
|
||||
contextBridge.exposeInMainWorld(API_INTERFACES.DIRECTORY, generateDirectoryInterface());
|
||||
contextBridge.exposeInMainWorld(API_INTERFACES.MODEL_DEFINITION, generateModelRepositoryInterface());
|
||||
contextBridge.exposeInMainWorld(API_INTERFACES.SETTINGS, generateSettingsInterface());
|
||||
contextBridge.exposeInMainWorld(API_INTERFACES.AUTHENTICATION, generateAuthenticationInterface());
|
||||
|
|
|
@ -8,8 +8,6 @@ import * as path from 'path';
|
|||
import { generateMenu } from './factories/menuFactory';
|
||||
import { PLATFORMS, MESSAGE_CHANNELS } from './constants';
|
||||
import { onSettingsHighContrast } from './handlers/settingsHandler';
|
||||
import { onGetInterfaceDefinition } from './handlers/modelRepositoryHandler';
|
||||
import { onGetDirectories } from './handlers/directoryHandler';
|
||||
import { formatError } from './utils/errorHelper';
|
||||
import { AuthProvider } from './utils/authProvider';
|
||||
import '../dist/server/serverElectron';
|
||||
|
@ -30,8 +28,6 @@ class Main {
|
|||
|
||||
private static setMessageHandlers(): void {
|
||||
Main.registerHandler(MESSAGE_CHANNELS.SETTING_HIGH_CONTRAST, onSettingsHighContrast);
|
||||
Main.registerHandler(MESSAGE_CHANNELS.MODEL_REPOSITORY_GET_DEFINITION, onGetInterfaceDefinition);
|
||||
Main.registerHandler(MESSAGE_CHANNELS.DIRECTORY_GET_DIRECTORIES, onGetDirectories);
|
||||
Main.registerHandler(MESSAGE_CHANNELS.AUTHENTICATION_LOGIN, Main.onLogin);
|
||||
Main.registerHandler(MESSAGE_CHANNELS.AUTHENTICATION_LOGOUT, Main.onLogout);
|
||||
Main.registerHandler(MESSAGE_CHANNELS.AUTHENTICATION_GET_PROFILE_TOKEN, Main.onGetProfileToken);
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { MESSAGE_CHANNELS } from '../constants';
|
||||
import { DirectoryInterface, GetDirectoriesParameters } from '../interfaces/directoryInterface';
|
||||
import { invokeInMainWorld } from '../utils/invokeHelper';
|
||||
|
||||
export const generateDirectoryInterface = (): DirectoryInterface => {
|
||||
return {
|
||||
getDirectories: async (params: GetDirectoriesParameters): Promise<string[]> => {
|
||||
return invokeInMainWorld<string[]>(MESSAGE_CHANNELS.DIRECTORY_GET_DIRECTORIES, params);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -1,15 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { MESSAGE_CHANNELS } from '../constants';
|
||||
import { ModelRepositoryInterface, GetInterfaceDefinitionParameters, GetDirectoriesParameters } from '../interfaces/modelRepositoryInterface';
|
||||
import { invokeInMainWorld } from '../utils/invokeHelper';
|
||||
|
||||
export const generateModelRepositoryInterface = (): ModelRepositoryInterface => {
|
||||
return {
|
||||
getInterfaceDefinition: async (params: GetInterfaceDefinitionParameters): Promise<object | undefined> => {
|
||||
return invokeInMainWorld<object | undefined>(MESSAGE_CHANNELS.MODEL_REPOSITORY_GET_DEFINITION, params);
|
||||
}
|
||||
};
|
||||
};
|
|
@ -1,44 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { IpcMainInvokeEvent } from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as util from 'util';
|
||||
import * as child_process from 'child_process';
|
||||
import { GetDirectoriesParameters } from '../interfaces/directoryInterface';
|
||||
|
||||
export const onGetDirectories = async (event: IpcMainInvokeEvent, params: GetDirectoriesParameters): Promise<string[]> => {
|
||||
return params.path === '$DEFAULT' ?
|
||||
await fetchDrivesOnWindows() :
|
||||
Promise.resolve(fetchDirectories(params.path));
|
||||
};
|
||||
|
||||
const fetchDrivesOnWindows = async (): Promise<string[]> => {
|
||||
const promise = util.promisify(child_process.exec);
|
||||
const {stdout} = await promise('wmic logicaldisk get name')
|
||||
if (stdout) {
|
||||
const drives = stdout.split(/\r\n/).map(drive => drive.trim()).filter(drive => drive !== '');
|
||||
drives.shift(); // remove header
|
||||
return drives.map(drive => `${drive}/`);
|
||||
}
|
||||
|
||||
throw new Error();
|
||||
};
|
||||
|
||||
const fetchDirectories = (dir: string): string[] => {
|
||||
const result: string[] = [];
|
||||
for (const item of fs.readdirSync(dir)) {
|
||||
try {
|
||||
const stat = fs.statSync(path.join(dir, item));
|
||||
if (stat.isDirectory()) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// some item cannot be checked by isDirectory(), swallow error and continue the loop
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
|
@ -1,61 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { IpcMainInvokeEvent } from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import { GetInterfaceDefinitionParameters, MODEL_PARSE_ERROR } from '../interfaces/modelRepositoryInterface';
|
||||
|
||||
// tslint:disable-next-line: cyclomatic-complexity
|
||||
export const onGetInterfaceDefinition = (event: IpcMainInvokeEvent, { interfaceId, path }: GetInterfaceDefinitionParameters): object | undefined => {
|
||||
if (!interfaceId || !path) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const fileNames = fs.readdirSync(path);
|
||||
const parseErrors: string[] = [];
|
||||
for (const fileName of fileNames) {
|
||||
if (!isFileExtensionJson(fileName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const definition = getInterfaceDefinition(path, fileName, interfaceId);
|
||||
if (definition) {
|
||||
return definition;
|
||||
}
|
||||
} catch {
|
||||
parseErrors.push(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
if (parseErrors.length > 0) {
|
||||
const error = new Error(MODEL_PARSE_ERROR);
|
||||
// tslint:disable-next-line: no-any
|
||||
(error as any).fileNames = fileNames;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getInterfaceDefinition = (filePath: string, fileName: string, interfaceId: string): object | undefined => {
|
||||
const data = readFileFromLocal(filePath, fileName);
|
||||
const parsedData = JSON.parse(data);
|
||||
if (Array.isArray(parsedData)) {
|
||||
for (const pd of parsedData) {
|
||||
if (pd['@id']?.toString() === interfaceId) {
|
||||
return pd;
|
||||
}
|
||||
}
|
||||
} else if (parsedData['@id']?.toString() === interfaceId) {
|
||||
return parsedData;
|
||||
}
|
||||
};
|
||||
|
||||
const readFileFromLocal = (filePath: string, fileName: string) => {
|
||||
return fs.readFileSync(`${filePath}/${fileName}`, 'utf-8');
|
||||
};
|
||||
|
||||
const isFileExtensionJson = (fileName: string) => {
|
||||
const i = fileName.lastIndexOf('.');
|
||||
return i > 0 && fileName.substr(i) === '.json';
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
export interface GetDirectoriesParameters {
|
||||
path: string;
|
||||
}
|
||||
export interface DirectoryInterface {
|
||||
getDirectories(params: GetDirectoriesParameters): Promise<string[]>;
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
export const MODEL_PARSE_ERROR = 'modelParseError';
|
||||
export interface ModelParseErrorData {
|
||||
fileNames: string[];
|
||||
}
|
||||
|
||||
export interface GetDirectoriesParameters {
|
||||
path: string;
|
||||
}
|
||||
export interface GetInterfaceDefinitionParameters {
|
||||
interfaceId: string;
|
||||
path: string;
|
||||
}
|
||||
export interface ModelRepositoryInterface {
|
||||
getInterfaceDefinition(params: GetInterfaceDefinitionParameters): Promise<object | undefined>;
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { GetInterfaceDefinitionParameters, ModelRepositoryInterface } from '../../../../public/interfaces/modelRepositoryInterface';
|
||||
import { DirectoryInterface, GetDirectoriesParameters } from '../../../../public/interfaces/directoryInterface';
|
||||
import { READ_FILE, CONTROLLER_API_ENDPOINT, DataPlaneStatusCode, GET_DIRECTORIES, DEFAULT_DIRECTORY } from '../../constants/apiConstants';
|
||||
import { ModelDefinitionNotFound } from '../models/modelDefinitionNotFoundError';
|
||||
import { ModelDefinitionNotValidJsonError } from '../models/modelDefinitionNotValidJsonError';
|
||||
|
||||
export class LocalRepoServiceHandler implements DirectoryInterface, ModelRepositoryInterface {
|
||||
public getDirectories = async (params: GetDirectoriesParameters): Promise<string[]> => {
|
||||
const response = await fetch(`${CONTROLLER_API_ENDPOINT}${GET_DIRECTORIES}/${encodeURIComponent(params.path)}`);
|
||||
if (params.path === DEFAULT_DIRECTORY) {
|
||||
// only possible when platform is windows, expecting drives to be returned
|
||||
const responseText = await response.text();
|
||||
const drives = responseText.split(/\r\n/).map(drive => drive.trim()).filter(drive => drive !== '');
|
||||
drives.shift(); // remove header
|
||||
return drives.map(drive => `${drive}/`); // add trailing slash for drives
|
||||
}
|
||||
else {
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
public getInterfaceDefinition = async (params: GetInterfaceDefinitionParameters): Promise<object | undefined> => {
|
||||
const response = await fetch(`${CONTROLLER_API_ENDPOINT}${READ_FILE}/${encodeURIComponent(params.path)}/${encodeURIComponent(params.interfaceId)}`);
|
||||
if (await response.status === DataPlaneStatusCode.NoContentSuccess || response.status === DataPlaneStatusCode.InternalServerError) {
|
||||
throw new ModelDefinitionNotFound();
|
||||
}
|
||||
if (await response.status === DataPlaneStatusCode.NotFound) {
|
||||
throw new ModelDefinitionNotValidJsonError(await response.text());
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
}
|
|
@ -2,96 +2,87 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { DEFAULT_DIRECTORY } from './../../constants/apiConstants';
|
||||
import { fetchLocalFile, fetchDirectories } from './localRepoService';
|
||||
import { CONTROLLER_API_ENDPOINT, GET_DIRECTORIES, READ_FILE, READ_FILE_NAIVE } from './../../constants/apiConstants';
|
||||
import { fetchLocalFile, fetchDirectories, fetchLocalFileNaive } from './localRepoService';
|
||||
import { ModelDefinitionNotFound } from '../models/modelDefinitionNotFoundError';
|
||||
import { ModelDefinitionNotValidJsonError } from '../models/modelDefinitionNotValidJsonError';
|
||||
import { MODEL_PARSE_ERROR } from '../../../../public/interfaces/modelRepositoryInterface';
|
||||
import * as interfaceUtils from '../shared/interfaceUtils';
|
||||
|
||||
describe('fetchLocalFile', () => {
|
||||
it('returns file content when response is 200', async () => {
|
||||
// tslint:disable
|
||||
const content = {
|
||||
"@id": "urn:FlyYing:EnvironmentalSensor;1",
|
||||
"@type": "Interface",
|
||||
"@context": "https://azureiot.com/v1/contexts/IoTModel.json",
|
||||
displayName: "Environmental Sensor",
|
||||
description: "Provides functionality to report temperature, humidity. Provides telemetry, commands and read-write properties",
|
||||
comment: "Requires temperature and humidity sensors.",
|
||||
contents: [
|
||||
{
|
||||
"@type": "Property",
|
||||
displayName: "Device State",
|
||||
description: "The state of the device. Two states online/offline are available.",
|
||||
name: "state",
|
||||
schema: "boolean"
|
||||
}
|
||||
],
|
||||
};
|
||||
describe('localRepoService', () => {
|
||||
describe('fetchLocalFile', () => {
|
||||
it('calls fetch with expected params', async () => {
|
||||
// tslint:disable
|
||||
const response = {
|
||||
json: () => {
|
||||
return { headers:{}, body:{}}
|
||||
},
|
||||
status: 200
|
||||
} as any;
|
||||
// tslint:enable
|
||||
jest.spyOn(window, 'fetch').mockResolvedValue(response);
|
||||
|
||||
const getInterfaceDefinition = jest.fn().mockResolvedValue(content);
|
||||
jest.spyOn(interfaceUtils, 'getLocalModelRepositoryInterface').mockReturnValue({
|
||||
getInterfaceDefinition
|
||||
});
|
||||
|
||||
const result = await fetchLocalFile('f:', 'test.json');
|
||||
expect(getInterfaceDefinition).toHaveBeenCalledWith({ path: 'f:', interfaceId: 'test.json' });
|
||||
expect(result).toEqual(content);
|
||||
});
|
||||
|
||||
it('throws error when getInterfaceDefinition throws', async () => {
|
||||
const error = new Error('bad');
|
||||
const getInterfaceDefinition = jest.fn().mockImplementation(() => {throw error})
|
||||
jest.spyOn(interfaceUtils, 'getLocalModelRepositoryInterface').mockReturnValue({
|
||||
getInterfaceDefinition
|
||||
});
|
||||
|
||||
try {
|
||||
await fetchLocalFile('f:', 'test.json');
|
||||
expect(true).toEqual(false);
|
||||
} catch(e) {
|
||||
expect(e instanceof ModelDefinitionNotFound).toEqual(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('throws ModelDefinitionError on MODEL_PARSE_ERROR', async () => {
|
||||
const fileNames = ['file1', 'file2'];
|
||||
const getInterfaceDefinition = jest.fn().mockImplementation(() => {throw { message: MODEL_PARSE_ERROR, data: { fileNames }}})
|
||||
jest.spyOn(interfaceUtils, 'getLocalModelRepositoryInterface').mockReturnValue({
|
||||
getInterfaceDefinition
|
||||
});
|
||||
|
||||
try {
|
||||
await fetchLocalFile('f:', 'test.json');
|
||||
expect(true).toEqual(false);
|
||||
} catch(e) {
|
||||
expect(e instanceof ModelDefinitionNotValidJsonError).toEqual(true);
|
||||
expect(e.message).toEqual(JSON.stringify(fileNames));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchDirectories', () => {
|
||||
it('calls api.fetchDirectories with expected parameters when path is falsy', async () => {
|
||||
const getDirectories = jest.fn().mockResolvedValue(['a','b','c']);
|
||||
jest.spyOn(interfaceUtils, 'getDirectoryInterface').mockReturnValue({
|
||||
getDirectories
|
||||
expect(fetch).toBeCalledWith(`${CONTROLLER_API_ENDPOINT}${READ_FILE}/${encodeURIComponent('f:')}/${encodeURIComponent('test.json')}`);
|
||||
});
|
||||
|
||||
const result = await fetchDirectories('');
|
||||
expect(result).toEqual(['a', 'b', 'c']);
|
||||
expect(getDirectories).toHaveBeenCalledWith({ path: DEFAULT_DIRECTORY });
|
||||
it('throws ModelDefinitionNotFound when response status is 500', async () => {
|
||||
// tslint:disable
|
||||
const response = {
|
||||
json: () => {return {
|
||||
body: {},
|
||||
headers:{}
|
||||
}},
|
||||
status: 500
|
||||
} as any;
|
||||
// tslint:enable
|
||||
jest.spyOn(window, 'fetch').mockResolvedValue(response);
|
||||
await expect(fetchLocalFile('f:', 'test.json')).rejects.toThrow(new ModelDefinitionNotFound()).catch();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls api.fetchDirectories with expected parameters when path is truthy', async () => {
|
||||
const getDirectories = jest.fn().mockResolvedValue(['a','b','c']);
|
||||
jest.spyOn(interfaceUtils, 'getDirectoryInterface').mockReturnValue({
|
||||
getDirectories
|
||||
describe('fetchLocalFileNaive', () => {
|
||||
it('calls fetch with expected params', async () => {
|
||||
// tslint:disable
|
||||
const response = {
|
||||
json: () => {
|
||||
return { headers:{}, body:{}}
|
||||
},
|
||||
status: 200
|
||||
} as any;
|
||||
// tslint:enable
|
||||
jest.spyOn(window, 'fetch').mockResolvedValue(response);
|
||||
|
||||
await fetchLocalFileNaive('f:', 'test.json');
|
||||
expect(fetch).toBeCalledWith(`${CONTROLLER_API_ENDPOINT}${READ_FILE_NAIVE}/${encodeURIComponent('f:')}/${encodeURIComponent('test.json')}`);
|
||||
});
|
||||
|
||||
const result = await fetchDirectories('path');
|
||||
expect(result).toEqual(['a', 'b', 'c']);
|
||||
expect(getDirectories).toHaveBeenCalledWith({ path: 'path'});
|
||||
it('throws ModelDefinitionNotFound when response status is 500', async () => {
|
||||
// tslint:disable
|
||||
const response = {
|
||||
json: () => {return {
|
||||
body: {},
|
||||
headers:{}
|
||||
}},
|
||||
status: 500
|
||||
} as any;
|
||||
// tslint:enable
|
||||
jest.spyOn(window, 'fetch').mockResolvedValue(response);
|
||||
await expect(fetchLocalFileNaive('f:', 'test.json')).rejects.toThrow(new ModelDefinitionNotFound()).catch();
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchDirectories', () => {
|
||||
it('calls fetch with expected params', async () => {
|
||||
// tslint:disable
|
||||
const response = {
|
||||
json: () => {
|
||||
return { headers:{}, body:{}}
|
||||
},
|
||||
status: 200
|
||||
} as any;
|
||||
// tslint:enable
|
||||
jest.spyOn(window, 'fetch').mockResolvedValue(response);
|
||||
|
||||
await fetchDirectories('f:');
|
||||
expect(fetch).toBeCalledWith(`${CONTROLLER_API_ENDPOINT}${GET_DIRECTORIES}/${encodeURIComponent('f:')}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,38 +2,42 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { DEFAULT_DIRECTORY } from './../../constants/apiConstants';
|
||||
import { CONTROLLER_API_ENDPOINT, DataPlaneStatusCode, DEFAULT_DIRECTORY, GET_DIRECTORIES, READ_FILE, READ_FILE_NAIVE } from './../../constants/apiConstants';
|
||||
import { ModelDefinitionNotFound } from '../models/modelDefinitionNotFoundError';
|
||||
import { ModelDefinitionNotValidJsonError } from '../models/modelDefinitionNotValidJsonError';
|
||||
import { MODEL_PARSE_ERROR, ModelParseErrorData } from '../../../../public/interfaces/modelRepositoryInterface';
|
||||
import { ContextBridgeError } from '../../../../public/utils/errorHelper';
|
||||
import { getLocalModelRepositoryInterface, getDirectoryInterface } from '../shared/interfaceUtils';
|
||||
|
||||
export const fetchLocalFile = async (path: string, fileName: string): Promise<object> => {
|
||||
const api = getLocalModelRepositoryInterface();
|
||||
|
||||
try {
|
||||
const result = await api.getInterfaceDefinition({
|
||||
interfaceId: fileName,
|
||||
path
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new ModelDefinitionNotFound();
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error.message === MODEL_PARSE_ERROR) {
|
||||
const fileNames = (error as ContextBridgeError<ModelParseErrorData>).data.fileNames;
|
||||
throw new ModelDefinitionNotValidJsonError(JSON.stringify(fileNames));
|
||||
}
|
||||
|
||||
const response = await fetch(`${CONTROLLER_API_ENDPOINT}${READ_FILE}/${encodeURIComponent(path)}/${encodeURIComponent(fileName)}`);
|
||||
if (await response.status === DataPlaneStatusCode.NoContentSuccess || response.status === DataPlaneStatusCode.InternalServerError) {
|
||||
throw new ModelDefinitionNotFound();
|
||||
}
|
||||
if (await response.status === DataPlaneStatusCode.NotFound) {
|
||||
throw new ModelDefinitionNotValidJsonError(await response.text());
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const fetchLocalFileNaive = async (path: string, fileName: string): Promise<object> => {
|
||||
const response = await fetch(`${CONTROLLER_API_ENDPOINT}${READ_FILE_NAIVE}/${encodeURIComponent(path)}/${encodeURIComponent(fileName)}`);
|
||||
if (await response.status === DataPlaneStatusCode.NoContentSuccess ||
|
||||
response.status === DataPlaneStatusCode.InternalServerError ||
|
||||
response.status === DataPlaneStatusCode.NotFound)
|
||||
{
|
||||
throw new ModelDefinitionNotFound();
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const fetchDirectories = async (path: string): Promise<string[]> => {
|
||||
const api = getDirectoryInterface();
|
||||
const result = await api.getDirectories({path: path || DEFAULT_DIRECTORY});
|
||||
return result;
|
||||
const response = await fetch(`${CONTROLLER_API_ENDPOINT}${GET_DIRECTORIES}/${encodeURIComponent(path || DEFAULT_DIRECTORY)}`);
|
||||
if (!path) {
|
||||
// only possible when platform is windows, expecting drives to be returned
|
||||
const responseText = await response.text();
|
||||
const drives = responseText.split(/\r\n/).map(drive => drive.trim()).filter(drive => drive !== '');
|
||||
drives.shift(); // remove header
|
||||
return drives.map(drive => `${drive}/`); // add trailing slash for drives
|
||||
}
|
||||
else {
|
||||
return response.json();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -48,24 +48,3 @@ describe('browserSettingsApi', () => {
|
|||
expect(result).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDirectoryInterface', () => {
|
||||
it('calls expected factory when mode is electron', () => {
|
||||
appConfig.hostMode = HostMode.Electron;
|
||||
const factory = jest.spyOn(interfaceUtils, 'getElectronInterface');
|
||||
|
||||
interfaceUtils.getDirectoryInterface()
|
||||
expect(factory).toHaveBeenLastCalledWith(API_INTERFACES.DIRECTORY);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLocalModelRepositoryInterface', () => {
|
||||
it('calls expected factory when mode is electron', () => {
|
||||
appConfig.hostMode = HostMode.Electron;
|
||||
const factory = jest.spyOn(interfaceUtils, 'getElectronInterface');
|
||||
|
||||
interfaceUtils.getLocalModelRepositoryInterface()
|
||||
expect(factory).toHaveBeenLastCalledWith(API_INTERFACES.MODEL_DEFINITION);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -3,13 +3,10 @@
|
|||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import { SettingsInterface } from '../../../../public/interfaces/settingsInterface';
|
||||
import { DirectoryInterface } from '../../../../public/interfaces/directoryInterface';
|
||||
import { ModelRepositoryInterface } from '../../../../public/interfaces/modelRepositoryInterface';
|
||||
import { AuthenticationInterface } from './../../../../public/interfaces/authenticationInterface';
|
||||
import { API_INTERFACES } from '../../../../public/constants';
|
||||
import { appConfig, HostMode } from '../../../appConfig/appConfig';
|
||||
import { HIGH_CONTRAST } from '../../constants/browserStorage';
|
||||
import { LocalRepoServiceHandler } from '../handlers/localRepoServiceHandler';
|
||||
import { PublicDigitalTwinsModelRepoHelper, PublicDigitalTwinsModelInterface } from '../services/publicDigitalTwinsModelRepoHelper';
|
||||
|
||||
export const NOT_AVAILABLE = 'Feature is not available in this configuration';
|
||||
|
@ -29,22 +26,6 @@ export const getSettingsInterfaceForBrowser = (): SettingsInterface => {
|
|||
});
|
||||
};
|
||||
|
||||
export const getLocalModelRepositoryInterface = (): ModelRepositoryInterface => {
|
||||
if (appConfig.hostMode !== HostMode.Electron) {
|
||||
return new LocalRepoServiceHandler();
|
||||
}
|
||||
|
||||
return getElectronInterface(API_INTERFACES.MODEL_DEFINITION);
|
||||
};
|
||||
|
||||
export const getDirectoryInterface = (): DirectoryInterface => {
|
||||
if (appConfig.hostMode !== HostMode.Electron) {
|
||||
return new LocalRepoServiceHandler();
|
||||
}
|
||||
|
||||
return getElectronInterface(API_INTERFACES.DIRECTORY);
|
||||
};
|
||||
|
||||
export const getPublicDigitalTwinsModelInterface = (): PublicDigitalTwinsModelInterface => {
|
||||
return new PublicDigitalTwinsModelRepoHelper();
|
||||
};
|
||||
|
|
|
@ -84,7 +84,7 @@ describe('ConnectionStringEdit', () => {
|
|||
};
|
||||
|
||||
const wrapper = mount(<ConnectionStringEditView {...props}/>);
|
||||
act(() => wrapper.find(TextField).props().onChange(undefined, 'badConnectionString'));
|
||||
act(() => wrapper.find(TextField).props().onChange?.(undefined as any, 'badConnectionString'));
|
||||
wrapper.update();
|
||||
|
||||
const disabled = wrapper.find(PrimaryButton).props().disabled;
|
||||
|
@ -100,7 +100,7 @@ describe('ConnectionStringEdit', () => {
|
|||
};
|
||||
|
||||
const wrapper = mount(<ConnectionStringEditView {...props}/>);
|
||||
act(() => wrapper.find(TextField).props().onChange(undefined, connectionString));
|
||||
act(() => wrapper.find(TextField).props().onChange?.(undefined as any, connectionString));
|
||||
wrapper.update();
|
||||
|
||||
const disabled = wrapper.find(PrimaryButton).props().disabled;
|
||||
|
@ -117,12 +117,12 @@ describe('ConnectionStringEdit', () => {
|
|||
};
|
||||
|
||||
const wrapper = mount(<ConnectionStringEditView {...props}/>);
|
||||
act(() => wrapper.find(TextField).props().onChange(undefined, connectionString));
|
||||
act(() => wrapper.find(TextField).props().onChange?.(undefined as any, connectionString));
|
||||
wrapper.update();
|
||||
|
||||
const commitButton = wrapper.find(PrimaryButton);
|
||||
expect(commitButton.props().disabled).toEqual(false);
|
||||
commitButton.props().onClick(undefined);
|
||||
commitButton.props().onClick?.(undefined as any);
|
||||
|
||||
expect(onCommit).toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -20,7 +20,6 @@ import { AppInsightsClient } from '../../shared/appTelemetry/appInsightsClient';
|
|||
import { TELEMETRY_PAGE_NAMES } from '../../constants/telemetry';
|
||||
import { useConnectionStringContext } from '../context/connectionStringStateContext';
|
||||
import { ConnectionStringCommandBar } from './commandBar';
|
||||
import '../../css/_layouts.scss';
|
||||
import './connectionStringsView.scss';
|
||||
|
||||
// tslint:disable-next-line: cyclomatic-complexity
|
||||
|
|
|
@ -11,6 +11,7 @@ export const EVENTHUB = '/EventHub';
|
|||
export const MODELREPO = '/ModelRepo';
|
||||
|
||||
export const READ_FILE = '/ReadFile';
|
||||
export const READ_FILE_NAIVE = '/ReadFileNaive';
|
||||
export const GET_DIRECTORIES = '/Directories';
|
||||
export const DEFAULT_DIRECTORY = '$DEFAULT';
|
||||
|
||||
|
|
|
@ -6,5 +6,5 @@ export enum REPOSITORY_LOCATION_TYPE {
|
|||
Configurable = 'CONFIGURABLE',
|
||||
Public = 'PUBLIC',
|
||||
Local = 'LOCAL',
|
||||
LocalDMR = 'LOCAL_DMR'
|
||||
LocalDMR = 'DMR'
|
||||
}
|
||||
|
|
|
@ -40,12 +40,12 @@ describe('addDevice', () => {
|
|||
|
||||
// uncheck auto generate
|
||||
const autoGenerateButton = wrapper.find('.autoGenerateButton').first();
|
||||
act(() => autoGenerateButton.props().onChange(undefined));
|
||||
act(() => autoGenerateButton.props().onChange?.(undefined as any));
|
||||
wrapper.update();
|
||||
expect(wrapper.find(MaskedCopyableTextField).length).toEqual(3); // tslint:disable-line:no-magic-numbers
|
||||
|
||||
act(() => wrapper.find(MaskedCopyableTextField).at(1).props().onTextChange('test-key1'));
|
||||
act(() => wrapper.find(MaskedCopyableTextField).at(2).props().onTextChange('test-key2')); // tslint:disable-line:no-magic-numbers
|
||||
act(() => wrapper.find(MaskedCopyableTextField).at(1).props().onTextChange?.('test-key1'));
|
||||
act(() => wrapper.find(MaskedCopyableTextField).at(2).props().onTextChange?.('test-key2')); // tslint:disable-line:no-magic-numbers
|
||||
wrapper.update();
|
||||
const fields = wrapper.find(MaskedCopyableTextField);
|
||||
expect(fields.at(1).props().value).toEqual('test-key1');
|
||||
|
@ -58,12 +58,12 @@ describe('addDevice', () => {
|
|||
const wrapper = mount(<AddDevice/>);
|
||||
|
||||
const choiceGroup = wrapper.find(ChoiceGroup).first();
|
||||
act(() => choiceGroup.props().onChange(undefined, { key: DeviceAuthenticationType.SelfSigned, text: 'text' } ));
|
||||
act(() => choiceGroup.props().onChange?.(undefined as any, { key: DeviceAuthenticationType.SelfSigned, text: 'text' } ));
|
||||
wrapper.update();
|
||||
expect(wrapper.find(MaskedCopyableTextField).length).toEqual(3); // tslint:disable-line:no-magic-numbers
|
||||
|
||||
act(() => wrapper.find(MaskedCopyableTextField).at(1).props().onTextChange('test-thumbprint1'));
|
||||
act(() => wrapper.find(MaskedCopyableTextField).last().props().onTextChange('test-thumbprint2'));
|
||||
act(() => wrapper.find(MaskedCopyableTextField).at(1).props().onTextChange?.('test-thumbprint1'));
|
||||
act(() => wrapper.find(MaskedCopyableTextField).last().props().onTextChange?.('test-thumbprint2'));
|
||||
wrapper.update();
|
||||
const fields = wrapper.find(MaskedCopyableTextField);
|
||||
expect(fields.at(1).props().value).toEqual('test-thumbprint1');
|
||||
|
@ -76,8 +76,8 @@ describe('addDevice', () => {
|
|||
const addDeviceActionSpy = jest.spyOn(addDeviceAction, 'started');
|
||||
const wrapper = mount(<AddDevice/>);
|
||||
|
||||
act(() => wrapper.find(MaskedCopyableTextField).first().props().onTextChange('test-device'));
|
||||
act(() => wrapper.find(Toggle).first().props().onChange(undefined, false));
|
||||
act(() => wrapper.find(MaskedCopyableTextField).first().props().onTextChange?.('test-device'));
|
||||
act(() => wrapper.find(Toggle).first().props().onChange?.(undefined as any, false));
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find(MaskedCopyableTextField).first().props().value).toEqual('test-device');
|
||||
|
|
|
@ -21,10 +21,9 @@ import { addDeviceReducer } from '../reducer';
|
|||
import { addDeviceStateInitial } from '../state';
|
||||
import { addDeviceAction } from '../actions';
|
||||
import { ROUTE_PARTS, ROUTE_PARAMS } from '../../../constants/routes';
|
||||
import '../../../css/_addDevice.scss';
|
||||
import '../../../css/_layouts.scss';
|
||||
import { AppInsightsClient } from '../../../shared/appTelemetry/appInsightsClient';
|
||||
import { TELEMETRY_USER_ACTIONS } from '../../../constants/telemetry';
|
||||
import '../../../css/_addDevice.scss';
|
||||
|
||||
const initialKeyValue = {
|
||||
error: '',
|
||||
|
|
|
@ -32,7 +32,7 @@ describe('cloudToDeviceMessage', () => {
|
|||
const mockSendCloudToDeviceMessageSpy = jest.spyOn(cloudToDeviceMessageAction, 'started');
|
||||
const wrapper = mount(<CloudToDeviceMessage/>);
|
||||
const bodyTextField = wrapper.find(TextField).first();
|
||||
act(() => bodyTextField.props().onChange(undefined, 'hello world'));
|
||||
act(() => bodyTextField.props().onChange?.(undefined as any, 'hello world'));
|
||||
wrapper.update();
|
||||
const commandBar = wrapper.find(CommandBar).first();
|
||||
act(() => commandBar.props().items[0].onClick());
|
||||
|
@ -44,7 +44,7 @@ describe('cloudToDeviceMessage', () => {
|
|||
const mockSendCloudToDeviceMessageSpy = jest.spyOn(cloudToDeviceMessageAction, 'started');
|
||||
const wrapper = mount(<CloudToDeviceMessage/>);
|
||||
const checkbox = wrapper.find(Checkbox).first();
|
||||
act(() => checkbox.props().onChange(undefined, true));
|
||||
act(() => checkbox.props().onChange?.(undefined as any, true));
|
||||
wrapper.update();
|
||||
const commandBar = wrapper.find(CommandBar).first();
|
||||
const currentTime = new Date().toLocaleString();
|
||||
|
@ -62,12 +62,12 @@ describe('cloudToDeviceMessage', () => {
|
|||
wrapper.update();
|
||||
|
||||
const keyInput = wrapper.find(TextField).at(1);
|
||||
act(() => keyInput.props().onChange(undefined, 'foo'));
|
||||
act(() => keyInput.props().onChange?.(undefined as any, 'foo'));
|
||||
wrapper.update();
|
||||
expect(wrapper.find(TextField).at(1).props().value).toEqual('foo');
|
||||
|
||||
const valueInput = wrapper.find(TextField).at(2);
|
||||
act(() => valueInput.props().onChange(undefined, 'bar'));
|
||||
act(() => valueInput.props().onChange?.(undefined as any, 'bar'));
|
||||
wrapper.update();
|
||||
expect(wrapper.find(TextField).at(2).props().value).toEqual('bar');
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ describe('ConsumerGroup', () => {
|
|||
const setConsumerGroup = jest.fn();
|
||||
const wrapper = mount(<ConsumerGroup monitoringData={false} consumerGroup={''} setConsumerGroup={setConsumerGroup}/>);
|
||||
const textField = wrapper.find(TextField).first();
|
||||
act(() => textField.props().onChange(undefined, 'testGroup'));
|
||||
act(() => textField.props().onChange?.(undefined as any, 'testGroup'));
|
||||
expect(setConsumerGroup).toBeCalledWith('testGroup');
|
||||
});
|
||||
});
|
|
@ -43,11 +43,11 @@ describe('customEventHub', () => {
|
|||
setHasError={jest.fn()}
|
||||
/>);
|
||||
|
||||
act(() => wrapper.find(Toggle).first().props().onChange(undefined, false));
|
||||
act(() => wrapper.find(Toggle).first().props().onChange?.(undefined as any, false));
|
||||
wrapper.update();
|
||||
expect(mockSetUseBuiltInEventHub).toBeCalledWith(true);
|
||||
|
||||
act(() => wrapper.find(TextField).first().props().onChange(undefined, 'connectionString'));
|
||||
act(() => wrapper.find(TextField).first().props().onChange?.(undefined as any, 'connectionString'));
|
||||
wrapper.update();
|
||||
expect(mockSetCustomEventHubConnectionString).toBeCalledWith('connectionString');
|
||||
});
|
||||
|
|
|
@ -43,11 +43,11 @@ describe('startTime', () => {
|
|||
setHasError={MockSetHasError}
|
||||
/>);
|
||||
|
||||
act(() => wrapper.find(Toggle).first().props().onChange(undefined, false));
|
||||
act(() => wrapper.find(Toggle).first().props().onChange?.(undefined as any, false));
|
||||
wrapper.update();
|
||||
expect(mockSetSpecifyStartTime).toBeCalledWith(false);
|
||||
|
||||
act(() => wrapper.find(TextField).props().onChange(undefined, '2020/12/16/12/58/00'));
|
||||
act(() => wrapper.find(TextField).props().onChange?.(undefined as any, '2020/12/16/12/58/00'));
|
||||
wrapper.update();
|
||||
expect(mockSetStartTime).toBeCalledTimes(2);
|
||||
expect(mockSetStartTime.mock.calls[1][0]).toEqual(new Date(2020, 11, 16, 12, 58, 0));
|
||||
|
|
|
@ -27,9 +27,8 @@ import { DeviceIdentity } from '../../../api/models/deviceIdentity';
|
|||
import { Pnp } from '../../pnp/components/pnp';
|
||||
import { DeviceModules } from './deviceModules';
|
||||
import { CollapsibleButton } from '../../../shared/components/collapsibleButton';
|
||||
import '../../../css/_deviceContent.scss';
|
||||
import '../../../css/_layouts.scss';
|
||||
import { DeviceEventsStateContextProvider } from '../../deviceEvents/context/deviceEventsStateProvider';
|
||||
import '../../../css/_deviceContent.scss';
|
||||
|
||||
export const DeviceContent: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
|
|
@ -132,7 +132,7 @@ describe('deviceIdentity', () => {
|
|||
}
|
||||
})
|
||||
);
|
||||
act(() => wrapper.find(Toggle).first().props().onChange(undefined, false));
|
||||
act(() => wrapper.find(Toggle).first().props().onChange?.(undefined as any, false));
|
||||
wrapper.update();
|
||||
|
||||
const commandBar = wrapper.find(CommandBar).first();
|
||||
|
|
|
@ -25,7 +25,6 @@ import { SynchronizationStatus } from '../../../api/models/synchronizationStatus
|
|||
import { AppInsightsClient } from '../../../shared/appTelemetry/appInsightsClient';
|
||||
import { TELEMETRY_PAGE_NAMES } from '../../../../app/constants/telemetry';
|
||||
import '../../../css/_deviceList.scss';
|
||||
import '../../../css/_layouts.scss';
|
||||
|
||||
const SHIMMER_COUNT = 10;
|
||||
export const DeviceList: React.FC = () => {
|
||||
|
|
|
@ -32,7 +32,7 @@ describe('DeviceListQuery', () => {
|
|||
it('sets device id', () => {
|
||||
const wrapper = mount(getComponent());
|
||||
const textField = wrapper.find(TextField).first();
|
||||
act(() => textField.props().onChange(undefined, 'testDevice'));
|
||||
act(() => textField.props().onChange?.(undefined as any, 'testDevice'));
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find(TextField).props().value).toEqual('testDevice');
|
||||
|
|
|
@ -17,7 +17,7 @@ describe('directMethodForm', () => {
|
|||
const mockResponseTimeOut = jest.fn();
|
||||
const wrapper = mount(
|
||||
<DirectMethodForm
|
||||
methodName={undefined}
|
||||
methodName={undefined as any}
|
||||
connectionTimeOut={0}
|
||||
responseTimeOut={0}
|
||||
setMethodName={mockSetMethodName}
|
||||
|
@ -26,20 +26,20 @@ describe('directMethodForm', () => {
|
|||
setResponseTimeOut={mockResponseTimeOut}
|
||||
/>);
|
||||
|
||||
act(() => wrapper.find(TextField).first().props().onChange(undefined, 'testMethod'));
|
||||
act(() => wrapper.find(TextField).first().props().onChange?.(undefined as any, 'testMethod'));
|
||||
wrapper.update();
|
||||
expect(mockSetMethodName).toBeCalledWith('testMethod');
|
||||
|
||||
act(() => wrapper.find(TextField).at(1).props().onChange(undefined, 'payload'));
|
||||
act(() => wrapper.find(TextField).at(1).props().onChange?.(undefined as any, 'payload'));
|
||||
wrapper.update();
|
||||
expect(mockSetPayload).toBeCalledWith('payload');
|
||||
|
||||
act(() => wrapper.find(Slider).first().props().onChange(10));
|
||||
act(() => wrapper.find(Slider).first().props().onChange?.(10));
|
||||
wrapper.update();
|
||||
expect(mockConnectionTimeOut).toBeCalledWith(10);
|
||||
expect(mockResponseTimeOut).toBeCalledWith(10);
|
||||
|
||||
act(() => wrapper.find(Slider).at(1).props().onChange(20));
|
||||
act(() => wrapper.find(Slider).at(1).props().onChange?.(20));
|
||||
wrapper.update();
|
||||
expect(mockResponseTimeOut).toBeCalledWith(20);
|
||||
});
|
||||
|
|
|
@ -50,12 +50,12 @@ describe('devices/components/addModuleIdentity', () => {
|
|||
|
||||
// uncheck auto generate
|
||||
const autoGenerateButton = wrapper.find('.autoGenerateButton').first();
|
||||
act(() => autoGenerateButton.props().onChange(undefined));
|
||||
act(() => autoGenerateButton.props().onChange?.(undefined as any));
|
||||
wrapper.update();
|
||||
expect(wrapper.find(MaskedCopyableTextField).length).toEqual(3); // tslint:disable-line:no-magic-numbers
|
||||
|
||||
act(() => wrapper.find(MaskedCopyableTextField).at(1).props().onTextChange('test-key1'));
|
||||
act(() => wrapper.find(MaskedCopyableTextField).at(2).props().onTextChange('test-key2')); // tslint:disable-line:no-magic-numbers
|
||||
act(() => wrapper.find(MaskedCopyableTextField).at(1).props().onTextChange?.('test-key1'));
|
||||
act(() => wrapper.find(MaskedCopyableTextField).at(2).props().onTextChange?.('test-key2')); // tslint:disable-line:no-magic-numbers
|
||||
wrapper.update();
|
||||
const fields = wrapper.find(MaskedCopyableTextField);
|
||||
expect(fields.at(1).props().value).toEqual('test-key1');
|
||||
|
@ -68,12 +68,12 @@ describe('devices/components/addModuleIdentity', () => {
|
|||
const wrapper = mount(<AddModuleIdentity/>);
|
||||
|
||||
const choiceGroup = wrapper.find(ChoiceGroup).first();
|
||||
act(() => choiceGroup.props().onChange(undefined, { key: DeviceAuthenticationType.SelfSigned, text: 'text' } ));
|
||||
act(() => choiceGroup.props().onChange?.(undefined as any, { key: DeviceAuthenticationType.SelfSigned, text: 'text' } ));
|
||||
wrapper.update();
|
||||
expect(wrapper.find(MaskedCopyableTextField).length).toEqual(3); // tslint:disable-line:no-magic-numbers
|
||||
|
||||
act(() => wrapper.find(MaskedCopyableTextField).at(1).props().onTextChange('test-thumbprint1'));
|
||||
act(() => wrapper.find(MaskedCopyableTextField).last().props().onTextChange('test-thumbprint2'));
|
||||
act(() => wrapper.find(MaskedCopyableTextField).at(1).props().onTextChange?.('test-thumbprint1'));
|
||||
act(() => wrapper.find(MaskedCopyableTextField).last().props().onTextChange?.('test-thumbprint2'));
|
||||
wrapper.update();
|
||||
const fields = wrapper.find(MaskedCopyableTextField);
|
||||
expect(fields.at(1).props().value).toEqual('test-thumbprint1');
|
||||
|
|
|
@ -11,9 +11,8 @@ import {
|
|||
getModelDefinition,
|
||||
getModelDefinitionFromPublicRepo,
|
||||
getModelDefinitionFromLocalFile,
|
||||
validateModelDefinitionHelper,
|
||||
getFlattenedModel,
|
||||
getModelDefinitionFromConfigurableRepo,
|
||||
getModelDefinitionFromLocalDMR,
|
||||
expandFromExtendedModel
|
||||
} from './getModelDefinitionSaga';
|
||||
import { raiseNotificationToast } from '../../../notifications/components/notificationToast';
|
||||
|
@ -21,58 +20,19 @@ import { NotificationType } from '../../../api/models/notification';
|
|||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { getModelDefinitionAction, GetModelDefinitionActionParameters } from '../actions';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../../constants/repositoryLocationTypes';
|
||||
import { fetchLocalFile } from '../../../api/services/localRepoService';
|
||||
import { fetchLocalFile, fetchLocalFileNaive } from '../../../api/services/localRepoService';
|
||||
import { fetchModelDefinition } from '../../../api/services/publicDigitalTwinsModelRepoService';
|
||||
import { ModelDefinition } from './../../../api/models/modelDefinition';
|
||||
import { interfaceId, modelDefinition, modelDefinitionWithInlineComp, schemaId} from './testData';
|
||||
|
||||
describe('modelDefinition sagas', () => {
|
||||
describe('modelDefinition saga flow with no inline component', () => {
|
||||
const digitalTwinId = 'device_id';
|
||||
const interfaceId = 'urn:azureiot:ModelDiscovery:DigitalTwin:1';
|
||||
const params: GetModelDefinitionActionParameters = {
|
||||
digitalTwinId,
|
||||
interfaceId,
|
||||
locations: [{ repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public, value: '' }],
|
||||
};
|
||||
const action = getModelDefinitionAction.started(params);
|
||||
/* tslint:disable */
|
||||
const modelDefinition = {
|
||||
"@id": "urn:azureiot:ModelDiscovery:DigitalTwin:1",
|
||||
"@type": "Interface",
|
||||
"contents": [
|
||||
{
|
||||
"@type": "Property",
|
||||
"name": "modelInformation",
|
||||
"displayName": "Model Information",
|
||||
"description": "Providing model and optional interfaces information on a digital twin.",
|
||||
"schema": {
|
||||
"@type": "Object",
|
||||
"fields": [
|
||||
{
|
||||
"name": "modelId",
|
||||
"schema": "string"
|
||||
},
|
||||
{
|
||||
"name": "interfaces",
|
||||
"schema": {
|
||||
"@type": "Map",
|
||||
"mapKey": {
|
||||
"name": "name",
|
||||
"schema": "string"
|
||||
},
|
||||
"mapValue": {
|
||||
"name": "schema",
|
||||
"schema": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"@context": "https://azureiot.com/v1/contexts/Interface.json"
|
||||
};
|
||||
/* tslint:enable */
|
||||
|
||||
describe('getModelDefinitionSaga', () => {
|
||||
let getModelDefinitionSagaGenerator: SagaIteratorClone;
|
||||
|
@ -85,13 +45,10 @@ describe('modelDefinition sagas', () => {
|
|||
expect(getModelDefinitionSagaGenerator.next().value).toEqual(
|
||||
call(getModelDefinition, action, REPOSITORY_LOCATION_TYPE.Public)
|
||||
);
|
||||
|
||||
expect(getModelDefinitionSagaGenerator.next(modelDefinition).value).toEqual(
|
||||
call(validateModelDefinitionHelper, modelDefinition, REPOSITORY_LOCATION_TYPE.Public)
|
||||
);
|
||||
});
|
||||
|
||||
it('checks for extends', () => {
|
||||
expect(getModelDefinitionSagaGenerator.next(true).value).toEqual(
|
||||
expect(getModelDefinitionSagaGenerator.next(modelDefinition).value).toEqual(
|
||||
call(expandFromExtendedModel, action, REPOSITORY_LOCATION_TYPE.Public, modelDefinition)
|
||||
);
|
||||
});
|
||||
|
@ -166,6 +123,23 @@ describe('modelDefinition sagas', () => {
|
|||
expect(getModelDefinitionFromLocalFolderGenerator.next(modelDefinition).done).toEqual(true);
|
||||
});
|
||||
|
||||
describe('getModelDefinitionFromLocalDMR ', () => {
|
||||
const getModelDefinitionFromPublicRepoGenerator = cloneableGenerator(getModelDefinitionFromLocalDMR)(
|
||||
getModelDefinitionAction.started({
|
||||
digitalTwinId,
|
||||
interfaceId,
|
||||
locations: [{ repositoryLocationType: REPOSITORY_LOCATION_TYPE.LocalDMR, value: 'f:/dtmi' }],
|
||||
})
|
||||
);
|
||||
|
||||
expect(getModelDefinitionFromPublicRepoGenerator.next([interfaceId])).toEqual({
|
||||
done: false,
|
||||
value: call(fetchLocalFileNaive, 'f:/dtmi/com/example', 'thermostat-1.json')
|
||||
});
|
||||
|
||||
expect(getModelDefinitionFromPublicRepoGenerator.next(modelDefinition).done).toEqual(true);
|
||||
});
|
||||
|
||||
describe('getModelDefinition', () => {
|
||||
it('getModelDefinition from public repo', () => {
|
||||
const getModelDefinitionFromPublicRepoGenerator = cloneableGenerator(getModelDefinition)(action, REPOSITORY_LOCATION_TYPE.Public);
|
||||
|
@ -185,16 +159,11 @@ describe('modelDefinition sagas', () => {
|
|||
expect(getModelDefinitionFromDeviceGenerator.next().done).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFlattenedModel ', () => {
|
||||
expect(getFlattenedModel(modelDefinition, [interfaceId])).toEqual(modelDefinition);
|
||||
});
|
||||
});
|
||||
|
||||
describe('modelDefinition saga flow with inline component', () => {
|
||||
const digitalTwinId = 'device_id';
|
||||
const interfaceId = 'urn:azureiot:ModelDiscovery:DigitalTwin:1';
|
||||
const schemaId = 'dtmi:com:rido:inlineTests:inlineComp;2';
|
||||
const fullInterfaceId = `${interfaceId}/${schemaId}`;
|
||||
const params: GetModelDefinitionActionParameters = {
|
||||
digitalTwinId,
|
||||
|
@ -202,31 +171,7 @@ describe('modelDefinition sagas', () => {
|
|||
locations: [{ repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public, value: '' }],
|
||||
};
|
||||
const action = getModelDefinitionAction.started(params);
|
||||
/* tslint:disable */
|
||||
const modelDefinition: ModelDefinition = {
|
||||
"@context": "dtmi:dtdl:context;2",
|
||||
"@id": "urn:azureiot:ModelDiscovery:DigitalTwin:1",
|
||||
"@type": "Interface",
|
||||
"contents": [
|
||||
{
|
||||
"@id": "dtmi:com:rido:inlineComp;2",
|
||||
"@type": "Component",
|
||||
"name": "inLineComponent",
|
||||
"schema": {
|
||||
"@type": "Interface",
|
||||
"@id": schemaId,
|
||||
"contents": [
|
||||
{
|
||||
"@type" : "Property",
|
||||
"name" : "inlineProp",
|
||||
"schema" : "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
/* tslint:enable */
|
||||
const modelDefinition = modelDefinitionWithInlineComp;
|
||||
|
||||
describe('getModelDefinitionFromPublicRepo ', () => {
|
||||
const getModelDefinitionFromPublicRepoGenerator = cloneableGenerator(getModelDefinitionFromPublicRepo)(action);
|
||||
|
@ -277,9 +222,5 @@ describe('modelDefinition sagas', () => {
|
|||
|
||||
expect(getModelDefinitionFromConfigurableRepoGenerator.next(modelDefinition).done).toEqual(true);
|
||||
});
|
||||
|
||||
describe('getFlattenedModel ', () => {
|
||||
expect(getFlattenedModel(modelDefinition, [interfaceId, schemaId])).toEqual(modelDefinition.contents[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,11 +11,11 @@ import { NotificationType } from '../../../api/models/notification';
|
|||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { FetchModelParameters } from '../../../api/parameters/repoParameters';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../../constants/repositoryLocationTypes';
|
||||
import { fetchLocalFile } from '../../../api/services/localRepoService';
|
||||
import { fetchLocalFile, fetchLocalFileNaive } from '../../../api/services/localRepoService';
|
||||
import { ModelDefinition } from '../../../api/models/modelDefinition';
|
||||
import { ModelDefinitionNotValidJsonError } from '../../../api/models/modelDefinitionNotValidJsonError';
|
||||
import { GetModelDefinitionActionParameters, getModelDefinitionAction } from '../actions';
|
||||
import { ModelIdCasingNotMatchingException } from '../../../shared/utils/exceptions/modelIdCasingNotMatchingException';
|
||||
import { checkModelIdCasing, getDmrParams, getFlattenedModel, getLocationSettingValue, getSplitInterfaceId } from './utils';
|
||||
|
||||
export function* getModelDefinitionSaga(action: Action<GetModelDefinitionActionParameters>): SagaIterator {
|
||||
const { locations: configurations, interfaceId } = action.payload;
|
||||
|
@ -23,12 +23,11 @@ export function* getModelDefinitionSaga(action: Action<GetModelDefinitionActionP
|
|||
for (const configuration of configurations) { // try to get model definition in order according to user's location settings
|
||||
try {
|
||||
const modelDefinition: ModelDefinition = yield call(getModelDefinition, action, configuration.repositoryLocationType);
|
||||
const isModelValid: boolean = yield call(validateModelDefinitionHelper, modelDefinition, configuration.repositoryLocationType);
|
||||
const extendedModel: ModelDefinition = yield call(expandFromExtendedModel, action, configuration.repositoryLocationType, modelDefinition);
|
||||
yield put(getModelDefinitionAction.done(
|
||||
{
|
||||
params: action.payload,
|
||||
result: { isModelValid, modelDefinition, extendedModel, source: configuration.repositoryLocationType }
|
||||
result: { isModelValid: true, modelDefinition, extendedModel, source: configuration.repositoryLocationType }
|
||||
}));
|
||||
break; // found the model definition, break
|
||||
}
|
||||
|
@ -64,33 +63,6 @@ export function* getModelDefinitionSaga(action: Action<GetModelDefinitionActionP
|
|||
}
|
||||
}
|
||||
|
||||
export function* validateModelDefinitionHelper(modelDefinition: ModelDefinition, location: REPOSITORY_LOCATION_TYPE): SagaIterator {
|
||||
return true; // commenting out validating model until it aligns with local parser
|
||||
}
|
||||
|
||||
export const getSplitInterfaceId = (fullName: string) => {
|
||||
// when component definition is inline, interfaceId is compose of parent file name and inline schema id concatenated with a slash
|
||||
return fullName.split('/');
|
||||
};
|
||||
|
||||
export const getFlattenedModel = (model: ModelDefinition, splitInterfaceId: string[]) => {
|
||||
if (splitInterfaceId.length === 1) {
|
||||
return model;
|
||||
}
|
||||
else {
|
||||
// for inline component, the flattened model is defined under contents array's with matching schema @id
|
||||
const components = model.contents.filter((content: any) => // tslint:disable-line: no-any
|
||||
content['@type'] === 'Component' && typeof content.schema !== 'string' && content.schema['@id'] === splitInterfaceId[1]);
|
||||
return components[0];
|
||||
}
|
||||
};
|
||||
|
||||
export const checkModelIdCasing = (model: ModelDefinition, id: string) => {
|
||||
if (model['@id'] !== id) {
|
||||
throw new ModelIdCasingNotMatchingException();
|
||||
}
|
||||
};
|
||||
|
||||
export function* getModelDefinitionFromPublicRepo(action: Action<GetModelDefinitionActionParameters>): SagaIterator {
|
||||
const splitInterfaceId = getSplitInterfaceId(action.payload.interfaceId);
|
||||
const parameters: FetchModelParameters = {
|
||||
|
@ -103,9 +75,7 @@ export function* getModelDefinitionFromPublicRepo(action: Action<GetModelDefinit
|
|||
}
|
||||
|
||||
export function* getModelDefinitionFromConfigurableRepo(action: Action<GetModelDefinitionActionParameters>): SagaIterator {
|
||||
const configurableRepoUrls = action.payload.locations.filter(location => location.repositoryLocationType === REPOSITORY_LOCATION_TYPE.Configurable);
|
||||
const configurableRepoUrl = configurableRepoUrls && configurableRepoUrls[0] && configurableRepoUrls[0].value || '';
|
||||
const url = configurableRepoUrl.replace(/\/$/, ''); // remove trailing slash
|
||||
const url = getLocationSettingValue(action.payload.locations, REPOSITORY_LOCATION_TYPE.Configurable);
|
||||
const splitInterfaceId = getSplitInterfaceId(action.payload.interfaceId);
|
||||
const parameters: FetchModelParameters = {
|
||||
id: splitInterfaceId[0],
|
||||
|
@ -118,18 +88,26 @@ export function* getModelDefinitionFromConfigurableRepo(action: Action<GetModelD
|
|||
}
|
||||
|
||||
export function* getModelDefinitionFromLocalFile(action: Action<GetModelDefinitionActionParameters>): SagaIterator {
|
||||
const localFolderPaths = action.payload.locations.filter(location => location.repositoryLocationType === REPOSITORY_LOCATION_TYPE.Local);
|
||||
const localFolderPath = localFolderPaths && localFolderPaths[0] && localFolderPaths[0].value || '';
|
||||
const path = localFolderPath.replace(/\/$/, ''); // remove trailing slash
|
||||
const path = getLocationSettingValue(action.payload.locations, REPOSITORY_LOCATION_TYPE.Local);
|
||||
const splitInterfaceId = getSplitInterfaceId(action.payload.interfaceId);
|
||||
const model = yield call(fetchLocalFile, path, splitInterfaceId[0]);
|
||||
return getFlattenedModel(model, splitInterfaceId);
|
||||
}
|
||||
|
||||
export function* getModelDefinitionFromLocalDMR(action: Action<GetModelDefinitionActionParameters>): SagaIterator {
|
||||
const path = getLocationSettingValue(action.payload.locations, REPOSITORY_LOCATION_TYPE.LocalDMR);
|
||||
const splitInterfaceId = getSplitInterfaceId(action.payload.interfaceId);
|
||||
const {folderPath, fileName} = getDmrParams(path, splitInterfaceId[0]);
|
||||
const model = yield call(fetchLocalFileNaive, folderPath, fileName);
|
||||
return getFlattenedModel(model, splitInterfaceId);
|
||||
}
|
||||
|
||||
export function* getModelDefinition(action: Action<GetModelDefinitionActionParameters>, location: REPOSITORY_LOCATION_TYPE): SagaIterator {
|
||||
switch (location) {
|
||||
case REPOSITORY_LOCATION_TYPE.Local:
|
||||
return yield call(getModelDefinitionFromLocalFile, action);
|
||||
case REPOSITORY_LOCATION_TYPE.LocalDMR:
|
||||
return yield call(getModelDefinitionFromLocalDMR, action);
|
||||
case REPOSITORY_LOCATION_TYPE.Configurable:
|
||||
return yield call(getModelDefinitionFromConfigurableRepo, action);
|
||||
default:
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/* tslint:disable */
|
||||
export const interfaceId = 'dtmi:com:example:Thermostat;1';
|
||||
export const modelDefinition = {
|
||||
"@id": "dtmi:com:example:Thermostat;1",
|
||||
"@type": "Interface",
|
||||
"contents": [
|
||||
{
|
||||
"@type": "Property",
|
||||
"name": "modelInformation",
|
||||
"displayName": "Model Information",
|
||||
"description": "Providing model and optional interfaces information on a digital twin.",
|
||||
"schema": {
|
||||
"@type": "Object",
|
||||
"fields": [
|
||||
{
|
||||
"name": "modelId",
|
||||
"schema": "string"
|
||||
},
|
||||
{
|
||||
"name": "interfaces",
|
||||
"schema": {
|
||||
"@type": "Map",
|
||||
"mapKey": {
|
||||
"name": "name",
|
||||
"schema": "string"
|
||||
},
|
||||
"mapValue": {
|
||||
"name": "schema",
|
||||
"schema": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"@context": "https://azureiot.com/v1/contexts/Interface.json"
|
||||
};
|
||||
|
||||
export const schemaId = 'dtmi:com:rido:inlineTests:inlineComp;2';
|
||||
export const modelDefinitionWithInlineComp = {
|
||||
"@context": "dtmi:dtdl:context;2",
|
||||
"@id": "urn:azureiot:ModelDiscovery:DigitalTwin:1",
|
||||
"@type": "Interface",
|
||||
"contents": [
|
||||
{
|
||||
"@id": "dtmi:com:rido:inlineComp;2",
|
||||
"@type": "Component",
|
||||
"name": "inLineComponent",
|
||||
"schema": {
|
||||
"@type": "Interface",
|
||||
"@id": schemaId,
|
||||
"contents": [
|
||||
{
|
||||
"@type" : "Property",
|
||||
"name" : "inlineProp",
|
||||
"schema" : "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
/* tslint:enable */
|
|
@ -0,0 +1,43 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import 'jest';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../../constants/repositoryLocationTypes';
|
||||
import { interfaceId, modelDefinition, modelDefinitionWithInlineComp, schemaId } from "./testData";
|
||||
import { getFlattenedModel, getLocationSettingValue, getDmrParams } from "./utils";
|
||||
|
||||
describe('utils ', () => {
|
||||
it('flattens model definition when feasible', () => {
|
||||
expect(getFlattenedModel(modelDefinition, [interfaceId])).toEqual(modelDefinition);
|
||||
|
||||
expect(getFlattenedModel(modelDefinitionWithInlineComp, [interfaceId, schemaId])).toEqual(modelDefinitionWithInlineComp.contents?.[0]);
|
||||
});
|
||||
|
||||
it('gets specific location setting values', () => {
|
||||
const locations = [{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public,
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Configurable,
|
||||
value: 'test.com'
|
||||
},
|
||||
{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local,
|
||||
value: 'c:/test/folder/models/'
|
||||
},
|
||||
{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.LocalDMR,
|
||||
value: 'd:/test/dtmi'
|
||||
}];
|
||||
|
||||
expect(getLocationSettingValue(locations, REPOSITORY_LOCATION_TYPE.Configurable)).toEqual('test.com');
|
||||
expect(getLocationSettingValue(locations, REPOSITORY_LOCATION_TYPE.Local)).toEqual('c:/test/folder/models');
|
||||
expect(getLocationSettingValue(locations, REPOSITORY_LOCATION_TYPE.LocalDMR)).toEqual('d:/test/dtmi');
|
||||
});
|
||||
|
||||
it('gets expected dmr params', () => {
|
||||
expect(getDmrParams('d:/test/dtmi', interfaceId)).toEqual({folderPath: 'd:/test/dtmi/com/example', fileName: 'thermostat-1.json'});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
import { ModelIdCasingNotMatchingException } from '../../../shared/utils/exceptions/modelIdCasingNotMatchingException';
|
||||
import { ModelDefinition } from '../../../api/models/modelDefinition';
|
||||
import { ModelRepositoryConfiguration } from '../../../shared/modelRepository/state';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../../constants/repositoryLocationTypes';
|
||||
|
||||
export const getSplitInterfaceId = (fullName: string) => {
|
||||
// when component definition is inline, interfaceId is compose of parent file name and inline schema id concatenated with a slash
|
||||
return fullName.split('/');
|
||||
};
|
||||
|
||||
export const getFlattenedModel = (model: ModelDefinition, splitInterfaceId: string[]) => {
|
||||
if (splitInterfaceId.length === 1) {
|
||||
return model;
|
||||
}
|
||||
else {
|
||||
// for inline component, the flattened model is defined under contents array's with matching schema @id
|
||||
const components = model.contents.filter((content: any) => // tslint:disable-line: no-any
|
||||
content['@type'] === 'Component' && typeof content.schema !== 'string' && content.schema['@id'] === splitInterfaceId[1]);
|
||||
return components[0];
|
||||
}
|
||||
};
|
||||
|
||||
export const checkModelIdCasing = (model: ModelDefinition, id: string) => {
|
||||
if (model['@id'] !== id) {
|
||||
throw new ModelIdCasingNotMatchingException();
|
||||
}
|
||||
};
|
||||
|
||||
export const getLocationSettingValue = (locations: ModelRepositoryConfiguration[], type: REPOSITORY_LOCATION_TYPE): string => {
|
||||
const filteredValue = locations.filter(location => location.repositoryLocationType === type)?.[0]?.value || '';
|
||||
return filteredValue.replace(/\/$/, ''); // remove trailing slash
|
||||
};
|
||||
|
||||
export const getDmrParams = (path: string, interfaceId: string): {folderPath: string, fileName: string} => {
|
||||
// convert dtmi name to follow drm convention
|
||||
// for example: dtmi:com:example:Thermostat;1 -> dtmi/com/example/thermostat-1.json
|
||||
const fullPath = path.substring(0, path.lastIndexOf('/') + 1) + `${interfaceId.toLowerCase().replace(/:/g, '/').replace(';', '-')}.json`;
|
||||
// path will be converted to for example: original path/dtmi/com/example, file name will be thermostat-1.json
|
||||
return {folderPath: fullPath.substring(0, fullPath.lastIndexOf('/')), fileName: fullPath.substring(fullPath.lastIndexOf('/') + 1, fullPath.length)};
|
||||
};
|
|
@ -9,7 +9,7 @@ import { AppVersionMessageBar } from './appVersionMessageBar';
|
|||
import { HomeViewNavigation } from './homeViewNavigation';
|
||||
import { AuthenticationStateContextProvider } from '../../authentication/context/authenticationStateProvider';
|
||||
import { AuthenticationView } from '../../authentication/components/authenticationView';
|
||||
import { ModelRepositoryLocationView } from '../../modelRepository/components/modelRepositoryLocationView';
|
||||
import { ModelRepositoryLocationView } from '../../modelRepository/view';
|
||||
import { NotificationList } from '../../notifications/components/notificationList';
|
||||
|
||||
export const HomeView: React.FC = () => {
|
||||
|
|
|
@ -8,7 +8,6 @@ import { Nav, INavLink } from '@fluentui/react';
|
|||
import { ROUTE_PARTS } from '../../constants/routes';
|
||||
import { ResourceKeys } from '../../../localization/resourceKeys';
|
||||
import { CollapsibleButton } from '../../shared/components/collapsibleButton';
|
||||
import '../../css/_layouts.scss';
|
||||
import './homeViewNavigation.scss';
|
||||
|
||||
export interface HomeViewNavigationProps {
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`view matches snapshot when locations greater than 0 1`] = `
|
||||
<div>
|
||||
<Component
|
||||
message="common.navigation.confirm"
|
||||
when={false}
|
||||
/>
|
||||
<Commands
|
||||
formState={
|
||||
Array [
|
||||
Object {
|
||||
"dirty": false,
|
||||
"repositoryLocationSettings": Array [
|
||||
Object {
|
||||
"repositoryLocationType": "PUBLIC",
|
||||
"value": "",
|
||||
},
|
||||
Object {
|
||||
"repositoryLocationType": "CONFIGURABLE",
|
||||
"value": "d:/",
|
||||
},
|
||||
],
|
||||
"repositoryLocationSettingsErrors": Object {},
|
||||
},
|
||||
Object {
|
||||
"setDirtyFlag": [Function],
|
||||
"setRepositoryLocationSettings": [Function],
|
||||
"setRepositoryLocationSettingsErrors": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<ModelRepositoryInstruction />
|
||||
<ModelRepositoryLocationList
|
||||
formState={
|
||||
Array [
|
||||
Object {
|
||||
"dirty": false,
|
||||
"repositoryLocationSettings": Array [
|
||||
Object {
|
||||
"repositoryLocationType": "PUBLIC",
|
||||
"value": "",
|
||||
},
|
||||
Object {
|
||||
"repositoryLocationType": "CONFIGURABLE",
|
||||
"value": "d:/",
|
||||
},
|
||||
],
|
||||
"repositoryLocationSettingsErrors": Object {},
|
||||
},
|
||||
Object {
|
||||
"setDirtyFlag": [Function],
|
||||
"setRepositoryLocationSettings": [Function],
|
||||
"setRepositoryLocationSettingsErrors": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`view matches snapshot when no locations 1`] = `
|
||||
<div>
|
||||
<Component
|
||||
message="common.navigation.confirm"
|
||||
when={false}
|
||||
/>
|
||||
<Commands
|
||||
formState={
|
||||
Array [
|
||||
Object {
|
||||
"dirty": false,
|
||||
"repositoryLocationSettings": Array [
|
||||
Object {
|
||||
"repositoryLocationType": "PUBLIC",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"repositoryLocationSettingsErrors": Object {},
|
||||
},
|
||||
Object {
|
||||
"setDirtyFlag": [Function],
|
||||
"setRepositoryLocationSettings": [Function],
|
||||
"setRepositoryLocationSettingsErrors": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<ModelRepositoryInstruction />
|
||||
<ModelRepositoryLocationList
|
||||
formState={
|
||||
Array [
|
||||
Object {
|
||||
"dirty": false,
|
||||
"repositoryLocationSettings": Array [
|
||||
Object {
|
||||
"repositoryLocationType": "PUBLIC",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"repositoryLocationSettingsErrors": Object {},
|
||||
},
|
||||
Object {
|
||||
"setDirtyFlag": [Function],
|
||||
"setRepositoryLocationSettings": [Function],
|
||||
"setRepositoryLocationSettingsErrors": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,30 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ListItemConfigurableRepo matches snapshot 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="labelSection"
|
||||
>
|
||||
<div
|
||||
className="label"
|
||||
>
|
||||
modelRepository.types.configurable.label
|
||||
</div>
|
||||
<div
|
||||
className="description"
|
||||
>
|
||||
modelRepository.types.configurable.infoText
|
||||
</div>
|
||||
</div>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="modelRepository.types.configurable.textBoxLabel"
|
||||
className="local-folder-textbox"
|
||||
errorMessage=""
|
||||
label="modelRepository.types.configurable.textBoxLabel"
|
||||
onChange={[Function]}
|
||||
prefix="https://"
|
||||
readOnly={false}
|
||||
value="test.com"
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,77 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ListItemLocal matches snapshot for local folder 1`] = `
|
||||
<Fragment>
|
||||
<ListItemLocalLabel
|
||||
repoType="LOCAL"
|
||||
/>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="modelRepository.types.local.textBoxLabel"
|
||||
className="local-folder-textbox"
|
||||
errorMessage=""
|
||||
label="modelRepository.types.local.textBoxLabel"
|
||||
onChange={[Function]}
|
||||
readOnly={false}
|
||||
value="c:/models"
|
||||
/>
|
||||
<CustomizedDefaultButton
|
||||
ariaLabel="modelRepository.types.local.folderPicker.command.openPicker"
|
||||
className="local-folder-launch"
|
||||
onClick={[Function]}
|
||||
text="modelRepository.types.local.folderPicker.command.openPicker"
|
||||
/>
|
||||
<div
|
||||
role="dialog"
|
||||
>
|
||||
<Dialog
|
||||
dialogContentProps={
|
||||
Object {
|
||||
"subText": "modelRepository.types.local.folderPicker.dialog.subText",
|
||||
}
|
||||
}
|
||||
hidden={true}
|
||||
modalProps={
|
||||
Object {
|
||||
"className": "folder-picker-dialog",
|
||||
"isBlocking": false,
|
||||
}
|
||||
}
|
||||
onDismiss={[Function]}
|
||||
title="modelRepository.types.local.folderPicker.dialog.title"
|
||||
>
|
||||
<div
|
||||
className="folder-links"
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
ariaLabel="modelRepository.types.local.folderPicker.command.navigateToParent"
|
||||
className="folder-button"
|
||||
disabled={false}
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "NavigateBack",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
text="modelRepository.types.local.folderPicker.command.navigateToParent"
|
||||
/>
|
||||
<div
|
||||
className="no-folders-text"
|
||||
>
|
||||
modelRepository.types.local.folderPicker.dialog.noFolderFoundText
|
||||
</div>
|
||||
</div>
|
||||
<StyledDialogFooterBase>
|
||||
<CustomizedPrimaryButton
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
text="modelRepository.types.local.folderPicker.command.select"
|
||||
/>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
text="modelRepository.types.local.folderPicker.command.cancel"
|
||||
/>
|
||||
</StyledDialogFooterBase>
|
||||
</Dialog>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,50 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ListItemLocalLabel matches snapshot when repo type equals Local 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="labelSection"
|
||||
>
|
||||
<div
|
||||
className="label"
|
||||
>
|
||||
modelRepository.types.local.label
|
||||
</div>
|
||||
<div
|
||||
className="description"
|
||||
>
|
||||
modelRepository.types.local.infoText
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`ListItemLocalLabel matches snapshot when repo type equals LocalDMR 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="labelSection"
|
||||
>
|
||||
<div
|
||||
className="label"
|
||||
>
|
||||
modelRepository.types.dmr.label
|
||||
</div>
|
||||
<div
|
||||
className="description"
|
||||
>
|
||||
<Component
|
||||
components={
|
||||
Array [
|
||||
<StyledLinkBase
|
||||
href="https://github.com/Azure/iot-plugandplay-models-tools/wiki/Resolution-Convention"
|
||||
target="_blank"
|
||||
/>,
|
||||
]
|
||||
}
|
||||
>
|
||||
modelRepository.types.dmr.infoText
|
||||
</Component>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,39 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ListItemPublicRepo matches snapshot 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="labelSection"
|
||||
>
|
||||
<div
|
||||
className="label"
|
||||
>
|
||||
modelRepository.types.public.label
|
||||
</div>
|
||||
<div
|
||||
className="description"
|
||||
>
|
||||
<Component
|
||||
components={
|
||||
Array [
|
||||
<StyledLinkBase
|
||||
href="https://github.com/Azure/iot-plugandplay-models"
|
||||
target="_blank"
|
||||
/>,
|
||||
]
|
||||
}
|
||||
>
|
||||
modelRepository.types.public.infoText
|
||||
</Component>
|
||||
</div>
|
||||
</div>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="modelRepository.types.configurable.textBoxLabel"
|
||||
className="local-folder-textbox"
|
||||
label="modelRepository.types.configurable.textBoxLabel"
|
||||
prefix="https://"
|
||||
readOnly={true}
|
||||
value="devicemodels.azure.com"
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
|
@ -49,53 +49,3 @@ exports[`ModelRepositoryInstruction matches snapshot 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ModelRepositoryInstruction matches snapshot 2`] = `
|
||||
<div
|
||||
className="model-repository-instruction"
|
||||
>
|
||||
<div>
|
||||
<span>
|
||||
modelRepository.description.description
|
||||
</span>
|
||||
<NavLink
|
||||
className="embedded-link"
|
||||
to="/home/repos"
|
||||
>
|
||||
Home.
|
||||
</NavLink>
|
||||
</div>
|
||||
<h3
|
||||
aria-level={1}
|
||||
role="heading"
|
||||
>
|
||||
settings.questions.headerText
|
||||
</h3>
|
||||
<StyledLinkBase
|
||||
href="settings.questions.questions.documentation.link"
|
||||
target="_blank"
|
||||
>
|
||||
modelRepository.description.help
|
||||
</StyledLinkBase>
|
||||
<h3
|
||||
aria-level={1}
|
||||
role="heading"
|
||||
>
|
||||
modelRepository.description.header
|
||||
</h3>
|
||||
modelRepository.instruction
|
||||
<div
|
||||
className="privacy-statement"
|
||||
>
|
||||
<span>
|
||||
settings.questions.questions.privacy.text
|
||||
</span>
|
||||
<StyledLinkBase
|
||||
href="settings.questions.questions.privacy.link"
|
||||
target="_blank"
|
||||
>
|
||||
settings.questions.questions.privacy.linkText
|
||||
</StyledLinkBase>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/settings/modelRepositoryLocationList matches snapshot with each type item 1`] = `
|
||||
exports[`ModelRepositoryLocationList matches snapshot with all items 1`] = `
|
||||
<div
|
||||
className="location-list"
|
||||
>
|
||||
|
@ -13,35 +13,183 @@ exports[`components/settings/modelRepositoryLocationList matches snapshot with e
|
|||
key="PUBLIC"
|
||||
>
|
||||
<ModelRepositoryLocationListItem
|
||||
formState={
|
||||
Array [
|
||||
Object {
|
||||
"dirty": false,
|
||||
"repositoryLocationSettings": Array [
|
||||
Object {
|
||||
"repositoryLocationType": "PUBLIC",
|
||||
"value": "",
|
||||
},
|
||||
Object {
|
||||
"repositoryLocationType": "CONFIGURABLE",
|
||||
"value": "test.com",
|
||||
},
|
||||
Object {
|
||||
"repositoryLocationType": "LOCAL",
|
||||
"value": "d:/myModels",
|
||||
},
|
||||
Object {
|
||||
"repositoryLocationType": "DMR",
|
||||
"value": "c:/dtmi",
|
||||
},
|
||||
],
|
||||
"repositoryLocationSettingsErrors": Object {},
|
||||
},
|
||||
Object {
|
||||
"setDirtyFlag": [Function],
|
||||
"setRepositoryLocationSettings": [Function],
|
||||
"setRepositoryLocationSettingsErrors": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
index={0}
|
||||
item={
|
||||
Object {
|
||||
"repositoryLocationType": "PUBLIC",
|
||||
"value": "",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</n>
|
||||
<n
|
||||
key="CONFIGURABLE"
|
||||
>
|
||||
<ModelRepositoryLocationListItem
|
||||
formState={
|
||||
Array [
|
||||
Object {
|
||||
"dirty": false,
|
||||
"repositoryLocationSettings": Array [
|
||||
Object {
|
||||
"repositoryLocationType": "PUBLIC",
|
||||
"value": "",
|
||||
},
|
||||
Object {
|
||||
"repositoryLocationType": "CONFIGURABLE",
|
||||
"value": "test.com",
|
||||
},
|
||||
Object {
|
||||
"repositoryLocationType": "LOCAL",
|
||||
"value": "d:/myModels",
|
||||
},
|
||||
Object {
|
||||
"repositoryLocationType": "DMR",
|
||||
"value": "c:/dtmi",
|
||||
},
|
||||
],
|
||||
"repositoryLocationSettingsErrors": Object {},
|
||||
},
|
||||
Object {
|
||||
"setDirtyFlag": [Function],
|
||||
"setRepositoryLocationSettings": [Function],
|
||||
"setRepositoryLocationSettingsErrors": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
index={1}
|
||||
item={
|
||||
Object {
|
||||
"repositoryLocationType": "CONFIGURABLE",
|
||||
"value": "test.com",
|
||||
}
|
||||
}
|
||||
onChangeRepositoryLocationSettingValue={[Function]}
|
||||
onRemoveRepositoryLocationSetting={[Function]}
|
||||
/>
|
||||
</n>
|
||||
<n
|
||||
key="LOCAL"
|
||||
>
|
||||
<ModelRepositoryLocationListItem
|
||||
index={1}
|
||||
formState={
|
||||
Array [
|
||||
Object {
|
||||
"dirty": false,
|
||||
"repositoryLocationSettings": Array [
|
||||
Object {
|
||||
"repositoryLocationType": "PUBLIC",
|
||||
"value": "",
|
||||
},
|
||||
Object {
|
||||
"repositoryLocationType": "CONFIGURABLE",
|
||||
"value": "test.com",
|
||||
},
|
||||
Object {
|
||||
"repositoryLocationType": "LOCAL",
|
||||
"value": "d:/myModels",
|
||||
},
|
||||
Object {
|
||||
"repositoryLocationType": "DMR",
|
||||
"value": "c:/dtmi",
|
||||
},
|
||||
],
|
||||
"repositoryLocationSettingsErrors": Object {},
|
||||
},
|
||||
Object {
|
||||
"setDirtyFlag": [Function],
|
||||
"setRepositoryLocationSettings": [Function],
|
||||
"setRepositoryLocationSettingsErrors": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
index={2}
|
||||
item={
|
||||
Object {
|
||||
"repositoryLocationType": "LOCAL",
|
||||
"value": "d:/myModels",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</n>
|
||||
<n
|
||||
key="DMR"
|
||||
>
|
||||
<ModelRepositoryLocationListItem
|
||||
formState={
|
||||
Array [
|
||||
Object {
|
||||
"dirty": false,
|
||||
"repositoryLocationSettings": Array [
|
||||
Object {
|
||||
"repositoryLocationType": "PUBLIC",
|
||||
"value": "",
|
||||
},
|
||||
Object {
|
||||
"repositoryLocationType": "CONFIGURABLE",
|
||||
"value": "test.com",
|
||||
},
|
||||
Object {
|
||||
"repositoryLocationType": "LOCAL",
|
||||
"value": "d:/myModels",
|
||||
},
|
||||
Object {
|
||||
"repositoryLocationType": "DMR",
|
||||
"value": "c:/dtmi",
|
||||
},
|
||||
],
|
||||
"repositoryLocationSettingsErrors": Object {},
|
||||
},
|
||||
Object {
|
||||
"setDirtyFlag": [Function],
|
||||
"setRepositoryLocationSettings": [Function],
|
||||
"setRepositoryLocationSettingsErrors": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
index={3}
|
||||
item={
|
||||
Object {
|
||||
"repositoryLocationType": "DMR",
|
||||
"value": "c:/dtmi",
|
||||
}
|
||||
}
|
||||
onChangeRepositoryLocationSettingValue={[Function]}
|
||||
onRemoveRepositoryLocationSetting={[Function]}
|
||||
/>
|
||||
</n>
|
||||
</t>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/settings/modelRepositoryLocationList matches snapshot with no items 1`] = `
|
||||
exports[`ModelRepositoryLocationList matches snapshot with no items 1`] = `
|
||||
<div
|
||||
className="location-list"
|
||||
>
|
||||
|
@ -52,30 +200,3 @@ exports[`components/settings/modelRepositoryLocationList matches snapshot with n
|
|||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/settings/modelRepositoryLocationList matches snapshot with public item 1`] = `
|
||||
<div
|
||||
className="location-list"
|
||||
>
|
||||
<t
|
||||
behaviour="move"
|
||||
onDrop={[Function]}
|
||||
orientation="vertical"
|
||||
>
|
||||
<n
|
||||
key="PUBLIC"
|
||||
>
|
||||
<ModelRepositoryLocationListItem
|
||||
index={0}
|
||||
item={
|
||||
Object {
|
||||
"repositoryLocationType": "PUBLIC",
|
||||
}
|
||||
}
|
||||
onChangeRepositoryLocationSettingValue={[Function]}
|
||||
onRemoveRepositoryLocationSetting={[Function]}
|
||||
/>
|
||||
</n>
|
||||
</t>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/settings/modelRepositoryLocationListItem matches snapshot for local 1`] = `
|
||||
exports[`ModelRepositoryLocationListItem matches snapshot for configurable repo 1`] = `
|
||||
<div
|
||||
className="item"
|
||||
role="list"
|
||||
|
@ -8,7 +8,7 @@ exports[`components/settings/modelRepositoryLocationListItem matches snapshot fo
|
|||
<div
|
||||
className="numbering"
|
||||
>
|
||||
1
|
||||
2
|
||||
</div>
|
||||
<div
|
||||
className="location-item"
|
||||
|
@ -17,84 +17,34 @@ exports[`components/settings/modelRepositoryLocationListItem matches snapshot fo
|
|||
<div
|
||||
className="item-details"
|
||||
>
|
||||
<div
|
||||
className="labelSection"
|
||||
>
|
||||
<Memo()
|
||||
calloutContent="modelRepository.types.local.infoText"
|
||||
required={true}
|
||||
>
|
||||
modelRepository.types.local.label
|
||||
</Memo()>
|
||||
</div>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="modelRepository.types.local.textBoxLabel"
|
||||
className="local-folder-textbox"
|
||||
errorMessage=""
|
||||
label="modelRepository.types.local.textBoxLabel"
|
||||
onChange={[Function]}
|
||||
readOnly={false}
|
||||
value="f:/"
|
||||
/>
|
||||
<CustomizedDefaultButton
|
||||
ariaLabel="modelRepository.types.local.folderPicker.command.openPicker"
|
||||
className="local-folder-launch"
|
||||
onClick={[Function]}
|
||||
text="modelRepository.types.local.folderPicker.command.openPicker"
|
||||
/>
|
||||
<div
|
||||
role="dialog"
|
||||
>
|
||||
<Dialog
|
||||
dialogContentProps={
|
||||
<ListItemConfigurableRepo
|
||||
formState={
|
||||
Array [
|
||||
Object {
|
||||
"subText": "modelRepository.types.local.folderPicker.dialog.subText",
|
||||
}
|
||||
}
|
||||
hidden={true}
|
||||
modalProps={
|
||||
Object {
|
||||
"className": "folder-picker-dialog",
|
||||
"isBlocking": false,
|
||||
}
|
||||
}
|
||||
onDismiss={[Function]}
|
||||
title="modelRepository.types.local.folderPicker.dialog.title"
|
||||
>
|
||||
<div
|
||||
className="folder-links"
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
ariaLabel="modelRepository.types.local.folderPicker.command.navigateToParent"
|
||||
className="folder-button"
|
||||
disabled={false}
|
||||
iconProps={
|
||||
"dirty": false,
|
||||
"repositoryLocationSettings": Array [
|
||||
Object {
|
||||
"iconName": "NavigateBack",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
text="modelRepository.types.local.folderPicker.command.navigateToParent"
|
||||
/>
|
||||
<div
|
||||
className="no-folders-text"
|
||||
>
|
||||
modelRepository.types.local.folderPicker.dialog.noFolderFoundText
|
||||
</div>
|
||||
</div>
|
||||
<StyledDialogFooterBase>
|
||||
<CustomizedPrimaryButton
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
text="modelRepository.types.local.folderPicker.command.select"
|
||||
/>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
text="modelRepository.types.local.folderPicker.command.cancel"
|
||||
/>
|
||||
</StyledDialogFooterBase>
|
||||
</Dialog>
|
||||
</div>
|
||||
"repositoryLocationType": "PUBLIC",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"repositoryLocationSettingsErrors": Object {},
|
||||
},
|
||||
Object {
|
||||
"setDirtyFlag": [Function],
|
||||
"setRepositoryLocationSettings": [Function],
|
||||
"setRepositoryLocationSettingsErrors": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
index={1}
|
||||
item={
|
||||
Object {
|
||||
"repositoryLocationType": "CONFIGURABLE",
|
||||
"value": "test.com",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="modelRepository.commands.remove.ariaLabel"
|
||||
|
@ -111,7 +61,7 @@ exports[`components/settings/modelRepositoryLocationListItem matches snapshot fo
|
|||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/settings/modelRepositoryLocationListItem matches snapshot for local with error 1`] = `
|
||||
exports[`ModelRepositoryLocationListItem matches snapshot for local folder 1`] = `
|
||||
<div
|
||||
className="item"
|
||||
role="list"
|
||||
|
@ -119,7 +69,7 @@ exports[`components/settings/modelRepositoryLocationListItem matches snapshot fo
|
|||
<div
|
||||
className="numbering"
|
||||
>
|
||||
1
|
||||
3
|
||||
</div>
|
||||
<div
|
||||
className="location-item"
|
||||
|
@ -128,84 +78,35 @@ exports[`components/settings/modelRepositoryLocationListItem matches snapshot fo
|
|||
<div
|
||||
className="item-details"
|
||||
>
|
||||
<div
|
||||
className="labelSection"
|
||||
>
|
||||
<Memo()
|
||||
calloutContent="modelRepository.types.local.infoText"
|
||||
required={true}
|
||||
>
|
||||
modelRepository.types.local.label
|
||||
</Memo()>
|
||||
</div>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="modelRepository.types.local.textBoxLabel"
|
||||
className="local-folder-textbox"
|
||||
errorMessage="error"
|
||||
label="modelRepository.types.local.textBoxLabel"
|
||||
onChange={[Function]}
|
||||
readOnly={false}
|
||||
value="f:/"
|
||||
/>
|
||||
<CustomizedDefaultButton
|
||||
ariaLabel="modelRepository.types.local.folderPicker.command.openPicker"
|
||||
className="local-folder-launch"
|
||||
onClick={[Function]}
|
||||
text="modelRepository.types.local.folderPicker.command.openPicker"
|
||||
/>
|
||||
<div
|
||||
role="dialog"
|
||||
>
|
||||
<Dialog
|
||||
dialogContentProps={
|
||||
<ListItemLocal
|
||||
formState={
|
||||
Array [
|
||||
Object {
|
||||
"subText": "modelRepository.types.local.folderPicker.dialog.subText",
|
||||
}
|
||||
}
|
||||
hidden={true}
|
||||
modalProps={
|
||||
Object {
|
||||
"className": "folder-picker-dialog",
|
||||
"isBlocking": false,
|
||||
}
|
||||
}
|
||||
onDismiss={[Function]}
|
||||
title="modelRepository.types.local.folderPicker.dialog.title"
|
||||
>
|
||||
<div
|
||||
className="folder-links"
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
ariaLabel="modelRepository.types.local.folderPicker.command.navigateToParent"
|
||||
className="folder-button"
|
||||
disabled={false}
|
||||
iconProps={
|
||||
"dirty": false,
|
||||
"repositoryLocationSettings": Array [
|
||||
Object {
|
||||
"iconName": "NavigateBack",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
text="modelRepository.types.local.folderPicker.command.navigateToParent"
|
||||
/>
|
||||
<div
|
||||
className="no-folders-text"
|
||||
>
|
||||
modelRepository.types.local.folderPicker.dialog.noFolderFoundText
|
||||
</div>
|
||||
</div>
|
||||
<StyledDialogFooterBase>
|
||||
<CustomizedPrimaryButton
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
text="modelRepository.types.local.folderPicker.command.select"
|
||||
/>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
text="modelRepository.types.local.folderPicker.command.cancel"
|
||||
/>
|
||||
</StyledDialogFooterBase>
|
||||
</Dialog>
|
||||
</div>
|
||||
"repositoryLocationType": "PUBLIC",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"repositoryLocationSettingsErrors": Object {},
|
||||
},
|
||||
Object {
|
||||
"setDirtyFlag": [Function],
|
||||
"setRepositoryLocationSettings": [Function],
|
||||
"setRepositoryLocationSettingsErrors": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
index={2}
|
||||
item={
|
||||
Object {
|
||||
"repositoryLocationType": "LOCAL",
|
||||
"value": "c:/models",
|
||||
}
|
||||
}
|
||||
repoType="LOCAL"
|
||||
/>
|
||||
</div>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="modelRepository.commands.remove.ariaLabel"
|
||||
|
@ -222,7 +123,7 @@ exports[`components/settings/modelRepositoryLocationListItem matches snapshot fo
|
|||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/settings/modelRepositoryLocationListItem matches snapshot for public 1`] = `
|
||||
exports[`ModelRepositoryLocationListItem matches snapshot for local repo 1`] = `
|
||||
<div
|
||||
className="item"
|
||||
role="list"
|
||||
|
@ -230,7 +131,7 @@ exports[`components/settings/modelRepositoryLocationListItem matches snapshot fo
|
|||
<div
|
||||
className="numbering"
|
||||
>
|
||||
1
|
||||
4
|
||||
</div>
|
||||
<div
|
||||
className="location-item"
|
||||
|
@ -239,20 +140,34 @@ exports[`components/settings/modelRepositoryLocationListItem matches snapshot fo
|
|||
<div
|
||||
className="item-details"
|
||||
>
|
||||
<div
|
||||
className="labelSection"
|
||||
>
|
||||
<StyledLabelBase>
|
||||
modelRepository.types.public.label
|
||||
</StyledLabelBase>
|
||||
</div>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="modelRepository.types.configurable.textBoxLabel"
|
||||
className="local-folder-textbox"
|
||||
label="modelRepository.types.configurable.textBoxLabel"
|
||||
prefix="https://"
|
||||
readOnly={true}
|
||||
value="devicemodels.azure.com"
|
||||
<ListItemLocal
|
||||
formState={
|
||||
Array [
|
||||
Object {
|
||||
"dirty": false,
|
||||
"repositoryLocationSettings": Array [
|
||||
Object {
|
||||
"repositoryLocationType": "PUBLIC",
|
||||
"value": "",
|
||||
},
|
||||
],
|
||||
"repositoryLocationSettingsErrors": Object {},
|
||||
},
|
||||
Object {
|
||||
"setDirtyFlag": [Function],
|
||||
"setRepositoryLocationSettings": [Function],
|
||||
"setRepositoryLocationSettingsErrors": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
index={3}
|
||||
item={
|
||||
Object {
|
||||
"repositoryLocationType": "DMR",
|
||||
"value": "c:/dtmi",
|
||||
}
|
||||
}
|
||||
repoType="DMR"
|
||||
/>
|
||||
</div>
|
||||
<CustomizedIconButton
|
||||
|
@ -269,3 +184,37 @@ exports[`components/settings/modelRepositoryLocationListItem matches snapshot fo
|
|||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ModelRepositoryLocationListItem matches snapshot for public repo 1`] = `
|
||||
<div
|
||||
className="item"
|
||||
role="list"
|
||||
>
|
||||
<div
|
||||
className="numbering"
|
||||
>
|
||||
1
|
||||
</div>
|
||||
<div
|
||||
className="location-item"
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
className="item-details"
|
||||
>
|
||||
<ListItemPublicRepo />
|
||||
</div>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="modelRepository.commands.remove.ariaLabel"
|
||||
className="remove-button"
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Cancel",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
title="modelRepository.commands.remove.label"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -1,213 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`modelRepositoryLocationView matches snapshot when locations greater than 0 1`] = `
|
||||
<div>
|
||||
<Component
|
||||
message="common.navigation.confirm"
|
||||
when={false}
|
||||
/>
|
||||
<StyledCommandBarBase
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"ariaLabel": "modelRepository.commands.save.ariaLabel",
|
||||
"disabled": true,
|
||||
"iconProps": Object {
|
||||
"iconName": "Save",
|
||||
},
|
||||
"key": "save",
|
||||
"onClick": [Function],
|
||||
"text": "modelRepository.commands.save.label",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "modelRepository.commands.add.ariaLabel",
|
||||
"disabled": false,
|
||||
"iconProps": Object {
|
||||
"iconName": "Add",
|
||||
},
|
||||
"key": "add",
|
||||
"subMenuProps": Object {
|
||||
"items": Array [
|
||||
Object {
|
||||
"ariaLabel": "modelRepository.commands.addPublicSource.ariaLabel",
|
||||
"disabled": true,
|
||||
"iconProps": Object {
|
||||
"iconName": "Repo",
|
||||
},
|
||||
"key": "PUBLIC",
|
||||
"onClick": [Function],
|
||||
"text": "modelRepository.commands.addPublicSource.label",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "modelRepository.commands.addConfigurableRepoSource.label",
|
||||
"disabled": true,
|
||||
"iconProps": Object {
|
||||
"iconName": "Repo",
|
||||
},
|
||||
"key": "CONFIGURABLE",
|
||||
"onClick": [Function],
|
||||
"text": "modelRepository.commands.addConfigurableRepoSource.label",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "modelRepository.commands.addLocalSource.ariaLabel",
|
||||
"disabled": false,
|
||||
"iconProps": Object {
|
||||
"iconName": "OpenFolderHorizontal",
|
||||
},
|
||||
"key": "LOCAL",
|
||||
"onClick": [Function],
|
||||
"text": "modelRepository.commands.addLocalSource.label",
|
||||
},
|
||||
],
|
||||
},
|
||||
"text": "modelRepository.commands.add.label",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "modelRepository.commands.revert.ariaLabel",
|
||||
"disabled": true,
|
||||
"iconProps": Object {
|
||||
"iconName": "Undo",
|
||||
},
|
||||
"key": "revert",
|
||||
"onClick": [Function],
|
||||
"text": "modelRepository.commands.revert.label",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "modelRepository.commands.help.ariaLabel",
|
||||
"iconProps": Object {
|
||||
"iconName": "Help",
|
||||
},
|
||||
"key": "help",
|
||||
"onClick": [Function],
|
||||
"text": "modelRepository.commands.help.label",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<ModelRepositoryInstruction
|
||||
empty={false}
|
||||
/>
|
||||
<ModelRepositoryLocationList
|
||||
onChangeRepositoryLocationSettings={[Function]}
|
||||
repositoryLocationSettings={
|
||||
Array [
|
||||
Object {
|
||||
"repositoryLocationType": "PUBLIC",
|
||||
"value": "",
|
||||
},
|
||||
Object {
|
||||
"repositoryLocationType": "CONFIGURABLE",
|
||||
"value": "d:/",
|
||||
},
|
||||
]
|
||||
}
|
||||
repositoryLocationSettingsErrors={Object {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`modelRepositoryLocationView matches snapshot when no locations 1`] = `
|
||||
<div>
|
||||
<Component
|
||||
message="common.navigation.confirm"
|
||||
when={false}
|
||||
/>
|
||||
<StyledCommandBarBase
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"ariaLabel": "modelRepository.commands.save.ariaLabel",
|
||||
"disabled": true,
|
||||
"iconProps": Object {
|
||||
"iconName": "Save",
|
||||
},
|
||||
"key": "save",
|
||||
"onClick": [Function],
|
||||
"text": "modelRepository.commands.save.label",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "modelRepository.commands.add.ariaLabel",
|
||||
"disabled": false,
|
||||
"iconProps": Object {
|
||||
"iconName": "Add",
|
||||
},
|
||||
"key": "add",
|
||||
"subMenuProps": Object {
|
||||
"items": Array [
|
||||
Object {
|
||||
"ariaLabel": "modelRepository.commands.addPublicSource.ariaLabel",
|
||||
"disabled": true,
|
||||
"iconProps": Object {
|
||||
"iconName": "Repo",
|
||||
},
|
||||
"key": "PUBLIC",
|
||||
"onClick": [Function],
|
||||
"text": "modelRepository.commands.addPublicSource.label",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "modelRepository.commands.addConfigurableRepoSource.label",
|
||||
"disabled": false,
|
||||
"iconProps": Object {
|
||||
"iconName": "Repo",
|
||||
},
|
||||
"key": "CONFIGURABLE",
|
||||
"onClick": [Function],
|
||||
"text": "modelRepository.commands.addConfigurableRepoSource.label",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "modelRepository.commands.addLocalSource.ariaLabel",
|
||||
"disabled": false,
|
||||
"iconProps": Object {
|
||||
"iconName": "OpenFolderHorizontal",
|
||||
},
|
||||
"key": "LOCAL",
|
||||
"onClick": [Function],
|
||||
"text": "modelRepository.commands.addLocalSource.label",
|
||||
},
|
||||
],
|
||||
},
|
||||
"text": "modelRepository.commands.add.label",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "modelRepository.commands.revert.ariaLabel",
|
||||
"disabled": true,
|
||||
"iconProps": Object {
|
||||
"iconName": "Undo",
|
||||
},
|
||||
"key": "revert",
|
||||
"onClick": [Function],
|
||||
"text": "modelRepository.commands.revert.label",
|
||||
},
|
||||
Object {
|
||||
"ariaLabel": "modelRepository.commands.help.ariaLabel",
|
||||
"iconProps": Object {
|
||||
"iconName": "Help",
|
||||
},
|
||||
"key": "help",
|
||||
"onClick": [Function],
|
||||
"text": "modelRepository.commands.help.label",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<ModelRepositoryInstruction
|
||||
empty={false}
|
||||
/>
|
||||
<ModelRepositoryLocationList
|
||||
onChangeRepositoryLocationSettings={[Function]}
|
||||
repositoryLocationSettings={
|
||||
Array [
|
||||
Object {
|
||||
"repositoryLocationType": "PUBLIC",
|
||||
"value": "",
|
||||
},
|
||||
]
|
||||
}
|
||||
repositoryLocationSettingsErrors={Object {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,35 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { } from 'enzyme';
|
||||
import { validateRepositoryLocationSettings } from './commands';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../constants/repositoryLocationTypes';
|
||||
import { ResourceKeys } from '../../../localization/resourceKeys';
|
||||
|
||||
describe('validateRepositoryLocationSettings', () => {
|
||||
it('adds validation error when local repository lacks value', () => {
|
||||
const result = validateRepositoryLocationSettings([
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local, value: ''}
|
||||
]);
|
||||
|
||||
expect(result[REPOSITORY_LOCATION_TYPE.Local]).toEqual(ResourceKeys.modelRepository.types.mandatory);
|
||||
});
|
||||
|
||||
it('passes public repository', () => {
|
||||
const result = validateRepositoryLocationSettings([
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public, value: ''}
|
||||
]);
|
||||
|
||||
expect(result[REPOSITORY_LOCATION_TYPE.Public]).toBeFalsy();
|
||||
});
|
||||
|
||||
it('passes validation when local validation has value', () => {
|
||||
const result = validateRepositoryLocationSettings([
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local, value: 'value'}
|
||||
]);
|
||||
|
||||
expect(result[REPOSITORY_LOCATION_TYPE.Local]).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -4,36 +4,27 @@
|
|||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Prompt, useHistory, useLocation } from 'react-router-dom';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { CommandBar, ICommandBarItemProps, IContextualMenuItem } from '@fluentui/react';
|
||||
import { ResourceKeys } from '../../../localization/resourceKeys';
|
||||
import { ModelRepositoryLocationList } from './modelRepositoryLocationList';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../constants/repositoryLocationTypes';
|
||||
import { appConfig, HostMode } from '../../../appConfig/appConfig';
|
||||
import { StringMap } from '../../api/models/stringMap';
|
||||
import { ROUTE_PARAMS } from '../../constants/routes';
|
||||
import { SAVE, ADD, UNDO, HELP, NAVIGATE_BACK, REPO, FOLDER } from '../../constants/iconNames';
|
||||
import { ModelRepositoryInstruction } from './modelRepositoryInstruction';
|
||||
import { ModelRepositoryStateInterface } from '../../shared/modelRepository/state';
|
||||
import { ModelRepositoryFormType } from '../hooks/useModelRepositoryForm';
|
||||
import { useModelRepositoryContext } from '../../shared/modelRepository/context/modelRepositoryStateContext';
|
||||
import { useBreadcrumbEntry } from '../../navigation/hooks/useBreadcrumbEntry';
|
||||
import { AppInsightsClient } from '../../shared/appTelemetry/appInsightsClient';
|
||||
import { TELEMETRY_PAGE_NAMES } from '../../constants/telemetry';
|
||||
import '../../css/_layouts.scss';
|
||||
|
||||
export const ModelRepositoryLocationView: React.FC = () => {
|
||||
export const Commands: React.FC<{formState: ModelRepositoryFormType}> = ({formState}) => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const { search } = useLocation();
|
||||
useBreadcrumbEntry({ name: t(ResourceKeys.breadcrumb.ioTPlugAndPlay)});
|
||||
const [{repositoryLocationSettings, dirty}, {setRepositoryLocationSettings, setRepositoryLocationSettingsErrors, setDirtyFlag}] = formState;
|
||||
|
||||
const params = new URLSearchParams(search);
|
||||
const navigationBackAvailable = params.has(ROUTE_PARAMS.NAV_FROM);
|
||||
|
||||
const [ modelRepositoryState, { setRepositoryLocations } ] = useModelRepositoryContext();
|
||||
const [ repositoryLocationSettings, setRepositoryLocationSettings ] = React.useState<ModelRepositoryStateInterface>(modelRepositoryState);
|
||||
const [ repositoryLocationSettingsErrors, setRepositoryLocationSettingsErrors ] = React.useState<StringMap<string>>({});
|
||||
const [ dirty, setDirtyFlag ] = React.useState<boolean>(false);
|
||||
|
||||
const getCommandBarItems = (): ICommandBarItemProps[] => {
|
||||
const addItems = getCommandBarItemsAdd();
|
||||
|
@ -90,8 +81,6 @@ export const ModelRepositoryLocationView: React.FC = () => {
|
|||
};
|
||||
|
||||
const getCommandBarItemsAdd = (): IContextualMenuItem[] => {
|
||||
const hostModeBrowser: boolean = appConfig.hostMode === HostMode.Browser;
|
||||
|
||||
return [
|
||||
{
|
||||
ariaLabel: t(ResourceKeys.modelRepository.commands.addPublicSource.ariaLabel),
|
||||
|
@ -100,7 +89,7 @@ export const ModelRepositoryLocationView: React.FC = () => {
|
|||
iconName: REPO
|
||||
},
|
||||
key: REPOSITORY_LOCATION_TYPE.Public,
|
||||
onClick: onAddRepositoryLocationPublic,
|
||||
onClick: onAddRepositoryLocation(REPOSITORY_LOCATION_TYPE.Public),
|
||||
text: t(ResourceKeys.modelRepository.commands.addPublicSource.label),
|
||||
},
|
||||
{
|
||||
|
@ -110,20 +99,28 @@ export const ModelRepositoryLocationView: React.FC = () => {
|
|||
iconName: REPO
|
||||
},
|
||||
key: REPOSITORY_LOCATION_TYPE.Configurable,
|
||||
onClick: onAddRepositoryLocationConfigurable,
|
||||
onClick: onAddRepositoryLocation(REPOSITORY_LOCATION_TYPE.Configurable),
|
||||
text: t(ResourceKeys.modelRepository.commands.addConfigurableRepoSource.label)
|
||||
},
|
||||
{
|
||||
ariaLabel: t(ResourceKeys.modelRepository.commands.addLocalSource.ariaLabel),
|
||||
disabled: hostModeBrowser || repositoryLocationSettings.some(item => item.repositoryLocationType === REPOSITORY_LOCATION_TYPE.Local),
|
||||
disabled: repositoryLocationSettings.some(item => item.repositoryLocationType === REPOSITORY_LOCATION_TYPE.Local),
|
||||
iconProps: {
|
||||
iconName: FOLDER
|
||||
},
|
||||
key: REPOSITORY_LOCATION_TYPE.Local,
|
||||
onClick: onAddRepositoryLocationLocal,
|
||||
text: t(hostModeBrowser ?
|
||||
ResourceKeys.modelRepository.commands.addLocalSource.labelInBrowser :
|
||||
ResourceKeys.modelRepository.commands.addLocalSource.label)
|
||||
onClick: onAddRepositoryLocation(REPOSITORY_LOCATION_TYPE.Local),
|
||||
text: t(ResourceKeys.modelRepository.commands.addLocalSource.label)
|
||||
},
|
||||
{
|
||||
ariaLabel: t(ResourceKeys.modelRepository.commands.addLocalDMRSource.ariaLabel),
|
||||
disabled: repositoryLocationSettings.some(item => item.repositoryLocationType === REPOSITORY_LOCATION_TYPE.LocalDMR),
|
||||
iconProps: {
|
||||
iconName: FOLDER
|
||||
},
|
||||
key: REPOSITORY_LOCATION_TYPE.LocalDMR,
|
||||
onClick: onAddRepositoryLocation(REPOSITORY_LOCATION_TYPE.LocalDMR),
|
||||
text: t(ResourceKeys.modelRepository.commands.addLocalDMRSource.label)
|
||||
}
|
||||
];
|
||||
};
|
||||
|
@ -150,77 +147,31 @@ export const ModelRepositoryLocationView: React.FC = () => {
|
|||
setRepositoryLocationSettings(modelRepositoryState);
|
||||
};
|
||||
|
||||
const onAddRepositoryLocationPublic = () => {
|
||||
const onAddRepositoryLocation = (repositoryLocationType: REPOSITORY_LOCATION_TYPE) => () => {
|
||||
setDirtyFlag(true);
|
||||
setRepositoryLocationSettings([
|
||||
...repositoryLocationSettings,
|
||||
{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public,
|
||||
repositoryLocationType,
|
||||
value: ''
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
const onAddRepositoryLocationConfigurable = () => {
|
||||
setDirtyFlag(true);
|
||||
setRepositoryLocationSettings([
|
||||
...repositoryLocationSettings,
|
||||
{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Configurable,
|
||||
value: ''
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
const onAddRepositoryLocationLocal = () => {
|
||||
setDirtyFlag(true);
|
||||
setRepositoryLocationSettings([
|
||||
...repositoryLocationSettings,
|
||||
{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local,
|
||||
value: ''
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
const onChangeRepositoryLocationSettings = (updatedRepositoryLocationSettings: ModelRepositoryStateInterface) => {
|
||||
setDirtyFlag(true);
|
||||
setRepositoryLocationSettingsErrors(validateRepositoryLocationSettings(updatedRepositoryLocationSettings));
|
||||
setRepositoryLocationSettings([
|
||||
...updatedRepositoryLocationSettings
|
||||
]);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
AppInsightsClient.getInstance()?.trackPageView({name: TELEMETRY_PAGE_NAMES.PNP_REPO_SETTINGS});
|
||||
}, []); // tslint:disable-line: align
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Prompt when={dirty} message={t(ResourceKeys.common.navigation.confirm)}/>
|
||||
<CommandBar
|
||||
items={getCommandBarItems()}
|
||||
/>
|
||||
<div>
|
||||
<ModelRepositoryInstruction empty={repositoryLocationSettings.length === 0}/>
|
||||
<ModelRepositoryLocationList
|
||||
repositoryLocationSettings={repositoryLocationSettings}
|
||||
repositoryLocationSettingsErrors={repositoryLocationSettingsErrors}
|
||||
onChangeRepositoryLocationSettings={onChangeRepositoryLocationSettings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CommandBar
|
||||
items={getCommandBarItems()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const validateRepositoryLocationSettings = (repositoryLocationSettings: ModelRepositoryStateInterface): StringMap<string> => {
|
||||
const errors: StringMap<string> = {};
|
||||
repositoryLocationSettings.forEach(s => {
|
||||
if (s.repositoryLocationType === REPOSITORY_LOCATION_TYPE.Local && !s.value) {
|
||||
errors[REPOSITORY_LOCATION_TYPE.Local] = ResourceKeys.modelRepository.types.local.folderPicker.errors.mandatory;
|
||||
}
|
||||
if (s.repositoryLocationType === REPOSITORY_LOCATION_TYPE.Configurable && !s.value) {
|
||||
errors[REPOSITORY_LOCATION_TYPE.Configurable] = ResourceKeys.modelRepository.types.configurable.errors.mandatory;
|
||||
if (!s.value) {
|
||||
if (s.repositoryLocationType === REPOSITORY_LOCATION_TYPE.Local || s.repositoryLocationType === REPOSITORY_LOCATION_TYPE.LocalDMR || s.repositoryLocationType === REPOSITORY_LOCATION_TYPE.Configurable) {
|
||||
errors[s.repositoryLocationType] = ResourceKeys.modelRepository.types.mandatory;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import 'jest';
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { ListItemConfigurableRepo } from './listItemConfigurableRepo';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../constants/repositoryLocationTypes';
|
||||
import { getInitialModelRepositoryFormState } from '../state';
|
||||
import { getInitialModelRepositoryFormOps } from '../interface';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { TextField } from '@fluentui/react';
|
||||
|
||||
describe('ListItemConfigurableRepo', () => {
|
||||
it('matches snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<ListItemConfigurableRepo
|
||||
index={1}
|
||||
item={{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Configurable,
|
||||
value: 'test.com'
|
||||
}}
|
||||
formState={[getInitialModelRepositoryFormState(), getInitialModelRepositoryFormOps()]}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('calls actions with expected params', () => {
|
||||
const setDirtyFlag = jest.fn();
|
||||
const setRepositoryLocationSettings = jest.fn();
|
||||
const wrapper = shallow(
|
||||
<ListItemConfigurableRepo
|
||||
index={0}
|
||||
item={{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Configurable,
|
||||
value: 'test.com'
|
||||
}}
|
||||
formState={[{...getInitialModelRepositoryFormState(), repositoryLocationSettings: [{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Configurable, value: 'old.com'}]},
|
||||
{...getInitialModelRepositoryFormOps(), setDirtyFlag, setRepositoryLocationSettings}]}
|
||||
/>
|
||||
);
|
||||
act(() => wrapper.find(TextField).props().onChange?.(undefined as any, 'test.com'));
|
||||
expect(setDirtyFlag).toBeCalledWith(true);
|
||||
expect(setRepositoryLocationSettings).toBeCalledWith([{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Configurable, value: 'test.com'}])
|
||||
});
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextField } from '@fluentui/react';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../constants/repositoryLocationTypes';
|
||||
import { ResourceKeys } from '../../../localization/resourceKeys';
|
||||
import { ModelRepositoryConfiguration } from '../../shared/modelRepository/state';
|
||||
import { ModelRepositoryFormType } from '../hooks/useModelRepositoryForm';
|
||||
|
||||
export interface ListItemConfigurableRepoProps{
|
||||
index: number;
|
||||
item: ModelRepositoryConfiguration;
|
||||
formState: ModelRepositoryFormType;
|
||||
}
|
||||
|
||||
export const ListItemConfigurableRepo: React.FC<ListItemConfigurableRepoProps> = ({index, item, formState}) => {
|
||||
const { t } = useTranslation();
|
||||
const [{repositoryLocationSettings, repositoryLocationSettingsErrors }, {setRepositoryLocationSettings, setDirtyFlag}] = formState;
|
||||
const errorKey = repositoryLocationSettingsErrors[item.repositoryLocationType];
|
||||
|
||||
let initialConfigurableRepositoryPath = '';
|
||||
if (item && item.repositoryLocationType === REPOSITORY_LOCATION_TYPE.Configurable) {
|
||||
initialConfigurableRepositoryPath = item.value;
|
||||
}
|
||||
const [ currentConfigurableRepositoryPath, setCurrentConfigurableRepositoryPath ] = React.useState(initialConfigurableRepositoryPath);
|
||||
|
||||
React.useEffect(() => {
|
||||
setCurrentConfigurableRepositoryPath(initialConfigurableRepositoryPath);
|
||||
}, [initialConfigurableRepositoryPath]); // tslint:disable-line: align
|
||||
|
||||
const onChangeRepositoryLocationSettingValue = (value: string) => {
|
||||
const updatedRepositoryLocationSettings = repositoryLocationSettings.map((setting, i) => {
|
||||
if (i === index) {
|
||||
const updatedSetting = {...setting};
|
||||
updatedSetting.value = value;
|
||||
return updatedSetting;
|
||||
} else {
|
||||
return setting;
|
||||
}
|
||||
});
|
||||
|
||||
setDirtyFlag(true);
|
||||
setRepositoryLocationSettings(updatedRepositoryLocationSettings);
|
||||
};
|
||||
|
||||
const repositoryEndpointChange = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
|
||||
setCurrentConfigurableRepositoryPath(newValue);
|
||||
onChangeRepositoryLocationSettingValue(newValue);
|
||||
};
|
||||
|
||||
return(
|
||||
<>
|
||||
<div className="labelSection">
|
||||
<div className="label">{t(ResourceKeys.modelRepository.types.configurable.label)}</div>
|
||||
<div className="description">{t(ResourceKeys.modelRepository.types.configurable.infoText)}</div>
|
||||
</div>
|
||||
<TextField
|
||||
className="local-folder-textbox"
|
||||
label={t(ResourceKeys.modelRepository.types.configurable.textBoxLabel)}
|
||||
ariaLabel={t(ResourceKeys.modelRepository.types.configurable.textBoxLabel)}
|
||||
value={currentConfigurableRepositoryPath}
|
||||
readOnly={false}
|
||||
errorMessage={errorKey ? t(errorKey) : ''}
|
||||
onChange={repositoryEndpointChange}
|
||||
prefix="https://"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,87 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import 'jest';
|
||||
import * as React from 'react';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Dialog } from '@fluentui/react';
|
||||
import { ListItemLocal } from './listItemLocal';
|
||||
import * as Utils from '../../shared/utils/utils';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../constants/repositoryLocationTypes';
|
||||
import { getInitialModelRepositoryFormState } from '../state';
|
||||
import { getInitialModelRepositoryFormOps } from '../interface';
|
||||
import { ResourceKeys } from '../../../localization/resourceKeys';
|
||||
|
||||
describe('ListItemLocal', () => {
|
||||
it('matches snapshot for local folder', () => {
|
||||
const wrapper = shallow(
|
||||
<ListItemLocal
|
||||
index={0}
|
||||
item={{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local,
|
||||
value: 'c:/models'
|
||||
}}
|
||||
repoType={REPOSITORY_LOCATION_TYPE.Local}
|
||||
formState={[getInitialModelRepositoryFormState(), getInitialModelRepositoryFormOps()]}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
||||
it('renders no folder text when no sub folder is retrieved', () => {
|
||||
jest.spyOn(Utils, 'getRootFolder').mockReturnValue('c:/models');
|
||||
|
||||
const wrapper = mount(
|
||||
<ListItemLocal
|
||||
index={0}
|
||||
item={{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local,
|
||||
value: 'c:/models'
|
||||
}}
|
||||
repoType={REPOSITORY_LOCATION_TYPE.Local}
|
||||
formState={[getInitialModelRepositoryFormState(), getInitialModelRepositoryFormOps()]}
|
||||
/>
|
||||
);
|
||||
|
||||
act(() => wrapper.find('.local-folder-launch').first().props().onClick(undefined));
|
||||
wrapper.update();
|
||||
|
||||
const dialog = wrapper.find(Dialog).first();
|
||||
expect(dialog.children().props().hidden).toBeFalsy();
|
||||
expect(dialog.children().props().children[0].props.children[0].props.disabled).toBeTruthy();
|
||||
expect(dialog.children().props().children[0].props.children[1].props.children).toEqual(ResourceKeys.modelRepository.types.local.folderPicker.dialog.noFolderFoundText);
|
||||
});
|
||||
|
||||
it('renders folders when sub folders retrieved', () => {
|
||||
const subFolders = ['documents', 'pictures'];
|
||||
jest.spyOn(Utils, 'getRootFolder').mockReturnValue('d:/');
|
||||
|
||||
const realUseState = React.useState;
|
||||
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(subFolders));
|
||||
|
||||
const wrapper = mount(
|
||||
<ListItemLocal
|
||||
index={0}
|
||||
item={{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local,
|
||||
value: 'c:/models'
|
||||
}}
|
||||
repoType={REPOSITORY_LOCATION_TYPE.Local}
|
||||
formState={[getInitialModelRepositoryFormState(), getInitialModelRepositoryFormOps()]}
|
||||
/>
|
||||
);
|
||||
|
||||
act(() => wrapper.find('.local-folder-launch').first().props().onClick(null));
|
||||
wrapper.update();
|
||||
|
||||
const dialog = wrapper.find(Dialog).first();
|
||||
expect(dialog.children().props().hidden).toBeFalsy();
|
||||
expect(dialog.children().props().children[0].props.children[0].props.text).toEqual(ResourceKeys.modelRepository.types.local.folderPicker.command.navigateToParent);
|
||||
expect(dialog.children().props().children[0].props.children[0].props.disabled).toBeFalsy();
|
||||
expect(dialog.children().props().children[0].props.children[1].length).toEqual(subFolders.length);
|
||||
expect(dialog.children().props().children[0].props.children[1][0].props.text).toEqual(subFolders[0]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,167 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DefaultButton, PrimaryButton, Dialog, DialogFooter, TextField, Link } from '@fluentui/react';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../constants/repositoryLocationTypes';
|
||||
import { ResourceKeys } from '../../../localization/resourceKeys';
|
||||
import { NAVIGATE_BACK, FOLDER } from '../../constants/iconNames';
|
||||
import { fetchDirectories } from '../../api/services/localRepoService';
|
||||
import { getRootFolder, getParentFolder } from '../../shared/utils/utils';
|
||||
import { ModelRepositoryConfiguration } from '../../shared/modelRepository/state';
|
||||
import { ModelRepositoryFormType } from '../hooks/useModelRepositoryForm';
|
||||
import { ListItemLocalLabel } from './listItemLocalLabel';
|
||||
|
||||
export interface ListItemLocalProps {
|
||||
index: number;
|
||||
item: ModelRepositoryConfiguration;
|
||||
formState: ModelRepositoryFormType;
|
||||
repoType: REPOSITORY_LOCATION_TYPE;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: cyclomatic-complexity
|
||||
export const ListItemLocal: React.FC<ListItemLocalProps> = ({ item, index, repoType, formState }) => {
|
||||
const { t } = useTranslation();
|
||||
const [{repositoryLocationSettings, repositoryLocationSettingsErrors }, {setRepositoryLocationSettings, setDirtyFlag}] = formState;
|
||||
const errorKey = repositoryLocationSettingsErrors[item.repositoryLocationType];
|
||||
|
||||
let initialCurrentFolder = '';
|
||||
if (item && item.repositoryLocationType === repoType) {
|
||||
initialCurrentFolder = item.value || getRootFolder();
|
||||
}
|
||||
|
||||
const [ subFolders, setSubFolders ] = React.useState([]);
|
||||
const [ currentFolder, setCurrentFolder ] = React.useState(initialCurrentFolder);
|
||||
const [ showError, setShowError ] = React.useState<boolean>(false);
|
||||
const [ showFolderPicker, setShowFolderPicker ] = React.useState<boolean>(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setCurrentFolder(initialCurrentFolder);
|
||||
}, [initialCurrentFolder]); // tslint:disable-line: align
|
||||
|
||||
const onChangeRepositoryLocationSettingValue = (value: string) => {
|
||||
const updatedRepositoryLocationSettings = repositoryLocationSettings.map((setting, i) => {
|
||||
if (i === index) {
|
||||
const updatedSetting = {...setting};
|
||||
updatedSetting.value = value;
|
||||
return updatedSetting;
|
||||
} else {
|
||||
return setting;
|
||||
}
|
||||
});
|
||||
|
||||
setDirtyFlag(true);
|
||||
setRepositoryLocationSettings(updatedRepositoryLocationSettings);
|
||||
};
|
||||
|
||||
const onFolderPathChange = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
|
||||
setCurrentFolder(newValue);
|
||||
onChangeRepositoryLocationSettingValue(newValue);
|
||||
};
|
||||
|
||||
const onShowFolderPicker = () => {
|
||||
fetchSubDirectoriesInfo(currentFolder);
|
||||
setShowFolderPicker(true);
|
||||
};
|
||||
|
||||
const dismissFolderPicker = () => {
|
||||
setCurrentFolder(item.value || getRootFolder());
|
||||
setShowFolderPicker(false);
|
||||
};
|
||||
|
||||
const onSelectFolder = () => {
|
||||
onChangeRepositoryLocationSettingValue(currentFolder);
|
||||
setShowFolderPicker(false);
|
||||
};
|
||||
|
||||
const onClickFolderName = (folder: string) => () => {
|
||||
const newDir = currentFolder ? `${currentFolder.replace(/\/$/, '')}/${folder}` : folder;
|
||||
setCurrentFolder(newDir);
|
||||
fetchSubDirectoriesInfo(newDir);
|
||||
};
|
||||
|
||||
const onNavigateBack = () => {
|
||||
const parentFolder = getParentFolder(currentFolder);
|
||||
setCurrentFolder(parentFolder);
|
||||
fetchSubDirectoriesInfo(parentFolder);
|
||||
};
|
||||
|
||||
const fetchSubDirectoriesInfo = (folderName: string) => {
|
||||
fetchDirectories(folderName).then(result => {
|
||||
setShowError(false);
|
||||
setSubFolders(result);
|
||||
}).catch(error => {
|
||||
setShowError(true);
|
||||
});
|
||||
};
|
||||
|
||||
const renderFolderPicker = () => {
|
||||
return (
|
||||
<div role="dialog">
|
||||
<Dialog
|
||||
hidden={!showFolderPicker}
|
||||
title={t(ResourceKeys.modelRepository.types.local.folderPicker.dialog.title)}
|
||||
modalProps={{
|
||||
className: 'folder-picker-dialog',
|
||||
isBlocking: false
|
||||
}}
|
||||
dialogContentProps={{
|
||||
subText: currentFolder && t(ResourceKeys.modelRepository.types.local.folderPicker.dialog.subText, {folder: currentFolder})
|
||||
}}
|
||||
onDismiss={dismissFolderPicker}
|
||||
>
|
||||
<div className="folder-links">
|
||||
<DefaultButton
|
||||
className="folder-button"
|
||||
iconProps={{ iconName: NAVIGATE_BACK }}
|
||||
text={t(ResourceKeys.modelRepository.types.local.folderPicker.command.navigateToParent)}
|
||||
ariaLabel={t(ResourceKeys.modelRepository.types.local.folderPicker.command.navigateToParent)}
|
||||
onClick={onNavigateBack}
|
||||
disabled={currentFolder === getRootFolder()}
|
||||
/>
|
||||
{showError ? <div className="no-folders-text">{t(ResourceKeys.modelRepository.types.local.folderPicker.dialog.error)}</div> :
|
||||
subFolders && subFolders.length > 0 ?
|
||||
subFolders.map(folder =>
|
||||
<DefaultButton
|
||||
className="folder-button"
|
||||
iconProps={{ iconName: FOLDER }}
|
||||
key={folder}
|
||||
text={folder}
|
||||
onClick={onClickFolderName(folder)}
|
||||
/>)
|
||||
:
|
||||
<div className="no-folders-text">{t(ResourceKeys.modelRepository.types.local.folderPicker.dialog.noFolderFoundText)}</div>}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<PrimaryButton onClick={onSelectFolder} text={t(ResourceKeys.modelRepository.types.local.folderPicker.command.select)} disabled={!currentFolder}/>
|
||||
<DefaultButton onClick={dismissFolderPicker} text={t(ResourceKeys.modelRepository.types.local.folderPicker.command.cancel)} />
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return(
|
||||
<>
|
||||
<ListItemLocalLabel repoType={repoType}/>
|
||||
<TextField
|
||||
className="local-folder-textbox"
|
||||
label={t(ResourceKeys.modelRepository.types.local.textBoxLabel)}
|
||||
ariaLabel={t(ResourceKeys.modelRepository.types.local.textBoxLabel)}
|
||||
value={currentFolder}
|
||||
readOnly={false}
|
||||
errorMessage={errorKey ? t(errorKey) : ''}
|
||||
onChange={onFolderPathChange}
|
||||
/>
|
||||
<DefaultButton
|
||||
className="local-folder-launch"
|
||||
text={t(ResourceKeys.modelRepository.types.local.folderPicker.command.openPicker)}
|
||||
ariaLabel={t(ResourceKeys.modelRepository.types.local.folderPicker.command.openPicker)}
|
||||
onClick={onShowFolderPicker}
|
||||
/>
|
||||
{renderFolderPicker()}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import 'jest';
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { ListItemLocalLabel } from './listItemLocalLabel';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../constants/repositoryLocationTypes';
|
||||
|
||||
describe('ListItemLocalLabel', () => {
|
||||
it('matches snapshot when repo type equals Local', () => {
|
||||
const wrapper = shallow(
|
||||
<ListItemLocalLabel repoType={REPOSITORY_LOCATION_TYPE.Local}/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when repo type equals LocalDMR', () => {
|
||||
const wrapper = shallow(
|
||||
<ListItemLocalLabel repoType={REPOSITORY_LOCATION_TYPE.LocalDMR}/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Link } from '@fluentui/react';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../constants/repositoryLocationTypes';
|
||||
import { ResourceKeys } from '../../../localization/resourceKeys';
|
||||
|
||||
export const ListItemLocalLabel: React.FC<{repoType: REPOSITORY_LOCATION_TYPE}> = ({repoType}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return(
|
||||
<>
|
||||
{repoType === REPOSITORY_LOCATION_TYPE.Local &&
|
||||
<div className="labelSection">
|
||||
<div className="label">{t(ResourceKeys.modelRepository.types.local.label)}</div>
|
||||
<div className="description">{t(ResourceKeys.modelRepository.types.local.infoText)}</div>
|
||||
</div>}
|
||||
{repoType === REPOSITORY_LOCATION_TYPE.LocalDMR &&
|
||||
<div className="labelSection">
|
||||
<div className="label">{t(ResourceKeys.modelRepository.types.dmr.label)}</div>
|
||||
<div className="description">
|
||||
<Trans components={[<Link key="0" href="https://github.com/Azure/iot-plugandplay-models-tools/wiki/Resolution-Convention" target="_blank"/>]}>
|
||||
{ResourceKeys.modelRepository.types.dmr.infoText}
|
||||
</Trans>
|
||||
</div>
|
||||
</div>}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import 'jest';
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { ListItemPublicRepo } from './listItemPublicRepo';
|
||||
|
||||
describe('ListItemPublicRepo', () => {
|
||||
it('matches snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<ListItemPublicRepo />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Link, TextField } from '@fluentui/react';
|
||||
import { ResourceKeys } from '../../../localization/resourceKeys';
|
||||
import { PUBLIC_REPO_HOSTNAME } from '../../constants/apiConstants';
|
||||
|
||||
export const ListItemPublicRepo: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return(
|
||||
<>
|
||||
<div className="labelSection">
|
||||
<div className="label">{t(ResourceKeys.modelRepository.types.public.label)}</div>
|
||||
<div className="description">
|
||||
<Trans components={[<Link key="0" href="https://github.com/Azure/iot-plugandplay-models" target="_blank"/>]}>
|
||||
{ResourceKeys.modelRepository.types.public.infoText}
|
||||
</Trans></div>
|
||||
</div>
|
||||
<TextField
|
||||
className="local-folder-textbox"
|
||||
label={t(ResourceKeys.modelRepository.types.configurable.textBoxLabel)}
|
||||
ariaLabel={t(ResourceKeys.modelRepository.types.configurable.textBoxLabel)}
|
||||
value={PUBLIC_REPO_HOSTNAME}
|
||||
readOnly={true}
|
||||
prefix="https://"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -8,7 +8,6 @@ import { ModelRepositoryInstruction } from './modelRepositoryInstruction';
|
|||
|
||||
describe('ModelRepositoryInstruction', () => {
|
||||
it('matches snapshot', () => {
|
||||
expect(shallow(<ModelRepositoryInstruction empty={true}/>)).toMatchSnapshot();
|
||||
expect(shallow(<ModelRepositoryInstruction empty={false}/>)).toMatchSnapshot();
|
||||
expect(shallow(<ModelRepositoryInstruction/>)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,20 +10,27 @@ import { ResourceKeys } from '../../../localization/resourceKeys';
|
|||
import { ROUTE_PARTS } from '../../constants/routes';
|
||||
import './modelRepositoryInstruction.scss';
|
||||
|
||||
export interface ModelRepositoryInstructionDataProps {
|
||||
empty: boolean;
|
||||
}
|
||||
|
||||
export const ModelRepositoryInstruction: React.FC<ModelRepositoryInstructionDataProps> = props => {
|
||||
export const ModelRepositoryInstruction: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const privacyStatement = (
|
||||
<div className="privacy-statement">
|
||||
<span>{t(ResourceKeys.settings.questions.questions.privacy.text)}</span>
|
||||
<Link
|
||||
href={t(ResourceKeys.settings.questions.questions.privacy.link)}
|
||||
target="_blank"
|
||||
>
|
||||
{t(ResourceKeys.settings.questions.questions.privacy.linkText)}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="model-repository-instruction">
|
||||
<div>
|
||||
<span>{t(ResourceKeys.modelRepository.description.description)}</span>
|
||||
<NavLink to={`/${ROUTE_PARTS.HOME}/${ROUTE_PARTS.MODEL_REPOS}`} className="embedded-link">Home.</NavLink>
|
||||
</div>
|
||||
|
||||
<h3 role="heading" aria-level={1}>{t(ResourceKeys.settings.questions.headerText)}</h3>
|
||||
<Link
|
||||
href={t(ResourceKeys.settings.questions.questions.documentation.link)}
|
||||
|
@ -33,21 +40,7 @@ export const ModelRepositoryInstruction: React.FC<ModelRepositoryInstructionData
|
|||
</Link>
|
||||
<h3 role="heading" aria-level={1}>{t(ResourceKeys.modelRepository.description.header)}</h3>
|
||||
{t(ResourceKeys.modelRepository.instruction)}
|
||||
{RenderPrivaryStatement()}
|
||||
</div>);
|
||||
};
|
||||
|
||||
const RenderPrivaryStatement = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="privacy-statement">
|
||||
<span>{t(ResourceKeys.settings.questions.questions.privacy.text)}</span>
|
||||
<Link
|
||||
href={t(ResourceKeys.settings.questions.questions.privacy.link)}
|
||||
target="_blank"
|
||||
>
|
||||
{t(ResourceKeys.settings.questions.questions.privacy.linkText)}
|
||||
</Link>
|
||||
{privacyStatement}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
@import '../../css/themes';
|
||||
|
||||
.location-list {
|
||||
margin-left: 20px;
|
||||
max-width: 500px;
|
||||
margin: 0 40px 0 20px;
|
||||
max-width: 650px;
|
||||
.item {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr;
|
||||
|
@ -38,6 +38,17 @@
|
|||
margin-top: 8px;
|
||||
border-radius: 2px;
|
||||
|
||||
.labelSection {
|
||||
.label{
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 700;
|
||||
}
|
||||
.description {
|
||||
font-style: italic;
|
||||
font-size: smaller;
|
||||
}
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
padding: {
|
||||
bottom: 14px;
|
||||
left: 22px;
|
||||
|
|
|
@ -4,105 +4,33 @@
|
|||
**********************************************************/
|
||||
import 'jest';
|
||||
import * as React from 'react';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import { Container } from 'react-smooth-dnd';
|
||||
import { shallow } from 'enzyme';
|
||||
import { ModelRepositoryLocationList } from './modelRepositoryLocationList';
|
||||
import { ModelRepositoryLocationListItem } from './modelRepositoryLocationListItem';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../constants/repositoryLocationTypes';
|
||||
import { getInitialModelRepositoryFormState } from '../state';
|
||||
import { getInitialModelRepositoryFormOps } from '../interface';
|
||||
|
||||
describe('components/settings/modelRepositoryLocationList', () => {
|
||||
describe('ModelRepositoryLocationList', () => {
|
||||
|
||||
it('matches snapshot with no items', () => {
|
||||
const component = (
|
||||
<ModelRepositoryLocationList
|
||||
repositoryLocationSettings={null}
|
||||
repositoryLocationSettingsErrors={{}}
|
||||
onChangeRepositoryLocationSettings={jest.fn()}
|
||||
formState={[{...getInitialModelRepositoryFormState(), repositoryLocationSettings: []}, getInitialModelRepositoryFormOps()]}
|
||||
/>
|
||||
);
|
||||
expect(shallow(component)).toMatchSnapshot();
|
||||
});
|
||||
it('matches snapshot with public item', () => {
|
||||
it('matches snapshot with all items', () => {
|
||||
const component = (
|
||||
<ModelRepositoryLocationList
|
||||
repositoryLocationSettings={[{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public}]}
|
||||
repositoryLocationSettingsErrors={{}}
|
||||
onChangeRepositoryLocationSettings={jest.fn()}
|
||||
formState={[{...getInitialModelRepositoryFormState(), repositoryLocationSettings: [
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public, value: '' },
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Configurable, value: 'test.com' },
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local, value: 'd:/myModels' },
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.LocalDMR, value: 'c:/dtmi' }
|
||||
]}, getInitialModelRepositoryFormOps()]}
|
||||
/>
|
||||
);
|
||||
expect(shallow(component)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot with each type item', () => {
|
||||
const component = (
|
||||
<ModelRepositoryLocationList
|
||||
repositoryLocationSettings={[
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public,},
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local}
|
||||
]}
|
||||
repositoryLocationSettingsErrors={{}}
|
||||
onChangeRepositoryLocationSettings={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(shallow(component)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('calls onChangeRepositoryLocationSettings when location value changes', () => {
|
||||
const spy = jest.fn();
|
||||
const wrapper = mount(
|
||||
<ModelRepositoryLocationList
|
||||
repositoryLocationSettings={[
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local, value: 'folder'},
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public},
|
||||
]}
|
||||
repositoryLocationSettingsErrors={{}}
|
||||
onChangeRepositoryLocationSettings={spy}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.find(ModelRepositoryLocationListItem).first().props().onChangeRepositoryLocationSettingValue(0, 'newFolder');
|
||||
expect(spy).toHaveBeenCalledWith([
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local, value: 'newFolder'},
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public},
|
||||
]);
|
||||
});
|
||||
|
||||
it('calls onChangeRepositoryLocationSettings when location removed', () => {
|
||||
const spy = jest.fn();
|
||||
const wrapper = mount(
|
||||
<ModelRepositoryLocationList
|
||||
repositoryLocationSettings={[
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local, value: 'folder'},
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public},
|
||||
]}
|
||||
repositoryLocationSettingsErrors={{}}
|
||||
onChangeRepositoryLocationSettings={spy}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.find(ModelRepositoryLocationListItem).first().props().onRemoveRepositoryLocationSetting(0);
|
||||
expect(spy).toHaveBeenCalledWith([
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public},
|
||||
]);
|
||||
});
|
||||
|
||||
it('calls onChangeRepositoryLocationSettings when location dropped', () => {
|
||||
const spy = jest.fn();
|
||||
const wrapper = mount(
|
||||
<ModelRepositoryLocationList
|
||||
repositoryLocationSettings={[
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local, value: 'folder'},
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public},
|
||||
]}
|
||||
repositoryLocationSettingsErrors={{}}
|
||||
onChangeRepositoryLocationSettings={spy}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.find(Container).first().props().onDrop({addedIndex: 0, removedIndex: 1});
|
||||
expect(spy).toHaveBeenCalledWith([
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public},
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local, value: 'folder'}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,57 +6,29 @@ import * as React from 'react';
|
|||
import { Container, Draggable } from 'react-smooth-dnd';
|
||||
import { ModelRepositoryLocationListItem } from './modelRepositoryLocationListItem';
|
||||
import { ModelRepositoryConfiguration } from '../../shared/modelRepository/state';
|
||||
import { StringMap } from '../../api/models/stringMap';
|
||||
import { ModelRepositoryFormType } from '../hooks/useModelRepositoryForm';
|
||||
import './modelRepositoryLocationList.scss';
|
||||
|
||||
export interface ModelRepositoryLocationListProps {
|
||||
repositoryLocationSettings: ModelRepositoryConfiguration[];
|
||||
repositoryLocationSettingsErrors: StringMap<string>;
|
||||
onChangeRepositoryLocationSettings(settings: ModelRepositoryConfiguration[]): void;
|
||||
}
|
||||
|
||||
export const ModelRepositoryLocationList: React.FC<ModelRepositoryLocationListProps> = props => {
|
||||
const { repositoryLocationSettings, repositoryLocationSettingsErrors, onChangeRepositoryLocationSettings} = props;
|
||||
export const ModelRepositoryLocationList: React.FC<{formState: ModelRepositoryFormType}> = ({formState}) => {
|
||||
const[{ repositoryLocationSettings }, {setRepositoryLocationSettings, setDirtyFlag}] = formState;
|
||||
|
||||
const onDrop = (e: {addedIndex: number, removedIndex: number}) => {
|
||||
const updatedRepositoryLocationSettings = [...repositoryLocationSettings];
|
||||
updatedRepositoryLocationSettings.splice(e.addedIndex, 0, updatedRepositoryLocationSettings.splice(e.removedIndex, 1)[0]);
|
||||
|
||||
onChangeRepositoryLocationSettings(updatedRepositoryLocationSettings);
|
||||
};
|
||||
|
||||
const onRemoveRepositoryLocationSetting = (index: number) => {
|
||||
const updatedRepositoryLocationSettings = [...props.repositoryLocationSettings];
|
||||
updatedRepositoryLocationSettings.splice(index, 1);
|
||||
|
||||
onChangeRepositoryLocationSettings(updatedRepositoryLocationSettings);
|
||||
};
|
||||
|
||||
const onChangeRepositoryLocationSettingValue = (index: number, value: string) => {
|
||||
const updatedRepositoryLocationSettings = repositoryLocationSettings.map((setting, i) => {
|
||||
if (i === index) {
|
||||
const updatedSetting = {...setting};
|
||||
updatedSetting.value = value;
|
||||
return updatedSetting;
|
||||
} else {
|
||||
return setting;
|
||||
}
|
||||
});
|
||||
|
||||
onChangeRepositoryLocationSettings(updatedRepositoryLocationSettings);
|
||||
setDirtyFlag(true);
|
||||
setRepositoryLocationSettings(updatedRepositoryLocationSettings);
|
||||
};
|
||||
|
||||
const renderModelRepositoryLocationListItem = (item: ModelRepositoryConfiguration, index: number) => {
|
||||
return (
|
||||
<Draggable key={item.repositoryLocationType} >
|
||||
<ModelRepositoryLocationListItem
|
||||
index={index}
|
||||
item={item}
|
||||
errorKey={repositoryLocationSettingsErrors[item.repositoryLocationType]}
|
||||
onChangeRepositoryLocationSettingValue={onChangeRepositoryLocationSettingValue}
|
||||
onRemoveRepositoryLocationSetting={onRemoveRepositoryLocationSetting}
|
||||
/>
|
||||
</Draggable>
|
||||
<Draggable key={item.repositoryLocationType} >
|
||||
<ModelRepositoryLocationListItem
|
||||
index={index}
|
||||
item={item}
|
||||
formState={formState}
|
||||
/>
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -17,9 +17,6 @@
|
|||
.ms-Dialog-header{
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.labelSection {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.folder-button {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
|
|
@ -4,109 +4,88 @@
|
|||
**********************************************************/
|
||||
import 'jest';
|
||||
import * as React from 'react';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import { shallow } from 'enzyme';
|
||||
import { IconButton } from '@fluentui/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Dialog } from '@fluentui/react';
|
||||
import { ModelRepositoryLocationListItem } from './modelRepositoryLocationListItem';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../constants/repositoryLocationTypes';
|
||||
import { ResourceKeys } from '../../../localization/resourceKeys';
|
||||
import * as Utils from '../../shared/utils/utils';
|
||||
import { getInitialModelRepositoryFormState } from '../state';
|
||||
import { getInitialModelRepositoryFormOps } from '../interface';
|
||||
|
||||
describe('components/settings/modelRepositoryLocationListItem', () => {
|
||||
it('matches snapshot for public', () => {
|
||||
describe('ModelRepositoryLocationListItem', () => {
|
||||
it('matches snapshot for public repo', () => {
|
||||
const wrapper = shallow(
|
||||
<ModelRepositoryLocationListItem
|
||||
index={0}
|
||||
item={{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public,
|
||||
value: ''
|
||||
}}
|
||||
onChangeRepositoryLocationSettingValue={jest.fn()}
|
||||
onRemoveRepositoryLocationSetting={jest.fn()}
|
||||
formState={[getInitialModelRepositoryFormState(), getInitialModelRepositoryFormOps()]}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('matches snapshot for local', () => {
|
||||
|
||||
it('matches snapshot for configurable repo', () => {
|
||||
const wrapper = shallow(
|
||||
<ModelRepositoryLocationListItem
|
||||
index={1}
|
||||
item={{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Configurable,
|
||||
value: 'test.com'
|
||||
}}
|
||||
formState={[getInitialModelRepositoryFormState(), getInitialModelRepositoryFormOps()]}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot for local folder', () => {
|
||||
const wrapper = shallow(
|
||||
<ModelRepositoryLocationListItem
|
||||
index={2}
|
||||
item={{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local,
|
||||
value: 'c:/models'
|
||||
}}
|
||||
formState={[getInitialModelRepositoryFormState(), getInitialModelRepositoryFormOps()]}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot for local repo', () => {
|
||||
const wrapper = shallow(
|
||||
<ModelRepositoryLocationListItem
|
||||
index={3}
|
||||
item={{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.LocalDMR,
|
||||
value: 'c:/dtmi'
|
||||
}}
|
||||
formState={[getInitialModelRepositoryFormState(), getInitialModelRepositoryFormOps()]}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('remove item calls expected operations', () => {
|
||||
const setRepositoryLocationSettings = jest.fn();
|
||||
const setDirtyFlag = jest.fn();
|
||||
const wrapper = shallow(
|
||||
<ModelRepositoryLocationListItem
|
||||
index={0}
|
||||
item={{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local,
|
||||
value: 'f:/'
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public,
|
||||
value: ''
|
||||
}}
|
||||
onChangeRepositoryLocationSettingValue={jest.fn()}
|
||||
onRemoveRepositoryLocationSetting={jest.fn()}
|
||||
formState={[getInitialModelRepositoryFormState(), {...getInitialModelRepositoryFormOps(), setRepositoryLocationSettings, setDirtyFlag}]}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('matches snapshot for local with error', () => {
|
||||
const wrapper = shallow(
|
||||
<ModelRepositoryLocationListItem
|
||||
errorKey={'error'}
|
||||
index={0}
|
||||
item={{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local,
|
||||
value: 'f:/'
|
||||
}}
|
||||
onChangeRepositoryLocationSettingValue={jest.fn()}
|
||||
onRemoveRepositoryLocationSetting={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
const removeButton = wrapper.find(IconButton).first();
|
||||
act(() => removeButton.props().onClick(undefined));
|
||||
|
||||
});
|
||||
it('renders no folder text when no sub folder is retrieved', () => {
|
||||
jest.spyOn(Utils, 'getRootFolder').mockReturnValue('f:/');
|
||||
|
||||
const wrapper = mount(
|
||||
<ModelRepositoryLocationListItem
|
||||
index={0}
|
||||
item={{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local,
|
||||
value: 'f:/'
|
||||
}}
|
||||
onChangeRepositoryLocationSettingValue={jest.fn()}
|
||||
onRemoveRepositoryLocationSetting={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
act(() => wrapper.find('.local-folder-launch').first().props().onClick(undefined));
|
||||
wrapper.update();
|
||||
|
||||
const dialog = wrapper.find(Dialog).first();
|
||||
expect(dialog.children().props().hidden).toBeFalsy();
|
||||
expect(dialog.children().props().children[0].props.children[0].props.disabled).toBeTruthy();
|
||||
expect(dialog.children().props().children[0].props.children[1].props.children).toEqual(ResourceKeys.modelRepository.types.local.folderPicker.dialog.noFolderFoundText);
|
||||
});
|
||||
|
||||
it('renders folders when sub folders retrieved', () => {
|
||||
const subFolders = ['documents', 'pictures'];
|
||||
jest.spyOn(Utils, 'getRootFolder').mockReturnValue('d:/');
|
||||
|
||||
const realUseState = React.useState;
|
||||
jest.spyOn(React, 'useState').mockImplementationOnce(() => realUseState(subFolders));
|
||||
|
||||
const wrapper = mount(
|
||||
<ModelRepositoryLocationListItem
|
||||
index={0}
|
||||
item={{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local,
|
||||
value: 'f:/'
|
||||
}}
|
||||
onChangeRepositoryLocationSettingValue={jest.fn()}
|
||||
onRemoveRepositoryLocationSetting={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
act(() => wrapper.find('.local-folder-launch').first().props().onClick(null));
|
||||
wrapper.update();
|
||||
|
||||
const dialog = wrapper.find(Dialog).first();
|
||||
expect(dialog.children().props().hidden).toBeFalsy();
|
||||
expect(dialog.children().props().children[0].props.children[0].props.text).toEqual(ResourceKeys.modelRepository.types.local.folderPicker.command.navigateToParent);
|
||||
expect(dialog.children().props().children[0].props.children[0].props.disabled).toBeFalsy();
|
||||
expect(dialog.children().props().children[0].props.children[1].length).toEqual(subFolders.length);
|
||||
expect(dialog.children().props().children[0].props.children[1][0].props.text).toEqual(subFolders[0]);
|
||||
expect(setRepositoryLocationSettings).toBeCalledWith([]);
|
||||
expect(setDirtyFlag).toBeCalledWith(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,243 +4,59 @@
|
|||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IconButton, DefaultButton, PrimaryButton, Label, Dialog, DialogFooter, TextField } from '@fluentui/react';
|
||||
import { IconButton } from '@fluentui/react';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../constants/repositoryLocationTypes';
|
||||
import { ResourceKeys } from '../../../localization/resourceKeys';
|
||||
import { CANCEL, NAVIGATE_BACK, FOLDER } from '../../constants/iconNames';
|
||||
import { fetchDirectories } from '../../api/services/localRepoService';
|
||||
import { LabelWithRichCallout } from '../../shared/components/labelWithRichCallout';
|
||||
import { getRootFolder, getParentFolder } from '../../shared/utils/utils';
|
||||
import { CANCEL } from '../../constants/iconNames';
|
||||
import { ModelRepositoryConfiguration } from '../../shared/modelRepository/state';
|
||||
import { PUBLIC_REPO_HOSTNAME } from '../../constants/apiConstants';
|
||||
import { ModelRepositoryFormType } from '../hooks/useModelRepositoryForm';
|
||||
import { ListItemPublicRepo } from './listItemPublicRepo';
|
||||
import { ListItemConfigurableRepo } from './listItemConfigurableRepo';
|
||||
import { ListItemLocal } from './listItemLocal';
|
||||
import './modelRepositoryLocationListItem.scss';
|
||||
|
||||
export interface ModelRepositoryLocationListItemProps {
|
||||
errorKey?: string;
|
||||
index: number;
|
||||
item: ModelRepositoryConfiguration;
|
||||
onChangeRepositoryLocationSettingValue: (index: number, path: string) => void;
|
||||
onRemoveRepositoryLocationSetting: (index: number) => void;
|
||||
formState: ModelRepositoryFormType;
|
||||
}
|
||||
|
||||
export interface RepositoryLocationListItemState {
|
||||
currentFolder: string;
|
||||
subFolders: string[];
|
||||
showFolderPicker: boolean;
|
||||
showError: boolean;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: cyclomatic-complexity
|
||||
export const ModelRepositoryLocationListItem: React.FC<ModelRepositoryLocationListItemProps> = (props: ModelRepositoryLocationListItemProps) => {
|
||||
export const ModelRepositoryLocationListItem: React.FC<ModelRepositoryLocationListItemProps> = ({index, item, formState}) => {
|
||||
const { t } = useTranslation();
|
||||
const [{repositoryLocationSettings }, {setRepositoryLocationSettings, setDirtyFlag}] = formState;
|
||||
|
||||
const { item, index, onChangeRepositoryLocationSettingValue, onRemoveRepositoryLocationSetting } = props;
|
||||
let initialCurrentFolder = '';
|
||||
if (item && item.repositoryLocationType === REPOSITORY_LOCATION_TYPE.Local) {
|
||||
initialCurrentFolder = item.value || getRootFolder();
|
||||
}
|
||||
let initialConfigurableRepositoryPath = '';
|
||||
if (item && item.repositoryLocationType === REPOSITORY_LOCATION_TYPE.Configurable) {
|
||||
initialConfigurableRepositoryPath = item.value;
|
||||
}
|
||||
const onRemove = () => {
|
||||
const updatedRepositoryLocationSettings = [...repositoryLocationSettings];
|
||||
updatedRepositoryLocationSettings.splice(index, 1);
|
||||
|
||||
const [ subFolders, setSubFolders ] = React.useState([]);
|
||||
const [ currentFolder, setCurrentFolder ] = React.useState(initialCurrentFolder);
|
||||
const [ currentConfigurableRepositoryPath, setCurrentConfigurableRepositoryPath ] = React.useState(initialConfigurableRepositoryPath);
|
||||
const [ showError, setShowError ] = React.useState<boolean>(false);
|
||||
const [ showFolderPicker, setShowFolderPicker ] = React.useState<boolean>(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setCurrentConfigurableRepositoryPath(initialConfigurableRepositoryPath);
|
||||
}, [initialConfigurableRepositoryPath]); // tslint:disable-line: align
|
||||
|
||||
React.useEffect(() => {
|
||||
setCurrentFolder(initialCurrentFolder);
|
||||
}, [initialCurrentFolder]); // tslint:disable-line: align
|
||||
|
||||
const onRemove = () => onRemoveRepositoryLocationSetting(index);
|
||||
setDirtyFlag(true);
|
||||
setRepositoryLocationSettings(updatedRepositoryLocationSettings);
|
||||
};
|
||||
|
||||
const renderItemDetail = () => {
|
||||
return (
|
||||
<div className="item-details">
|
||||
{item.repositoryLocationType === REPOSITORY_LOCATION_TYPE.Public &&
|
||||
renderPublicRepoItem()
|
||||
<ListItemPublicRepo/>
|
||||
}
|
||||
{item.repositoryLocationType === REPOSITORY_LOCATION_TYPE.Local &&
|
||||
renderLocalFolderItem()
|
||||
{(item.repositoryLocationType === REPOSITORY_LOCATION_TYPE.Local || item.repositoryLocationType === REPOSITORY_LOCATION_TYPE.LocalDMR) &&
|
||||
<ListItemLocal
|
||||
index={index}
|
||||
item={item}
|
||||
formState={formState}
|
||||
repoType={item.repositoryLocationType}
|
||||
/>
|
||||
}
|
||||
{item.repositoryLocationType === REPOSITORY_LOCATION_TYPE.Configurable &&
|
||||
renderConfigurableRepoItem()
|
||||
<ListItemConfigurableRepo
|
||||
index={index}
|
||||
item={item}
|
||||
formState={formState}
|
||||
/>
|
||||
}
|
||||
</div>);
|
||||
};
|
||||
|
||||
const renderPublicRepoItem = () => {
|
||||
return(
|
||||
<>
|
||||
<div className="labelSection">
|
||||
<Label>{t(ResourceKeys.modelRepository.types.public.label)}</Label>
|
||||
</div>
|
||||
<TextField
|
||||
className="local-folder-textbox"
|
||||
label={t(ResourceKeys.modelRepository.types.configurable.textBoxLabel)}
|
||||
ariaLabel={t(ResourceKeys.modelRepository.types.configurable.textBoxLabel)}
|
||||
value={PUBLIC_REPO_HOSTNAME}
|
||||
readOnly={true}
|
||||
prefix="https://"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderLocalFolderItem = () => {
|
||||
return(
|
||||
<>
|
||||
<div className="labelSection">
|
||||
<LabelWithRichCallout
|
||||
calloutContent={t(ResourceKeys.modelRepository.types.local.infoText)}
|
||||
required={true}
|
||||
>
|
||||
{t(ResourceKeys.modelRepository.types.local.label)}
|
||||
</LabelWithRichCallout>
|
||||
</div>
|
||||
<TextField
|
||||
className="local-folder-textbox"
|
||||
label={t(ResourceKeys.modelRepository.types.local.textBoxLabel)}
|
||||
ariaLabel={t(ResourceKeys.modelRepository.types.local.textBoxLabel)}
|
||||
value={currentFolder}
|
||||
readOnly={false}
|
||||
errorMessage={props.errorKey ? t(props.errorKey) : ''}
|
||||
onChange={onFolderPathChange}
|
||||
/>
|
||||
<DefaultButton
|
||||
className="local-folder-launch"
|
||||
text={t(ResourceKeys.modelRepository.types.local.folderPicker.command.openPicker)}
|
||||
ariaLabel={t(ResourceKeys.modelRepository.types.local.folderPicker.command.openPicker)}
|
||||
onClick={onShowFolderPicker}
|
||||
/>
|
||||
{renderFolderPicker()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderConfigurableRepoItem = () => {
|
||||
return(
|
||||
<>
|
||||
<div className="labelSection">
|
||||
<LabelWithRichCallout
|
||||
calloutContent={t(ResourceKeys.modelRepository.types.configurable.infoText)}
|
||||
required={true}
|
||||
>
|
||||
{t(ResourceKeys.modelRepository.types.configurable.label)}
|
||||
</LabelWithRichCallout>
|
||||
</div>
|
||||
<TextField
|
||||
className="local-folder-textbox"
|
||||
label={t(ResourceKeys.modelRepository.types.configurable.textBoxLabel)}
|
||||
ariaLabel={t(ResourceKeys.modelRepository.types.configurable.textBoxLabel)}
|
||||
value={currentConfigurableRepositoryPath}
|
||||
readOnly={false}
|
||||
errorMessage={props.errorKey ? t(props.errorKey) : ''}
|
||||
onChange={repositoryEndpointChange}
|
||||
prefix="https://"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const repositoryEndpointChange = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
|
||||
setCurrentConfigurableRepositoryPath(newValue);
|
||||
onChangeRepositoryLocationSettingValue(index, newValue);
|
||||
};
|
||||
|
||||
const onFolderPathChange = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
|
||||
setCurrentFolder(newValue);
|
||||
onChangeRepositoryLocationSettingValue(index, newValue);
|
||||
};
|
||||
|
||||
const onShowFolderPicker = () => {
|
||||
fetchSubDirectoriesInfo(currentFolder);
|
||||
setShowFolderPicker(true);
|
||||
};
|
||||
|
||||
const dismissFolderPicker = () => {
|
||||
setCurrentFolder(item.value || getRootFolder());
|
||||
setShowFolderPicker(false);
|
||||
};
|
||||
|
||||
const onSelectFolder = () => {
|
||||
onChangeRepositoryLocationSettingValue(index, currentFolder);
|
||||
setShowFolderPicker(false);
|
||||
};
|
||||
|
||||
const onClickFolderName = (folder: string) => () => {
|
||||
const newDir = currentFolder ? `${currentFolder.replace(/\/$/, '')}/${folder}` : folder;
|
||||
setCurrentFolder(newDir);
|
||||
fetchSubDirectoriesInfo(newDir);
|
||||
};
|
||||
|
||||
const onNavigateBack = () => {
|
||||
const parentFolder = getParentFolder(currentFolder);
|
||||
setCurrentFolder(parentFolder);
|
||||
fetchSubDirectoriesInfo(parentFolder);
|
||||
};
|
||||
|
||||
const fetchSubDirectoriesInfo = (folderName: string) => {
|
||||
fetchDirectories(folderName).then(result => {
|
||||
setShowError(false);
|
||||
setSubFolders(result);
|
||||
}).catch(error => {
|
||||
setShowError(true);
|
||||
});
|
||||
};
|
||||
|
||||
const renderFolderPicker = () => {
|
||||
return (
|
||||
<div role="dialog">
|
||||
<Dialog
|
||||
hidden={!showFolderPicker}
|
||||
title={t(ResourceKeys.modelRepository.types.local.folderPicker.dialog.title)}
|
||||
modalProps={{
|
||||
className: 'folder-picker-dialog',
|
||||
isBlocking: false
|
||||
}}
|
||||
dialogContentProps={{
|
||||
subText: currentFolder && t(ResourceKeys.modelRepository.types.local.folderPicker.dialog.subText, {folder: currentFolder})
|
||||
}}
|
||||
onDismiss={dismissFolderPicker}
|
||||
>
|
||||
<div className="folder-links">
|
||||
<DefaultButton
|
||||
className="folder-button"
|
||||
iconProps={{ iconName: NAVIGATE_BACK }}
|
||||
text={t(ResourceKeys.modelRepository.types.local.folderPicker.command.navigateToParent)}
|
||||
ariaLabel={t(ResourceKeys.modelRepository.types.local.folderPicker.command.navigateToParent)}
|
||||
onClick={onNavigateBack}
|
||||
disabled={currentFolder === getRootFolder()}
|
||||
/>
|
||||
{showError ? <div className="no-folders-text">{t(ResourceKeys.modelRepository.types.local.folderPicker.dialog.error)}</div> :
|
||||
subFolders && subFolders.length > 0 ?
|
||||
subFolders.map(folder =>
|
||||
<DefaultButton
|
||||
className="folder-button"
|
||||
iconProps={{ iconName: FOLDER }}
|
||||
key={folder}
|
||||
text={folder}
|
||||
onClick={onClickFolderName(folder)}
|
||||
/>)
|
||||
:
|
||||
<div className="no-folders-text">{t(ResourceKeys.modelRepository.types.local.folderPicker.dialog.noFolderFoundText)}</div>}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<PrimaryButton onClick={onSelectFolder} text={t(ResourceKeys.modelRepository.types.local.folderPicker.command.select)} disabled={!currentFolder}/>
|
||||
<DefaultButton onClick={dismissFolderPicker} text={t(ResourceKeys.modelRepository.types.local.folderPicker.command.cancel)} />
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="item" role="list">
|
||||
<div className="numbering">
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { ModelRepositoryLocationView, validateRepositoryLocationSettings } from './modelRepositoryLocationView';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../constants/repositoryLocationTypes';
|
||||
import { ResourceKeys } from '../../../localization/resourceKeys';
|
||||
import * as ModelRepositoryContext from '../../shared/modelRepository/context/modelRepositoryStateContext';
|
||||
import { getInitialModelRepositoryState } from '../../shared/modelRepository/state';
|
||||
import { getInitialModelRepositoryActions } from '../../shared/modelRepository/interface';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: () => ({ push: jest.fn() }),
|
||||
useLocation: () => ({ search: '' }),
|
||||
useRouteMatch: () => ({ url: 'url', path: 'path'})
|
||||
}));
|
||||
|
||||
describe('modelRepositoryLocationView', () => {
|
||||
it('matches snapshot when no locations', () => {
|
||||
jest.spyOn(ModelRepositoryContext, 'useModelRepositoryContext').mockReturnValueOnce([getInitialModelRepositoryState(), getInitialModelRepositoryActions()]);
|
||||
expect(shallow(<ModelRepositoryLocationView/>)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when locations greater than 0', () => {
|
||||
const initialState = [
|
||||
...getInitialModelRepositoryState(),
|
||||
{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Configurable,
|
||||
value: 'd:/'
|
||||
}
|
||||
];
|
||||
jest.spyOn(ModelRepositoryContext, 'useModelRepositoryContext').mockReturnValueOnce([initialState, getInitialModelRepositoryActions()]);
|
||||
expect(shallow(<ModelRepositoryLocationView/>)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateRepositoryLocationSettings', () => {
|
||||
it('adds validation error when local repository lacks value', () => {
|
||||
const result = validateRepositoryLocationSettings([
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local, value: ''}
|
||||
]);
|
||||
|
||||
expect(result[REPOSITORY_LOCATION_TYPE.Local]).toEqual(ResourceKeys.modelRepository.types.local.folderPicker.errors.mandatory);
|
||||
});
|
||||
|
||||
it('passes public repository', () => {
|
||||
const result = validateRepositoryLocationSettings([
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public, value: ''}
|
||||
]);
|
||||
|
||||
expect(result[REPOSITORY_LOCATION_TYPE.Public]).toBeFalsy();
|
||||
});
|
||||
|
||||
it('passes validation when local validation has value', () => {
|
||||
const result = validateRepositoryLocationSettings([
|
||||
{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Local, value: 'value'}
|
||||
]);
|
||||
|
||||
expect(result[REPOSITORY_LOCATION_TYPE.Local]).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { useModelRepositoryContext } from '../../shared/modelRepository/context/modelRepositoryStateContext';
|
||||
import { StringMap } from '../../api/models/stringMap';
|
||||
import { ModelRepositoryStateInterface } from '../../shared/modelRepository/state';
|
||||
import { ModelRepositoryFormStateInterface } from '../state';
|
||||
import { ModelRepositoryFormOpsInterface } from '../interface';
|
||||
|
||||
export type ModelRepositoryFormType = [ModelRepositoryFormStateInterface, ModelRepositoryFormOpsInterface];
|
||||
export const useModelRepositoryForm = (): ModelRepositoryFormType => {
|
||||
const [ modelRepositoryState, ] = useModelRepositoryContext();
|
||||
const [ repositoryLocationSettings, setRepositoryLocationSettings ] = React.useState<ModelRepositoryStateInterface>(modelRepositoryState);
|
||||
const [ repositoryLocationSettingsErrors, setRepositoryLocationSettingsErrors ] = React.useState<StringMap<string>>({});
|
||||
const [ dirty, setDirtyFlag ] = React.useState<boolean>(false);
|
||||
|
||||
return [{repositoryLocationSettings, repositoryLocationSettingsErrors, dirty }, {setRepositoryLocationSettings, setRepositoryLocationSettingsErrors, setDirtyFlag}];
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
import { ModelRepositoryStateInterface } from "../shared/modelRepository/state";
|
||||
import { StringMap } from '../api/models/stringMap';
|
||||
|
||||
export interface ModelRepositoryFormOpsInterface {
|
||||
setDirtyFlag: (dirty: boolean) => void;
|
||||
setRepositoryLocationSettings: (settings: ModelRepositoryStateInterface) => void;
|
||||
setRepositoryLocationSettingsErrors: (errors: StringMap<string> ) => void;
|
||||
}
|
||||
|
||||
export const getInitialModelRepositoryFormOps = (): ModelRepositoryFormOpsInterface => ({
|
||||
setDirtyFlag: () => undefined,
|
||||
setRepositoryLocationSettings: () => undefined,
|
||||
setRepositoryLocationSettingsErrors: () => undefined
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import { ModelRepositoryStateInterface } from "../shared/modelRepository/state";
|
||||
import { StringMap } from '../api/models/stringMap';
|
||||
import { REPOSITORY_LOCATION_TYPE } from "../constants/repositoryLocationTypes";
|
||||
|
||||
export interface ModelRepositoryFormStateInterface {
|
||||
dirty: boolean;
|
||||
repositoryLocationSettings: ModelRepositoryStateInterface;
|
||||
repositoryLocationSettingsErrors: StringMap<string>;
|
||||
}
|
||||
|
||||
export const getInitialModelRepositoryFormState = (): ModelRepositoryFormStateInterface => ({
|
||||
dirty: false,
|
||||
repositoryLocationSettings: [{repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public, value: '' }],
|
||||
repositoryLocationSettingsErrors: {}
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { ModelRepositoryLocationView, } from './view';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../constants/repositoryLocationTypes';
|
||||
import * as ModelRepositoryContext from '../shared/modelRepository/context/modelRepositoryStateContext';
|
||||
import { getInitialModelRepositoryState } from '../shared/modelRepository/state';
|
||||
import { getInitialModelRepositoryActions } from '../shared/modelRepository/interface';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: () => ({ push: jest.fn() }),
|
||||
useLocation: () => ({ search: '' }),
|
||||
useRouteMatch: () => ({ url: 'url', path: 'path'})
|
||||
}));
|
||||
|
||||
describe('view', () => {
|
||||
it('matches snapshot when no locations', () => {
|
||||
jest.spyOn(ModelRepositoryContext, 'useModelRepositoryContext').mockReturnValueOnce([getInitialModelRepositoryState(), getInitialModelRepositoryActions()]);
|
||||
expect(shallow(<ModelRepositoryLocationView/>)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when locations greater than 0', () => {
|
||||
const initialState = [
|
||||
...getInitialModelRepositoryState(),
|
||||
{
|
||||
repositoryLocationType: REPOSITORY_LOCATION_TYPE.Configurable,
|
||||
value: 'd:/'
|
||||
}
|
||||
];
|
||||
jest.spyOn(ModelRepositoryContext, 'useModelRepositoryContext').mockReturnValueOnce([initialState, getInitialModelRepositoryActions()]);
|
||||
expect(shallow(<ModelRepositoryLocationView/>)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Prompt } from 'react-router-dom';
|
||||
import { ResourceKeys } from '../../localization/resourceKeys';
|
||||
import { ModelRepositoryLocationList } from './components/modelRepositoryLocationList';
|
||||
import { ModelRepositoryInstruction } from './components/modelRepositoryInstruction';
|
||||
import { useBreadcrumbEntry } from '../navigation/hooks/useBreadcrumbEntry';
|
||||
import { AppInsightsClient } from '../shared/appTelemetry/appInsightsClient';
|
||||
import { TELEMETRY_PAGE_NAMES } from '../constants/telemetry';
|
||||
import { Commands } from './components/commands';
|
||||
import { useModelRepositoryForm } from './hooks/useModelRepositoryForm';
|
||||
|
||||
export const ModelRepositoryLocationView: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
useBreadcrumbEntry({ name: t(ResourceKeys.breadcrumb.ioTPlugAndPlay)});
|
||||
const formState = useModelRepositoryForm();
|
||||
|
||||
React.useEffect(() => {
|
||||
AppInsightsClient.getInstance()?.trackPageView({name: TELEMETRY_PAGE_NAMES.PNP_REPO_SETTINGS});
|
||||
}, []); // tslint:disable-line: align
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Prompt when={formState[0].dirty} message={t(ResourceKeys.common.navigation.confirm)}/>
|
||||
<Commands formState={formState}/>
|
||||
<div>
|
||||
<ModelRepositoryInstruction/>
|
||||
<ModelRepositoryLocationList formState={formState}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -12,8 +12,8 @@ import { NotificationsInterface } from '../interface';
|
|||
export const useNotificationState = (): [NotificationsStateInterface, NotificationsInterface] => {
|
||||
const [state, dispatch] = React.useReducer(notificationsReducer, getInitialNotificationsState());
|
||||
return [state, {
|
||||
addNotification: (item: Notification)=> dispatch(addNotificationAction(item)),
|
||||
addNotification: (item: Notification) => dispatch(addNotificationAction(item)),
|
||||
clearNotifications: () => dispatch(clearNotificationsAction),
|
||||
markAllAsRead: () => dispatch(markAllNotificationsAsReadAction)
|
||||
}]
|
||||
}];
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@ export interface NotificationsInterface {
|
|||
}
|
||||
|
||||
export const getInitialNotificationsActions = (): NotificationsInterface => ({
|
||||
clearNotifications: () => undefined,
|
||||
addNotification: () => undefined,
|
||||
clearNotifications: () => undefined,
|
||||
markAllAsRead: () => undefined
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ import i18n from './i18n';
|
|||
import { Application } from './app/shared/components/application';
|
||||
import { GlobalContextProvider } from './app/shared/global/context/globalContext';
|
||||
import './app/css/_index.scss';
|
||||
import './app/css/_layouts.scss';
|
||||
|
||||
initializeIcons();
|
||||
|
||||
|
|
|
@ -947,9 +947,12 @@
|
|||
},
|
||||
"addLocalSource": {
|
||||
"label": "Local folder",
|
||||
"labelInBrowser":"Local folder (This is only enabled in the desktop version from: https://github.com/Azure/azure-iot-explorer/releases)",
|
||||
"ariaLabel": "Add local folder as model source"
|
||||
},
|
||||
"addLocalDMRSource": {
|
||||
"label": "Local repository",
|
||||
"ariaLabel": "Add local repository as model source"
|
||||
},
|
||||
"remove": {
|
||||
"label": "Remove",
|
||||
"ariaLabel": "Remove"
|
||||
|
@ -965,15 +968,13 @@
|
|||
},
|
||||
"types": {
|
||||
"public" : {
|
||||
"label": "Public Repository"
|
||||
"label": "Public Repository",
|
||||
"infoText": "The <0>iot-plugandplay-models GitHub repository</0> includes DTDL models that are made publicly available on https://devicemodels.azure.com."
|
||||
},
|
||||
"configurable" : {
|
||||
"label": "Configurable Repository",
|
||||
"infoText": "Configure your own repository endpoint",
|
||||
"textBoxLabel": "Repository endpoint",
|
||||
"errors": {
|
||||
"mandatory": "Please specify the endpoint."
|
||||
}
|
||||
"infoText": "Configure your own repository endpoint.",
|
||||
"textBoxLabel": "Repository endpoint"
|
||||
},
|
||||
"local": {
|
||||
"label": "Local Folder",
|
||||
|
@ -986,9 +987,6 @@
|
|||
"select": "Select",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"errors": {
|
||||
"mandatory": "Please specify a folder."
|
||||
},
|
||||
"dialog": {
|
||||
"title": "Select a folder as a local model repository",
|
||||
"subText": "Selected folder: {{folder}}",
|
||||
|
@ -997,7 +995,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"notAvailable": "--"
|
||||
"dmr": {
|
||||
"label": "Local Repository",
|
||||
"infoText": "Use your local folder as a model repository following the <0>resolution convention spec</0>. Please configure the file path to be your local DTMI folder."
|
||||
},
|
||||
"notAvailable": "--",
|
||||
"mandatory": "Input is required."
|
||||
}
|
||||
},
|
||||
"authentication": {
|
||||
|
|
|
@ -792,10 +792,13 @@ export class ResourceKeys {
|
|||
ariaLabel : "modelRepository.commands.addConfigurableRepoSource.ariaLabel",
|
||||
label : "modelRepository.commands.addConfigurableRepoSource.label",
|
||||
},
|
||||
addLocalDMRSource : {
|
||||
ariaLabel : "modelRepository.commands.addLocalDMRSource.ariaLabel",
|
||||
label : "modelRepository.commands.addLocalDMRSource.label",
|
||||
},
|
||||
addLocalSource : {
|
||||
ariaLabel : "modelRepository.commands.addLocalSource.ariaLabel",
|
||||
label : "modelRepository.commands.addLocalSource.label",
|
||||
labelInBrowser : "modelRepository.commands.addLocalSource.labelInBrowser",
|
||||
},
|
||||
addPublicSource : {
|
||||
ariaLabel : "modelRepository.commands.addPublicSource.ariaLabel",
|
||||
|
@ -830,13 +833,14 @@ export class ResourceKeys {
|
|||
instruction : "modelRepository.instruction",
|
||||
types : {
|
||||
configurable : {
|
||||
errors : {
|
||||
mandatory : "modelRepository.types.configurable.errors.mandatory",
|
||||
},
|
||||
infoText : "modelRepository.types.configurable.infoText",
|
||||
label : "modelRepository.types.configurable.label",
|
||||
textBoxLabel : "modelRepository.types.configurable.textBoxLabel",
|
||||
},
|
||||
dmr : {
|
||||
infoText : "modelRepository.types.dmr.infoText",
|
||||
label : "modelRepository.types.dmr.label",
|
||||
},
|
||||
local : {
|
||||
folderPicker : {
|
||||
command : {
|
||||
|
@ -851,16 +855,15 @@ export class ResourceKeys {
|
|||
subText : "modelRepository.types.local.folderPicker.dialog.subText",
|
||||
title : "modelRepository.types.local.folderPicker.dialog.title",
|
||||
},
|
||||
errors : {
|
||||
mandatory : "modelRepository.types.local.folderPicker.errors.mandatory",
|
||||
},
|
||||
},
|
||||
infoText : "modelRepository.types.local.infoText",
|
||||
label : "modelRepository.types.local.label",
|
||||
textBoxLabel : "modelRepository.types.local.textBoxLabel",
|
||||
},
|
||||
mandatory : "modelRepository.types.mandatory",
|
||||
notAvailable : "modelRepository.types.notAvailable",
|
||||
public : {
|
||||
infoText : "modelRepository.types.public.infoText",
|
||||
label : "modelRepository.types.public.label",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -13,7 +13,7 @@ import fetch from 'node-fetch';
|
|||
import { EventHubConsumerClient, Subscription, ReceivedEventData } from '@azure/event-hubs';
|
||||
import { generateDataPlaneRequestBody, generateDataPlaneResponse } from './dataPlaneHelper';
|
||||
import { convertIotHubToEventHubsConnectionString } from './eventHubHelper';
|
||||
import { fetchDirectories, fetchDrivesOnWindows, findMatchingFile } from './utils';
|
||||
import { fetchDirectories, fetchDrivesOnWindows, findMatchingFile, readFileFromLocal } from './utils';
|
||||
|
||||
export const SERVER_ERROR = 500;
|
||||
export const SUCCESS = 200;
|
||||
|
@ -53,6 +53,7 @@ export class ServerBase {
|
|||
app.post(eventHubStopUri, handleEventHubStopPostRequest);
|
||||
app.post(modelRepoUri, handleModelRepoPostRequest);
|
||||
app.get(readFileUri, handleReadFileRequest);
|
||||
app.get(readFileNaiveUri, handleReadFileNaiveRequest);
|
||||
app.get(getDirectoriesUri, handleGetDirectoriesRequest);
|
||||
|
||||
//initialize a simple http server
|
||||
|
@ -109,6 +110,25 @@ export const handleReadFileRequest = (req: express.Request, res: express.Respons
|
|||
}
|
||||
};
|
||||
|
||||
const readFileNaiveUri = '/api/ReadFileNaive/:path/:file';
|
||||
export const handleReadFileNaiveRequest = (req: express.Request, res: express.Response) => {
|
||||
try {
|
||||
const filePath = req.params.path;
|
||||
const expectedFileName = req.params.file;
|
||||
if (!filePath || !expectedFileName) {
|
||||
res.status(BAD_REQUEST).send();
|
||||
}
|
||||
else {
|
||||
const data = readFileFromLocal(filePath, expectedFileName);
|
||||
JSON.parse(data); // try parse the data to validate json format
|
||||
res.status(SUCCESS).send(data);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
res.status(SERVER_ERROR).send(error);
|
||||
}
|
||||
};
|
||||
|
||||
const getDirectoriesUri = '/api/Directories/:path';
|
||||
export const handleGetDirectoriesRequest = (req: express.Request, res: express.Response) => {
|
||||
try {
|
||||
|
|
Загрузка…
Ссылка в новой задаче