This commit is contained in:
lucadruda 2022-08-11 16:02:27 +02:00
Родитель ea0efeac75
Коммит da500d2897
52 изменённых файлов: 7378 добавлений и 12170 удалений

4
.gitignore поставляемый
Просмотреть файл

@ -21,3 +21,7 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# env
.env

7
.prettierrc.json Normal file
Просмотреть файл

@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": true,
"jsxSingleQuote": true
}

102
README.md
Просмотреть файл

@ -1,76 +1,70 @@
# Iotc-migrator
# Getting Started with Create React App
A Companion Experience that enables you to move devices between Azure IoT Central applications or move devices from an Azure IoT Central application to an Azure IoT Hub.
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Requirement
## Available Scripts
1. An IoT Central Application
In the project directory, you can run:
> Go to [IoT Central](https://apps.azureiotcentral.com/home) to create one.
### `npm start`
2. Azure Active Directory Application (AAD).
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
> Go to [Azure Portal > AAD > App registration](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps) to create a new AAD Application and follow the [instruction below](#create-an-aad-application).
The page will reload when you make changes.\
You may also see any lint errors in the console.
3. An Azure IoT Hub instance
### `npm test`
> Go to [Azure Portal > IoT Hub](https://portal.azure.com/#create/Microsoft.IotHub) to create one IoT Hub.
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
4. An Azure IoT Hub Device Provisioning Services (DPS) associated to the IoT Hub instance
### `npm run build`
> Go to [Azure Portal > DPS](https://portal.azure.com/#create/Microsoft.IoTDeviceProvisioning) to create one DPS.
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
## Setup
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
Update the [config.ts](./src/config.ts) using the information from your AAD application.
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
```typescript
{
AADLoginServer: 'https://login.microsoftonline.com',
AADClientID: '<your-AAD-Application-(client)-ID>',
AADDirectoryID: '<your-AAD-Directory-(tenant)-ID>',
AADRedirectURI: 'http://localhost:3000',
applicationHost: '<your-iot-central-app>.azureiotcentral.com'
}
```
### `npm run eject`
> Make sure that the `AADRedirectURI` in the config file and the `Redirect URIs` specified in your AAD Application are the same.
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
## First run
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
You can run the SPA in the development mode using:
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
`npm start`
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
Open in the browser the `AADRedirectURI url` previously defined in your `config.ts` (by default is [http://localhost:3000](http://localhost:3000)) to view it in the browser.
Once the Application is loaded, follow the guidelines in the UI to perform a migration from the IoT Central application to Azure IoT Hub.
> NOTE: To perform successfully the migration, make sure that the _device template_ associated to your devices has a Command capability named __DeviceMove__.
### Create an AAD Application
1. Go to [Azure Portal > Azure Active Directory > App Registration](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps)
2. Click on _New Registration_
![New registration](/assets/registerApp.png)
3. Fill the form using making sure to put in the `Redirect URI` the same value defined in the `config.ts` under _AADRedirectURI_
![Create app](/assets/newApp.png)
4. Click _Register_ and once the app is created you will get the paramaters needed in the `config.ts`
![App created](/assets/appCreated.png)
### Codebase
Iotc-migrator is a React SPA application written in Typescript that runs 100% in the browser. It was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
The project consume the [IoT Central Rest APIs](https://docs.microsoft.com/rest/api/iotcentral/) with the following version: _1.1-preview_.
The Authentication is performed using [Microsoft Authentication Library (MSAL)](https://www.npmjs.com/package/msal).
### Learn More
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

14506
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -1,52 +1,17 @@
{
"name": "iotc-migrator",
"version": "1.0.0",
"name": "iotc-migrator-v2",
"version": "0.1.0",
"private": true,
"description": "A Companion Experience that enables you to move devices between Azure IoT Central applications or move devices from an Azure IoT Central application to an Azure IoT Hub",
"engines": {
"node": ">=16"
},
"author": "Azure IoT",
"license": "MIT",
"dependencies": {
"@azure/msal-browser": "^2.23.0",
"@fluentui/react": "^8.64.1",
"@microsoft/azure-iot-ux-fluent-controls": "^8.0.8",
"@microsoft/azure-iot-ux-fluent-css": "^8.1.8",
"axios": "^0.26.1",
"react": "16.x",
"react-dom": "16.x",
"react-hook-form": "^7.29.0",
"react-router-dom": "^6.3.0",
"util": "^0.12.4",
"uuidv4": "^6.2.13"
},
"devDependencies": {
"classnames": "^2.3.1",
"node-sass": "^7.0.1",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^12.1.4",
"@testing-library/user-event": "^13.5.0",
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2",
"@types/react-router-dom": "^5.1.7",
"@typescript-eslint/eslint-plugin": "^5.19.0",
"@typescript-eslint/parser": "^5.19.0",
"eslint": "^8.13.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-etc": "^2.0.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.4.0",
"react-scripts": "^5.0.1",
"typescript": "^4.2.3",
"@azure/msal-browser": "^2.26.0",
"@fluentui/react": "^8.77.0",
"@fluentui/react-hooks": "^8.6.0",
"crypto-js": "^4.1.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"uuid": "^8.3.2",
"web-vitals": "^2.1.4"
},
"peerDependencies": {
"react": "16.x",
"react-dom": "16.x"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
@ -70,5 +35,18 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@svgr/webpack": "^6.3.1",
"@types/axios": "^0.14.0",
"@types/crypto-js": "^4.1.1",
"@types/uuid": "^8.3.4",
"eslint": "^8.21.0",
"prettier": "^2.7.1",
"react-scripts": "^5.0.1",
"typescript": "^4.7.4"
},
"overrides": {
"@svgr/webpack": "$@svgr/webpack"
}
}

Двоичные данные
public/favicon.ico

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 15 KiB

После

Ширина:  |  Высота:  |  Размер: 3.8 KiB

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

@ -1,15 +1,15 @@
<!DOCTYPE html>
<html lang="en" theme="light">
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="A Companion Experience that enables you to move devices between Azure IoT Central applications or move devices from an Azure IoT Central application to an Azure IoT Hub"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png">
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>IoTC Migrator</title>
<title>IoT Central device migration tool</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

Двоичные данные
public/logo192.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 5.2 KiB

Двоичные данные
public/logo512.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 9.4 KiB

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

@ -1,11 +1,21 @@
{
"short_name": "iotc migrator",
"name": "iotc migrator",
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",

108
src/App.css Normal file
Просмотреть файл

@ -0,0 +1,108 @@
.container {
height: 100%;
}
.masthead {
height: 3rem;
background-color: #3c3c41;
color: white;
display: flex;
padding-left: 2rem;
padding-right: 2rem;
align-items: center;
justify-content: space-between;
}
.nav {
height: 100%;
}
.signOut {
cursor: pointer;
}
.content {
display: flex;
background-color: #f3f2f1;
height: 100%;
}
.page {
padding-left: 3rem;
padding-right: 3rem;
background-color: white;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
.formHeader {
margin-bottom: 2em;
}
.formInput {
width: 400px;
margin-bottom: 2em;
}
.spaced-right {
margin-right: 2em;
}
.spaced-left {
margin-left: 2em;
}
.center-vertical {
display: flex;
align-items: center;
}
.flex {
display: flex;
}
.flex.horizontal {
flex-direction: row;
}
.flex.vertical {
flex-direction: column;
}
.center-horizontal {
display: flex;
justify-content: center;
}
.section {
padding: 2em;
border: 1px solid gray;
width: fit-content;
margin-bottom: 2em;
}
.section > label {
margin-top: -3.5em;
text-align: center;
background-color: white;
}
.migration-arrow {
/* transform: scale(-2, -4) translate(0, -0.5em); */
font-size: x-large;
}
.migration-arrow-disabled {
/* transform: scale(-2, -4) translate(0, -0.5em); */
font-size: x-large;
color: gray;
}
.bottom-margin {
margin-bottom: 3em;
}
.padding1 {
padding: 1em;
}

145
src/App.tsx Normal file
Просмотреть файл

@ -0,0 +1,145 @@
import { AccountInfo } from '@azure/msal-browser'
import {
IButtonStyles,
Icon,
IconButton,
Modal,
ProgressIndicator,
} from '@fluentui/react'
import { useBoolean } from '@fluentui/react-hooks'
import React, { useCallback, useEffect, useState } from 'react'
import { login, loginSilent, logout } from './api'
import './App.css'
import { NavigatorItem, Navigator } from './navigation'
import MigrationStatus from './pages/migrationStatus'
import NewMigration from './pages/newMigration'
import { ApiError } from './types'
const iconButtonStyles: Partial<IButtonStyles> = {
root: {
color: 'black',
marginLeft: 'auto',
marginTop: '4px',
marginRight: '2px',
},
rootHovered: {
color: 'black',
},
}
export default function App() {
const [expanded, { toggle: toggleCollapse }] = useBoolean(true)
const [account, setAccount] = useState<AccountInfo | null>(null)
const [pageShown, setPageShown] = useState<'new' | 'status'>('new')
const [modalData, setModalData] = useState<{
isOpen: boolean
title: string
message: string
}>({ isOpen: false, title: '', message: '' })
const loginToAzure = React.useCallback(async () => {
let auth = await loginSilent()
if (!auth) {
auth = (await login()).account
}
setAccount(auth)
}, [])
const setErrorMessage = useCallback((err: ApiError) => {
setModalData({ isOpen: true, message: err.message, title: err.title })
}, [])
useEffect(() => {
if (!account) {
loginToAzure()
}
}, [loginToAzure, account])
return (
<div className='container'>
<div className='masthead'>
<h4>Azure IoT Device Migrator</h4>
<div className='center-vertical'>
{account && (
<>
<span className='spaced-right'>{account.name}</span>
<Icon
className='signOut'
iconName='SignOut'
onClick={async () => {
await logout()
}}
/>
</>
)}
</div>
</div>
<div className='content'>
<nav className={`nav`}>
<Navigator
expanded={expanded}
onItemSelected={(key) => {
setPageShown(key as any)
}}
>
<NavigatorItem
key='collapse'
title=''
icon={{ iconName: 'GlobalNavButton' }}
onClick={() => {
toggleCollapse()
}}
/>
<NavigatorItem
key='new'
title='New migration'
icon={{ iconName: 'TurnRight' }}
onClick={() => {}}
/>
<NavigatorItem
key='status'
title='Migration status'
icon={{ iconName: 'Sync' }}
onClick={() => {}}
/>
</Navigator>
</nav>
{account ? (
pageShown === 'new' ? (
<NewMigration setErrorMessage={setErrorMessage} />
) : (
<MigrationStatus setErrorMessage={setErrorMessage} />
)
) : (
<div className='page'>
<h3>Please wait</h3>
<ProgressIndicator label='Waiting for authentication' />
</div>
)}
<Modal
isOpen={modalData.isOpen}
onDismiss={() => {
setModalData((cur) => ({ ...cur, isOpen: false }))
}}
>
<div className='flex vertical spaced-left spaced-right padding1'>
<div className='center-vertical'>
<h3 className=''>{modalData.title}</h3>
<IconButton
iconProps={{ iconName: 'Cancel' }}
styles={iconButtonStyles}
ariaLabel='Close popup modal'
onClick={() =>
setModalData((cur) => ({
...cur,
isOpen: false,
}))
}
/>
</div>
<span>{modalData.message}</span>
</div>
</Modal>
</div>
</div>
)
}

359
src/api.ts Normal file
Просмотреть файл

@ -0,0 +1,359 @@
import {
AccountInfo,
LogLevel,
PublicClientApplication,
} from '@azure/msal-browser'
import {
API_VERSIONS,
TOKEN_AUDIENCES,
ApiError,
JOB_DESCRIPTION,
JobResult,
JobPayload,
} from './types'
import { v4 as uuid } from 'uuid'
export const msalConfig = {
auth: {
clientId: process.env['REACT_APP_AAD_APP_CLIENT_ID'] || '',
authority: `https://login.microsoftonline.com/${process.env['REACT_APP_AAD_APP_TENANT_ID']}`,
redirectUri: process.env['REACT_APP_AAD_APP_REDIRECT_URI'] || '',
},
cache: {
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: false,
},
system: {
loggerOptions: {
loggerCallback: (
level: LogLevel,
message: string,
containsPii: boolean
) => {
if (containsPii) {
return
}
switch (level) {
case LogLevel.Error:
console.error(message)
return
case LogLevel.Info:
console.info(message)
return
case LogLevel.Verbose:
console.debug(message)
return
case LogLevel.Warning:
console.warn(message)
return
}
},
},
},
}
const msalInstance = new PublicClientApplication(msalConfig)
const basicAuthParameters = {
scopes: ['user.read'],
extraScopesToConsent: ['https://management.azure.com/user_impersonation'],
}
let account: AccountInfo | null
function getAccount(): AccountInfo | null {
// need to call getAccount here?
const cache = msalInstance.getTokenCache()
const currentAccounts = msalInstance.getAllAccounts()
if (currentAccounts === null) {
console.log('No accounts detected')
return null
}
if (currentAccounts.length > 1) {
// Add choose account code here
console.log(
'Multiple accounts detected, need to add choose account code.'
)
return currentAccounts[0]
} else if (currentAccounts.length === 1) {
return currentAccounts[0]
} else {
return null
}
}
export async function loginSilent(): Promise<AccountInfo | null> {
if (!account) {
account = getAccount()
}
return account
}
export async function login(resource?: string) {
const auth = await msalInstance.loginPopup(
resource ? { scopes: [resource] } : basicAuthParameters
)
if (auth && auth.account) {
account = auth.account
} else {
account = getAccount()
}
return auth
}
export async function logout() {
await msalInstance.logoutPopup({
postLogoutRedirectUri: process.env['REACT_APP_AAD_APP_REDIRECT_URI'],
mainWindowRedirectUri: process.env['REACT_APP_AAD_APP_REDIRECT_URI'],
})
}
export async function getToken(resource: string) {
try {
if (!account) {
throw new Error('Account not found')
}
const authResult = await msalInstance.acquireTokenSilent({
scopes: [resource],
account,
})
return authResult.accessToken
} catch (e) {
console.log(e)
return (await login(resource)).accessToken
}
}
export async function getHubKeys(hubId: string, policyName: string) {
const armToken = await getToken(
'https://management.azure.com/user_impersonation'
)
const params = {
method: 'POST',
headers: {
Authorization: `Bearer ${armToken}`,
},
}
const hubResp = await fetch(
`https://management.azure.com${hubId}/IoTHubKeys/${policyName}/listkeys?api-version=${API_VERSIONS.IoTHubArm}`,
params
)
return hubResp.json()
}
export async function listDPSs() {
const armToken = await getToken(
'https://management.azure.com/user_impersonation'
)
const params = {
method: 'GET',
headers: {
Authorization: `Bearer ${armToken}`,
},
}
const subResp = await fetch(
`https://management.azure.com/subscriptions?api-version=${API_VERSIONS.ResourceManager}`,
params
)
const subs = (await subResp.json()).value
const resources = await Promise.all(
subs.map(async (sub: any) => {
const resResp = await fetch(
`https://management.azure.com/subscriptions/${sub.subscriptionId}/resources?api-version=${API_VERSIONS.ResourceManager}&$filter=resourceType eq 'Microsoft.Devices/provisioningServices'`,
params
)
const resources = (await resResp.json()).value
return resources.map((r: any) => ({
...r,
dpsLink: `https://portal.azure.com/#@${sub.tenantId}/resource${r.id}`,
}))
})
)
const data = await Promise.all(
resources.flat().map(async (res: any) => {
const resResp = await fetch(
`https://management.azure.com${res.id}?api-version=${API_VERSIONS.DPS}`,
params
)
const data = await resResp.json()
return { ...data, dpsLink: res.dpsLink }
})
)
return data
}
export async function listHubs() {
const armToken = await getToken(
'https://management.azure.com/user_impersonation'
)
const params = {
method: 'GET',
headers: {
Authorization: `Bearer ${armToken}`,
},
}
const subResp = await fetch(
`https://management.azure.com/subscriptions?api-version=${API_VERSIONS.ResourceManager}`,
params
)
const subs = (await subResp.json()).value
const resources = await Promise.all(
subs.map(async (sub: any) => {
const resResp = await fetch(
`https://management.azure.com/subscriptions/${sub.subscriptionId}/resources?api-version=${API_VERSIONS.ResourceManager}&$filter=resourceType eq 'Microsoft.Devices/IotHubs'`,
params
)
const resources = (await resResp.json()).value
return resources.map((r: any) => ({
...r,
hubLink: `https://portal.azure.com/#@${sub.tenantId}/resource${r.id}`,
}))
})
)
const data = await Promise.all(
resources.flat().map(async (res: any) => {
const resResp = await fetch(
`https://management.azure.com${res.id}?api-version=${API_VERSIONS.IoTHubArm}`,
params
)
const data = await resResp.json()
return { ...data, hubLink: res.hubLink }
})
)
return data
}
export async function listDevicesInHub(
hubHost: string,
sasToken: string
): Promise<any[]> {
const params = {
method: 'POST',
headers: {
Authorization: sasToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: 'select * from devices',
}),
}
const listResp = await fetch(
`https://${hubHost}/devices/query?api-version=${API_VERSIONS.IoTHubData}`,
params
)
return listResp.json()
}
export async function listCentralApps() {
const armToken = await getToken(
'https://management.azure.com/user_impersonation'
)
const params = {
method: 'GET',
headers: {
Authorization: `Bearer ${armToken}`,
},
}
const subResp = await fetch(
`https://management.azure.com/subscriptions?api-version=${API_VERSIONS.ResourceManager}`,
params
)
const subs = (await subResp.json()).value
const resources = await Promise.all(
subs.map(async (sub: any) => {
const resResp = await fetch(
`https://management.azure.com/subscriptions/${sub.subscriptionId}/providers/Microsoft.IoTCentral/iotApps?api-version=2021-06-01`,
params
)
const resources = (await resResp.json()).value
return resources
})
)
return resources.flat()
}
export async function listDeviceGroups(appDomain: string) {
const centralToken = await getToken(TOKEN_AUDIENCES.Central)
const params = {
method: 'GET',
headers: {
Authorization: `Bearer ${centralToken}`,
},
}
const groups = await fetch(
`https://${appDomain}.azureiotcentral.com/api/deviceGroups?api-version=${API_VERSIONS.Central}`,
params
)
return (await groups.json()).value
}
export async function listJobs(appDomain: string) {
const centralToken = await getToken(TOKEN_AUDIENCES.Central)
const params = {
method: 'GET',
headers: {
Authorization: `Bearer ${centralToken}`,
},
}
const jobs = await fetch(
`https://${appDomain}.azureiotcentral.com/api/jobs?api-version=${API_VERSIONS.Central}`,
params
)
const res: JobResult[] = (await jobs.json()).value
return res.filter((r) => r.description === JOB_DESCRIPTION)
}
export async function listDeviceTemplates(appDomain: string) {
const centralToken = await getToken(TOKEN_AUDIENCES.Central)
const params = {
method: 'GET',
headers: {
Authorization: `Bearer ${centralToken}`,
},
}
const templates = await fetch(
`https://${appDomain}.azureiotcentral.com/api/deviceTemplates?api-version=${API_VERSIONS.Central}`,
params
)
return (await templates.json()).value
}
/**
*
* @param appDomain
* @param migrationData
* @returns migrationStatus
* @throws ApiError
*/
export async function createMigrationJob(
appDomain: string,
migrationData: JobPayload
) {
const centralToken = await getToken(TOKEN_AUDIENCES.Central)
const params = {
method: 'PUT',
body: JSON.stringify(migrationData),
headers: {
Authorization: `Bearer ${centralToken}`,
'Content-Type': 'application/json',
},
}
const job = await fetch(
`https://${appDomain}.azureiotcentral.com/api/jobs/${uuid()}?api-version=${
API_VERSIONS.Central
}`,
params
)
if (!job.ok) {
const errbody = await job.json()
let message = errbody.error.message
if (job.status === 422) {
//bad template
message = `${message}\n.Make sure you have the "DeviceMigration" component in your model definition.\nCheck documentation for instructions.`
}
throw new ApiError(errbody.error.code, message)
}
return job.json()
}

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

@ -1,8 +0,0 @@
export const Config = {
AADLoginServer: 'https://login.microsoftonline.com',
AADClientID: '<your-AAD-Application-(client)-ID>',
AADDirectoryID: '<your-AAD-Directory-(tenant)-ID>',
AADRedirectURI: 'http://localhost:3000',
applicationHost: '<your-iot-central-app>.azureiotcentral.com'
}

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

@ -1,74 +0,0 @@
import * as React from 'react';
import * as IotcAPI from '../iotcAPI';
import { AuthContextInterface } from './authContext';
export interface AppDataContextInterface {
appDataReady: boolean;
groups: {key: string, text: string}[];
templates: {key: string, text: string}[];
fetchAppData: (authContext: AuthContextInterface) => Promise<void>;
error: any;
}
export const AppDataContext = React.createContext<AppDataContextInterface>({} as AppDataContextInterface);
export function AppDataProvider({ children }: { children: any }) {
// turns the results array into an associative array with the id as key. Used for look ups
const getDeviceGroups = async (authContext: AuthContextInterface) => {
const res = await IotcAPI.getGroups(authContext);
return res.map(x => {
return {
key: x.id,
text: x.displayName
};
});
}
// turns the results array into an associative array with the id as key. Used for look ups
const getTemplates = async (authContext: AuthContextInterface) => {
const res = await IotcAPI.getTemplates(authContext);
return res.map(x => {
return {
key: x.id,
text: x.displayName
};
});
}
// this works because its single app
const fetchAppData = async (authContext: AuthContextInterface) => {
try {
const groups = await getDeviceGroups(authContext);
const templates = await getTemplates(authContext);
setAppState({
...appState,
groups: groups || [],
templates: templates || [],
appDataReady: true,
error: undefined
});
} catch (error) {
setAppState({
...appState,
appDataReady: false,
error,
});
}
}
const [appState, setAppState] = React.useState<AppDataContextInterface>({
appDataReady: false,
groups: [],
templates: [],
fetchAppData,
error: undefined
});
return (
<AppDataContext.Provider value={appState}>
{children}
</AppDataContext.Provider>
)
}

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

@ -1,165 +0,0 @@
import * as msal from '@azure/msal-browser';
import * as React from 'react';
import { Config } from '../config';
export interface AppConfig {
applicationId: string;
directoryId: string;
applicationHost: string;
}
export const Scopes = {
Graph: 'User.Read',
Central: 'https://apps.azureiotcentral.com/user_impersonation',
ARM: 'https://management.azure.com/user_impersonation'
}
export interface AuthContextInterface {
authenticated: boolean;
applicationHost: string;
loginAccount: msal.AccountInfo | undefined;
signIn: (silent: boolean) => Promise<void>;
signOut: () => Promise<void>;
getAccessToken: (authContext?: msal.AccountInfo, scope?: string) => Promise<string>;
error: any;
}
export const AuthContext = React.createContext<AuthContextInterface>({} as AuthContextInterface);
export function AuthProvider({ children }: { children: any }) {
const urlParams = new URLSearchParams(window.location.search);
const applicationHost = urlParams?.get?.('appHost');
const msalConfig = React.useMemo(() => {
return {
auth: {
clientId: Config.AADClientID,
authority: `${Config.AADLoginServer}/${Config.AADDirectoryID}`,
redirectUri: Config.AADRedirectURI
},
cache: {
cacheLocation: 'localStorage'
}
};
}, []);
const msalInstance = new msal.PublicClientApplication(msalConfig);
async function signIn(silent = false) {
if (state.authenticated) {
return;
}
if(Config.AADClientID === '<your-AAD-client-id>'){
setState({
...state,
loginAccount: undefined,
authenticated: true,
error: {
message: 'Please update the config object in src/config.ts'
}
});
return;
}
let loginAccount: msal.AuthenticationResult = {} as msal.AuthenticationResult;
// @returns Token response or null. If the return value is null, then no auth redirect was detected.
let res = await msalInstance.handleRedirectPromise();
try {
loginAccount = res
? (res as any).data.value[0]
: msalInstance.getAllAccounts()[0];
try {
res = await getAccessTokenForScope({ silentFail: silent, msalInstance, scope: Scopes.Graph, options: loginAccount ? { account: loginAccount } : null });
} catch (error) {
//swallow error and try with a IoTCentral scope
try {
res = await getAccessTokenForScope({ silentFail: silent, msalInstance, scope: Scopes.Central, options: loginAccount ? { account: loginAccount } : null });
} catch (error) {
//swallow error and try with a ARM scope
res = await getAccessTokenForScope({ silentFail: silent, msalInstance, scope: Scopes.ARM, options: loginAccount ? { account: loginAccount } : null });
}
}
// at this point we should have a token otherwise the catch will have thrown an error on the modal
msalInstance.setActiveAccount(res?.account as msal.AccountInfo);
setState({
...state,
loginAccount: res?.account as msal.AccountInfo,
authenticated: true,
error: undefined
});
} catch (err) {
setState({
...state,
error: err,
});
}
}
async function getAccessToken(authContext?: msal.AccountInfo | undefined, scope?: string) {
const res = await getAccessTokenForScope({ silentFail: true, msalInstance, scope: scope || Scopes.Central, options: { account: state.loginAccount || authContext } });
return res?.accessToken;
}
async function signOut() {
await msalInstance.logoutRedirect();
}
const [state, setState] = React.useState<AuthContextInterface>({
authenticated: false,
applicationHost: applicationHost || Config.applicationHost,
loginAccount: undefined,
signIn,
signOut,
getAccessToken,
error: undefined,
});
return (
<AuthContext.Provider value={state}>
{children}
</AuthContext.Provider>
)
}
interface AccessTokenForScope {
silentFail: boolean;
msalInstance: msal.PublicClientApplication;
scope: string;
options: any;
}
async function getAccessTokenForScope({ msalInstance, scope, options }: AccessTokenForScope): Promise<msal.AuthenticationResult | null> {
const tokenRequest = {
scopes: Array.isArray(scope) ? scope : [scope],
forceRefresh: false,
redirectUri: Config.AADRedirectURI,
...options,
};
try {
// try to get token silently if the user is already signed in
return await msalInstance.acquireTokenSilent(tokenRequest);
} catch (err) {
console.log('login error', err);
try {
// show the login popup if the user is not signed in
return await msalInstance.acquireTokenPopup(tokenRequest)
} catch (error: any) {
console.log('login error', error);
if (error.name === 'BrowserAuthError') {
return await msalInstance.acquireTokenPopup(tokenRequest);
} else {
throw error;
}
}
}
}

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

@ -1,58 +0,0 @@
import * as React from 'react';
import Modal from './modal';
interface ErrorBoundaryProperties {
children?: React.ReactNode;
/**
* A custom render function to display the service error.
* If not specified, defaults to `ErrorView`.
*/
render?: (err: any) => React.ReactNode;
/**
* Callback to do something when the error is caught.
*/
onDidCatch?: (err: any) => void;
}
interface State {
error?: any;
location?: string; // location href where the error occurs
}
/**
* Used to catch errors in a child component tree, log them, and display a fallback UI.
* https://reactjs.org/docs/error-boundaries.html
*/
export class ErrorBoundary extends React.Component<ErrorBoundaryProperties, State> {
constructor(props: ErrorBoundaryProperties) {
super(props);
this.state = { error: undefined };
}
static getDerivedStateFromError(error: Error) {
return { error, location: getLocation() };
}
componentDidCatch(error: Error) {
this.props.onDidCatch?.(error);
}
render() {
const { error, location } = this.state;
if (error && getLocation() === location) {
const { render } = this.props;
return render ? render(error) : <Modal error={error} />;
}
return this.props.children;
}
}
function getLocation() {
return typeof window !== 'undefined' ? window.location.href : '';
}

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

@ -1,14 +0,0 @@
.modal-content {
width: 600px;
padding: 20px;
}
.modal-title-container {
display: flex;
align-items: center;
flex-direction: row;
}
.modal-title {
flex-grow: 1;
}

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

@ -1,39 +0,0 @@
import React from 'react';
import classnames from 'classnames/bind';
import { IIconProps } from '@fluentui/react/lib/Icon';
import { IconButton, Modal as FluentModal } from '@fluentui/react';
const cx = classnames.bind(require('./modal.scss'));
export default React.memo(function Modal({ error, closeModal }: {
error: any;
closeModal?: () => void;
}): JSX.Element {
const cancelIcon: IIconProps = { iconName: 'Cancel' };
return <FluentModal
titleAriaId={'Error Modal'}
isOpen={!!error.title}
onDismiss={closeModal}
isBlocking={true}
>
<div className={cx('modal-content')}>
<div className={cx('modal-title-container')}>
<h3 className={cx('modal-title')} id={'Error Modal'}>{error.title}</h3>
{closeModal && <IconButton
iconProps={cancelIcon}
ariaLabel="Close popup modal"
onClick={closeModal}
/>}
</div>
<div className={cx('modal-message')}>
{error.message}
<br />
<span>Check the browser console for more details or refresh the page.</span>
</div>
</div>
</FluentModal>;
});

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

@ -1,24 +0,0 @@
import * as React from 'react';
const usePromise = () => {
const [loading, setLoading] = React.useState(false);
const [data, setData] = React.useState<any>(null);
const [error, setError] = React.useState<any>(null);
const callPromise = async ({ promiseFn }: { promiseFn: any }) => {
setLoading(true);
setData(null);
setError(null);
try {
const res = await promiseFn();
setData(res);
} catch (error) {
setError(error);
}
setLoading(false);
};
return [loading, data, error, callPromise];
};
export default usePromise

16
src/index.css Normal file
Просмотреть файл

@ -0,0 +1,16 @@
html,body,#root{
height: 100%;
}
body {
margin: 0;
font-family: "Segoe UI", "Frutiger", "Frutiger Linotype", "Dejavu Sans",
"Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

20
src/index.js Normal file
Просмотреть файл

@ -0,0 +1,20 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { initializeIcons } from '@fluentui/font-icons-mdl2';
initializeIcons();
const root = ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>
, document.getElementById('root'));
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

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

@ -1,15 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Roboto', 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
h1 {
font-weight: 600 !important;
font-size: 1.7rem !important;
}

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

@ -1,28 +0,0 @@
import './index.scss';
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom'
import Shell from './shell/shell';
import { AuthProvider } from './context/authContext';
import { AppDataProvider } from './context/appDataContext';
import { initializeIcons } from '@fluentui/react/lib/Icons';
import { ErrorBoundary } from './controls/errorBoundary';
initializeIcons();
ReactDOM.render(
<React.StrictMode>
<Router>
<AuthProvider>
<AppDataProvider>
<ErrorBoundary>
<Shell />
</ErrorBoundary>
</AppDataProvider>
</AuthProvider>
</Router>
</React.StrictMode>,
document.getElementById('root')
);

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

@ -1,207 +0,0 @@
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
import { AuthContextInterface, Scopes } from './context/authContext';
interface IOTCentralResponse<T> {
data: {
value: T[];
};
}
export interface DeviceGroup {
id: string;
displayName: string;
organizations: string[];
}
export interface DeviceTemplate {
id: string;
types: string[];
capabilityModel: Object;
solutionModel: Object;
displayName: string;
etag: string;
}
export interface Job { }
export interface DPS {
subscriptionId: string;
}
export interface AppSubs {
subscriptionId: string;
displayName: string;
}
export async function getGroups(authContext: AuthContextInterface): Promise<DeviceGroup[]> {
try {
const token = await authContext.getAccessToken(authContext.loginAccount, Scopes.Central);
const res: IOTCentralResponse<DeviceGroup> = await axios(`https://${authContext.applicationHost}/api/preview/deviceGroups`, { headers: { 'Authorization': 'Bearer ' + token } });
return res.data.value;
} catch (error) {
throw error;
}
}
export async function getTemplates(authContext: AuthContextInterface): Promise<DeviceTemplate[]> {
try {
const token = await authContext.getAccessToken(authContext.loginAccount, Scopes.Central);
const res: IOTCentralResponse<DeviceTemplate> = await axios(`https://${authContext.applicationHost}/api/preview/deviceTemplates`, { headers: { 'Authorization': 'Bearer ' + token } });
return res.data.value;
} catch (error) {
throw error;
}
}
export interface JobPayload {
group: string;
migrationName: string;
migrationOption: 'App' | 'Hub';
target: string;
template: boolean;
}
export async function postJob(authContext: AuthContextInterface, payload: JobPayload): Promise<Job> {
try {
const token = await authContext.getAccessToken(authContext.loginAccount, Scopes.Central);
const res: IOTCentralResponse<Job> = await axios.put(`https://${authContext.applicationHost}/api/jobs/${uuidv4()}?api-version=1.1-preview`,
{
displayName: payload.migrationName,
description: 'AUTOMATED-DEVICE-MOVE',
group: payload.group,
data: [
{
type: 'command',
target: payload.template,
path: 'DeviceMove',
value: payload.target
}
]
},
{ headers: { 'Authorization': 'Bearer ' + token } }
);
return res.data;
} catch (error) {
throw error;
}
}
export interface Row {
key: string;
id: string;
name: string;
dgroup: string;
status: string;
}
export async function getJobs(authContext: AuthContextInterface) {
try {
const token = await authContext.getAccessToken();
const res = await axios(`https://${authContext.applicationHost}/api/preview/jobs`,
{
headers: { 'Authorization': 'Bearer ' + token }
});
const rows: Row[] = [];
for (const i in res.data.value) {
const job = res.data.value[i];
if (job.description && job.description.startsWith('AUTOMATED-DEVICE-MOVE')) {
rows.push({
key: i,
id: job.id,
name: job.displayName,
dgroup: job.group,
status: job.status
})
}
}
return rows;
} catch (error) {
throw error;
}
}
interface App {
key: string;
text: string;
app: Object;
sub: Object;
}
export async function getDPS(authContext: AuthContextInterface) {
try {
const armToken = await authContext.getAccessToken(authContext.loginAccount, Scopes.ARM);
let apps: App[] = [];
const res: IOTCentralResponse<DPS> = await axios.get(`https://management.azure.com/subscriptions?api-version=2020-01-01`,
{
headers: { Authorization: 'Bearer ' + armToken }
});
const subs = res.data.value;
if (!subs || subs.length === 0) {
return apps;
}
for (const i in subs) {
const sub = subs[i];
const subscriptionResponse = await axios.get(`https://management.azure.com/subscriptions/${sub.subscriptionId}/providers/Microsoft.Devices/provisioningServices?api-version=2018-01-22`,
{
headers: { Authorization: 'Bearer ' + armToken }
});
for (const i in subscriptionResponse.data.value) {
const app = subscriptionResponse.data.value[i];
apps.push({
key: app.properties.idScope,
text: app.properties.serviceOperationsHostName,
app,
sub
});
}
return apps;
}
} catch (error) {
throw error;
}
}
export async function getSubscriptionsApps(authContext: AuthContextInterface) {
try {
const armToken = await authContext.getAccessToken(authContext.loginAccount, Scopes.ARM);
let apps: App[] = [];
const response: IOTCentralResponse<AppSubs> = await axios.get(`https://management.azure.com/subscriptions?api-version=2020-01-01`,
{
headers: { Authorization: 'Bearer ' + armToken }
});
const subs = response.data.value;
if (!subs || subs.length === 0) {
return apps;
}
for (const i in subs) {
const sub = subs[i];
const subscriptionResponse = await axios.get(`https://management.azure.com/subscriptions/${sub.subscriptionId}/providers/Microsoft.IoTCentral/IoTApps?api-version=2018-09-01`,
{
headers: { Authorization: 'Bearer ' + armToken }
});
for (const i in subscriptionResponse.data.value) {
const app = subscriptionResponse.data.value[i];
apps.push({
key: app.properties.applicationId,
text: `(${sub.displayName}) - ${app.properties.displayName}`,
app,
sub
})
}
return apps;
}
} catch (error) {
throw error;
}
}

1
src/logo.svg Normal file
Просмотреть файл

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

После

Ширина:  |  Высота:  |  Размер: 2.6 KiB

115
src/navigation.tsx Normal file
Просмотреть файл

@ -0,0 +1,115 @@
import { FontWeights, Icon, IIconProps } from '@fluentui/react'
import { useBoolean } from '@fluentui/react-hooks'
import React, { MouseEventHandler, useState } from 'react'
export interface NavigatorItemProps {
icon?: IIconProps
key: string
title: string
text?: string
onClick: MouseEventHandler<HTMLLIElement>
}
const NavigatorItemStyle = {
li: {
listStyleType: 'none',
paddingTop: 3,
paddingBottom: 3,
paddingLeft: '1em',
height: 40,
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
},
selected: {
backgroundColor: 'white',
fontWeight: FontWeights.semibold,
},
icon: {
paddingRight: 15,
fontSize: 16,
},
text: {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
} as React.CSSProperties,
}
export const NavigatorItem: React.FC<NavigatorItemProps> = () => null
const NavigatorItemInternal: React.FC<
NavigatorItemProps & { selected: boolean }
> = ({ icon, title, onClick, selected }) => {
const [hover, { toggle: toggleHover }] = useBoolean(false)
return (
<li
style={{
...NavigatorItemStyle.li,
...(selected ? NavigatorItemStyle.selected : {}),
...(hover ? { backgroundColor: '#dadada' } : {}),
}}
onClick={onClick}
onMouseEnter={toggleHover}
onMouseLeave={toggleHover}
>
{icon && <Icon style={NavigatorItemStyle.icon} {...icon} />}
<span style={NavigatorItemStyle.text}>{title}</span>
</li>
)
}
const NavigatorStyle = {
expanded: {
width: 200,
height: '100%',
paddingLeft: 0,
margin: 0,
transition: 'width 0.2s',
},
collapsed: {
width: '3rem',
height: '100%',
paddingLeft: 0,
margin: 0,
transition: 'width 0.2s',
},
}
interface INavigatorProps {
children:
| React.ReactElement<NavigatorItemProps>
| React.ReactElement<NavigatorItemProps>[]
onItemSelected: (itemKey: string) => void
expanded?: boolean
}
export const Navigator = ({
children,
onItemSelected,
expanded = true,
}: INavigatorProps) => {
const [selected, setSelected] = useState(1)
return (
<ul style={NavigatorStyle[expanded ? 'expanded' : 'collapsed']}>
{Array.isArray(children)
? children.map((item, idx) => {
const { onClick: onItemClick, ...otherProps } = item.props
return (
<NavigatorItemInternal
{...otherProps}
onClick={(e) => {
if (idx !== 0) {
setSelected(idx)
}
onItemClick(e)
onItemSelected(item.key as any)
}}
selected={selected === idx}
key={`navitem-${idx}`}
/>
)
})
: children}
</ul>
)
}

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

@ -0,0 +1,121 @@
import {
DetailsList,
IColumn,
IconButton,
SelectionMode,
ShimmeredDetailsList,
} from '@fluentui/react'
import React, { useCallback, useEffect, useState } from 'react'
import { listCentralApps, listJobs } from '../api'
import { ApiError, JobResult } from '../types'
const columns: IColumn[] = [
{
key: 'appname',
name: 'Application',
fieldName: 'appName',
minWidth: 16,
maxWidth: 200,
},
{
key: 'jobName',
name: 'Migration Job',
fieldName: 'displayName',
maxWidth: 200,
minWidth: 16,
},
{
key: 'groupName',
name: 'DeviceGroup',
fieldName: 'group',
maxWidth: 200,
minWidth: 16,
},
{
key: 'dpsName',
name: 'DPS',
maxWidth: 200,
minWidth: 16,
},
{
key: 'jobStatus',
name: 'Migration Status',
fieldName: 'status',
maxWidth: 200,
minWidth: 16,
},
]
function _onRenderItemColumn(
item?: JobResult,
index?: number,
column?: IColumn
) {
const fieldContent = item?.[column?.fieldName as keyof JobResult] as string
switch (column?.key) {
case 'jobName':
return (
<a href={item?.jobLink} target='_blank'>
{fieldContent}
</a>
)
case 'dpsName':
return (
<a href={item?.data[0].value.dpsId} target='_blank'>
{item?.data[0].value.dpsName}
</a>
)
default:
return <span>{fieldContent}</span>
}
}
export default React.memo<{ setErrorMessage: (err: ApiError) => void }>(() => {
const [items, setItems] = useState<JobResult[]>([])
const fetchJobs = useCallback(async () => {
const apps = await listCentralApps()
const jobs = await Promise.all(
apps.map(async (a) => {
const appJobs: JobResult[] = (
await listJobs(a.properties.subdomain)
)
.filter((job: JobResult) => job.id)
.map((job: JobResult) => ({
...job,
appName: a.name,
jobLink: `https://${a.properties.subdomain}.azureiotcentral.com/jobs/instances/${job.id}`,
}))
return appJobs
})
)
setItems(jobs.flat())
}, [])
useEffect(() => {
fetchJobs()
}, [])
return (
<div className='page'>
<h2>Migration status</h2>
<div className='formHeader center-vertical'>
<span>Watch all the IoT Central to IoT Hub migration processes.</span>
<IconButton
iconProps={{ iconName: 'Sync' }}
onClick={async () => {
setItems([])
await fetchJobs()
}}
/>
</div>
<ShimmeredDetailsList
columns={columns}
items={items}
enableShimmer={items.length === 0}
selectionMode={SelectionMode.none}
onRenderItemColumn={_onRenderItemColumn}
/>
</div>
)
})

767
src/pages/newMigration.tsx Normal file
Просмотреть файл

@ -0,0 +1,767 @@
import {
Checkbox,
ChoiceGroup,
Dropdown,
Icon,
IDropdownOption,
Label,
PrimaryButton,
Spinner,
SpinnerSize,
Stack,
TextField,
} from '@fluentui/react'
import { useId, useBoolean } from '@fluentui/react-hooks'
import React, { useState } from 'react'
import {
listCentralApps,
listDPSs,
listDeviceGroups,
listDeviceTemplates,
createMigrationJob,
getToken,
getHubKeys,
listHubs,
listDevicesInHub,
} from '../api'
import {
FormValues,
MigrationMode,
ServiceType,
ApiError,
JOB_DESCRIPTION,
TOKEN_AUDIENCES,
CentralSourceService,
DPSSourceService,
CentralSourceParams,
DPSSourceParams,
DPSTargetParams,
DPSTargetService,
RecursivePartial,
CentralTargetService,
} from '../types'
import { filterHubs, findComponent, generateSaSToken } from '../utils'
const modeOptions = [
{ key: MigrationMode.ToHub, text: 'IoT Central -> IoT Hub' },
{ key: MigrationMode.Central, text: 'IoT Central -> IoT Central' },
{ key: MigrationMode.FromHub, text: 'IoT Hub -> IoT Central' },
]
function _onDataLoading(text: string, loadingInstances: boolean) {
return (
<Stack
horizontal
verticalAlign='center'
horizontalAlign='space-between'
>
<span>{text}</span>
<Spinner
size={SpinnerSize.small}
style={{ visibility: loadingInstances ? 'visible' : 'hidden' }}
/>
</Stack>
)
}
const _onDropDownOpen = async (
fn: () => Promise<any[]>,
setData: (data: any) => void,
start: () => void,
stop: () => void
) => {
start()
const data = await fn()
stop()
setData(
data?.flat().map((r, idx) => ({
key: r.id || `opt-${idx}`,
text: r.name || r.displayName,
data: r,
}))
)
}
export default React.memo<{ setErrorMessage: (err: ApiError) => void }>(
({ setErrorMessage }) => {
const nameId = useId()
const modeId = useId()
const centralAppsId = useId()
const centralAppsId2 = useId()
const dpsId = useId()
const deviceGroupsId = useId()
const deviceTemplateId = useId()
const [values, setValues] = useState<FormValues>({
name: '',
mode: MigrationMode.ToHub,
source: {
type: ServiceType.Central,
id: '',
} as CentralSourceService,
target: { type: ServiceType.DPS, id: '' } as DPSTargetService,
})
const [submitting, { setTrue: startSubmit, setFalse: stopSubmit }] =
useBoolean(false)
//#region DROPDOWN ITEMS LOADING STATE
const [
loadingApps,
{ setTrue: startLoadingApps, setFalse: stopLoadingApps },
] = useBoolean(false)
const [
loadingDPSs,
{ setTrue: startLoadingDPSs, setFalse: stopLoadingDPSs },
] = useBoolean(false)
const [
loadingGroups,
{ setTrue: startLoadingGroups, setFalse: stopLoadingGroups },
] = useBoolean(false)
const [
loadingTemplates,
{ setTrue: startLoadingTemplates, setFalse: stopLoadingTemplates },
] = useBoolean(false)
const [
loadingHubs,
{ setTrue: startLoadingHubs, setFalse: stopLoadingHubs },
] = useBoolean(false)
//#endregion
//#region DROPDOWN ITEMS STATE
const [centralApps, setCentralApps] = React.useState<IDropdownOption[]>(
[]
)
const [DPSs, setDPSs] = React.useState<IDropdownOption[]>([])
const [deviceGroups, setDeviceGroups] = React.useState<
IDropdownOption[]
>([])
const [deviceTemplates, setDeviceTemplates] = React.useState<
IDropdownOption[]
>([])
//#endregion
//#region DATA LOADING CALLBACK
const onCentralAppLoading = React.useCallback(() => {
return _onDataLoading(
'Select an IoT Central application...',
loadingApps
)
}, [loadingApps])
const onDPSLoading = React.useCallback(() => {
return _onDataLoading('Select an IoT DPS instance...', loadingDPSs)
}, [loadingDPSs])
const onDeviceGroupLoading = React.useCallback(() => {
return _onDataLoading('Select a device group...', loadingGroups)
}, [loadingGroups])
const onTemplatesLoading = React.useCallback(() => {
return _onDataLoading(
'Select a device template...',
loadingTemplates
)
}, [loadingTemplates])
//#endregion
//#region DROPDOWNS IN OPENING STATE
const onCentralDropDownOpen = React.useCallback(async () => {
if (centralApps.length === 0) {
await _onDropDownOpen(
listCentralApps,
setCentralApps,
startLoadingApps,
stopLoadingApps
)
}
}, [centralApps, startLoadingApps, stopLoadingApps])
const onDpsDropDownOpen = React.useCallback(async () => {
if (DPSs.length === 0) {
await _onDropDownOpen(
listDPSs,
setDPSs,
startLoadingDPSs,
stopLoadingDPSs
)
}
}, [DPSs, startLoadingDPSs, stopLoadingDPSs])
const onDeviceGroupDropDownOpen = React.useCallback(async () => {
await _onDropDownOpen(
() => {
return listDeviceGroups(values.source.id)
},
setDeviceGroups,
startLoadingGroups,
stopLoadingGroups
)
}, [startLoadingGroups, stopLoadingGroups, values.source])
const onDeviceTemplateDropDownOpen = React.useCallback(async () => {
await _onDropDownOpen(
() => {
const appId =
values.mode === MigrationMode.ToHub
? values.source.id
: values.target.id
return listDeviceTemplates(appId)
},
setDeviceTemplates,
startLoadingTemplates,
stopLoadingTemplates
)
}, [startLoadingTemplates, stopLoadingTemplates, values])
//#endregion
//#region SET VALUES
const setCentralSourceService = React.useCallback(
(source: RecursivePartial<CentralSourceService>) => {
setValues((cur) => ({
...cur,
source: {
...cur.source,
...source,
type: ServiceType.Central,
params: {
...cur.source.params,
...source.params,
},
} as CentralSourceService,
}))
},
[setValues]
)
const setCentralTargetService = React.useCallback(
(target: RecursivePartial<CentralTargetService>) => {
setValues((cur) => ({
...cur,
target: {
...cur.target,
...target,
type: ServiceType.Central,
params: {
...cur.target.params,
...target.params,
},
} as CentralTargetService,
}))
},
[setValues]
)
const setDPSTargetService = React.useCallback(
(target: RecursivePartial<DPSTargetService>) => {
setValues((cur) => ({
...cur,
target: {
...cur.target,
...target,
type: ServiceType.DPS,
params: {
...cur.target.params,
...target.params,
},
} as DPSTargetService,
}))
},
[setValues]
)
const setDPSSourceService = React.useCallback(
(source: RecursivePartial<DPSSourceService>) => {
setValues((cur) => ({
...cur,
source: {
...cur.source,
...source,
type: ServiceType.DPS,
params: {
...cur.source.params,
...source.params,
},
} as DPSSourceService,
}))
},
[setValues]
)
const selectSourceHub = React.useCallback(
(hubIndex: number, selected: boolean) => {
setValues((cur) => {
const iothubs = (cur.source.params as DPSSourceParams)
.iothubs
return {
...cur,
type: ServiceType.Central,
source: {
...cur.source,
params: {
...cur.source.params,
iothubs: [
...iothubs.slice(0, hubIndex),
{ ...iothubs[hubIndex], selected },
...iothubs.slice(hubIndex + 1),
],
},
} as DPSSourceService,
}
})
},
[setValues]
)
//#endregion
//#region GENERAL CALLBACKS
const onDeviceTemplateSelected = React.useCallback(
async (template) => {
const { capabilityModel } = template
const migrationComponent = findComponent(
capabilityModel,
'dtmi:azureiot:DeviceMigration;1'
)
if (!migrationComponent) {
setErrorMessage(
new ApiError(
'Invalid device template',
'The selected device template does not contain the Device Migration component.\nPlease include the component as descrived in the documentation.'
)
)
} else {
setCentralSourceService({
params: {
deviceTemplateId: template['@id'],
componentName: migrationComponent.name,
},
})
}
},
[setErrorMessage, setCentralSourceService]
)
const onSourceDPSSelected = React.useCallback(
async (dpsData) => {
startLoadingHubs()
const availableHubs = await listHubs()
const associatedHubNames = dpsData.data.properties.iotHubs.map(
(h: any) => h.name
)
const associatedHubs = filterHubs(
availableHubs,
associatedHubNames
)
setDPSSourceService({
id: dpsData?.key as string,
params: {
iothubs: await Promise.all(
associatedHubs.map(async (h: any) => {
const hubKeys = await getHubKeys(
h.id,
'registryRead'
)
const sasToken = generateSaSToken(
h.properties.hostName,
hubKeys.primaryKey,
'registryRead'
)
return {
name: h.name,
host: h.properties.hostName,
id: h.id,
sasToken,
selected: false,
}
})
),
},
})
stopLoadingHubs()
},
[startLoadingHubs, stopLoadingHubs, setDPSSourceService]
)
const onSubmit = React.useCallback(async () => {
if (
values.source.type === ServiceType.Central &&
values.target.type === ServiceType.DPS
) {
// create migration job
try {
await createMigrationJob(values.source.id, {
displayName: values.name,
group: values.source.params.groupId,
description: JOB_DESCRIPTION,
data: [
{
type: 'command',
target: values.source.params.deviceTemplateId,
path: `${values.source.params.componentName}.DeviceMove`,
value: values.target.params,
},
],
})
} catch (err) {
setErrorMessage(err as ApiError)
}
} else if (
values.source.type === ServiceType.DPS &&
values.target.type === ServiceType.Central
) {
// list devices in selected hubs
const devices = (
await Promise.all(
values.source.params.iothubs
.filter((h) => h.selected)
.map(async (h) => {
return listDevicesInHub(h.host, h.sasToken)
})
)
).flat()
console.log(devices)
}
stopSubmit()
}, [values, setErrorMessage, stopSubmit])
//#endregion
return (
<div className='page'>
<h2>New Migration</h2>
<div className='formHeader'>
<span>
Move devices from an Azure IoT Central application to
another one or to an Azure IoT Hub and back.
</span>
</div>
<div className='formInput'>
<Label htmlFor={nameId} required>
Name
</Label>
<TextField
id={nameId}
name='name'
onChange={(_, name) =>
setValues((cur) => ({
...cur,
name: name!,
}))
}
disabled={submitting}
/>
</div>
<div className='formInput'>
<Label htmlFor={modeId}>Migration mode</Label>
<ChoiceGroup
id={modeId}
options={modeOptions}
selectedKey={values.mode}
onChange={(_, mode) =>
setValues((cur) => ({
...cur,
mode: MigrationMode[
mode?.key as keyof typeof MigrationMode
],
}))
}
disabled={submitting}
/>
</div>
<div className='center-vertical'>
<div className='spaced-right flex horizontal'>
{/** ----------------- SOURCE -------------------- */}
<div id='source' className='section'>
<Label htmlFor='source'>Source</Label>
{values.mode === MigrationMode.FromHub ? (
<>
<div className='formInput'>
<Label htmlFor={dpsId} required>
IoT DPS instance
</Label>
<Dropdown
id={dpsId}
options={DPSs}
onRenderPlaceholder={onDPSLoading}
disabled={submitting}
onClick={onDpsDropDownOpen}
onChange={async (_, val) => {
await onSourceDPSSelected(val)
}}
/>
</div>
{values.source.id &&
(values.source as DPSSourceService)
.params.iothubs && (
<Stack className='formInput'>
<Label>Associated Hubs</Label>
{(
values.source as DPSSourceService
).params.iothubs.map(
(iothub, idx) => (
<Checkbox
key={`hub-${idx}`}
label={iothub.name}
checked={
iothub.selected
}
onChange={(
_,
val
) =>
selectSourceHub(
idx,
val!
)
}
/>
)
)}
</Stack>
)}
{loadingHubs && (
<Spinner
size={SpinnerSize.medium}
className='spaced-left'
/>
)}
</>
) : (
<>
<div className='formInput'>
<Label htmlFor={centralAppsId} required>
IoT Central Application
</Label>
<Dropdown
id={centralAppsId}
options={centralApps}
disabled={submitting}
onRenderPlaceholder={
onCentralAppLoading
}
onClick={onCentralDropDownOpen}
onChange={(_, val) => {
setCentralTargetService({
id: val?.data.properties
.subdomain,
})
setDeviceGroups([])
setDeviceTemplates([])
}}
/>
</div>
<div className='formInput'>
<Label
htmlFor={deviceGroupsId}
required
>
Device group
</Label>
<Dropdown
id={deviceGroupsId}
options={deviceGroups}
onRenderPlaceholder={
onDeviceGroupLoading
}
onClick={async () => {
if (
values.source.id &&
!submitting
) {
await onDeviceGroupDropDownOpen()
}
}}
disabled={
!values.source.id || submitting
}
onChange={(_, val) => {
setCentralSourceService({
params: {
groupId: val?.data.id,
},
})
setDeviceTemplates([])
}}
/>
</div>
<div className='formInput'>
<Label
htmlFor={deviceTemplateId}
required
>
Filter by template
</Label>
<Dropdown
id={deviceTemplateId}
options={deviceTemplates}
onRenderPlaceholder={
onTemplatesLoading
}
onClick={async () => {
if (
values.source.id &&
!submitting
) {
await onDeviceTemplateDropDownOpen()
}
}}
disabled={
!values.source.id || submitting
}
onChange={(_, val) =>
onDeviceTemplateSelected(
val?.data
)
}
/>
</div>
</>
)}
</div>
<div className='center-vertical spaced-right spaced-left'>
<Icon
iconName='DoubleChevronRight'
className={`migration-arrow${
submitting ? '-disabled' : ''
}`}
/>
</div>
{/** ----------------- TARGET -------------------- */}
<div id='target' className='section'>
<Label htmlFor='target'>Target</Label>
{values.mode === MigrationMode.ToHub ? (
<div className='formInput'>
<Label htmlFor={dpsId} required>
IoT DPS instance
</Label>
<Dropdown
id={dpsId}
options={DPSs}
onRenderPlaceholder={onDPSLoading}
onClick={onDpsDropDownOpen}
onChange={(_, val) => {
setValues((cur) => ({
...cur,
target: {
type: ServiceType.DPS,
id: val?.key as string,
params: {
...cur.target.params,
idScope:
val?.data.properties
.idScope,
dpsName: val?.data.name,
dpsId: val?.data
.dpsLink,
},
},
}))
}}
disabled={submitting}
/>
</div>
) : (
<>
<div className='formInput'>
<Label
htmlFor={centralAppsId2}
required
>
IoT Central Application
</Label>
<Dropdown
id={centralAppsId2}
options={centralApps}
onRenderPlaceholder={
onCentralAppLoading
}
onClick={onCentralDropDownOpen}
onChange={(_, val) => {
setCentralTargetService({
id: val?.data.properties
.subdomain,
})
}}
disabled={submitting}
/>
</div>
<div className='formInput'>
<Label htmlFor={deviceTemplateId}>
Assign to template (optional)
</Label>
<Dropdown
id={deviceTemplateId}
options={deviceTemplates}
onRenderPlaceholder={
onTemplatesLoading
}
onClick={async () => {
if (
values.target.id &&
!submitting
) {
await onDeviceTemplateDropDownOpen()
}
}}
disabled={
!values.target.id || submitting
}
onChange={(_, val) => {
setCentralTargetService({
params: {
deviceTemplateId:
val?.data['@id'],
},
})
}}
/>
</div>
</>
)}
</div>
</div>
</div>
<div className='center-vertical bottom-margin'>
<PrimaryButton
text='Migrate'
disabled={
submitting ||
!values.name ||
!values.source.id ||
!values.target.id
}
onClick={() => {
startSubmit()
onSubmit()
}}
className='spaced-right'
/>
<PrimaryButton
text='Test'
onClick={async () => {
const policy = 'registryRead'
const keys = await getHubKeys(
'/subscriptions/2efa8bb6-25bf-4895-ba64-33806dd00780/resourceGroups/paas/providers/Microsoft.Devices/IotHubs/migratorhub',
policy
)
console.log(
generateSaSToken(
'migratorhub.devices.net',
keys.primaryKey,
policy
)
)
}}
className='spaced-right'
/>
{submitting && (
<Spinner
size={SpinnerSize.medium}
className='spaced-left'
/>
)}
</div>
</div>
)
}
)

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

@ -1,56 +0,0 @@
.workspace {
height: 100%;
display: flex;
flex-direction: column;
}
.workspace-container {
padding: 0 32px;
background-color: var(--color-content-background-tertiary);
height: 100%;
}
.action-bar {
border-bottom: 1px solid lightgrey;
}
.title {
margin-bottom: 0;
}
.device-count {
> span:nth-child(1) {
font-weight: 1000;
margin-right: 0.25rem;
}
}
.field-group:not(:first-child) {
margin-top: 3rem;
}
.loading {
opacity: 0.2;
}
.no-margin-bottom {
margin-bottom: 0;
}
.no-margin-top {
margin-top: 0 !important;
}
.progress-indicator {
height: 16px;
background-color: var(--color-content-background-tertiary);
}
.message-bar {
width: 800px;
margin: 20px 0;
}
.app-name {
font-weight: bold;
}

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

@ -1,260 +0,0 @@
import classnames from 'classnames/bind';
import React from 'react';
import { Link } from 'react-router-dom'
import { Controller, useForm } from 'react-hook-form';
import { TextField } from '@fluentui/react/lib/TextField';
import { ChoiceGroup, IChoiceGroupOption } from '@fluentui/react/lib/ChoiceGroup';
import { Dropdown } from '@fluentui/react/lib/Dropdown';
import { Checkbox } from '@fluentui/react/lib/Checkbox';
import { PrimaryButton } from '@fluentui/react';
import { CommandBar, ICommandBarItemProps } from '@fluentui/react/lib/CommandBar';
import { MessageBar, MessageBarType } from '@fluentui/react/lib/MessageBar';
import { ProgressIndicator } from '@fluentui/react/lib/ProgressIndicator';
import { getTextFieldStyles, dropdownStyles } from '../../styles/fluentStyles';
import usePromise from '../../hooks/usePromise';
import { AuthContext } from '../../context/authContext';
import { AppDataContext } from '../../context/appDataContext';
import { getDPS, getSubscriptionsApps, JobPayload, postJob } from '../../iotcAPI';
import { Config } from '../../config';
const cx = classnames.bind(require('./newMigration.scss'));
const enum MigrationOptions {
App = 'App',
Hub = 'Hub'
}
const options: IChoiceGroupOption[] = [
{ key: MigrationOptions.Hub, text: 'Move to your own Azure IoT Hub' },
{ key: MigrationOptions.App, text: 'Move to another Azure IoT Central application', disabled: true }
];
export default React.memo(function NewMigration() {
const formRef = React.useRef<any>(null);
const authContext = React.useContext(AuthContext);
const appDataContext = React.useContext(AppDataContext);
const { control, register, handleSubmit, watch } = useForm<JobPayload>();
const optionWatch = watch('migrationOption', undefined);
const [loadingTargets, targetsList, fetchTargetsError, fetchTargets] = usePromise();
const [submittingJob, jobResult, submitError, createJob] = usePromise();
const pageDisabled = !submittingJob && !!jobResult;
const cmdSubmit = () => { formRef.current.click(); }
const onSubmit = (data) => {
createJob({ promiseFn: () => postJob(authContext, data) });
}
const [error, setError] = React.useState<boolean>(false);
const cmdBar: ICommandBarItemProps[] = React.useMemo(() => [{
key: '1',
text: 'Migrate',
iconProps: {
iconName: 'TurnRight'
},
onClick: cmdSubmit,
disabled: pageDisabled
}], [pageDisabled]);
const cmdBarNew: ICommandBarItemProps[] = React.useMemo(() => [{
key: '2',
text: 'New migration',
iconProps: {
iconName: 'Add'
},
onClick: cmdSubmit,
disabled: submittingJob
}], [submittingJob]);
React.useEffect(() => {
if (optionWatch === MigrationOptions.App) {
fetchTargets({ promiseFn: () => getSubscriptionsApps(authContext) });
}
if (optionWatch === MigrationOptions.Hub) {
fetchTargets({ promiseFn: () => getDPS(authContext) });
}
// eslint-disable-next-line
}, [authContext, optionWatch]);
React.useEffect(() => {
if (submitError || fetchTargetsError) {
setError(true);
}
else {
setError(false);
}
}, [fetchTargetsError, submitError]);
const errorData = submitError || fetchTargetsError;
return (
<div className={cx('workspace')}>
<CommandBar className={cx('action-bar')} items={!!pageDisabled ? cmdBarNew : cmdBar} />
{error && <MessageBar
messageBarType={MessageBarType.error}
isMultiline={true}
onDismiss={() => setError(false)}>
<p>{JSON.stringify(errorData?.response?.data?.error?.message || errorData.message || 'Something went wrong. Please try again')}</p>
</MessageBar>}
{pageDisabled && <MessageBar
messageBarType={MessageBarType.success}
isMultiline={false}>
{`Migration job submitted with status ${jobResult.status}`}
<Link to="/status">Migration status</Link>
{' page to see progress of migration'}
</MessageBar>}
<div className={cx('workspace-container')}>
<div className={cx('workspace-title')}>
<h1 className={cx('title')}>New migration</h1>
<p>Move devices from <span className={cx('app-name')}>{Config.applicationHost}</span> to another Azure IoT Central application or to your own Azure IoT Hub.</p>
</div>
<div className={cx('workspace-content')}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className={cx('workspace-narrow')}>
<div className={cx('field-group')}>
<div className={cx('text-field')}>
<TextField
autoFocus
required={true}
disabled={pageDisabled}
autoComplete='off'
styles={getTextFieldStyles}
label='Name'
{...register('migrationName')}
/>
</div>
</div>
<div className={cx('field-group')}>
<h3>Target devices</h3>
<p>Choose the device group containing the devices to migrate.</p>
<Controller
control={control}
name='group'
render={({ field }) =>
<Dropdown
{...field}
disabled={pageDisabled}
onChange={(_e, v: any) => { field.onChange(v.key) }}
required={true}
placeholder='Select a device group'
label='Device group'
options={appDataContext.groups || []}
styles={dropdownStyles}
/>
}
/>
<br />
<p>Choose the device template that contains the device move Direct Method convention.</p>
<Controller
control={control}
name='template'
render={({ field }) =>
<Dropdown
{...field}
disabled={pageDisabled}
onChange={(_e, v: any) => { field.onChange(v.key) }}
required={true}
placeholder='Select a device template'
label='Filter group by device template'
options={appDataContext.templates || []}
styles={dropdownStyles}
/>
}
/>
</div>
<div className={cx('field-group')}>
<h3>Migration options</h3>
<Controller
control={control}
name='migrationOption'
render={({ field }) =>
<ChoiceGroup
{...field}
disabled={pageDisabled}
onChange={(_e, v: any) => { field.onChange(v.key) }}
options={options}
label='Select a migration option'
required={true}
/>}
/>
<div className={cx('progress-indicator')}>{optionWatch && loadingTargets && <ProgressIndicator />}</div>
</div>
{!!optionWatch && <>
<div className={cx('field-group', { 'loading': optionWatch && loadingTargets })}>
<h3>Migration target</h3>
{optionWatch === MigrationOptions.Hub && <>
<p className={cx("no-margin-bottom")}>Choose the Device Provisioning Service (DPS) linked to an Azure IoT Hub where the devices will be moved.</p>
<p>You can set the same group SAS tokens in both IoT Hub and IoT Central so that your devices can connect to either solutions.</p>
<Controller
control={control}
name='target'
render={({ field }) =>
<Dropdown
{...field}
disabled={pageDisabled}
onChange={(_e, v: any) => { field.onChange(v.key) }}
required={true}
placeholder='Select a DPS'
label='Target DPS'
options={targetsList || []}
styles={dropdownStyles} />
}
/>
<MessageBar className={cx('message-bar')} messageBarType={MessageBarType.warning}>
Please ensure that the device group enrollment or X.509 authentication details are copied from the Central application to this DPS instance
</MessageBar>
</>}
</div>
{optionWatch === MigrationOptions.App &&
<div className={cx('field-group', 'no-margin-top', { 'loading': optionWatch && loadingTargets })}>
<p className={cx("no-margin-bottom")}>Choose the target application where the devices will be moved.</p>
<p>You can set the same group SAS tokens in both IoT Hub and IoT Central so that your devices can connect to either solutions.</p>
<Controller
control={control}
name='target'
render={({ field }) =>
<Dropdown
{...field}
disabled={pageDisabled}
onChange={(_e, v: any) => { field.onChange(v.key) }}
required={true}
placeholder='Select an application'
label='Target application'
options={targetsList || []}
styles={dropdownStyles} />
}
/>
<h3>Device template</h3>
<Controller
control={control}
name='template'
defaultValue={false}
render={({ field }) =>
<Checkbox disabled={pageDisabled} label='Copy the associated device template' {...field} />
}
/>
</div>}
</>
}
<div className={cx('field-group')}>
<PrimaryButton disabled={pageDisabled} type='submit' text='Migrate' />
{/* this is hidden to allow the cmd bar to submit too */}
<button hidden={true} ref={formRef} type='submit' />
</div>
</div>
</form>
</div>
</div >
</div >
);
});

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

@ -1,55 +0,0 @@
.workspace {
height: 100%;
display: flex;
flex-direction: column;
}
.workspace-container {
padding: 0 32px;
background-color: var(--color-content-background-tertiary);
height: 100%;
}
.workspace-title {
margin-bottom: 20px;
}
.action-bar {
border-bottom: 1px solid lightgrey;
}
.title {
margin-bottom: 0;
}
.status {
padding-left: 1rem;
text-align: left;
}
.row {
padding-top: 0.2rem;
font-size: 0.9rem;
height: 2.6rem;
border-bottom: 1px solid #f4f3f2;
}
.status-progress {
padding: 0 2rem;
height: 18px;
background-color: var(--color-content-background-tertiary);
}
.status-padding {
margin-top: 1rem;
}
.ms-DetailsHeader {
padding-top: 0;
}
.no-data {
padding: 1.5rem;
background-color: white;
border: 1px solid #f4f3f2;
}

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

@ -1,128 +0,0 @@
import classnames from 'classnames/bind';
import React from 'react';
import { DetailsList, DetailsRow, ConstrainMode, SelectionMode, MessageBar, MessageBarType } from '@fluentui/react'
import { CommandBar, ICommandBarItemProps } from '@fluentui/react/lib/CommandBar';
import usePromise from '../../hooks/usePromise';
import { AuthContext } from '../../context/authContext';
import { AppDataContext } from '../../context/appDataContext';
import { ProgressIndicator } from '@fluentui/react/lib/ProgressIndicator';
import { getJobs, Row } from '../../iotcAPI';
import { useNavigate } from 'react-router-dom';
const cx = classnames.bind(require('./status.scss'));
function Status() {
const authContext = React.useContext(AuthContext);
const appDataContext = React.useContext(AppDataContext);
const navigate = useNavigate();
const [tableData, setTableData] = React.useState<Row[]>([]);
const [loading, data, error, fetch] = usePromise();
const newMigration = React.useCallback(() => {
navigate('/');
}, [navigate]);
const cmdBar: ICommandBarItemProps[] = React.useMemo(() => [{
key: '1',
text: 'Refresh',
iconProps: {
iconName: 'Refresh'
},
onClick: () => { fetch({ promiseFn: () => getJobs(authContext) }); },
disabled: false
}, {
key: '2',
text: 'New migration',
iconProps: {
iconName: 'Add'
},
onClick: newMigration,
}], [authContext, fetch, newMigration]);
React.useEffect(() => {
const intervalId = setInterval(async () => {
const response = await fetch({ promiseFn: () => getJobs(authContext) });
if (!!response) {
setTableData(response);
}
}, 2000);
return () => clearInterval(intervalId);
}, [setTableData, fetch, authContext]);
React.useEffect(() => {
if (!data) { return; }
setTableData(data);
}, [data])
const _onRenderRow = (props) => {
if (!props) {
return null;
}
return <DetailsRow {...props} className={cx('row')} />;
};
const cols = React.useMemo(() => {
return [
{ key: '1', name: 'Migration job name', fieldName: 'id', isResizable: true, minWidth: 150, maxWidth: 350 },
{ key: '2', name: 'Device group', fieldName: 'dgroup', isResizable: true, minWidth: 150, maxWidth: 350 },
{ key: '3', name: 'Status', fieldName: 'status', isResizable: true, minWidth: 150, maxWidth: 350 }
];
}, []);
const _onRenderItemColumn = (item, _index, column) => {
switch (column.fieldName) {
case 'dgroup':
const groupId = item.dgroup;
const group = appDataContext.groups.find(g => g.key === groupId);
return <a target='_blank' rel='noreferrer' href={`https://${authContext.applicationHost}/device-groups/${group?.key}`}>
{group?.text}
</a>;
case 'id':
return <a target='_blank' rel='noreferrer' href={`https://${authContext.applicationHost}/jobs/instances/${item[column.fieldName]}`}>
{item.name}
</a>;
default:
return <span>{item[column.fieldName]}</span>;
}
}
return (
<div className={cx('workspace')}>
<CommandBar className={cx('action-bar')} items={cmdBar} />
{error && <MessageBar
messageBarType={MessageBarType.error}
isMultiline={true}>
<p>{JSON.stringify(error?.response?.data?.error?.message || error.message || 'Something went wrong. Please try again')}</p>
</MessageBar>}
<div className={cx('status-progress')}>{!!loading && <ProgressIndicator />}</div>
<div className={cx('workspace-container')}>
<div className={cx('workspace-title')}>
<h1 className={cx('title')}>Migration status</h1>
<p>Watch all the migration process for <span className={cx('app-name')}>{authContext.applicationHost}</span>.</p>
</div>
<div className={cx('workspace-content')}>
<div className={cx('workspace-table', { 'status-padding': loading })}>
<DetailsList
compact={true}
items={tableData}
columns={cols}
selectionMode={SelectionMode.none}
constrainMode={ConstrainMode.unconstrained}
onRenderRow={_onRenderRow}
onRenderItemColumn={_onRenderItemColumn}
setKey='set'
className={cx('table')}
/>
</div>
</div>
</div>
</div>
);
}
export default Status;

1
src/react-app-env.d.ts поставляемый
Просмотреть файл

@ -1 +0,0 @@
/// <reference types="react-scripts" />

13
src/reportWebVitals.js Normal file
Просмотреть файл

@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

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

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

@ -1,30 +0,0 @@
import classnames from 'classnames/bind';
import { Paths } from './routes';
import { NavLink } from 'react-router-dom';
import { FontIcon } from '@fluentui/react/lib/Icon';
const cx = classnames.bind(require('./shell.scss'));
export function Navigation() {
return (
<>
<NavItem to={Paths.home} title='Define a new migration' icon='TurnRight' text='New migration' />
<NavItem to={Paths.status} title='Check the status of your migration' icon='Sync' text='Migration status' />
</>
);
}
function NavItem({ to, title, icon, text }: {
to: string;
title: string;
icon: string;
text: string;
}) {
return (
<NavLink to={to} title={title} className={(navData) => cx('global-nav-item', { 'global-nav-item-active': navData.isActive })} >
<FontIcon iconName={icon} className={cx('global-nav-item-icon')} />
<span className={cx('inline-text-overflow', 'global-nav-item-text')}>{text}</span>
</ NavLink >
);
}

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

@ -1,23 +0,0 @@
import { Routes as ReactRoutes, Route, } from 'react-router-dom';
import NewMigration from '../pages/newMigration/newMigration';
import Status from '../pages/status/status';
import Shell from './shell';
export const Paths = {
home: '/',
status: '/status',
};
export function Routes({ appReady }: { appReady: boolean }) {
if (!appReady) {
return <Shell />;
}
return (
<ReactRoutes>
<Route path={Paths.home} element={<NewMigration />} />
<Route path={Paths.status} element={<Status />} />
</ReactRoutes>
);
}

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

@ -1,49 +0,0 @@
@import "../styles/_constants";
@import "../styles/_colors";
@import "../styles/_fonts";
@import "../styles/_icons";
@import "../styles/_mixins";
@import "../styles/_typography";
@import "~@microsoft/azure-iot-ux-fluent-controls/lib/components/Masthead/Masthead.module.scss";
@import "~@microsoft/azure-iot-ux-fluent-controls/lib/components/Shell/shell.module.scss";
@import "~@microsoft/azure-iot-ux-fluent-controls/lib/components/Navigation/Navigation.module.scss";
.shell {
height: 100%;
display: flex;
flex-direction: column;
}
.shell-initial {
padding: 2rem;
}
.sign-out {
margin-right: 0.5rem;
button {
cursor: pointer;
border: 0;
background-color: transparent;
}
button:hover {
color: lightgray;
}
}
.no-auth {
text-align: right;
padding: 0 1rem 1rem 0;
font-weight: 1000;
color: red;
}
.arrow-icon {
font-size: 0.6rem;
margin-left: 0.25rem;
}
.ms-Panel-content {
padding: 0 1rem;
}

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

@ -1,98 +0,0 @@
import React from 'react';
import { AuthContext } from '../context/authContext';
import { AppDataContext } from '../context/appDataContext';
import { Shell as FluentShell, NavigationProperties } from '@microsoft/azure-iot-ux-fluent-controls';
import { Navigation } from './navigation';
import { Routes } from './routes';
import { FontIcon } from '@fluentui/react/lib/Icon';
import { ProgressIndicator } from '@fluentui/react/lib/ProgressIndicator';
import classnames from 'classnames/bind';
import { Config } from '../config';
import Modal from '../controls/modal';
const cx = classnames.bind(require('./shell.scss'));
function signOut(signOutHandler) {
return <div className='sign-out'>
<button title='Sign Out' onClick={signOutHandler}><FontIcon iconName='SignOut' className='global-nav-item-icon' /></button>
</div>
}
export default React.memo(function Shell() {
const authContext = React.useContext(AuthContext);
const appDataContext = React.useContext(AppDataContext);
const [expanded, setExpanded] = React.useState(true);
const [error, setError] = React.useState<{ title: string | undefined, message: string | undefined; }>({
title: undefined,
message: undefined
});
const nav: NavigationProperties = {
isExpanded: expanded,
onClick: (() => setExpanded(!expanded)),
children: <Navigation />
}
React.useEffect(() => {
if (!authContext.authenticated && authContext?.error?.name !== 'BrowserAuthError') {
authContext.signIn(false);
} else if (!appDataContext.appDataReady && !appDataContext.error) {
appDataContext.fetchAppData(authContext);
}
}, [authContext, appDataContext.fetchAppData, authContext.authenticated, appDataContext]);
React.useEffect(() => {
if (!!authContext.error) {
setError({
title: 'Error during the authentication',
message: authContext.error.message
});
} else if (!!appDataContext.error) {
setError({
title: 'Error fetching data from IoT Central',
message: appDataContext.error.message
});
}
}, [authContext.error, appDataContext.error]);
const closeErrorModal = React.useCallback(() => setError({ title: undefined, message: undefined }), []);
const loadingText = React.useMemo(() => {
if (!authContext.authenticated) {
return 'Waiting for authentication...';
}
return `Loading application data from ${Config.applicationHost}...`;
}, [authContext]);
let content: JSX.Element =
<div className={cx("shell-initial")}>
<h2>Please wait</h2>
<ProgressIndicator label={loadingText} />
</div>;
if (!!error.title) {
content = <Modal closeModal={closeErrorModal} error={error} />;
}
if (authContext.authenticated && appDataContext.appDataReady) {
content = <Routes appReady={true} />;
}
return <div className={cx("shell")}>
<FluentShell
masthead={{
branding: `IoTC Migrator ${!!authContext.applicationHost && `- ${authContext.applicationHost}`}`,
user: signOut(authContext.signOut),
}}
navigation={nav}>
{content}
</FluentShell>
</div >
});

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

@ -1 +0,0 @@
@import "~@microsoft/azure-iot-ux-fluent-css/src/colors";

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

@ -1 +0,0 @@
@import "~@microsoft/azure-iot-ux-fluent-css/src/constants";

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

@ -1,9 +0,0 @@
@import "./constants";
// specify the segoe icons font src:
@font-face {
font-family: "icons";
font-display: "block"; // don't fallback to other fonts while downloading
src: url("./segmdl2.1.61.woff") format("woff"),
url("./segmdl2.1.61.ttf") format("truetype");
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -1 +0,0 @@
@import "~@microsoft/azure-iot-ux-fluent-css/src/mixins";

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

@ -1 +0,0 @@
@import "~@microsoft/azure-iot-ux-fluent-css/src/typography";

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

@ -1,23 +0,0 @@
import { ITextFieldStyleProps, ITextFieldStyles } from '@fluentui/react/lib/TextField';
import { IDropdownStyles } from '@fluentui/react/lib/Dropdown';
// this is required to style Office Fabric
export function getTextFieldStyles(props: ITextFieldStyleProps): Partial<ITextFieldStyles> {
return {
fieldGroup: [{
border: (props.focused ? '1px solid #136bfb' : '1px solid #cdcdcd'),
selectors: {
":hover": {
border: '1px solid #136bfb',
}
},
width: 400,
}]
};
}
export const dropdownStyles: Partial<IDropdownStyles> = {
dropdown: {
width: 400,
},
};

Двоичные данные
src/styles/segmdl2.1.61.ttf

Двоичный файл не отображается.

Двоичные данные
src/styles/segmdl2.1.61.woff

Двоичный файл не отображается.

135
src/types.ts Normal file
Просмотреть файл

@ -0,0 +1,135 @@
export const JOB_DESCRIPTION = 'AUTOMATED-DEVICE-MOVE'
export enum MigrationMode {
Central = 'Central',
FromHub = 'FromHub',
ToHub = 'ToHub',
}
export type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>
}
export interface FormValues {
name: string
mode: MigrationMode
source: SourceService
target: TargetService
}
export enum ServiceType {
Central = 'Central',
DPS = 'Dps',
DeviceGroup = 'DeviceGroup',
DeviceTemplate = 'DeviceTemplate',
}
export type CentralSourceParams = {
groupId: string
deviceTemplateId: string
componentName: string
}
export type CentralTargetParams = {
deviceTemplateId: string
}
export type DPSSourceParams = {
iothubs: {
name: string
host: string
id: string
sasToken: string
selected: boolean
}[]
}
export type DPSTargetParams = {
idScope: string
dpsName: string
dpsId: string
}
export interface ServiceBase {
id: string
}
export interface CentralSourceService extends ServiceBase {
type: ServiceType.Central
params: CentralSourceParams
}
export interface DPSSourceService extends ServiceBase {
type: ServiceType.DPS
params: DPSSourceParams
}
export interface CentralTargetService extends ServiceBase {
type: ServiceType.Central
params: CentralTargetParams
}
export interface DPSTargetService extends ServiceBase {
type: ServiceType.DPS
params: DPSTargetParams
}
export type SourceService = CentralSourceService | DPSSourceService
export type TargetService = CentralTargetService | DPSTargetService
export type CommandPayload = {
idScope: string
dpsName: string
dpsId: string
}
export type JobPayload = {
displayName: string
group: string
description: string
data: [
{
type: string
target: string
path: string
value: CommandPayload
}
]
}
export type JobResult = {
id: string
displayName: string
group: string
description?: string
data: {
type: string
target: string
path: string
value: CommandPayload
}[]
status: string
appName?: string
jobLink?: string
}
export const TOKEN_AUDIENCES = {
Arm: 'https://management.azure.com/user_impersonation',
Central: 'https://apps.azureiotcentral.com/user_impersonation',
IoTHub: 'https://iothubs.azure.net/.default',
}
export const API_VERSIONS = {
Central: '2022-07-31',
Central_Preview: '1.2-preview',
ResourceManager: '2021-04-01',
DPS: '2022-02-05',
IoTHubArm: '2018-04-01',
IoTHubData: '2020-05-31-preview',
}
export class ApiError extends Error {
constructor(public title: string, message: string) {
super(message)
}
}

57
src/utils.ts Normal file
Просмотреть файл

@ -0,0 +1,57 @@
import hmacSha256 from 'crypto-js/hmac-sha256'
import base64 from 'crypto-js/enc-base64'
type DTDLCapability = {
'@id'?: string
'@type': string
name: string
schema?: any
contents?: DTDLCapability[]
}
export function findComponent(
dtdlInterface: DTDLCapability,
componentId: string
): DTDLCapability | null {
let res = null
if (
dtdlInterface['@type'] === 'Component' &&
(dtdlInterface['@id'] === componentId ||
dtdlInterface.schema['@id'] === componentId)
) {
res = dtdlInterface
}
if (dtdlInterface.contents && dtdlInterface.contents.length > 0) {
res =
dtdlInterface.contents.find(
(c) => findComponent(c, componentId) !== null
) || null
}
return res
}
export function generateSaSToken(
hubName: string,
signingKey: string,
policyName: string
) {
const resourceUri = encodeURIComponent(hubName)
// Set expiration in seconds
const expires = Math.ceil(Date.now() / 1000 + 3600)
const toSign = resourceUri + '\n' + expires
// Use crypto
const hmac = hmacSha256(toSign, base64.parse(signingKey))
const base64UriEncoded = encodeURIComponent(base64.stringify(hmac))
// Construct authorization string
return `SharedAccessSignature sr=${resourceUri}&sig=${base64UriEncoded}&se=${expires}${
policyName ? `&skn=${policyName}` : ''
}`
}
export function filterHubs(hubs: any[], hubHosts: string[]) {
const hubNames = hubHosts.map((h) => h.split('.azure-devices.net')[0])
return hubs.filter((h) => hubNames.includes(h.name))
}

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

@ -1,30 +1,103 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"noImplicitAny": false,
"sourceMap": true,
},
"include": [
"src"
], "exclude": [
"node_modules/**/*"
]
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
"jsx": "react", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}