refactor device digital twins
This commit is contained in:
Родитель
efa07bddbe
Коммит
4252580e2f
|
@ -3,7 +3,7 @@
|
|||
exports[`devices/components/deviceTwin snapshot matches snapshot 1`] = `
|
||||
<Fragment>
|
||||
<StyledCommandBarBase
|
||||
className="command"
|
||||
className="command device-detail-command"
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -29,6 +29,7 @@ exports[`devices/components/deviceTwin snapshot matches snapshot 1`] = `
|
|||
}
|
||||
/>
|
||||
<HeaderView
|
||||
className="device-detail-header"
|
||||
headerText="deviceTwin.headerText"
|
||||
tooltip="deviceTwin.tooltip"
|
||||
/>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
exports[`moduleIdentityTwin snapshot matches snapshot after module twin is fetched 1`] = `
|
||||
<Fragment>
|
||||
<StyledCommandBarBase
|
||||
className="command"
|
||||
className="command device-detail-command"
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -29,15 +29,12 @@ exports[`moduleIdentityTwin snapshot matches snapshot after module twin is fetch
|
|||
]
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="device-detail"
|
||||
<article
|
||||
className="device-twin device-detail"
|
||||
>
|
||||
<article
|
||||
className="device-twin"
|
||||
>
|
||||
<JSONEditor
|
||||
className="json-editor"
|
||||
content="{
|
||||
<JSONEditor
|
||||
className="json-editor"
|
||||
content="{
|
||||
\\"authenticationType\\": \\"sas\\",
|
||||
\\"cloudToDeviceMessageCount\\": 0,
|
||||
\\"connectionState\\": \\"Disconnected\\",
|
||||
|
@ -55,17 +52,16 @@ exports[`moduleIdentityTwin snapshot matches snapshot after module twin is fetch
|
|||
\\"secondaryThumbprint\\": null
|
||||
}
|
||||
}"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</article>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`moduleIdentityTwin snapshot matches snapshot while loading 1`] = `
|
||||
<Fragment>
|
||||
<StyledCommandBarBase
|
||||
className="command"
|
||||
className="command device-detail-command"
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -91,10 +87,6 @@ exports[`moduleIdentityTwin snapshot matches snapshot while loading 1`] = `
|
|||
]
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="device-detail"
|
||||
>
|
||||
<MultiLineShimmer />
|
||||
</div>
|
||||
<MultiLineShimmer />
|
||||
</Fragment>
|
||||
`;
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DigitalTwinInterfacesList matches snapshot when empty model id is retrieved 1`] = `
|
||||
<Fragment>
|
||||
<StyledCommandBarBase
|
||||
className="command"
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"ariaLabel": "deviceEvents.command.refresh",
|
||||
"disabled": false,
|
||||
"iconProps": Object {
|
||||
"iconName": "Refresh",
|
||||
},
|
||||
"key": "Refresh",
|
||||
"name": "deviceEvents.command.refresh",
|
||||
"onClick": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<HeaderView
|
||||
headerText="digitalTwin.headerText"
|
||||
link="settings.questions.questions.documentation.link"
|
||||
tooltip="settings.questions.questions.documentation.text"
|
||||
/>
|
||||
<section
|
||||
className="device-detail"
|
||||
>
|
||||
<span>
|
||||
digitalTwin.steps.zero
|
||||
</span>
|
||||
<StyledLinkBase
|
||||
href="settings.questions.questions.documentation.link"
|
||||
target="_blank"
|
||||
>
|
||||
settings.questions.questions.documentation.text
|
||||
</StyledLinkBase>
|
||||
</section>
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,47 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DigitalTwinComponentList matches snapshot when empty model id is retrieved 1`] = `
|
||||
<Fragment>
|
||||
<h4>
|
||||
digitalTwin.steps.third
|
||||
</h4>
|
||||
<h5>
|
||||
digitalTwin.steps.explanation
|
||||
</h5>
|
||||
<StyledPivot
|
||||
aria-label="digitalTwin.pivot.ariaLabel"
|
||||
>
|
||||
<PivotItem
|
||||
headerText="digitalTwin.pivot.components"
|
||||
>
|
||||
<ErrorBoundary
|
||||
error="deviceInterfaces.interfaceListFailedToRender"
|
||||
>
|
||||
<StyledLabelBase
|
||||
className="no-component"
|
||||
>
|
||||
digitalTwin.modelContainsNoComponents
|
||||
</StyledLabelBase>
|
||||
<StyledAnnouncedBase
|
||||
message="digitalTwin.modelContainsNoComponents"
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
className="modelContent"
|
||||
headerText="digitalTwin.pivot.content"
|
||||
>
|
||||
<JSONEditor
|
||||
className="interface-definition-json-editor"
|
||||
content="{
|
||||
\\"@context\\": \\"dtmi:dtdl:context;2\\",
|
||||
\\"@id\\": \\"dtmi:plugnplay:hube2e:cm;1\\",
|
||||
\\"@type\\": \\"Interface\\",
|
||||
\\"contents\\": [],
|
||||
\\"displayName\\": \\"IoT Hub E2E Tests\\"
|
||||
}"
|
||||
/>
|
||||
</PivotItem>
|
||||
</StyledPivot>
|
||||
</Fragment>
|
||||
`;
|
|
@ -3,6 +3,7 @@
|
|||
exports[`DigitalTwinDetail matches snapshot 1`] = `
|
||||
<StyledPivot
|
||||
aria-label="digitalTwin.pivot.ariaLabel"
|
||||
className="digitaltwin-pivot"
|
||||
onLinkClick={[Function]}
|
||||
overflowBehavior="menu"
|
||||
selectedKey="interfaces"
|
|
@ -0,0 +1,13 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DigitalTwinInterfacesList matches snapshot 1`] = `
|
||||
<Fragment>
|
||||
<Command />
|
||||
<HeaderView
|
||||
headerText="digitalTwin.headerText"
|
||||
link="settings.questions.questions.documentation.link"
|
||||
tooltip="settings.questions.questions.documentation.text"
|
||||
/>
|
||||
<DigitaltwinPnpConfigurationSteps />
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,27 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DigitalTwinModelDefinition matches snapshot 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="step-two"
|
||||
>
|
||||
<h4>
|
||||
digitalTwin.steps.secondFailure
|
||||
</h4>
|
||||
<InterfaceNotFoundMessageBar />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`DigitalTwinModelDefinition matches snapshot when empty model id is retrieved 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="step-two"
|
||||
>
|
||||
<h4>
|
||||
digitalTwin.steps.secondFailure
|
||||
</h4>
|
||||
<InterfaceNotFoundMessageBar />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,33 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DigitalTwinModelId matches snapshot when empty model id is retrieved 1`] = `
|
||||
<div
|
||||
className="step-one"
|
||||
>
|
||||
<h4>
|
||||
digitalTwin.steps.first
|
||||
</h4>
|
||||
<MaskedCopyableTextField
|
||||
allowMask={false}
|
||||
ariaLabel="digitalTwin.modelId"
|
||||
label="digitalTwin.modelId"
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DigitalTwinModelId matches snapshot when model id is retrieved 1`] = `
|
||||
<div
|
||||
className="step-one"
|
||||
>
|
||||
<h4>
|
||||
digitalTwin.steps.first
|
||||
</h4>
|
||||
<MaskedCopyableTextField
|
||||
allowMask={false}
|
||||
ariaLabel="digitalTwin.modelId"
|
||||
label="digitalTwin.modelId"
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,54 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DigitaltwinPnpConfigurationSteps matches snapshot when empty model id is retrieved 1`] = `
|
||||
<section
|
||||
className="device-detail"
|
||||
>
|
||||
<div
|
||||
className="digitalTwin-steps"
|
||||
>
|
||||
<span>
|
||||
digitalTwin.steps.zero
|
||||
</span>
|
||||
<StyledLinkBase
|
||||
href="settings.questions.questions.documentation.link"
|
||||
target="_blank"
|
||||
>
|
||||
settings.questions.questions.documentation.text
|
||||
</StyledLinkBase>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
exports[`DigitaltwinPnpConfigurationSteps matches snapshot when there is empty model id 1`] = `
|
||||
<section
|
||||
className="device-detail"
|
||||
>
|
||||
<div
|
||||
className="digitalTwin-steps"
|
||||
>
|
||||
<span>
|
||||
digitalTwin.steps.zero
|
||||
</span>
|
||||
<StyledLinkBase
|
||||
href="settings.questions.questions.documentation.link"
|
||||
target="_blank"
|
||||
>
|
||||
settings.questions.questions.documentation.text
|
||||
</StyledLinkBase>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
exports[`DigitaltwinPnpConfigurationSteps matches snapshot when there is model id 1`] = `
|
||||
<section
|
||||
className="device-detail"
|
||||
>
|
||||
<div
|
||||
className="digitalTwin-steps"
|
||||
>
|
||||
<DigitalTwinModelId />
|
||||
<DigitalTwinModelDefinition />
|
||||
</div>
|
||||
</section>
|
||||
`;
|
|
@ -0,0 +1,46 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { CommandBar, ICommandBarItemProps } from '@fluentui/react';
|
||||
import { ResourceKeys } from '../../../../../localization/resourceKeys';
|
||||
import { usePnpStateContext } from '../../../../shared/contexts/pnpStateContext';
|
||||
import './digitalTwinDetail.scss';
|
||||
import { REFRESH } from '../../../../constants/iconNames';
|
||||
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
|
||||
import { dispatchGetTwinAction } from '../../utils';
|
||||
|
||||
export const Command: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { search } = useLocation();
|
||||
const { pnpState, dispatch, } = usePnpStateContext();
|
||||
const twinSynchronizationStatus = pnpState.twin.synchronizationStatus;
|
||||
const isTwinLoading = twinSynchronizationStatus === SynchronizationStatus.working;
|
||||
|
||||
const onRefresh = (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
|
||||
dispatchGetTwinAction(search, dispatch);
|
||||
};
|
||||
|
||||
const createCommandBarItems = (): ICommandBarItemProps[] => {
|
||||
return [
|
||||
{
|
||||
ariaLabel: t(ResourceKeys.deviceEvents.command.refresh),
|
||||
disabled: isTwinLoading,
|
||||
iconProps: {iconName: REFRESH},
|
||||
key: REFRESH,
|
||||
name: t(ResourceKeys.deviceEvents.command.refresh),
|
||||
onClick: onRefresh
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandBar
|
||||
className="command"
|
||||
items={createCommandBarItems()}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -5,14 +5,12 @@
|
|||
import 'jest';
|
||||
import * as React from 'react';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import { Label , Announced, MessageBar,Pivot, PivotItem } from '@fluentui/react';
|
||||
import { DigitalTwinInterfacesList } from './digitalTwinInterfacesList';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { MultiLineShimmer } from '../../../shared/components/multiLineShimmer';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../../constants/repositoryLocationTypes';
|
||||
import { pnpStateInitial, PnpStateInterface } from '../state';
|
||||
import * as pnpStateContext from '../../../shared/contexts/pnpStateContext';
|
||||
import { SynchronizationStatus } from '../../../api/models/synchronizationStatus';
|
||||
import { Announced, Pivot, PivotItem } from '@fluentui/react';
|
||||
import { DigitalTwinComponentList } from './digitalTwinComponentList';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../../../constants/repositoryLocationTypes';
|
||||
import { pnpStateInitial, PnpStateInterface } from '../../state';
|
||||
import * as pnpStateContext from '../../../../shared/contexts/pnpStateContext';
|
||||
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
|
||||
|
||||
const interfaceId = 'urn:azureiot:samplemodel;1';
|
||||
|
||||
|
@ -48,20 +46,7 @@ jest.mock('react-router-dom', () => ({
|
|||
useRouteMatch: () => ({ url: pathname })
|
||||
}));
|
||||
|
||||
describe('DigitalTwinInterfacesList', () => {
|
||||
it('shows shimmer when model id is not retrieved', () => {
|
||||
const initialState: PnpStateInterface = pnpStateInitial().merge({
|
||||
twin: {
|
||||
synchronizationStatus: SynchronizationStatus.working
|
||||
}
|
||||
});
|
||||
|
||||
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: initialState, dispatch: jest.fn()});
|
||||
|
||||
const wrapper = mount(<DigitalTwinInterfacesList/>);
|
||||
expect(wrapper.find(MultiLineShimmer)).toHaveLength(1);
|
||||
});
|
||||
|
||||
describe('DigitalTwinComponentList', () => {
|
||||
it('matches snapshot when empty model id is retrieved', () => {
|
||||
const initialState: PnpStateInterface = pnpStateInitial().merge({
|
||||
twin: {
|
||||
|
@ -70,68 +55,27 @@ describe('DigitalTwinInterfacesList', () => {
|
|||
moduleId: ''
|
||||
} as any,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
}
|
||||
});
|
||||
|
||||
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: initialState, dispatch: jest.fn()});
|
||||
|
||||
const wrapper = shallow(<DigitalTwinInterfacesList/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows model id with no model definition found', () => {
|
||||
const initialState: PnpStateInterface = pnpStateInitial().merge({
|
||||
twin: {
|
||||
payload: deviceTwin,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
},
|
||||
modelDefinitionWithSource: {
|
||||
payload: null,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
}
|
||||
});
|
||||
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: initialState, dispatch: jest.fn()});
|
||||
|
||||
const wrapper = mount(<DigitalTwinInterfacesList/>);
|
||||
const labels = wrapper.find(Label);
|
||||
expect(labels).toHaveLength(1);
|
||||
expect(labels.first().props().children).toEqual(ResourceKeys.digitalTwin.modelId);
|
||||
|
||||
const h4 = wrapper.find('h4');
|
||||
expect(h4).toHaveLength(2); // tslint:disable-line:no-magic-numbers
|
||||
expect(h4.at(1).props().children).toEqual(ResourceKeys.digitalTwin.steps.secondFailure);
|
||||
});
|
||||
|
||||
it('shows model id with null model definition found', () => {
|
||||
const initialState: PnpStateInterface = pnpStateInitial().merge({
|
||||
twin: {
|
||||
payload: deviceTwin,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
},
|
||||
modelDefinitionWithSource: {
|
||||
payload: {
|
||||
isModelValid: false,
|
||||
modelDefinition: null,
|
||||
source: REPOSITORY_LOCATION_TYPE.Local
|
||||
isModelValid: true,
|
||||
modelDefinition: {
|
||||
'@context': 'dtmi:dtdl:context;2',
|
||||
'@id': 'dtmi:plugnplay:hube2e:cm;1',
|
||||
'@type': 'Interface',
|
||||
'contents': [],
|
||||
'displayName': 'IoT Hub E2E Tests',
|
||||
},
|
||||
source: REPOSITORY_LOCATION_TYPE.Public
|
||||
},
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
}
|
||||
});
|
||||
|
||||
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: initialState, dispatch: jest.fn()});
|
||||
|
||||
const wrapper = mount(<DigitalTwinInterfacesList/>);
|
||||
|
||||
const h4 = wrapper.find('h4');
|
||||
expect(h4).toHaveLength(2); // tslint:disable-line:no-magic-numbers
|
||||
expect(h4.at(1).props().children).toEqual(ResourceKeys.digitalTwin.steps.secondSuccess);
|
||||
|
||||
const labels = wrapper.find(Label);
|
||||
expect(labels).toHaveLength(2); // tslint:disable-line:no-magic-numbers
|
||||
expect(labels.at(1).props().children).toEqual([ResourceKeys.deviceInterfaces.columns.source, ': ', ResourceKeys.modelRepository.types.local.label]);
|
||||
|
||||
const messageBar = wrapper.find(MessageBar);
|
||||
expect(messageBar).toHaveLength(1);
|
||||
expect(messageBar.first().props().children).toEqual(ResourceKeys.deviceInterfaces.interfaceNotValid);
|
||||
const wrapper = shallow(<DigitalTwinComponentList/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows model id with valid model definition found but has no component', () => {
|
||||
|
@ -157,16 +101,7 @@ describe('DigitalTwinInterfacesList', () => {
|
|||
});
|
||||
|
||||
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: initialState, dispatch: jest.fn()});
|
||||
const wrapper = mount(<DigitalTwinInterfacesList/>);
|
||||
|
||||
const h4 = wrapper.find('h4');
|
||||
expect(h4).toHaveLength(3); // tslint:disable-line:no-magic-numbers
|
||||
expect(h4.at(2).props().children).toEqual(ResourceKeys.digitalTwin.steps.third); // tslint:disable-line:no-magic-numbers
|
||||
expect(wrapper.find('h5').first().props().children).toEqual(ResourceKeys.digitalTwin.steps.explanation);
|
||||
|
||||
const labels = wrapper.find(Label);
|
||||
expect(labels).toHaveLength(3); // tslint:disable-line:no-magic-numbers
|
||||
expect(labels.at(1).props().children).toEqual([ResourceKeys.deviceInterfaces.columns.source, ': ', ResourceKeys.modelRepository.types.public.label]);
|
||||
const wrapper = mount(<DigitalTwinComponentList/>);
|
||||
|
||||
expect(wrapper.find(Announced)).toHaveLength(1);
|
||||
expect(wrapper.find(Pivot)).toHaveLength(1);
|
||||
|
@ -211,7 +146,7 @@ describe('DigitalTwinInterfacesList', () => {
|
|||
});
|
||||
|
||||
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: initialState, dispatch: jest.fn()});
|
||||
const wrapper = shallow(<DigitalTwinInterfacesList/>);
|
||||
const wrapper = shallow(<DigitalTwinComponentList/>);
|
||||
|
||||
expect(wrapper.find(Announced)).toHaveLength(0);
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NavLink, useLocation, useRouteMatch } from 'react-router-dom';
|
||||
import { Announced, DetailsList, IColumn, Label, Pivot, PivotItem, SelectionMode } from '@fluentui/react';
|
||||
import { ResourceKeys } from '../../../../../localization/resourceKeys';
|
||||
import { usePnpStateContext } from '../../../../shared/contexts/pnpStateContext';
|
||||
import './digitalTwinDetail.scss';
|
||||
import { getComponentNameAndInterfaceIdArray } from '../../utils';
|
||||
import { getDeviceIdFromQueryString, getModuleIdentityIdFromQueryString } from '../../../../shared/utils/queryStringHelper';
|
||||
import { ROUTE_PARAMS, ROUTE_PARTS } from '../../../../constants/routes';
|
||||
import { DEFAULT_COMPONENT_FOR_DIGITAL_TWIN } from '../../../../constants/devices';
|
||||
import { LARGE_COLUMN_WIDTH } from '../../../../constants/columnWidth';
|
||||
import { ErrorBoundary } from '../../../shared/components/errorBoundary';
|
||||
import { JSONEditor } from '../../../../shared/components/jsonEditor';
|
||||
|
||||
interface ModelContent {
|
||||
link: string;
|
||||
componentName: string;
|
||||
interfaceId: string;
|
||||
}
|
||||
|
||||
export const DigitalTwinComponentList: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { url } = useRouteMatch();
|
||||
const { search } = useLocation();
|
||||
const { pnpState } = usePnpStateContext();
|
||||
const deviceId = getDeviceIdFromQueryString(search);
|
||||
const modelDefinitionWithSource = pnpState.modelDefinitionWithSource.payload;
|
||||
const modelDefinition = modelDefinitionWithSource && modelDefinitionWithSource.modelDefinition;
|
||||
const twin = pnpState.twin.payload;
|
||||
const modelId = twin?.modelId;
|
||||
const moduleId = getModuleIdentityIdFromQueryString(search);
|
||||
const componentNameToIds = getComponentNameAndInterfaceIdArray(modelDefinition);
|
||||
|
||||
const modelContents: ModelContent[] = componentNameToIds && componentNameToIds.map(nameToId => {
|
||||
let link = `${url}${ROUTE_PARTS.DIGITAL_TWINS_DETAIL}/${ROUTE_PARTS.INTERFACES}/` +
|
||||
`?${ROUTE_PARAMS.DEVICE_ID}=${encodeURIComponent(deviceId)}` +
|
||||
`&${ROUTE_PARAMS.COMPONENT_NAME}=${nameToId.componentName}` +
|
||||
`&${ROUTE_PARAMS.INTERFACE_ID}=${nameToId.interfaceId}`;
|
||||
if (moduleId) {
|
||||
link += `&${ROUTE_PARAMS.MODULE_ID}=${moduleId}`;
|
||||
}
|
||||
|
||||
if (nameToId.componentName === DEFAULT_COMPONENT_FOR_DIGITAL_TWIN && nameToId.interfaceId === modelId) {
|
||||
return{
|
||||
componentName: t(ResourceKeys.digitalTwin.pivot.defaultComponent),
|
||||
interfaceId: nameToId.interfaceId,
|
||||
link
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
...nameToId,
|
||||
link
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const getColumns = (): IColumn[] => {
|
||||
return [
|
||||
{ fieldName: 'componentName', isMultiline: true, isResizable: true, key: 'name',
|
||||
maxWidth: LARGE_COLUMN_WIDTH, minWidth: 100, name: t(ResourceKeys.digitalTwin.componentName) },
|
||||
{ fieldName: 'interfaceId', isMultiline: true, isResizable: true, key: 'id',
|
||||
maxWidth: LARGE_COLUMN_WIDTH, minWidth: 100, name: t(ResourceKeys.digitalTwin.interfaceId)}
|
||||
];
|
||||
};
|
||||
|
||||
const renderItemColumn = () => (item: ModelContent, index: number, column: IColumn) => {
|
||||
switch (column.key) {
|
||||
case 'name':
|
||||
return (
|
||||
<NavLink key={column.key} to={item.link}>
|
||||
{item.componentName}
|
||||
</NavLink>
|
||||
);
|
||||
case 'id':
|
||||
return (
|
||||
<Label
|
||||
key={column.key}
|
||||
>
|
||||
{item.interfaceId}
|
||||
</Label>
|
||||
);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const listView = (
|
||||
<>
|
||||
{
|
||||
modelContents.length !== 0 ?
|
||||
<div className="list-detail">
|
||||
<DetailsList
|
||||
onRenderItemColumn={renderItemColumn()}
|
||||
className="component-list"
|
||||
items={modelContents}
|
||||
columns={getColumns()}
|
||||
selectionMode={SelectionMode.none}
|
||||
/>
|
||||
</div> :
|
||||
<>
|
||||
<Label className="no-component">{t(ResourceKeys.digitalTwin.modelContainsNoComponents, {modelId })}</Label>
|
||||
<Announced
|
||||
message={t(ResourceKeys.digitalTwin.modelContainsNoComponents, { modelId })}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</>);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>{t(ResourceKeys.digitalTwin.steps.third)}</h4>
|
||||
<h5>{t(ResourceKeys.digitalTwin.steps.explanation, {modelId})}</h5>
|
||||
<Pivot aria-label={t(ResourceKeys.digitalTwin.pivot.ariaLabel)}>
|
||||
<PivotItem headerText={t(ResourceKeys.digitalTwin.pivot.components)}>
|
||||
<ErrorBoundary error={t(ResourceKeys.deviceInterfaces.interfaceListFailedToRender)}>
|
||||
{listView}
|
||||
</ErrorBoundary>
|
||||
</PivotItem>
|
||||
<PivotItem headerText={t(ResourceKeys.digitalTwin.pivot.content)} className="modelContent">
|
||||
<JSONEditor
|
||||
className="interface-definition-json-editor"
|
||||
content={JSON.stringify(modelDefinitionWithSource.modelDefinition, null, '\t')}
|
||||
/>
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -2,8 +2,8 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
@import '../../../css/themes';
|
||||
@import '../../../css/variables';
|
||||
@import '../../../../css/themes';
|
||||
@import '../../../../css/variables';
|
||||
|
||||
.digitaltwin-pivot {
|
||||
height: 100%;
|
|
@ -6,9 +6,9 @@ import 'jest';
|
|||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { DigitalTwinDetail } from './digitalTwinDetail';
|
||||
import * as pnpStateContext from '../../../shared/contexts/pnpStateContext';
|
||||
import { PnpStateInterface, pnpStateInitial } from '../state';
|
||||
import { SynchronizationStatus } from '../../../api/models/synchronizationStatus';
|
||||
import * as pnpStateContext from '../../../../shared/contexts/pnpStateContext';
|
||||
import { PnpStateInterface, pnpStateInitial } from '../../state';
|
||||
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
|
||||
|
||||
const search = '?id=device1&componentName=foo&interfaceId=urn:iotInterfaces:com:interface1;1';
|
||||
const pathname = `/#/devices/deviceDetail/ioTPlugAndPlay/${search}`;
|
|
@ -6,17 +6,17 @@ import * as React from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation, useHistory } from 'react-router-dom';
|
||||
import { Stack, Pivot, PivotItem } from '@fluentui/react';
|
||||
import { ROUTE_PARTS, ROUTE_PARAMS } from '../../../constants/routes';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { DeviceSettings } from './deviceSettings/deviceSettings';
|
||||
import { DeviceProperties } from './deviceProperties/deviceProperties';
|
||||
import { DeviceCommands } from './deviceCommands/deviceCommands';
|
||||
import { DeviceInterfaces } from './deviceInterfaces/deviceInterfaces';
|
||||
import { DeviceEvents } from '../../deviceEvents/components/deviceEvents';
|
||||
import { getDeviceIdFromQueryString, getInterfaceIdFromQueryString, getComponentNameFromQueryString, getModuleIdentityIdFromQueryString } from '../../../shared/utils/queryStringHelper';
|
||||
import { usePnpStateContext } from '../../../shared/contexts/pnpStateContext';
|
||||
import { ROUTE_PARTS, ROUTE_PARAMS } from '../../../../constants/routes';
|
||||
import { ResourceKeys } from '../../../../../localization/resourceKeys';
|
||||
import { DeviceSettings } from '../deviceSettings/deviceSettings';
|
||||
import { DeviceProperties } from '../deviceProperties/deviceProperties';
|
||||
import { DeviceCommands } from '../deviceCommands/deviceCommands';
|
||||
import { DeviceInterfaces } from '../deviceInterfaces/deviceInterfaces';
|
||||
import { DeviceEvents } from '../../../deviceEvents/components/deviceEvents';
|
||||
import { getDeviceIdFromQueryString, getInterfaceIdFromQueryString, getComponentNameFromQueryString, getModuleIdentityIdFromQueryString } from '../../../../shared/utils/queryStringHelper';
|
||||
import { usePnpStateContext } from '../../../../shared/contexts/pnpStateContext';
|
||||
import './digitalTwinDetail.scss';
|
||||
import { DeviceEventsStateContextProvider } from '../../deviceEvents/context/deviceEventsStateProvider';
|
||||
import { DeviceEventsStateContextProvider } from '../../../deviceEvents/context/deviceEventsStateProvider';
|
||||
|
||||
export const DigitalTwinDetail: React.FC = () => {
|
||||
const { t } = useTranslation();
|
|
@ -0,0 +1,78 @@
|
|||
/***********************************************************
|
||||
* 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 { DigitalTwinInterfacesList } from './digitalTwinInterfacesList';
|
||||
import { MultiLineShimmer } from '../../../../shared/components/multiLineShimmer';
|
||||
import { pnpStateInitial, PnpStateInterface } from '../../state';
|
||||
import * as pnpStateContext from '../../../../shared/contexts/pnpStateContext';
|
||||
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
|
||||
|
||||
const interfaceId = 'urn:azureiot:samplemodel;1';
|
||||
|
||||
/* tslint:disable */
|
||||
const deviceTwin: any = {
|
||||
"deviceId": "testDevice",
|
||||
"modelId": interfaceId,
|
||||
"properties" : {
|
||||
desired: {
|
||||
environmentalSensor: {
|
||||
brightness: 456,
|
||||
__t: 'c'
|
||||
}
|
||||
},
|
||||
reported: {
|
||||
environmentalSensor: {
|
||||
brightness: {
|
||||
value: 123,
|
||||
dv: 2
|
||||
},
|
||||
__t: 'c'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
/* tslint:enable */
|
||||
|
||||
const pathname = 'resources/TestHub.azure-devices.net/devices/deviceDetail/ioTPlugAndPlay/?deviceId=testDevice';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: () => ({ push: jest.fn()}),
|
||||
useLocation: () => ({ search: '?deviceId=testDevice' }),
|
||||
useRouteMatch: () => ({ url: pathname })
|
||||
}));
|
||||
|
||||
describe('DigitalTwinInterfacesList', () => {
|
||||
it('matches snapshot', () => {
|
||||
const initialState: PnpStateInterface = pnpStateInitial().merge({
|
||||
twin: {
|
||||
payload: deviceTwin,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
},
|
||||
modelDefinitionWithSource: {
|
||||
payload: null,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
}
|
||||
});
|
||||
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: initialState, dispatch: jest.fn()});
|
||||
|
||||
const wrapper = shallow(<DigitalTwinInterfacesList/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows shimmer when model id is not retrieved', () => {
|
||||
const initialState: PnpStateInterface = pnpStateInitial().merge({
|
||||
twin: {
|
||||
synchronizationStatus: SynchronizationStatus.working
|
||||
}
|
||||
});
|
||||
|
||||
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: initialState, dispatch: jest.fn()});
|
||||
|
||||
const wrapper = mount(<DigitalTwinInterfacesList/>);
|
||||
expect(wrapper.find(MultiLineShimmer)).toHaveLength(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { ResourceKeys } from '../../../../../localization/resourceKeys';
|
||||
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
|
||||
import { HeaderView } from '../../../../shared/components/headerView';
|
||||
import { MultiLineShimmer } from '../../../../shared/components/multiLineShimmer';
|
||||
import { usePnpStateContext } from '../../../../shared/contexts/pnpStateContext';
|
||||
import '../../../../css/_digitalTwinInterfaces.scss';
|
||||
import { AppInsightsClient } from '../../../../shared/appTelemetry/appInsightsClient';
|
||||
import { TELEMETRY_PAGE_NAMES } from '../../../../constants/telemetry';
|
||||
import { Command } from './command';
|
||||
import { DigitaltwinPnpConfigurationSteps } from './digitaltwinPnpConfigurationSteps';
|
||||
|
||||
// tslint:disable-next-line: cyclomatic-complexity
|
||||
export const DigitalTwinInterfacesList: React.FC = () => {
|
||||
const { pnpState } = usePnpStateContext();
|
||||
const twinSynchronizationStatus = pnpState.twin.synchronizationStatus;
|
||||
const isTwinLoading = twinSynchronizationStatus === SynchronizationStatus.working;
|
||||
|
||||
React.useEffect(() => {
|
||||
AppInsightsClient.getInstance()?.trackPageView({name: TELEMETRY_PAGE_NAMES.PNP_HOME});
|
||||
}, []); // tslint:disable-line: align
|
||||
|
||||
return (
|
||||
<>
|
||||
<Command/>
|
||||
<HeaderView
|
||||
headerText={ResourceKeys.digitalTwin.headerText}
|
||||
link={ResourceKeys.settings.questions.questions.documentation.link}
|
||||
tooltip={ResourceKeys.settings.questions.questions.documentation.text}
|
||||
/>
|
||||
{isTwinLoading ?
|
||||
<MultiLineShimmer/> :
|
||||
<DigitaltwinPnpConfigurationSteps/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,122 @@
|
|||
/***********************************************************
|
||||
* 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 { Label , Announced, MessageBar,Pivot, PivotItem } from '@fluentui/react';
|
||||
import { DigitalTwinModelDefinition } from './digitalTwinModelDefinition';
|
||||
import { ResourceKeys } from '../../../../../localization/resourceKeys';
|
||||
import { MultiLineShimmer } from '../../../../shared/components/multiLineShimmer';
|
||||
import { REPOSITORY_LOCATION_TYPE } from '../../../../constants/repositoryLocationTypes';
|
||||
import { pnpStateInitial, PnpStateInterface } from '../../state';
|
||||
import * as pnpStateContext from '../../../../shared/contexts/pnpStateContext';
|
||||
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
|
||||
|
||||
const interfaceId = 'urn:azureiot:samplemodel;1';
|
||||
|
||||
/* tslint:disable */
|
||||
const deviceTwin: any = {
|
||||
"deviceId": "testDevice",
|
||||
"modelId": interfaceId,
|
||||
"properties" : {
|
||||
desired: {
|
||||
environmentalSensor: {
|
||||
brightness: 456,
|
||||
__t: 'c'
|
||||
}
|
||||
},
|
||||
reported: {
|
||||
environmentalSensor: {
|
||||
brightness: {
|
||||
value: 123,
|
||||
dv: 2
|
||||
},
|
||||
__t: 'c'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
/* tslint:enable */
|
||||
|
||||
const pathname = 'resources/TestHub.azure-devices.net/devices/deviceDetail/ioTPlugAndPlay/?deviceId=testDevice';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: () => ({ push: jest.fn()}),
|
||||
useLocation: () => ({ search: '?deviceId=testDevice' }),
|
||||
useRouteMatch: () => ({ url: pathname })
|
||||
}));
|
||||
|
||||
describe('DigitalTwinModelDefinition', () => {
|
||||
it('matches snapshot', () => {
|
||||
const initialState: PnpStateInterface = pnpStateInitial().merge({
|
||||
twin: {
|
||||
payload: deviceTwin,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
},
|
||||
modelDefinitionWithSource: {
|
||||
payload: null,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
}
|
||||
});
|
||||
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: initialState, dispatch: jest.fn()});
|
||||
|
||||
const wrapper = shallow(<DigitalTwinModelDefinition/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows shimmer when model id is not retrieved', () => {
|
||||
const initialState: PnpStateInterface = pnpStateInitial().merge({
|
||||
twin: {
|
||||
synchronizationStatus: SynchronizationStatus.working
|
||||
},
|
||||
modelDefinitionWithSource: {
|
||||
payload: null,
|
||||
synchronizationStatus: SynchronizationStatus.working
|
||||
}
|
||||
});
|
||||
|
||||
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: initialState, dispatch: jest.fn()});
|
||||
|
||||
const wrapper = mount(<DigitalTwinModelDefinition/>);
|
||||
expect(wrapper.find(MultiLineShimmer)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('matches snapshot when empty model id is retrieved', () => {
|
||||
const initialState: PnpStateInterface = pnpStateInitial().merge({
|
||||
twin: {
|
||||
payload: {
|
||||
deviceId: 'testDevice',
|
||||
moduleId: ''
|
||||
} as any,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
}
|
||||
});
|
||||
|
||||
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: initialState, dispatch: jest.fn()});
|
||||
|
||||
const wrapper = shallow(<DigitalTwinModelDefinition/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows model id with no model definition found', () => {
|
||||
const initialState: PnpStateInterface = pnpStateInitial().merge({
|
||||
twin: {
|
||||
payload: deviceTwin,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
},
|
||||
modelDefinitionWithSource: {
|
||||
payload: null,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
}
|
||||
});
|
||||
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: initialState, dispatch: jest.fn()});
|
||||
|
||||
const wrapper = mount(<DigitalTwinModelDefinition/>);
|
||||
|
||||
const h4 = wrapper.find('h4');
|
||||
expect(h4).toHaveLength(1); // tslint:disable-line:no-magic-numbers
|
||||
expect(h4.at(0).props().children).toEqual(ResourceKeys.digitalTwin.steps.secondFailure);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { MessageBar, MessageBarType } from '@fluentui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ResourceKeys } from '../../../../../localization/resourceKeys';
|
||||
import { usePnpStateContext } from '../../../../shared/contexts/pnpStateContext';
|
||||
import './digitalTwinDetail.scss';
|
||||
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
|
||||
import { ModelDefinitionSourceView } from '../../../shared/components/modelDefinitionSource';
|
||||
import { InterfaceNotFoundMessageBar } from '../../../shared/components/interfaceNotFoundMessageBar';
|
||||
import { MultiLineShimmer } from '../../../../shared/components/multiLineShimmer';
|
||||
import { JSONEditor } from '../../../../shared/components/jsonEditor';
|
||||
import { DigitalTwinComponentList } from './digitalTwinComponentList';
|
||||
|
||||
export const DigitalTwinModelDefinition: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { pnpState } = usePnpStateContext();
|
||||
const modelDefinitionWithSource = pnpState.modelDefinitionWithSource.payload;
|
||||
const modelDefinitionSynchronizationStatus = pnpState.modelDefinitionWithSource.synchronizationStatus;
|
||||
const isModelDefinitionLoading = modelDefinitionSynchronizationStatus === SynchronizationStatus.working;
|
||||
const twin = pnpState.twin.payload;
|
||||
const modelId = twin?.modelId;
|
||||
|
||||
const renderModelDefinition = () => {
|
||||
if (isModelDefinitionLoading) {
|
||||
return <MultiLineShimmer/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{modelDefinitionWithSource ?
|
||||
<>
|
||||
<div className="step-two">
|
||||
<h4>{t(ResourceKeys.digitalTwin.steps.secondSuccess)}</h4>
|
||||
<ModelDefinitionSourceView
|
||||
source={modelDefinitionWithSource.source}
|
||||
/>
|
||||
</div>
|
||||
<div className="step-three">
|
||||
{modelDefinitionWithSource.isModelValid ?
|
||||
<DigitalTwinComponentList/> :
|
||||
<>
|
||||
<MessageBar messageBarType={MessageBarType.error}>
|
||||
{t(ResourceKeys.deviceInterfaces.interfaceNotValid)}
|
||||
</MessageBar>
|
||||
<JSONEditor
|
||||
className="interface-definition-json-editor"
|
||||
content={JSON.stringify(modelDefinitionWithSource.modelDefinition, null, '\t')}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</> :
|
||||
<div className="step-two">
|
||||
<h4>{t(ResourceKeys.digitalTwin.steps.secondFailure, {modelId})}</h4>
|
||||
<InterfaceNotFoundMessageBar/>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return renderModelDefinition();
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import 'jest';
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { DigitalTwinModelId } from './digitalTwinModelId';
|
||||
import { pnpStateInitial, PnpStateInterface } from '../../state';
|
||||
import * as pnpStateContext from '../../../../shared/contexts/pnpStateContext';
|
||||
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
|
||||
|
||||
const pathname = 'resources/TestHub.azure-devices.net/devices/deviceDetail/ioTPlugAndPlay/?deviceId=testDevice';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: () => ({ push: jest.fn()}),
|
||||
useLocation: () => ({ search: '?deviceId=testDevice' }),
|
||||
useRouteMatch: () => ({ url: pathname })
|
||||
}));
|
||||
|
||||
describe('DigitalTwinModelId', () => {
|
||||
it('matches snapshot when empty model id is retrieved', () => {
|
||||
const initialState: PnpStateInterface = pnpStateInitial().merge({
|
||||
twin: {
|
||||
payload: {
|
||||
deviceId: 'testDevice',
|
||||
moduleId: ''
|
||||
} as any,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
}
|
||||
});
|
||||
|
||||
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: initialState, dispatch: jest.fn()});
|
||||
|
||||
const wrapper = shallow(<DigitalTwinModelId/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when model id is retrieved', () => {
|
||||
const initialState: PnpStateInterface = pnpStateInitial().merge({
|
||||
twin: {
|
||||
payload: {
|
||||
deviceId: 'testDevice',
|
||||
moduleId: 'moduleId'
|
||||
} as any,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
}
|
||||
});
|
||||
|
||||
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: initialState, dispatch: jest.fn()});
|
||||
|
||||
const wrapper = shallow(<DigitalTwinModelId/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { ResourceKeys } from '../../../../../localization/resourceKeys';
|
||||
import { usePnpStateContext } from '../../../../shared/contexts/pnpStateContext';
|
||||
import './digitalTwinDetail.scss';
|
||||
import { MaskedCopyableTextField } from '../../../../shared/components/maskedCopyableTextField';
|
||||
import { getModuleIdentityIdFromQueryString } from '../../../../shared/utils/queryStringHelper';
|
||||
|
||||
export const DigitalTwinModelId: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { search } = useLocation();
|
||||
const { pnpState } = usePnpStateContext();
|
||||
const twin = pnpState.twin.payload;
|
||||
const modelId = twin?.modelId;
|
||||
const moduleId = getModuleIdentityIdFromQueryString(search);
|
||||
|
||||
return (
|
||||
<div className="step-one">
|
||||
<h4>{moduleId ? t(ResourceKeys.digitalTwin.steps.firstModule) : t(ResourceKeys.digitalTwin.steps.first)}</h4>
|
||||
<MaskedCopyableTextField
|
||||
ariaLabel={t(ResourceKeys.digitalTwin.modelId)}
|
||||
label={t(ResourceKeys.digitalTwin.modelId)}
|
||||
value={modelId}
|
||||
allowMask={false}
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,120 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import 'jest';
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { DigitaltwinPnpConfigurationSteps } from './digitaltwinPnpConfigurationSteps';
|
||||
import { pnpStateInitial, PnpStateInterface } from '../../state';
|
||||
import * as pnpStateContext from '../../../../shared/contexts/pnpStateContext';
|
||||
import { SynchronizationStatus } from '../../../../api/models/synchronizationStatus';
|
||||
|
||||
const interfaceId = 'urn:azureiot:samplemodel;1';
|
||||
|
||||
const pathname = 'resources/TestHub.azure-devices.net/devices/deviceDetail/ioTPlugAndPlay/?deviceId=testDevice';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: () => ({ push: jest.fn()}),
|
||||
useLocation: () => ({ search: '?deviceId=testDevice' }),
|
||||
useRouteMatch: () => ({ url: pathname })
|
||||
}));
|
||||
|
||||
describe('DigitaltwinPnpConfigurationSteps', () => {
|
||||
it('matches snapshot when there is empty model id', () => {
|
||||
/* tslint:disable */
|
||||
const deviceTwin: any = {
|
||||
"deviceId": "testDevice",
|
||||
"modelId": "",
|
||||
"properties" : {
|
||||
desired: {
|
||||
environmentalSensor: {
|
||||
brightness: 456,
|
||||
__t: 'c'
|
||||
}
|
||||
},
|
||||
reported: {
|
||||
environmentalSensor: {
|
||||
brightness: {
|
||||
value: 123,
|
||||
dv: 2
|
||||
},
|
||||
__t: 'c'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
/* tslint:enable */
|
||||
const initialState: PnpStateInterface = pnpStateInitial().merge({
|
||||
twin: {
|
||||
payload: deviceTwin,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
},
|
||||
modelDefinitionWithSource: {
|
||||
payload: null,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
}
|
||||
});
|
||||
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: initialState, dispatch: jest.fn()});
|
||||
|
||||
const wrapper = shallow(<DigitaltwinPnpConfigurationSteps/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when there is model id', () => {
|
||||
/* tslint:disable */
|
||||
const deviceTwin: any = {
|
||||
"deviceId": "testDevice",
|
||||
"modelId": "modelId",
|
||||
"properties" : {
|
||||
desired: {
|
||||
environmentalSensor: {
|
||||
brightness: 456,
|
||||
__t: 'c'
|
||||
}
|
||||
},
|
||||
reported: {
|
||||
environmentalSensor: {
|
||||
brightness: {
|
||||
value: 123,
|
||||
dv: 2
|
||||
},
|
||||
__t: 'c'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
/* tslint:enable */
|
||||
const initialState: PnpStateInterface = pnpStateInitial().merge({
|
||||
twin: {
|
||||
payload: deviceTwin,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
},
|
||||
modelDefinitionWithSource: {
|
||||
payload: null,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
}
|
||||
});
|
||||
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: initialState, dispatch: jest.fn()});
|
||||
|
||||
const wrapper = shallow(<DigitaltwinPnpConfigurationSteps/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot when empty model id is retrieved', () => {
|
||||
const initialState: PnpStateInterface = pnpStateInitial().merge({
|
||||
twin: {
|
||||
payload: {
|
||||
deviceId: 'testDevice',
|
||||
moduleId: ''
|
||||
} as any,
|
||||
synchronizationStatus: SynchronizationStatus.fetched
|
||||
}
|
||||
});
|
||||
|
||||
jest.spyOn(pnpStateContext, 'usePnpStateContext').mockReturnValue({ pnpState: initialState, dispatch: jest.fn()});
|
||||
|
||||
const wrapper = shallow(<DigitaltwinPnpConfigurationSteps/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Link } from '@fluentui/react';
|
||||
import { ResourceKeys } from '../../../../../localization/resourceKeys';
|
||||
import { usePnpStateContext } from '../../../../shared/contexts/pnpStateContext';
|
||||
import './digitalTwinDetail.scss';
|
||||
import { getModuleIdentityIdFromQueryString } from '../../../../shared/utils/queryStringHelper';
|
||||
import { DigitalTwinModelId } from './digitalTwinModelId';
|
||||
import { DigitalTwinModelDefinition } from './digitalTwinModelDefinition';
|
||||
|
||||
export const DigitaltwinPnpConfigurationSteps: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { search } = useLocation();
|
||||
const { pnpState } = usePnpStateContext();
|
||||
const twin = pnpState.twin.payload;
|
||||
const modelId = twin?.modelId;
|
||||
const moduleId = getModuleIdentityIdFromQueryString(search);
|
||||
|
||||
return (
|
||||
<section className="device-detail">
|
||||
<div className="digitalTwin-steps">
|
||||
{modelId ?
|
||||
<>
|
||||
<DigitalTwinModelId/>
|
||||
<DigitalTwinModelDefinition/>
|
||||
</> :
|
||||
<>
|
||||
<span>{moduleId ? t(ResourceKeys.digitalTwin.steps.zeroModule) : t(ResourceKeys.digitalTwin.steps.zero)}</span>
|
||||
<Link
|
||||
href={t(ResourceKeys.settings.questions.questions.documentation.link)}
|
||||
target="_blank"
|
||||
>
|
||||
{t(ResourceKeys.settings.questions.questions.documentation.text)}
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
|
@ -5,7 +5,7 @@ exports[`deviceInterfaces shows Shimmer when status is working 1`] = `<MultiLine
|
|||
exports[`deviceInterfaces shows interface information when status is fetched 1`] = `
|
||||
<Fragment>
|
||||
<StyledCommandBarBase
|
||||
className="command"
|
||||
className="command deviceInterface-command"
|
||||
farItems={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -39,33 +39,40 @@ exports[`deviceInterfaces shows interface information when status is fetched 1`]
|
|||
<section
|
||||
className="pnp-interface-info scrollable-lg"
|
||||
>
|
||||
<ModelDefinitionSourceView
|
||||
source="PUBLIC"
|
||||
/>
|
||||
<MaskedCopyableTextField
|
||||
allowMask={false}
|
||||
ariaLabel="deviceInterfaces.columns.id"
|
||||
label="deviceInterfaces.columns.id"
|
||||
readOnly={true}
|
||||
value={null}
|
||||
/>
|
||||
<MaskedCopyableTextField
|
||||
allowMask={false}
|
||||
ariaLabel="deviceInterfaces.columns.displayName"
|
||||
label="deviceInterfaces.columns.displayName"
|
||||
readOnly={true}
|
||||
value="--"
|
||||
/>
|
||||
<MaskedCopyableTextField
|
||||
allowMask={false}
|
||||
ariaLabel="deviceInterfaces.columns.description"
|
||||
label="deviceInterfaces.columns.description"
|
||||
readOnly={true}
|
||||
value="--"
|
||||
/>
|
||||
<JSONEditor
|
||||
className="interface-definition-json-editor"
|
||||
content="{
|
||||
<div
|
||||
className="pnp-interface-info-detail"
|
||||
>
|
||||
<ModelDefinitionSourceView
|
||||
source="PUBLIC"
|
||||
/>
|
||||
<MaskedCopyableTextField
|
||||
allowMask={false}
|
||||
ariaLabel="deviceInterfaces.columns.id"
|
||||
label="deviceInterfaces.columns.id"
|
||||
readOnly={true}
|
||||
value={null}
|
||||
/>
|
||||
<MaskedCopyableTextField
|
||||
allowMask={false}
|
||||
ariaLabel="deviceInterfaces.columns.displayName"
|
||||
label="deviceInterfaces.columns.displayName"
|
||||
readOnly={true}
|
||||
value="--"
|
||||
/>
|
||||
<MaskedCopyableTextField
|
||||
allowMask={false}
|
||||
ariaLabel="deviceInterfaces.columns.description"
|
||||
label="deviceInterfaces.columns.description"
|
||||
readOnly={true}
|
||||
value="--"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="pnp-interface-info-editor"
|
||||
>
|
||||
<JSONEditor
|
||||
className="interface-definition-json-editor"
|
||||
content="{
|
||||
\\"@id\\": \\"urn:azureiot:ModelDiscovery:DigitalTwin:1\\",
|
||||
\\"@type\\": \\"Interface\\",
|
||||
\\"contents\\": [
|
||||
|
@ -101,7 +108,8 @@ exports[`deviceInterfaces shows interface information when status is fetched 1`]
|
|||
],
|
||||
\\"@context\\": \\"http://azureiot.com/v1/contexts/Interface.json\\"
|
||||
}"
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</ErrorBoundary>
|
||||
</Fragment>
|
||||
|
|
|
@ -1,277 +0,0 @@
|
|||
/***********************************************************
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License
|
||||
**********************************************************/
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Label, CommandBar, ICommandBarItemProps, DetailsList, IColumn, SelectionMode, MessageBar, MessageBarType, Announced, Pivot, PivotItem, Link } from '@fluentui/react';
|
||||
import { NavLink, useLocation, useRouteMatch } from 'react-router-dom';
|
||||
import { ROUTE_PARTS, ROUTE_PARAMS } from '../../../constants/routes';
|
||||
import { getDeviceIdFromQueryString, getModuleIdentityIdFromQueryString } from '../../../shared/utils/queryStringHelper';
|
||||
import { ResourceKeys } from '../../../../localization/resourceKeys';
|
||||
import { SynchronizationStatus } from '../../../api/models/synchronizationStatus';
|
||||
import { REFRESH } from '../../../constants/iconNames';
|
||||
import { LARGE_COLUMN_WIDTH } from '../../../constants/columnWidth';
|
||||
import { InterfaceNotFoundMessageBar } from '../../shared/components/interfaceNotFoundMessageBar';
|
||||
import { ModelDefinitionSourceView } from '../../shared/components/modelDefinitionSource';
|
||||
import { MaskedCopyableTextField } from '../../../shared/components/maskedCopyableTextField';
|
||||
import { JSONEditor } from '../../../shared/components/jsonEditor';
|
||||
import { HeaderView } from '../../../shared/components/headerView';
|
||||
import { MultiLineShimmer } from '../../../shared/components/multiLineShimmer';
|
||||
import { usePnpStateContext } from '../../../shared/contexts/pnpStateContext';
|
||||
import { ModelDefinition } from '../../../api/models/modelDefinition';
|
||||
import { ComponentAndInterfaceId, JsonSchemaAdaptor } from '../../../shared/utils/jsonSchemaAdaptor';
|
||||
import { DEFAULT_COMPONENT_FOR_DIGITAL_TWIN } from '../../../constants/devices';
|
||||
import { ErrorBoundary } from '../../shared/components/errorBoundary';
|
||||
import '../../../css/_digitalTwinInterfaces.scss';
|
||||
import { dispatchGetTwinAction } from '../utils';
|
||||
import { AppInsightsClient } from '../../../shared/appTelemetry/appInsightsClient';
|
||||
import { TELEMETRY_PAGE_NAMES } from '../../../../app/constants/telemetry';
|
||||
|
||||
interface ModelContent {
|
||||
link: string;
|
||||
componentName: string;
|
||||
interfaceId: string;
|
||||
}
|
||||
|
||||
const getComponentNameAndInterfaceIdArray = (modelDefinition: ModelDefinition): ComponentAndInterfaceId[] => {
|
||||
if (!modelDefinition) {
|
||||
return [];
|
||||
}
|
||||
const jsonSchemaAdaptor = new JsonSchemaAdaptor(modelDefinition);
|
||||
const components = jsonSchemaAdaptor.getComponentNameAndInterfaceIdArray();
|
||||
// check if model contains no-component items
|
||||
if (jsonSchemaAdaptor.getNonWritableProperties().length +
|
||||
jsonSchemaAdaptor.getWritableProperties().length +
|
||||
jsonSchemaAdaptor.getCommands().length +
|
||||
jsonSchemaAdaptor.getTelemetry().length > 0) {
|
||||
components.unshift({
|
||||
componentName: DEFAULT_COMPONENT_FOR_DIGITAL_TWIN,
|
||||
interfaceId: modelDefinition['@id']
|
||||
});
|
||||
}
|
||||
return components;
|
||||
};
|
||||
|
||||
// tslint:disable-next-line: cyclomatic-complexity
|
||||
export const DigitalTwinInterfacesList: React.FC = () => {
|
||||
const { search } = useLocation();
|
||||
const { url } = useRouteMatch();
|
||||
const deviceId = getDeviceIdFromQueryString(search);
|
||||
const moduleId = getModuleIdentityIdFromQueryString(search);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { pnpState, dispatch, } = usePnpStateContext();
|
||||
const modelDefinitionWithSource = pnpState.modelDefinitionWithSource.payload;
|
||||
const modelDefinition = modelDefinitionWithSource && modelDefinitionWithSource.modelDefinition;
|
||||
const twin = pnpState.twin.payload;
|
||||
const twinSynchronizationStatus = pnpState.twin.synchronizationStatus;
|
||||
const modelDefinitionSynchronizationStatus = pnpState.modelDefinitionWithSource.synchronizationStatus;
|
||||
const isModelDefinitionLoading = modelDefinitionSynchronizationStatus === SynchronizationStatus.working;
|
||||
const isTwinLoading = twinSynchronizationStatus === SynchronizationStatus.working;
|
||||
const modelId = twin?.modelId;
|
||||
const componentNameToIds = getComponentNameAndInterfaceIdArray(modelDefinition);
|
||||
|
||||
const onRefresh = (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
|
||||
dispatchGetTwinAction(search, dispatch);
|
||||
};
|
||||
|
||||
const modelContents: ModelContent[] = componentNameToIds && componentNameToIds.map(nameToId => {
|
||||
let link = `${url}${ROUTE_PARTS.DIGITAL_TWINS_DETAIL}/${ROUTE_PARTS.INTERFACES}/` +
|
||||
`?${ROUTE_PARAMS.DEVICE_ID}=${encodeURIComponent(deviceId)}` +
|
||||
`&${ROUTE_PARAMS.COMPONENT_NAME}=${nameToId.componentName}` +
|
||||
`&${ROUTE_PARAMS.INTERFACE_ID}=${nameToId.interfaceId}`;
|
||||
if (moduleId) {
|
||||
link += `&${ROUTE_PARAMS.MODULE_ID}=${moduleId}`;
|
||||
}
|
||||
|
||||
if (nameToId.componentName === DEFAULT_COMPONENT_FOR_DIGITAL_TWIN && nameToId.interfaceId === modelId) {
|
||||
return{
|
||||
componentName: t(ResourceKeys.digitalTwin.pivot.defaultComponent),
|
||||
interfaceId: nameToId.interfaceId,
|
||||
link
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
...nameToId,
|
||||
link
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const createCommandBarItems = (): ICommandBarItemProps[] => {
|
||||
return [
|
||||
{
|
||||
ariaLabel: t(ResourceKeys.deviceEvents.command.refresh),
|
||||
disabled: isTwinLoading,
|
||||
iconProps: {iconName: REFRESH},
|
||||
key: REFRESH,
|
||||
name: t(ResourceKeys.deviceEvents.command.refresh),
|
||||
onClick: onRefresh
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
const getColumns = (): IColumn[] => {
|
||||
return [
|
||||
{ fieldName: 'componentName', isMultiline: true, isResizable: true, key: 'name',
|
||||
maxWidth: LARGE_COLUMN_WIDTH, minWidth: 100, name: t(ResourceKeys.digitalTwin.componentName) },
|
||||
{ fieldName: 'interfaceId', isMultiline: true, isResizable: true, key: 'id',
|
||||
maxWidth: LARGE_COLUMN_WIDTH, minWidth: 100, name: t(ResourceKeys.digitalTwin.interfaceId)}
|
||||
];
|
||||
};
|
||||
|
||||
const renderItemColumn = () => (item: ModelContent, index: number, column: IColumn) => {
|
||||
switch (column.key) {
|
||||
case 'name':
|
||||
return (
|
||||
<NavLink key={column.key} to={item.link}>
|
||||
{item.componentName}
|
||||
</NavLink>
|
||||
);
|
||||
case 'id':
|
||||
return (
|
||||
<Label
|
||||
key={column.key}
|
||||
>
|
||||
{item.interfaceId}
|
||||
</Label>
|
||||
);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const renderComponentList = () => {
|
||||
const listView = (
|
||||
<>
|
||||
{
|
||||
modelContents.length !== 0 ?
|
||||
<div className="list-detail">
|
||||
<DetailsList
|
||||
onRenderItemColumn={renderItemColumn()}
|
||||
className="component-list"
|
||||
items={modelContents}
|
||||
columns={getColumns()}
|
||||
selectionMode={SelectionMode.none}
|
||||
/>
|
||||
</div> :
|
||||
<>
|
||||
<Label className="no-component">{t(ResourceKeys.digitalTwin.modelContainsNoComponents, {modelId })}</Label>
|
||||
<Announced
|
||||
message={t(ResourceKeys.digitalTwin.modelContainsNoComponents, { modelId })}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</>);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>{t(ResourceKeys.digitalTwin.steps.third)}</h4>
|
||||
<h5>{t(ResourceKeys.digitalTwin.steps.explanation, {modelId})}</h5>
|
||||
<Pivot aria-label={t(ResourceKeys.digitalTwin.pivot.ariaLabel)}>
|
||||
<PivotItem headerText={t(ResourceKeys.digitalTwin.pivot.components)}>
|
||||
<ErrorBoundary error={t(ResourceKeys.deviceInterfaces.interfaceListFailedToRender)}>
|
||||
{listView}
|
||||
</ErrorBoundary>
|
||||
</PivotItem>
|
||||
<PivotItem headerText={t(ResourceKeys.digitalTwin.pivot.content)} className="modelContent">
|
||||
<JSONEditor
|
||||
className="interface-definition-json-editor"
|
||||
content={JSON.stringify(modelDefinitionWithSource.modelDefinition, null, '\t')}
|
||||
/>
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderModelDefinition = () => {
|
||||
if (isModelDefinitionLoading) {
|
||||
return <MultiLineShimmer/>;
|
||||
}
|
||||
|
||||
if (!modelDefinitionWithSource) {
|
||||
return (
|
||||
<div className="step-two">
|
||||
<h4>{t(ResourceKeys.digitalTwin.steps.secondFailure, {modelId})}</h4>
|
||||
<InterfaceNotFoundMessageBar/>
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="step-two">
|
||||
<h4>{t(ResourceKeys.digitalTwin.steps.secondSuccess)}</h4>
|
||||
<ModelDefinitionSourceView
|
||||
source={modelDefinitionWithSource.source}
|
||||
/>
|
||||
</div>
|
||||
<div className="step-three">
|
||||
{modelDefinitionWithSource.isModelValid ?
|
||||
renderComponentList() :
|
||||
<>
|
||||
<MessageBar messageBarType={MessageBarType.error}>
|
||||
{t(ResourceKeys.deviceInterfaces.interfaceNotValid)}
|
||||
</MessageBar>
|
||||
<JSONEditor
|
||||
className="interface-definition-json-editor"
|
||||
content={JSON.stringify(modelDefinitionWithSource.modelDefinition, null, '\t')}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
AppInsightsClient.getInstance()?.trackPageView({name: TELEMETRY_PAGE_NAMES.PNP_HOME});
|
||||
}, []); // tslint:disable-line: align
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommandBar
|
||||
className="command"
|
||||
items={createCommandBarItems()}
|
||||
/>
|
||||
<HeaderView
|
||||
headerText={ResourceKeys.digitalTwin.headerText}
|
||||
link={ResourceKeys.settings.questions.questions.documentation.link}
|
||||
tooltip={ResourceKeys.settings.questions.questions.documentation.text}
|
||||
/>
|
||||
{isTwinLoading ?
|
||||
<MultiLineShimmer/> :
|
||||
<section className="device-detail">
|
||||
<div className="digitalTwin-steps">
|
||||
{modelId ?
|
||||
<>
|
||||
<div className="step-one">
|
||||
<h4>{moduleId ? t(ResourceKeys.digitalTwin.steps.firstModule) : t(ResourceKeys.digitalTwin.steps.first)}</h4>
|
||||
<MaskedCopyableTextField
|
||||
ariaLabel={t(ResourceKeys.digitalTwin.modelId)}
|
||||
label={t(ResourceKeys.digitalTwin.modelId)}
|
||||
value={modelId}
|
||||
allowMask={false}
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
{renderModelDefinition()}
|
||||
</> :
|
||||
<>
|
||||
<span>{moduleId ? t(ResourceKeys.digitalTwin.steps.zeroModule) : t(ResourceKeys.digitalTwin.steps.zero)}</span>
|
||||
<Link
|
||||
href={t(ResourceKeys.settings.questions.questions.documentation.link)}
|
||||
target="_blank"
|
||||
>
|
||||
{t(ResourceKeys.settings.questions.questions.documentation.text)}
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -8,7 +8,7 @@ import { ROUTE_PARTS } from '../../../constants/routes';
|
|||
import { getDeviceIdFromQueryString, getInterfaceIdFromQueryString, getComponentNameFromQueryString, getModuleIdentityIdFromQueryString } from '../../../shared/utils/queryStringHelper';
|
||||
import { getModelDefinitionAction } from '../actions';
|
||||
import { PnpStateContextProvider } from '../../../shared/contexts/pnpStateContext';
|
||||
import { DigitalTwinDetail } from './digitalTwinDetail';
|
||||
import { DigitalTwinDetail } from './deviceDigitalTwin/digitalTwinDetail';
|
||||
import { useAsyncSagaReducer } from '../../../shared/hooks/useAsyncSagaReducer';
|
||||
import { pnpReducer } from '../reducer';
|
||||
import { pnpSaga } from '../saga';
|
||||
|
@ -16,7 +16,7 @@ import { pnpStateInitial } from '../state';
|
|||
import { RepositoryLocationSettings } from '../../../shared/global/state';
|
||||
import { useGlobalStateContext } from '../../../shared/contexts/globalStateContext';
|
||||
import { getRepositoryLocationSettings } from '../../../modelRepository/dataHelper';
|
||||
import { DigitalTwinInterfacesList } from './digitalTwinInterfacesList';
|
||||
import { DigitalTwinInterfacesList } from './deviceDigitalTwin/digitalTwinInterfacesList';
|
||||
import { BreadcrumbRoute } from '../../../navigation/components/breadcrumbRoute';
|
||||
import '../../../css/_digitalTwinInterfaces.scss';
|
||||
import { dispatchGetTwinAction } from '../utils';
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
import { getDeviceIdFromQueryString, getModuleIdentityIdFromQueryString } from '../../shared/utils/queryStringHelper';
|
||||
import { ROUTE_PARAMS } from '../../constants/routes';
|
||||
import { getDeviceTwinAction, getModuleTwinAction } from './actions';
|
||||
import { ModelDefinition } from '../../api/models/modelDefinition';
|
||||
import { ComponentAndInterfaceId, JsonSchemaAdaptor } from '../../shared/utils/jsonSchemaAdaptor';
|
||||
import { DEFAULT_COMPONENT_FOR_DIGITAL_TWIN } from '../../constants/devices';
|
||||
|
||||
export const getBackUrl = (path: string, search: string) => {
|
||||
const deviceId = getDeviceIdFromQueryString(search);
|
||||
|
@ -26,3 +29,22 @@ export const dispatchGetTwinAction = (search: string, dispatch: (action: any) =>
|
|||
dispatch(getDeviceTwinAction.started(deviceId));
|
||||
}
|
||||
};
|
||||
|
||||
export const getComponentNameAndInterfaceIdArray = (modelDefinition: ModelDefinition): ComponentAndInterfaceId[] => {
|
||||
if (!modelDefinition) {
|
||||
return [];
|
||||
}
|
||||
const jsonSchemaAdaptor = new JsonSchemaAdaptor(modelDefinition);
|
||||
const components = jsonSchemaAdaptor.getComponentNameAndInterfaceIdArray();
|
||||
// check if model contains no-component items
|
||||
if (jsonSchemaAdaptor.getNonWritableProperties().length +
|
||||
jsonSchemaAdaptor.getWritableProperties().length +
|
||||
jsonSchemaAdaptor.getCommands().length +
|
||||
jsonSchemaAdaptor.getTelemetry().length > 0) {
|
||||
components.unshift({
|
||||
componentName: DEFAULT_COMPONENT_FOR_DIGITAL_TWIN,
|
||||
interfaceId: modelDefinition['@id']
|
||||
});
|
||||
}
|
||||
return components;
|
||||
};
|
||||
|
|
Загрузка…
Ссылка в новой задаче