зеркало из
1
0
Форкнуть 0
This commit is contained in:
Elsie4ever 2022-11-14 17:45:50 -08:00
Родитель efa07bddbe
Коммит 4252580e2f
27 изменённых файлов: 1027 добавлений и 470 удалений

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

@ -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;
};