Skip to content

Commit

Permalink
Local folder (#246)
Browse files Browse the repository at this point in the history
* enable read from local folder and add tests

* electron only for local files

* make route hiden if interface is not found

* remove focus if value is filled
  • Loading branch information
YingXue committed Jun 24, 2020
1 parent 3ce8e4c commit 4c2eaf1
Show file tree
Hide file tree
Showing 43 changed files with 1,002 additions and 232 deletions.
48 changes: 48 additions & 0 deletions src/app/api/services/localRepoService.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as LocalRepoService from './localRepoService';
describe('localRepoService', () => {

context('fetchLocalFile', () => {
it('returns file content when response is 200', async done => {
// tslint:disable
const content = {
"@id": "urn:FlyYing:EnvironmentalSensor:1",
"@type": "Interface",
"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"
}
],
"@context": "http://azureiot.com/v1/contexts/IoTModel.json"
};
const response = {
status: 200,
json: () => content,
headers: {},
ok: true
} as any;
// tslint:enable
jest.spyOn(window, 'fetch').mockResolvedValue(response);

const result = await LocalRepoService.fetchLocalFile('f:/test.json');
expect(result).toEqual(content);
done();
});

it('throw when response is 404', async done => {
window.fetch = jest.fn().mockRejectedValueOnce(new Error('Not found'));
await expect(LocalRepoService.fetchLocalFile('f:/test.json')).rejects.toThrowError('Not found');
done();
});
});
});
17 changes: 17 additions & 0 deletions src/app/api/services/localRepoService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { READ_FILE, CONTROLLER_API_ENDPOINT, DataPlaneStatusCode } from './../../constants/apiConstants';

export const fetchLocalFile = async (path: string): Promise<string> => {
try {
const response = await fetch(`${CONTROLLER_API_ENDPOINT}${READ_FILE}/${encodeURIComponent(path)}`);
if (await response.status === DataPlaneStatusCode.NotFound) {
throw new Error();
}
return await response.json();
} catch (error) {
throw error;
}
};
1 change: 1 addition & 0 deletions src/app/constants/apiConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const DATAPLANE = '/DataPlane';
export const EVENTHUB = '/EventHub';
export const MODELREPO = '/ModelRepo';
export const CLOUD_TO_DEVICE = '/CloudToDevice';
export const READ_FILE = '/ReadFile';

// model repo .net controller
export const INTERFACE_ID = '?interfaceId=';
Expand Down
1 change: 1 addition & 0 deletions src/app/constants/browserStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export const REPO_LOCATIONS = 'REPO_LOCATIONS'; // store repo locations in local
export const THEME_SELECTION = 'THEME_SELECTION';
export const HIGH_CONTRAST = 'HIGH_CONTRAST';
export const CUSTOM_CONTROLLER_PORT = 'CUSTOM_CONTROLLER_PORT';
export const LOCAL_FILE_EXPLORER_PATH_NAME = 'LOCAL_FILE_EXPLORER_PATH_NAME';
1 change: 1 addition & 0 deletions src/app/constants/iconNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export const ADD = 'Add';
export const ACCEPT = 'Accept';
export const CANCEL = 'Cancel';
export const CHECK = 'SkypeCheck';
Expand Down
3 changes: 2 additions & 1 deletion src/app/constants/repositoryLocationTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
export enum REPOSITORY_LOCATION_TYPE {
Public = 'PUBLIC',
Private = 'PRIVATE',
Device = 'DEVICE'
Device = 'DEVICE',
Local = 'LOCAL'
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,48 @@ exports[`components/devices/deviceCommands matches snapshot while interface cann
<Route
component={[Function]}
/>
<Connect(InterfaceNotFoundMessageBox) />
<DeviceCommandsPerInterface
componentName={null}
deviceId={null}
history={
Object {
"location": Object {
"pathname": "/#/devices/deviceDetail/ioTPlugAndPlay/ioTPlugAndPlayDetail/commands/?id=device1&componentName=foo&interfaceId=urn:iotInterfaces:com:interface1:1",
},
}
}
invokeDigitalTwinInterfaceCommand={[MockFunction]}
isLoading={false}
location={
Object {
"pathname": "/#/devices/deviceDetail/ioTPlugAndPlay/ioTPlugAndPlayDetail/commands/?id=device1&componentName=foo&interfaceId=urn:iotInterfaces:com:interface1:1",
}
}
match={Object {}}
refresh={[MockFunction]}
setComponentName={
[MockFunction] {
"calls": Array [
Array [
null,
],
Array [
null,
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
Object {
"type": "return",
"value": undefined,
},
],
}
}
/>
</Fragment>
`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { ResourceKeys } from '../../../../../localization/resourceKeys';
import { InvokeDigitalTwinInterfaceCommandActionParameters } from '../../actions';
import { getDeviceIdFromQueryString, getInterfaceIdFromQueryString, getComponentNameFromQueryString } from '../../../../shared/utils/queryStringHelper';
import { CommandSchema } from './deviceCommandsPerInterfacePerCommand';
import InterfaceNotFoundMessageBoxContainer from '../shared/interfaceNotFoundMessageBarContainer';
import { REFRESH, NAVIGATE_BACK } from '../../../../constants/iconNames';
import MultiLineShimmer from '../../../../shared/components/multiLineShimmer';
import { DigitalTwinHeaderContainer } from '../digitalTwin/digitalTwinHeaderView';
Expand Down Expand Up @@ -86,15 +85,13 @@ export default class DeviceCommands
return (
<>
<Route component={DigitalTwinHeaderContainer} />
{commandSchemas ?
commandSchemas.length === 0 ?
<Label className="no-pnp-content">{context.t(ResourceKeys.deviceCommands.noCommands, {componentName: getComponentNameFromQueryString(this.props)})}</Label> :
<DeviceCommandPerInterface
{...this.props}
componentName={getComponentNameFromQueryString(this.props)}
deviceId={getDeviceIdFromQueryString(this.props)}
/>
: <InterfaceNotFoundMessageBoxContainer/>
{commandSchemas && commandSchemas.length === 0 ?
<Label className="no-pnp-content">{context.t(ResourceKeys.deviceCommands.noCommands, {componentName: getComponentNameFromQueryString(this.props)})}</Label> :
<DeviceCommandPerInterface
{...this.props}
componentName={getComponentNameFromQueryString(this.props)}
deviceId={getDeviceIdFromQueryString(this.props)}
/>
}
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ export const getDeviceCommandPairs = (state: StateInterface): DeviceInterfaceWit
const modelDefinition = getModelDefinitionSelector(state);
const commands = modelDefinition && modelDefinition.contents && modelDefinition.contents.filter((item: CommandContent) => filterCommand(item));
return {
commandSchemas: commands && commands.map(command => ({
commandSchemas: commands ? commands.map(command => ({
commandModelDefinition: command,
parsedSchema: parseInterfaceCommandToJsonSchema(command),
}))
})) : []
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,60 @@ exports[`components/devices/deviceEventsPerInterface matches snapshot while inte
<Route
component={[Function]}
/>
<Connect(InterfaceNotFoundMessageBox) />
<StyledTextFieldBase
ariaLabel="deviceEvents.consumerGroups.label"
className="consumer-group-text-field"
disabled={false}
label="deviceEvents.consumerGroups.label"
onChange={[Function]}
onRenderLabel={[Function]}
underlined={true}
value="$Default"
/>
<StyledToggleBase
ariaLabel="deviceEvents.toggle.label"
checked={false}
className="toggle-button"
label="deviceEvents.toggle.label"
offText="deviceEvents.toggle.off"
onChange={[Function]}
onText="deviceEvents.toggle.on"
/>
<InfiniteScroll
className="device-events-container"
element="div"
getScrollParent={null}
hasMore={false}
initialLoad={true}
isReverse={true}
key="scroll"
loadMore={[Function]}
loader={
<div>
<div
className="events-loader"
>
<StyledSpinnerBase />
<h4>
deviceEvents.infiniteScroll.loading
</h4>
</div>
</div>
}
pageStart={0}
ref={null}
threshold={250}
useCapture={false}
useWindow={true}
>
<section
className="list-content"
>
<div
className="scrollable-pnp-telemetry"
/>
</section>
</InfiniteScroll>
</div>
`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { REFRESH, STOP, START, REMOVE, NAVIGATE_BACK } from '../../../../constan
import { ParsedJsonSchema } from '../../../../api/models/interfaceJsonParserOutput';
import { TelemetryContent } from '../../../../api/models/modelDefinition';
import { getInterfaceIdFromQueryString, getDeviceIdFromQueryString, getComponentNameFromQueryString } from '../../../../shared/utils/queryStringHelper';
import InterfaceNotFoundMessageBoxContainer from '../shared/interfaceNotFoundMessageBarContainer';
import { getNumberOfMapsInSchema } from '../../../../shared/utils/twinAndJsonSchemaDataConverter';
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
import { DEFAULT_CONSUMER_GROUP } from '../../../../constants/apiConstants';
Expand Down Expand Up @@ -104,24 +103,22 @@ export default class DeviceEventsPerInterfaceComponent extends React.Component<D
<div className="device-events" key="device-events">
{this.renderCommandBar(context)}
<Route component={DigitalTwinHeaderContainer} />
{telemetrySchema ?
telemetrySchema.length === 0 ?
<Label className="no-pnp-content">{context.t(ResourceKeys.deviceEvents.noEvent, {componentName: getComponentNameFromQueryString(this.props)})}</Label> :
<>
<TextField
className={'consumer-group-text-field'}
onRenderLabel={this.renderConsumerGroupLabel(context)}
label={context.t(ResourceKeys.deviceEvents.consumerGroups.label)}
ariaLabel={context.t(ResourceKeys.deviceEvents.consumerGroups.label)}
underlined={true}
value={this.state.consumerGroup}
disabled={this.state.monitoringData}
onChange={this.consumerGroupChange}
/>
{this.renderRawTelemetryToggle(context)}
{this.renderInfiniteScroll(context)}
</>
: <InterfaceNotFoundMessageBoxContainer/>
{telemetrySchema && telemetrySchema.length === 0 ?
<Label className="no-pnp-content">{context.t(ResourceKeys.deviceEvents.noEvent, {componentName: getComponentNameFromQueryString(this.props)})}</Label> :
<>
<TextField
className={'consumer-group-text-field'}
onRenderLabel={this.renderConsumerGroupLabel(context)}
label={context.t(ResourceKeys.deviceEvents.consumerGroups.label)}
ariaLabel={context.t(ResourceKeys.deviceEvents.consumerGroups.label)}
underlined={true}
value={this.state.consumerGroup}
disabled={this.state.monitoringData}
onChange={this.consumerGroupChange}
/>
{this.renderRawTelemetryToggle(context)}
{this.renderInfiniteScroll(context)}
</>
}
{this.state.loadingAnnounced}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import { TelemetrySchema } from './deviceEventsPerInterface';
export const getDeviceTelemetrySelector = (state: StateInterface): TelemetrySchema[] => {
const modelDefinition = getModelDefinitionSelector(state);
const telemetryContents = modelDefinition && modelDefinition.contents && modelDefinition.contents.filter((item: TelemetryContent) => filterTelemetry(item)) as TelemetryContent[];
return telemetryContents && telemetryContents.map(telemetry => ({
return telemetryContents ? telemetryContents.map(telemetry => ({
parsedSchema: parseInterfaceTelemetryToJsonSchema(telemetry),
telemetryModelDefinition: telemetry
}));
})) : [];
};

const filterTelemetry = (content: TelemetryContent) => {
Expand Down
Loading

0 comments on commit 4c2eaf1

Please sign in to comment.