зеркало из
1
0
Форкнуть 0
* working version1

* update tests

* fix build break

* update per comments1
This commit is contained in:
YingXue 2023-02-24 14:48:25 -08:00 коммит произвёл GitHub
Родитель b7826d54ba
Коммит b794202329
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
76 изменённых файлов: 1770 добавлений и 1597 удалений

Просмотреть файл

@ -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 {