diff --git a/src/app/api/constants.ts b/src/app/api/constants.ts deleted file mode 100644 index 341d75978..000000000 --- a/src/app/api/constants.ts +++ /dev/null @@ -1,20 +0,0 @@ -/*********************************************************** - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License - **********************************************************/ -export enum HTTP_OPERATION_TYPES { - Delete = 'DELETE', - Get = 'GET', - Patch = 'PATCH', - Post = 'POST', - Put = 'PUT' -} - -export const MILLISECONDS_PER_SECOND = 1000; -export const SECONDS_PER_MINUTE = 60; -export const APPLICATION_JSON = 'application/json'; -export const ERROR_TYPES = { - AUTHORIZATION_RULE_NOT_FOUND: 'authorizationRuleNotFound', - HTTP: 'http', - PORT_IS_IN_USE: 'portIsInUse' -}; diff --git a/src/app/api/models/authorizationRuleNotFoundError.ts b/src/app/api/models/authorizationRuleNotFoundError.ts index d13a73a67..e01c431f2 100644 --- a/src/app/api/models/authorizationRuleNotFoundError.ts +++ b/src/app/api/models/authorizationRuleNotFoundError.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License **********************************************************/ -import { ERROR_TYPES } from '../constants'; +import { ERROR_TYPES } from './../../constants/apiConstants'; export class AuthorizationRuleNotFoundError extends Error { public requiredPermissions: string[]; diff --git a/src/app/api/models/httpError.ts b/src/app/api/models/httpError.ts index bc5c6226a..d8762bff3 100644 --- a/src/app/api/models/httpError.ts +++ b/src/app/api/models/httpError.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License **********************************************************/ -import { ERROR_TYPES } from '../constants'; +import { ERROR_TYPES } from './../../constants/apiConstants'; export class HttpError extends Error { public httpCode: number; diff --git a/src/app/api/models/modelDefinitionWithSource.ts b/src/app/api/models/modelDefinitionWithSource.ts index ae649f560..78e1863b6 100644 --- a/src/app/api/models/modelDefinitionWithSource.ts +++ b/src/app/api/models/modelDefinitionWithSource.ts @@ -7,5 +7,6 @@ import { REPOSITORY_LOCATION_TYPE } from '../../constants/repositoryLocationType export interface ModelDefinitionWithSource { modelDefinition: ModelDefinition; - source?: REPOSITORY_LOCATION_TYPE; + source: REPOSITORY_LOCATION_TYPE; + isModelValid: boolean; } diff --git a/src/app/api/models/portIsInUseError.ts b/src/app/api/models/portIsInUseError.ts index 73dc39b71..9f3525931 100644 --- a/src/app/api/models/portIsInUseError.ts +++ b/src/app/api/models/portIsInUseError.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License **********************************************************/ -import { ERROR_TYPES } from '../constants'; +import { ERROR_TYPES } from './../../constants/apiConstants'; export class PortIsInUseError extends Error { constructor() { diff --git a/src/app/api/services/dataplaneServiceHelper.ts b/src/app/api/services/dataplaneServiceHelper.ts index c42b57d18..17858b6c1 100644 --- a/src/app/api/services/dataplaneServiceHelper.ts +++ b/src/app/api/services/dataplaneServiceHelper.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License **********************************************************/ import { DataPlaneParameters } from '../parameters/deviceParameters'; -import { CONTROLLER_API_ENDPOINT, DATAPLANE, DataPlaneStatusCode } from '../../constants/apiConstants'; -import { HTTP_OPERATION_TYPES } from '../constants'; +import { CONTROLLER_API_ENDPOINT, DATAPLANE, DataPlaneStatusCode, HTTP_OPERATION_TYPES } from '../../constants/apiConstants'; import { getConnectionInfoFromConnectionString, generateSasToken } from '../shared/utils'; import { PortIsInUseError } from '../models/portIsInUseError'; diff --git a/src/app/api/services/devicesService.spec.ts b/src/app/api/services/devicesService.spec.ts index 9bea7778a..29810a854 100644 --- a/src/app/api/services/devicesService.spec.ts +++ b/src/app/api/services/devicesService.spec.ts @@ -5,8 +5,7 @@ import 'jest'; import * as DevicesService from './devicesService'; import * as DataplaneService from './dataplaneServiceHelper'; -import { HTTP_OPERATION_TYPES } from '../constants'; -import { DIGITAL_TWIN_API_VERSION, CONTROLLER_API_ENDPOINT, CLOUD_TO_DEVICE } from '../../constants/apiConstants'; +import { DIGITAL_TWIN_API_VERSION, CONTROLLER_API_ENDPOINT, CLOUD_TO_DEVICE, HTTP_OPERATION_TYPES } from '../../constants/apiConstants'; import { CONNECTION_TIMEOUT_IN_SECONDS, RESPONSE_TIME_IN_SECONDS } from '../../constants/devices'; import { Twin } from '../models/device'; import { DeviceIdentity } from './../models/deviceIdentity'; diff --git a/src/app/api/services/devicesService.ts b/src/app/api/services/devicesService.ts index 3ec70203c..d88834bcc 100644 --- a/src/app/api/services/devicesService.ts +++ b/src/app/api/services/devicesService.ts @@ -17,8 +17,7 @@ import { PatchDigitalTwinInterfacePropertiesParameters, CloudToDeviceMessageParameters } from '../parameters/deviceParameters'; -import { CONTROLLER_API_ENDPOINT, EVENTHUB, DIGITAL_TWIN_API_VERSION, MONITOR, STOP, HEADERS, CLOUD_TO_DEVICE } from '../../constants/apiConstants'; -import { HTTP_OPERATION_TYPES } from '../constants'; +import { CONTROLLER_API_ENDPOINT, EVENTHUB, DIGITAL_TWIN_API_VERSION, MONITOR, STOP, HEADERS, CLOUD_TO_DEVICE, HTTP_OPERATION_TYPES } from '../../constants/apiConstants'; import { buildQueryString } from '../shared/utils'; import { CONNECTION_TIMEOUT_IN_SECONDS, RESPONSE_TIME_IN_SECONDS } from '../../constants/devices'; import { Message } from '../models/messages'; diff --git a/src/app/api/services/digitalTwinsModelService.spec.ts b/src/app/api/services/digitalTwinsModelService.spec.ts index 0ad62af06..b7b488910 100644 --- a/src/app/api/services/digitalTwinsModelService.spec.ts +++ b/src/app/api/services/digitalTwinsModelService.spec.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License **********************************************************/ import * as DigitalTwinsModelService from './digitalTwinsModelService'; -import { API_VERSION, DIGITAL_TWIN_API_VERSION } from '../../constants/apiConstants'; -import { HTTP_OPERATION_TYPES } from '../constants'; +import { API_VERSION, DIGITAL_TWIN_API_VERSION, MODEL_REPO_API_VERSION, HTTP_OPERATION_TYPES, PUBLIC_REPO_HOSTNAME_TEST } from '../../constants/apiConstants'; describe('digitalTwinsModelService', () => { @@ -111,4 +110,46 @@ describe('digitalTwinsModelService', () => { done(); }); }); + + context('validateModelDefinitions', () => { + const parameters = JSON.stringify([]); + + it('calls fetch with specified parameters and returns true when response is 200', async () => { + // tslint:disable + const response = { + json: () => {}, + ok: true + } as any; + // tslint:enable + jest.spyOn(window, 'fetch').mockResolvedValue(response); + + const result = await DigitalTwinsModelService.validateModelDefinitions(parameters); + + const apiVersionQueryString = `?${API_VERSION}${MODEL_REPO_API_VERSION}`; + const controllerRequest = { + body: parameters, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'x-ms-client-request-id': 'azure iot explorer: validate model definition' + }, + method: HTTP_OPERATION_TYPES.Post, + uri: `https://${PUBLIC_REPO_HOSTNAME_TEST}/models/validate${apiVersionQueryString}` + }; + + const validateModelParameters = { + body: JSON.stringify(controllerRequest), + cache: 'no-cache', + credentials: 'include', + headers: new Headers({ + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }), + method: HTTP_OPERATION_TYPES.Post + }; + + expect(fetch).toBeCalledWith(DigitalTwinsModelService.CONTROLLER_ENDPOINT, validateModelParameters); + expect(result).toEqual(true); + }); + }); }); diff --git a/src/app/api/services/digitalTwinsModelService.ts b/src/app/api/services/digitalTwinsModelService.ts index 4972fa39d..c04d94d16 100644 --- a/src/app/api/services/digitalTwinsModelService.ts +++ b/src/app/api/services/digitalTwinsModelService.ts @@ -9,8 +9,10 @@ import { CONTROLLER_API_ENDPOINT, DIGITAL_TWIN_API_VERSION, HEADERS, - MODELREPO } from '../../constants/apiConstants'; -import { HTTP_OPERATION_TYPES } from '../constants'; + MODELREPO, + MODEL_REPO_API_VERSION, + PUBLIC_REPO_HOSTNAME_TEST, + HTTP_OPERATION_TYPES } from '../../constants/apiConstants'; import { PnPModel } from '../models/metamodelMetadata'; export const fetchModel = async (parameters: FetchModelParameters): Promise => { @@ -69,6 +71,27 @@ export const fetchModelDefinition = async (parameters: FetchModelParameters) => } }; +export const validateModelDefinitions = async (modelDefinitions: string) => { + try { + const apiVersionQueryString = `?${API_VERSION}${MODEL_REPO_API_VERSION}`; + const controllerRequest: RequestInitWithUri = { + body: modelDefinitions, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'x-ms-client-request-id': 'azure iot explorer: validate model definition' + }, + method: HTTP_OPERATION_TYPES.Post, + uri: `https://${PUBLIC_REPO_HOSTNAME_TEST}/models/validate${apiVersionQueryString}` + }; + + return (await request(controllerRequest)).ok; + } + catch (error) { + throw new Error(error); + } +}; + export interface RequestInitWithUri extends RequestInit { uri: string; headers?: Record; @@ -82,6 +105,7 @@ export interface RepoConnectionSettings { } export const CONTROLLER_ENDPOINT = `${CONTROLLER_API_ENDPOINT}${MODELREPO}`; + const request = async (requestInit: RequestInitWithUri) => { return fetch( CONTROLLER_ENDPOINT, diff --git a/src/app/api/services/moduleService.spec.ts b/src/app/api/services/moduleService.spec.ts index f375f0afd..cadffb043 100644 --- a/src/app/api/services/moduleService.spec.ts +++ b/src/app/api/services/moduleService.spec.ts @@ -5,11 +5,11 @@ import 'jest'; import * as ModuleService from './moduleService'; import * as DataplaneService from './dataplaneServiceHelper'; -import { HTTP_OPERATION_TYPES } from '../constants'; import { getConnectionInfoFromConnectionString } from '../shared/utils'; import { DataPlaneParameters } from '../parameters/deviceParameters'; import { ModuleIdentity } from '../models/moduleIdentity'; import { ModuleTwin } from '../models/moduleTwin'; +import { HTTP_OPERATION_TYPES } from '../../constants/apiConstants'; const deviceId = 'deviceId'; const moduleId = 'moduleId'; diff --git a/src/app/api/services/moduleService.ts b/src/app/api/services/moduleService.ts index bdd68dbb4..1568df64c 100644 --- a/src/app/api/services/moduleService.ts +++ b/src/app/api/services/moduleService.ts @@ -8,12 +8,11 @@ import { ModuleIdentityTwinParameters, FetchModuleIdentityParameters } from '../parameters/moduleParameters'; -import { HTTP_OPERATION_TYPES } from '../constants'; import { DataPlaneResponse } from '../models/device'; import { ModuleIdentity } from '../models/moduleIdentity'; import { ModuleTwin } from '../models/moduleTwin'; import { dataPlaneConnectionHelper, dataPlaneResponseHelper, request, DATAPLANE_CONTROLLER_ENDPOINT, DataPlaneRequest } from './dataplaneServiceHelper'; -import { HEADERS } from '../../constants/apiConstants'; +import { HEADERS, HTTP_OPERATION_TYPES } from '../../constants/apiConstants'; export interface IoTHubConnectionSettings { hostName?: string; diff --git a/src/app/api/shared/utils.ts b/src/app/api/shared/utils.ts index 55a45002b..7ea7daa02 100644 --- a/src/app/api/shared/utils.ts +++ b/src/app/api/shared/utils.ts @@ -7,8 +7,7 @@ import { IoTHubConnectionSettings } from '../services/devicesService'; import { LIST_PLUG_AND_PLAY_DEVICES, SAS_EXPIRES_MINUTES } from '../../constants/devices'; import DeviceQuery, { QueryClause, ParameterType, OperationType } from '../models/deviceQuery'; import { RepoConnectionSettings } from '../services/digitalTwinsModelService'; -import { AppEnvironment } from '../../constants/shared'; -import { MILLISECONDS_PER_SECOND, SECONDS_PER_MINUTE } from '../constants'; +import { AppEnvironment, MILLISECONDS_PER_SECOND, SECONDS_PER_MINUTE } from '../../constants/shared'; export const enum PnPQueryPrefix { HAS_CAPABILITY_MODEL = 'HAS_CAPABILITYMODEL', diff --git a/src/app/azureResourceIdentifier/services/azureResourceIdentifierService.spec.ts b/src/app/azureResourceIdentifier/services/azureResourceIdentifierService.spec.ts index 5603fe34a..425d3473a 100644 --- a/src/app/azureResourceIdentifier/services/azureResourceIdentifierService.spec.ts +++ b/src/app/azureResourceIdentifier/services/azureResourceIdentifierService.spec.ts @@ -5,7 +5,7 @@ import { getAzureResourceIdentifiers, getAzureResourceIdentifier } from './azureResourceIdentifierService'; import { AzureResourceIdentifierType } from '../models/azureResourceIdentifierType'; import { HttpError } from '../../api/models/httpError'; -import { APPLICATION_JSON, HTTP_OPERATION_TYPES } from '../../api/constants'; +import { HTTP_OPERATION_TYPES, APPLICATION_JSON } from '../../constants/apiConstants'; describe('getAzureResourceIdentifiers', () => { it('calls fetch with specificed parameters', () => { diff --git a/src/app/azureResourceIdentifier/services/azureResourceIdentifierService.ts b/src/app/azureResourceIdentifier/services/azureResourceIdentifierService.ts index c0aba0b1e..46e312fc1 100644 --- a/src/app/azureResourceIdentifier/services/azureResourceIdentifierService.ts +++ b/src/app/azureResourceIdentifier/services/azureResourceIdentifierService.ts @@ -7,7 +7,7 @@ import { AzureResourceIdentifier } from '../models/azureResourceIdentifier'; import { AzureResourceIdentifierType } from '../models/azureResourceIdentifierType'; import { AzureResourceIdentifierQuery } from '../models/azureResourceIdentifierQuery'; import { AzureResourceIdentifierQueryResult } from '../models/azureResourceIdentifierQueryResult'; -import { APPLICATION_JSON, HTTP_OPERATION_TYPES } from '../../api/constants'; +import { APPLICATION_JSON, HTTP_OPERATION_TYPES } from '../../constants/apiConstants'; import { AzureResourceManagementEndpoint } from '../models/azureResourceManagementEndpoint'; import { HttpError } from '../../api/models/httpError'; import { mapPropertyArrayToObject } from '../../api/shared/mapUtils'; diff --git a/src/app/azureResourceIdentifier/services/azureSubscriptionService.spec.ts b/src/app/azureResourceIdentifier/services/azureSubscriptionService.spec.ts index c6d82e8f5..b73ea29c3 100644 --- a/src/app/azureResourceIdentifier/services/azureSubscriptionService.spec.ts +++ b/src/app/azureResourceIdentifier/services/azureSubscriptionService.spec.ts @@ -1,10 +1,11 @@ +import { HTTP_OPERATION_TYPES } from './../../constants/apiConstants'; /*********************************************************** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License **********************************************************/ import { getAzureSubscriptions } from './azureSubscriptionService'; import { HttpError } from '../../api/models/httpError'; -import { APPLICATION_JSON, HTTP_OPERATION_TYPES } from '../../api/constants'; +import { APPLICATION_JSON } from '../../constants/apiConstants'; describe('getAzureSubscriptions', () => { it('calls fetch with expected parameters', () => { diff --git a/src/app/azureResourceIdentifier/services/azureSubscriptionService.ts b/src/app/azureResourceIdentifier/services/azureSubscriptionService.ts index 2c7cc85b4..3ac8f9826 100644 --- a/src/app/azureResourceIdentifier/services/azureSubscriptionService.ts +++ b/src/app/azureResourceIdentifier/services/azureSubscriptionService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License **********************************************************/ import { AzureResourceManagementEndpoint } from '../models/azureResourceManagementEndpoint'; -import { APPLICATION_JSON, HTTP_OPERATION_TYPES } from '../../api/constants'; +import { APPLICATION_JSON, HTTP_OPERATION_TYPES } from '../../constants/apiConstants'; import { HttpError } from '../../api/models/httpError'; import { AzureSubscription } from '../models/azureSubscription'; diff --git a/src/app/constants/apiConstants.ts b/src/app/constants/apiConstants.ts index 8e390c056..0e118980d 100644 --- a/src/app/constants/apiConstants.ts +++ b/src/app/constants/apiConstants.ts @@ -20,6 +20,8 @@ export const MODEL_ID_REF = '/ref:modelId?'; export const MODEL_ID = 'modelId='; export const API_VERSION = 'api-version='; export const AND = '&'; +export const PUBLIC_REPO_HOSTNAME = 'repo.azureiotrepository.com'; +export const PUBLIC_REPO_HOSTNAME_TEST = 'repo.azureiotrepository-test.com'; // event hub controller export const MONITOR = '/monitor'; @@ -27,6 +29,7 @@ export const STOP = '/stop'; // digital twin api version export const DIGITAL_TWIN_API_VERSION = '2019-07-01-preview'; +export const MODEL_REPO_API_VERSION = '2020-05-01-preview'; export const HEADERS = { CONTINUATION_TOKEN: 'x-ms-continuation', @@ -56,3 +59,18 @@ export const CONTROLLER_API_ENDPOINT = appConfig.hostMode === HostMode.Browser ? `${localIp}:${appConfig.controllerPort}${apiPath}` : `${localIp}:${localStorage.getItem(CUSTOM_CONTROLLER_PORT) || appConfig.controllerPort}${apiPath}`; + +export enum HTTP_OPERATION_TYPES { + Delete = 'DELETE', + Get = 'GET', + Patch = 'PATCH', + Post = 'POST', + Put = 'PUT' +} + +export const APPLICATION_JSON = 'application/json'; +export const ERROR_TYPES = { + AUTHORIZATION_RULE_NOT_FOUND: 'authorizationRuleNotFound', + HTTP: 'http', + PORT_IS_IN_USE: 'portIsInUse' +}; diff --git a/src/app/constants/shared.ts b/src/app/constants/shared.ts index 1665b6ad3..edbd3fc20 100644 --- a/src/app/constants/shared.ts +++ b/src/app/constants/shared.ts @@ -2,21 +2,10 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License **********************************************************/ -export enum SYNC_STATUS { - Failed, - None, - Synced, - Syncing -} - -export enum DesiredStateStatus{ - Success = 200, - Synching = 202, - Error = 500 -} - +export const OFFSET_IN_MINUTES = 15; export const MILLISECONDS_IN_MINUTE = 60000; -export const PUBLIC_REPO_HOSTNAME = 'repo.azureiotrepository.com'; +export const MILLISECONDS_PER_SECOND = 1000; +export const SECONDS_PER_MINUTE = 60; export enum AppEnvironment { ProdElectron = 'prodElectron', diff --git a/src/app/devices/deviceContent/actions.ts b/src/app/devices/deviceContent/actions.ts index 847c176c1..ed788074e 100644 --- a/src/app/devices/deviceContent/actions.ts +++ b/src/app/devices/deviceContent/actions.ts @@ -5,11 +5,10 @@ import actionCreatorFactory from 'typescript-fsa'; import * as actionPrefixes from '../../constants/actionPrefixes'; import * as actionTypes from '../../constants/actionTypes'; -import { ModelDefinition } from '../../api/models/modelDefinition'; import { Twin } from '../../api/models/device'; import { DeviceIdentity } from '../../api/models/deviceIdentity'; import { DigitalTwinInterfaces } from './../../api/models/digitalTwinModels'; -import { REPOSITORY_LOCATION_TYPE } from './../../constants/repositoryLocationTypes'; +import { ModelDefinitionWithSource } from './../../api/models/modelDefinitionWithSource'; const deviceContentCreator = actionCreatorFactory(actionPrefixes.DEVICECONTENT); const clearModelDefinitionsAction = deviceContentCreator(actionTypes.CLEAR_MODEL_DEFINITIONS); @@ -17,7 +16,7 @@ const cloudToDeviceMessageAction = deviceContentCreator.async (actionTypes.GET_DEVICE_IDENTITY); const getDigitalTwinInterfacePropertiesAction = deviceContentCreator.async(actionTypes.GET_DIGITAL_TWIN_INTERFACE_PROPERTIES); const getTwinAction = deviceContentCreator.async(actionTypes.GET_TWIN); -const getModelDefinitionAction = deviceContentCreator.async(actionTypes.FETCH_MODEL_DEFINITION); +const getModelDefinitionAction = deviceContentCreator.async(actionTypes.FETCH_MODEL_DEFINITION); const invokeDirectMethodAction = deviceContentCreator.async(actionTypes.INVOKE_DEVICE_METHOD); const invokeDigitalTwinInterfaceCommandAction = deviceContentCreator.async(actionTypes.INVOKE_DIGITAL_TWIN_INTERFACE_COMMAND); const patchDigitalTwinInterfacePropertiesAction = deviceContentCreator.async(actionTypes.PATCH_DIGITAL_TWIN_INTERFACE_PROPERTIES); @@ -71,11 +70,6 @@ export interface UpdateTwinActionParameters { twin: Twin; } -export interface ModelDefinitionActionResult { - modelDefinition: ModelDefinition; - source: REPOSITORY_LOCATION_TYPE; -} - export interface GetModelDefinitionActionParameters { digitalTwinId: string; interfaceId: string; diff --git a/src/app/devices/deviceContent/components/deviceInterfaces/__snapshots__/deviceInterfaces.spec.tsx.snap b/src/app/devices/deviceContent/components/deviceInterfaces/__snapshots__/deviceInterfaces.spec.tsx.snap index 7fc9f6b89..0b6112ee8 100644 --- a/src/app/devices/deviceContent/components/deviceInterfaces/__snapshots__/deviceInterfaces.spec.tsx.snap +++ b/src/app/devices/deviceContent/components/deviceInterfaces/__snapshots__/deviceInterfaces.spec.tsx.snap @@ -471,47 +471,14 @@ exports[`components/devices/deviceInterfaces shows interface information when st -
- - - deviceInterfaces.columns.source - : - -- - - - deviceInterfaces.command.configure - - - - + deviceInterfaces.interfaceNotValid +
diff --git a/src/app/devices/deviceContent/components/deviceInterfaces/deviceInterfaces.spec.tsx b/src/app/devices/deviceContent/components/deviceInterfaces/deviceInterfaces.spec.tsx index c0ed904d2..0c00536bd 100644 --- a/src/app/devices/deviceContent/components/deviceInterfaces/deviceInterfaces.spec.tsx +++ b/src/app/devices/deviceContent/components/deviceInterfaces/deviceInterfaces.spec.tsx @@ -112,6 +112,7 @@ describe('components/devices/deviceInterfaces', () => { isLoading: false, modelDefinitionWithSource: { payload: { + isModelValid: true, modelDefinition, source: REPOSITORY_LOCATION_TYPE.Public, }, @@ -129,6 +130,7 @@ describe('components/devices/deviceInterfaces', () => { isLoading: false, modelDefinitionWithSource: { payload: { + isModelValid: true, modelDefinition, source: REPOSITORY_LOCATION_TYPE.Private, }, @@ -141,6 +143,7 @@ describe('components/devices/deviceInterfaces', () => { isLoading: false, modelDefinitionWithSource: { payload: { + isModelValid: true, modelDefinition, source: REPOSITORY_LOCATION_TYPE.Device, }, @@ -153,6 +156,7 @@ describe('components/devices/deviceInterfaces', () => { isLoading: false, modelDefinitionWithSource: { payload: { + isModelValid: true, modelDefinition, source: REPOSITORY_LOCATION_TYPE.Local }, @@ -165,6 +169,7 @@ describe('components/devices/deviceInterfaces', () => { isLoading: false, modelDefinitionWithSource: { payload: { + isModelValid: false, modelDefinition, source: undefined }, diff --git a/src/app/devices/deviceContent/components/deviceInterfaces/deviceInterfaces.tsx b/src/app/devices/deviceContent/components/deviceInterfaces/deviceInterfaces.tsx index 6dc5b3f1d..9a1f16f36 100644 --- a/src/app/devices/deviceContent/components/deviceInterfaces/deviceInterfaces.tsx +++ b/src/app/devices/deviceContent/components/deviceInterfaces/deviceInterfaces.tsx @@ -7,6 +7,10 @@ import { Label } from 'office-ui-fabric-react/lib/Label'; import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar'; import { ActionButton } from 'office-ui-fabric-react/lib/Button'; import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner'; +import { + MessageBar, + MessageBarType, +} from 'office-ui-fabric-react'; import { RouteComponentProps, Route } from 'react-router-dom'; import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../../shared/contexts/localizationContext'; import { ResourceKeys } from '../../../../../localization/resourceKeys'; @@ -88,11 +92,23 @@ export default class DeviceInterfaces extends React.Component {modelDefinitionWithSource && modelDefinitionWithSource.payload ? - -
- {this.renderInterfaceInfoDetail(context)} - {this.renderInterfaceViewer()} -
+ {modelDefinitionWithSource.payload.isModelValid ? + <> + +
+ {this.renderInterfaceInfoDetail(context)} + {this.renderInterfaceViewer()} +
+ : +
+ + {context.t(ResourceKeys.deviceInterfaces.interfaceNotValid)} + + {this.renderInterfaceViewer()} +
+ }
: } diff --git a/src/app/devices/deviceContent/components/shared/desiredStateStatus.tsx b/src/app/devices/deviceContent/components/shared/desiredStateStatus.tsx index 1137fbda3..859f6e29c 100644 --- a/src/app/devices/deviceContent/components/shared/desiredStateStatus.tsx +++ b/src/app/devices/deviceContent/components/shared/desiredStateStatus.tsx @@ -9,7 +9,12 @@ import LabelWithTooltip from '../../../../shared/components/labelWithTooltip'; import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../..//shared/contexts/localizationContext'; import { ResourceKeys } from '../../../../../localization/resourceKeys'; import { ACCEPT, WARNING, SYNCH } from '../../../../constants/iconNames'; -import { DesiredStateStatus } from '../../../../constants/shared'; + +export enum DesiredStateStatus{ + Success = 200, + Synching = 202, + Error = 500 +} export const RenderDesiredState = (state: DesiredState) => { return ( diff --git a/src/app/devices/deviceContent/reducer.spec.ts b/src/app/devices/deviceContent/reducer.spec.ts index eca62ccbe..313826fd6 100644 --- a/src/app/devices/deviceContent/reducer.spec.ts +++ b/src/app/devices/deviceContent/reducer.spec.ts @@ -75,6 +75,7 @@ describe('deviceContentStateReducer', () => { /* tslint:enable */ const modelDefinitionWithSource = { payload: { + isModelValid: true, modelDefinition, source: REPOSITORY_LOCATION_TYPE.Public }, @@ -87,9 +88,14 @@ describe('deviceContentStateReducer', () => { }); it (`handles ${FETCH_MODEL_DEFINITION}/ACTION_DONE action`, () => { - const action = getModelDefinitionAction.done({params: {digitalTwinId: 'testDevice', interfaceId: 'urn:azureiot:ModelDiscovery:ModelInformation:1'}, result: {modelDefinition, source: REPOSITORY_LOCATION_TYPE.Public }}); + const action = getModelDefinitionAction.done( + { + params: {digitalTwinId: 'testDevice', interfaceId: 'urn:azureiot:ModelDiscovery:ModelInformation:1'}, + result: {isModelValid: true, modelDefinition, source: REPOSITORY_LOCATION_TYPE.Public } + }); expect(reducer(deviceContentStateInitial(), action).modelDefinitionWithSource).toEqual({ payload: { + isModelValid: true, modelDefinition, source: REPOSITORY_LOCATION_TYPE.Public }, diff --git a/src/app/devices/deviceContent/reducer.ts b/src/app/devices/deviceContent/reducer.ts index a71b65a22..1f168c94e 100644 --- a/src/app/devices/deviceContent/reducer.ts +++ b/src/app/devices/deviceContent/reducer.ts @@ -16,13 +16,13 @@ import { updateDeviceIdentityAction, patchDigitalTwinInterfacePropertiesAction, PatchDigitalTwinInterfacePropertiesActionParameters, - ModelDefinitionActionResult, GetModelDefinitionActionParameters } from './actions'; import { Twin } from '../../api/models/device'; import { DeviceIdentity } from '../../api/models/deviceIdentity'; import { SynchronizationStatus } from '../../api/models/synchronizationStatus'; import { DigitalTwinInterfaces } from '../../api/models/digitalTwinModels'; +import { ModelDefinitionWithSource } from './../../api/models/modelDefinitionWithSource'; const reducer = reducerWithInitialState(deviceContentStateInitial()) //#region DeviceIdentity-related actions @@ -129,13 +129,10 @@ const reducer = reducerWithInitialState(deviceContentSta } }); }) - .case(getModelDefinitionAction.done, (state: DeviceContentStateType, payload: {params: GetModelDefinitionActionParameters} & {result: ModelDefinitionActionResult}) => { + .case(getModelDefinitionAction.done, (state: DeviceContentStateType, payload: {params: GetModelDefinitionActionParameters} & {result: ModelDefinitionWithSource}) => { return state.merge({ modelDefinitionWithSource: { - payload: { - modelDefinition: payload.result.modelDefinition, - source: payload.result.source - }, + payload: {...payload.result}, synchronizationStatus: SynchronizationStatus.fetched } }); diff --git a/src/app/devices/deviceContent/sagas/modelDefinitionSaga.spec.ts b/src/app/devices/deviceContent/sagas/modelDefinitionSaga.spec.ts index 74842a434..251a8ef9b 100644 --- a/src/app/devices/deviceContent/sagas/modelDefinitionSaga.spec.ts +++ b/src/app/devices/deviceContent/sagas/modelDefinitionSaga.spec.ts @@ -5,7 +5,7 @@ import 'jest'; import { select, call, put } from 'redux-saga/effects'; import { SagaIteratorClone, cloneableGenerator } from 'redux-saga/utils'; -import { getModelDefinitionSaga, getModelDefinition, getModelDefinitionFromPublicRepo, getModelDefinitionFromPrivateRepo, getModelDefinitionFromDevice, getModelDefinitionFromLocalFile } from './modelDefinitionSaga'; +import { getModelDefinitionSaga, getModelDefinition, getModelDefinitionFromPublicRepo, getModelDefinitionFromPrivateRepo, getModelDefinitionFromDevice, getModelDefinitionFromLocalFile, validateModelDefinitionHelper } from './modelDefinitionSaga'; import * as DevicesService from '../../../api/services/devicesService'; import { addNotificationAction } from '../../../notifications/actions'; import { NotificationType } from '../../../api/models/notification'; @@ -30,6 +30,44 @@ describe('modelDefinitionSaga', () => { }; const action = getModelDefinitionAction.started(params); const connectionString = 'connection_string'; + /* 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": "http://azureiot.com/v1/contexts/Interface.json" + }; + /* tslint:enable */ describe('getModelDefinitionSaga', () => { let getModelDefinitionSagaGenerator: SagaIteratorClone; @@ -49,16 +87,21 @@ describe('modelDefinitionSaga', () => { }]).value).toEqual( call(getModelDefinition, action, { repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public }) ); + + expect(getModelDefinitionSagaGenerator.next(modelDefinition).value).toEqual( + call(validateModelDefinitionHelper, modelDefinition, { repositoryLocationType: REPOSITORY_LOCATION_TYPE.Public }) + ); }); it('puts the successful action', () => { const success = getModelDefinitionSagaGenerator.clone(); - expect(success.next()).toEqual({ + expect(success.next(true)).toEqual({ done: false, value: put((getModelDefinitionAction.done({ params, result: { - modelDefinition: undefined, + isModelValid: true, + modelDefinition, source: REPOSITORY_LOCATION_TYPE.Public } }))) diff --git a/src/app/devices/deviceContent/sagas/modelDefinitionSaga.ts b/src/app/devices/deviceContent/sagas/modelDefinitionSaga.ts index a17c50c31..dabd9ce3d 100644 --- a/src/app/devices/deviceContent/sagas/modelDefinitionSaga.ts +++ b/src/app/devices/deviceContent/sagas/modelDefinitionSaga.ts @@ -4,7 +4,7 @@ **********************************************************/ import { call, put, select } from 'redux-saga/effects'; import { Action } from 'typescript-fsa'; -import { fetchModelDefinition } from '../../../api/services/digitalTwinsModelService'; +import { fetchModelDefinition, validateModelDefinitions } from '../../../api/services/digitalTwinsModelService'; import { addNotificationAction } from '../../../notifications/actions'; import { NotificationType } from '../../../api/models/notification'; import { ResourceKeys } from '../../../../localization/resourceKeys'; @@ -22,6 +22,7 @@ import { InterfaceNotImplementedException } from './../../../shared/utils/except import { modelDefinitionInterfaceId, modelDefinitionCommandName } from '../../../constants/modelDefinitionConstants'; import { FetchDigitalTwinInterfacePropertiesParameters } from '../../../api/parameters/deviceParameters'; import { fetchLocalFile } from './../../../api/services/localRepoService'; +import { ModelDefinition } from './../../../api/models/modelDefinition'; export function* getModelDefinitionSaga(action: Action) { try { @@ -31,8 +32,12 @@ export function* getModelDefinitionSaga(action: Action, location: RepositoryLocationSettings) { const repoConnectionStringInfo = getRepoConnectionInfoFromConnectionString(location.value); const parameters: FetchModelParameters = { diff --git a/src/app/devices/deviceList/sagas/listDeviceSaga.spec.ts b/src/app/devices/deviceList/sagas/listDeviceSaga.spec.ts index 632a29056..f73170836 100644 --- a/src/app/devices/deviceList/sagas/listDeviceSaga.spec.ts +++ b/src/app/devices/deviceList/sagas/listDeviceSaga.spec.ts @@ -14,7 +14,7 @@ import { DeviceIdentity } from '../../../api/models/deviceIdentity'; import { addNotificationAction } from '../../../notifications/actions'; import { ResourceKeys } from '../../../../localization/resourceKeys'; import { NotificationType } from '../../../api/models/notification'; -import { ERROR_TYPES } from '../../../api/constants'; +import { ERROR_TYPES } from '../../../constants/apiConstants'; describe('listDeviceSaga', () => { let listDevicesSagaGenerator: SagaIteratorClone; diff --git a/src/app/devices/deviceList/sagas/listDeviceSaga.ts b/src/app/devices/deviceList/sagas/listDeviceSaga.ts index d2f40afa1..7d36e1e91 100644 --- a/src/app/devices/deviceList/sagas/listDeviceSaga.ts +++ b/src/app/devices/deviceList/sagas/listDeviceSaga.ts @@ -11,7 +11,7 @@ import { listDevicesAction } from '../actions'; import { fetchDevices } from '../../../api/services/devicesService'; import DeviceQuery from '../../../api/models/deviceQuery'; import { getActiveAzureResourceConnectionStringSaga } from '../../../azureResource/sagas/getActiveAzureResourceConnectionStringSaga'; -import { ERROR_TYPES } from './../../../api/constants'; +import { ERROR_TYPES } from './../../../constants/apiConstants'; import { appConfig } from '../../../../appConfig/appConfig'; import { CUSTOM_CONTROLLER_PORT } from './../../../constants/browserStorage'; diff --git a/src/app/iotHub/sagas/getConnectionStringFromIotHubSaga.spec.ts b/src/app/iotHub/sagas/getConnectionStringFromIotHubSaga.spec.ts index ac74636b8..aed249127 100644 --- a/src/app/iotHub/sagas/getConnectionStringFromIotHubSaga.spec.ts +++ b/src/app/iotHub/sagas/getConnectionStringFromIotHubSaga.spec.ts @@ -7,8 +7,8 @@ import { getConnectionStringFromIotHubSaga } from './getConnectionStringFromIotH import { AzureResourceIdentifier } from '../../azureResourceIdentifier/models/azureResourceIdentifier'; import { AzureResourceIdentifierType } from '../../azureResourceIdentifier/models/azureResourceIdentifierType'; import { getSharedAccessSignatureAuthorizationRulesSaga } from './getSharedAccessSignatureAuthorizationRulesSaga'; -import { ERROR_TYPES } from '../../api/constants'; import { AccessRights } from '../models/accessRights'; +import { ERROR_TYPES } from '../../constants/apiConstants'; describe('getConnectionStringFromIotHubSaga', () => { const azureResourceIdentifier: AzureResourceIdentifier = { diff --git a/src/app/iotHub/sagas/getSharedAccessSignatureAuthorizationRulesSaga.ts b/src/app/iotHub/sagas/getSharedAccessSignatureAuthorizationRulesSaga.ts index 1163b38b2..1afb50976 100644 --- a/src/app/iotHub/sagas/getSharedAccessSignatureAuthorizationRulesSaga.ts +++ b/src/app/iotHub/sagas/getSharedAccessSignatureAuthorizationRulesSaga.ts @@ -11,7 +11,7 @@ import { executeAzureResourceManagementTokenRequest } from '../../login/services import { appConfig } from '../../../appConfig/appConfig'; import { StateInterface } from '../../shared/redux/state'; import { CacheWrapper } from '../../api/models/cacheWrapper'; -import { MILLISECONDS_PER_SECOND, SECONDS_PER_MINUTE } from '../../api/constants'; +import { SECONDS_PER_MINUTE, MILLISECONDS_PER_SECOND } from './../../constants/shared'; const cacheInMinutes = 4; export const cacheRetentionInMilliseconds = cacheInMinutes * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND; diff --git a/src/app/iotHub/services/iotHubService.spec.ts b/src/app/iotHub/services/iotHubService.spec.ts index 8f9aa0463..87314471c 100644 --- a/src/app/iotHub/services/iotHubService.spec.ts +++ b/src/app/iotHub/services/iotHubService.spec.ts @@ -4,8 +4,8 @@ **********************************************************/ import { getSharedAccessSignatureAuthorizationRules } from './iotHubService'; import { HttpError } from '../../api/models/httpError'; -import { APPLICATION_JSON, HTTP_OPERATION_TYPES } from '../../api/constants'; import { AccessRights } from '../models/accessRights'; +import { APPLICATION_JSON, HTTP_OPERATION_TYPES } from '../../constants/apiConstants'; describe('getSharedAccessSignatureAuthorizationRules', () => { it('calls fetch with specified parameters', () => { diff --git a/src/app/iotHub/services/iotHubService.ts b/src/app/iotHub/services/iotHubService.ts index 0b7d38bfa..6418b4777 100644 --- a/src/app/iotHub/services/iotHubService.ts +++ b/src/app/iotHub/services/iotHubService.ts @@ -5,7 +5,7 @@ import { SharedAccessSignatureAuthorizationRule } from '../models/sharedAccessSignatureAuthorizationRule'; import { AzureResourceManagementEndpoint } from '../../azureResourceIdentifier/models/azureResourceManagementEndpoint'; import { AzureResourceIdentifier } from '../../azureResourceIdentifier/models/azureResourceIdentifier'; -import { APPLICATION_JSON, HTTP_OPERATION_TYPES } from '../../api/constants'; +import { APPLICATION_JSON, HTTP_OPERATION_TYPES } from '../../constants/apiConstants'; import { HttpError } from '../../api/models/httpError'; const apiVersion = '2018-04-01'; diff --git a/src/app/settings/components/settingsPane.tsx b/src/app/settings/components/settingsPane.tsx index 9a884ed6d..05576ae2f 100644 --- a/src/app/settings/components/settingsPane.tsx +++ b/src/app/settings/components/settingsPane.tsx @@ -256,8 +256,8 @@ export default class SettingsPane extends React.Component { return { - connectionString: setting.value, - repositoryLocationType: setting.repositoryLocationType + repositoryLocationType: setting.repositoryLocationType, + value: setting.value }; }))], showConfirmationDialog: false diff --git a/src/app/settings/reducers.ts b/src/app/settings/reducers.ts index 41235c3f3..71c9a3907 100644 --- a/src/app/settings/reducers.ts +++ b/src/app/settings/reducers.ts @@ -4,11 +4,11 @@ **********************************************************/ import { reducerWithInitialState } from 'typescript-fsa-reducers'; import { setSettingsVisibilityAction, setSettingsRepositoryLocationsAction, updateRepoTokenAction } from './actions'; -import { applicationStateInitial, ApplicationStateType, OFFSET_IN_MINUTES, PrivateRepositorySettings, RepositoryLocationSettings } from './state'; +import { applicationStateInitial, ApplicationStateType, PrivateRepositorySettings, RepositoryLocationSettings } from './state'; import { REPO_LOCATIONS } from '../constants/browserStorage'; import { REPOSITORY_LOCATION_TYPE } from './../constants/repositoryLocationTypes'; import { PRIVATE_REPO_CONNECTION_STRING_NAME, LOCAL_FILE_EXPLORER_PATH_NAME } from './../constants/browserStorage'; -import { MILLISECONDS_IN_MINUTE } from '../constants/shared'; +import { MILLISECONDS_IN_MINUTE, OFFSET_IN_MINUTES } from '../constants/shared'; const reducer = reducerWithInitialState(applicationStateInitial()) .case(setSettingsVisibilityAction, (state: ApplicationStateType, payload: boolean) => { diff --git a/src/app/settings/state.ts b/src/app/settings/state.ts index e3bad273d..488a5ee6c 100644 --- a/src/app/settings/state.ts +++ b/src/app/settings/state.ts @@ -6,9 +6,9 @@ import { Record } from 'immutable'; import { IM } from '../shared/types/types'; import { REPOSITORY_LOCATION_TYPE } from '../constants/repositoryLocationTypes'; import { PRIVATE_REPO_CONNECTION_STRING_NAME, REPO_LOCATIONS, LOCAL_FILE_EXPLORER_PATH_NAME } from '../constants/browserStorage'; -import { MILLISECONDS_IN_MINUTE, PUBLIC_REPO_HOSTNAME } from '../constants/shared'; +import { MILLISECONDS_IN_MINUTE, OFFSET_IN_MINUTES } from '../constants/shared'; import { appConfig, HostMode } from '../../appConfig/appConfig'; -export const OFFSET_IN_MINUTES = 15; +import { PUBLIC_REPO_HOSTNAME } from '../constants/apiConstants'; export interface RepositoryLocationSettings { repositoryLocationType: REPOSITORY_LOCATION_TYPE; diff --git a/src/localization/locales/en.json b/src/localization/locales/en.json index abc8ddaa4..7c458713c 100644 --- a/src/localization/locales/en.json +++ b/src/localization/locales/en.json @@ -70,7 +70,7 @@ "label":"Company repository", "infoText": "", "textBoxLabel": "Company model repository connection string", - "placeholder": "Please fill in this required field in order to proceed" + "placeholder": "" }, "device": { "label":"On the connected device", @@ -79,9 +79,9 @@ "local": { "labelInElectron":"Local folder", "labelInBrowser":"Local folder (This is only enabled in the desktop version from: https://github.com/Azure/azure-iot-explorer/releases)", - "infoText": "Use your local folder as a model repository. The files in the folder should be named the same as the Interface ID with all ':' removed, and should be in the format of json. For example: urn:contoso:com:EnvironmentalSensor:1 should be named as urncontosocomEnvironmentalSensor1.json", + "infoText": "Use your local folder as a model repository. The interface files you wish to use should have a json extension and a file name equal to the Interface ID with all colons (':') removed. For example: urn:contoso:com:EnvironmentalSensor:1 should be named as urncontosocomEnvironmentalSensor1.json", "textBoxLabel": "Local folder path", - "placeholder": "Please fill in this required field in order to proceed. For example: f:/" + "placeholder": "f:/" } } }, @@ -476,7 +476,8 @@ }, "headerText": "Interface", "noInterfaces": "No interfaces", - "interfaceNotFound": "We are not able to retrieve interface model definition. Please click 'Configure' button below to see how we resolve model definition." + "interfaceNotFound": "We are not able to retrieve interface model definition. Please click 'Configure' button below to see how we resolve model definition.", + "interfaceNotValid": "Model definition is not valid. Please update the model definition and then proceed." }, "deviceEvents": { "command" : { diff --git a/src/localization/resourceKeys.ts b/src/localization/resourceKeys.ts index 5de5b4d5f..2561e010b 100644 --- a/src/localization/resourceKeys.ts +++ b/src/localization/resourceKeys.ts @@ -336,6 +336,7 @@ export class ResourceKeys { }, headerText : "deviceInterfaces.headerText", interfaceNotFound : "deviceInterfaces.interfaceNotFound", + interfaceNotValid : "deviceInterfaces.interfaceNotValid", noInterfaces : "deviceInterfaces.noInterfaces", }; public static deviceLists = {