add readme
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Microsoft
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
96
README.md
|
@ -1,70 +1,80 @@
|
|||
# Getting Started with Create React App
|
||||
# Azure IoT Central device migration tool
|
||||
A companion experience that enables you to move devices between IoT Central applications and from/to Azure IoT Hub.
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
## Requirements
|
||||
- An IoT Central application (or two if moving between applications)
|
||||
> Go to [IoT Central](https://apps.azureiotcentral.com/home) to create one.
|
||||
- An Azure Active Directory Application (AAD)
|
||||
> Follow instructions [here](./docs/appregistration.md).
|
||||
- An IoT Hub instance (if migrating from/to Azure IoT Hub)
|
||||
> Go to [Azure Portal > IoT Hub](https://portal.azure.com/#create/Microsoft.IotHub) to create one IoT Hub.
|
||||
- An Azure IoT Hub Device Provisioning Services (DPS) associated to the IoT Hub instance.
|
||||
> Go to [Azure Portal > DPS](https://portal.azure.com/#create/Microsoft.IoTDeviceProvisioning) to create one DPS.
|
||||
|
||||
## Available Scripts
|
||||
## Setup
|
||||
Create a file called _.env_ in project root with following content after completed AAD creation steps above.
|
||||
|
||||
In the project directory, you can run:
|
||||
```ini
|
||||
PORT=3002
|
||||
REACT_APP_AAD_APP_CLIENT_ID=<your-AAD-Application-(client)-ID>
|
||||
REACT_APP_AAD_APP_TENANT_ID=<your-AAD-Directory-(tenant)-ID>
|
||||
REACT_APP_AAD_APP_REDIRECT_URI=http://localhost:3002
|
||||
```
|
||||
|
||||
### `npm start`
|
||||
> Make sure that the REACT_APP_AAD_APP_REDIRECT_URI in the config file and the Redirect URIs specified in your AAD Application are the same.
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
### Model requirements
|
||||
To successfully perform a migration from an IoT Central application, the template for the devices to be migrated must comply to a specific configuration. The easiest and quickest way to define required capabilities is to import the _DeviceMigration_ component into the template.
|
||||
|
||||
The page will reload when you make changes.\
|
||||
You may also see any lint errors in the console.
|
||||
You can find the json file [here](./assets/deviceMigrationComponent.json).
|
||||
|
||||
### `npm test`
|
||||
Follow instructions on [IoT Central official documentation](https://docs.microsoft.com/en-us/azure/iot-central/core/howto-set-up-template#interfaces-and-components).
|
||||
|
||||
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.
|
||||
## Run
|
||||
You can run the tool in development mode using:
|
||||
```sh
|
||||
npm start
|
||||
```
|
||||
- Open in the browser the AADRedirectURI url previously defined in your _.env_ (by default is http://localhost:3002) to view it in the browser.
|
||||
|
||||
### `npm run build`
|
||||
- Authenticate the user.
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
### Options
|
||||
1. __IoT Central -> IoT Hub__
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
Pick the needed values from source and target forms. Provide a name and click on _Migrate_ button.
|
||||
|
||||
### `npm run eject`
|
||||
Follow instructions on the page to create a new enrollment group in the DPS instance then click on _Start migration_.
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||
<img src='./assets/c2h.png' width='500px'/>
|
||||
<img src='./assets/copykeys.png' width='500px'/>
|
||||
|
||||
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.
|
||||
2. __IoT Central -> IoT Central__
|
||||
|
||||
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.
|
||||
Pick the needed values from source and target forms.
|
||||
Optionally specify a target template to automatically assign migrating devices to a particular template in the application.
|
||||
Provide a name and click on _Migrate_ button.
|
||||
Wait for the migration job to be configured. After that the job will automatically start and you can follow the progress in the _Migration status_ page.
|
||||
|
||||
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.
|
||||
<img src='./assets/c2c.png' width='500px'/>
|
||||
|
||||
## Learn More
|
||||
3. __IoT Hub -> IoT Central__
|
||||
|
||||
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/).
|
||||
Select the hubs from which migrating devices, provide a name and click on _Migrate_ button. The job has been configured and available in the _Migration status_ page. Once there click on "Run" and provide the required keys following instructions.
|
||||
|
||||
### Code Splitting
|
||||
<img src='./assets/h2c.png' width='500px'/>
|
||||
<img src='./assets/status.png' width='500px'/>
|
||||
<img src='./assets/form.png' width='500px'/>
|
||||
|
||||
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)
|
||||
Wait for the job to complete.
|
||||
|
||||
### 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)
|
||||
### Codebase
|
||||
_iotc-migrator_ is a React SPA application written in Typescript that runs 100% in the browser. It was bootstrapped with Create React App.
|
||||
|
||||
### Making a Progressive Web App
|
||||
The project consume public Azure APIs on the latest stable versions.
|
||||
|
||||
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)
|
||||
The Authentication is performed using Microsoft Authentication Library (MSAL).
|
||||
|
||||
### 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)
|
||||
|
|
После Ширина: | Высота: | Размер: 86 KiB |
После Ширина: | Высота: | Размер: 82 KiB |
После Ширина: | Высота: | Размер: 149 KiB |
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"@id": "dtmi:azureiot:DeviceMigration;1",
|
||||
"@type": "Interface",
|
||||
"contents": [
|
||||
{
|
||||
"@type": "Command",
|
||||
"commandType": "synchronous",
|
||||
"displayName": {
|
||||
"en": "DeviceMove"
|
||||
},
|
||||
"name": "DeviceMove",
|
||||
"request": {
|
||||
"@type": "CommandPayload",
|
||||
"displayName": {
|
||||
"en": "Data"
|
||||
},
|
||||
"name": "data",
|
||||
"schema": {
|
||||
"@type": "Object",
|
||||
"displayName": {
|
||||
"en": "Object"
|
||||
},
|
||||
"fields": [
|
||||
{
|
||||
"displayName": {
|
||||
"en": "Id Scope"
|
||||
},
|
||||
"name": "idScope",
|
||||
"schema": "string"
|
||||
},
|
||||
{
|
||||
"displayName": {
|
||||
"en": "DPS name"
|
||||
},
|
||||
"name": "dpsName",
|
||||
"schema": "string"
|
||||
},
|
||||
{
|
||||
"displayName": {
|
||||
"en": "DPS Id"
|
||||
},
|
||||
"name": "dpsId",
|
||||
"schema": "string"
|
||||
},
|
||||
{
|
||||
"displayName": {
|
||||
"en": "IoT Central Destination Name"
|
||||
},
|
||||
"name": "centralAppName",
|
||||
"schema": "string"
|
||||
},
|
||||
{
|
||||
"displayName": {
|
||||
"en": "IoT Central Destination Subdomain"
|
||||
},
|
||||
"name": "centralAppSubdomain",
|
||||
"schema": "string"
|
||||
},
|
||||
{
|
||||
"displayName": {
|
||||
"en": "IoT Central Destination Template"
|
||||
},
|
||||
"name": "deviceTemplateId",
|
||||
"schema": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"displayName": {
|
||||
"en": "Device Migration"
|
||||
},
|
||||
"@context": [
|
||||
"dtmi:iotcentral:context;2",
|
||||
"dtmi:dtdl:context;2"
|
||||
]
|
||||
}
|
После Ширина: | Высота: | Размер: 104 KiB |
После Ширина: | Высота: | Размер: 83 KiB |
После Ширина: | Высота: | Размер: 97 KiB |
После Ширина: | Высота: | Размер: 72 KiB |
|
@ -0,0 +1,46 @@
|
|||
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 `.env` file under _REACT_APP_AAD_APP_REDIRECT_URI_
|
||||
![Create app](/assets/newApp.png)
|
||||
|
||||
4. Click _Register_ and once the app is created you will get the paramaters needed in the `.env` file.
|
||||
![App created](/assets/appCreated.png)
|
||||
|
||||
5. Go to _Manifest_ view and add the required permissions in the _requiredResourceAccess_ list.
|
||||
|
||||
![Manifest](/assets/manifest.png)
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"resourceAppId": "9edfcdd9-0bc5-4bd4-b287-c3afc716aac7",
|
||||
"resourceAccess": [
|
||||
{
|
||||
"id": "73792908-5709-46da-9a68-098589599db6",
|
||||
"type": "Scope"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"resourceAppId": "797f4846-ba00-4fd7-ba43-dac1f8f63013",
|
||||
"resourceAccess": [
|
||||
{
|
||||
"id": "41094075-9dad-400e-a0bd-54e686782033",
|
||||
"type": "Scope"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"resourceAppId": "00000003-0000-0000-c000-000000000000",
|
||||
"resourceAccess": [
|
||||
{
|
||||
"id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d",
|
||||
"type": "Scope"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
|
@ -6,7 +6,6 @@
|
|||
"@azure/msal-browser": "^2.26.0",
|
||||
"@fluentui/react": "^8.77.0",
|
||||
"@fluentui/react-hooks": "^8.6.0",
|
||||
"axios": "^0.27.2",
|
||||
"crypto-js": "^4.1.1",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
|
@ -17,7 +16,8 @@
|
|||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"eject": "react-scripts eject",
|
||||
"format": "prettier --write --loglevel warn \"**/*.{js,json,md,ts,yml,yaml}\""
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
|
|
11
src/App.css
|
@ -17,11 +17,18 @@
|
|||
.height100 {
|
||||
height: 100%;
|
||||
}
|
||||
.height300 {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.width100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.width90 {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.signOut {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -80,6 +87,10 @@
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex.horizontal.between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 2em;
|
||||
border: 1px solid gray;
|
||||
|
|
|
@ -8,12 +8,13 @@ import {
|
|||
} from '@fluentui/react'
|
||||
import { useBoolean } from '@fluentui/react-hooks'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { login, loginSilent, logout } from './api'
|
||||
import { getDPSKeys, 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'
|
||||
import { generateSaSToken } from './utils'
|
||||
|
||||
const iconButtonStyles: Partial<IButtonStyles> = {
|
||||
root: {
|
||||
|
@ -43,6 +44,8 @@ export default function App() {
|
|||
auth = (await login()).account
|
||||
}
|
||||
setAccount(auth)
|
||||
const keys=await getDPSKeys('/subscriptions/2efa8bb6-25bf-4895-ba64-33806dd00780/resourceGroups/paas/providers/Microsoft.Devices/provisioningServices/migratordps', 'provisioningserviceowner');
|
||||
console.log(generateSaSToken('migratordps.azure-devices-provisioning.net',keys.primaryKey,'provisioningserviceowner'));
|
||||
}, [])
|
||||
|
||||
const setModalMessage = useCallback((msg: ApiError | JSX.Element) => {
|
||||
|
|
После Ширина: | Высота: | Размер: 114 KiB |
220
src/api.ts
|
@ -13,7 +13,26 @@ import {
|
|||
EnrollmentGroup,
|
||||
} from './types'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import axios from 'axios'
|
||||
|
||||
//#region apitypes
|
||||
type EnrollmentGroupResponse = {
|
||||
id: string
|
||||
displayName: string
|
||||
enabled: boolean
|
||||
type: 'iot' | 'iotEdge'
|
||||
attestation: {
|
||||
type: 'symmetricKey' | 'x509'
|
||||
symmetricKey?: {
|
||||
primaryKey: string
|
||||
secondaryKey: string
|
||||
}
|
||||
x509?: {
|
||||
signingCertificates: any
|
||||
}
|
||||
}
|
||||
etag: string
|
||||
}
|
||||
//#endregion
|
||||
|
||||
export const msalConfig = {
|
||||
auth: {
|
||||
|
@ -64,7 +83,6 @@ let account: AccountInfo | null
|
|||
|
||||
function getAccount(): AccountInfo | null {
|
||||
// need to call getAccount here?
|
||||
const cache = msalInstance.getTokenCache()
|
||||
const currentAccounts = msalInstance.getAllAccounts()
|
||||
|
||||
if (currentAccounts === null) {
|
||||
|
@ -163,55 +181,6 @@ export async function getDPSKeys(dpsId: string, policyName: string) {
|
|||
return dpsResp.json()
|
||||
}
|
||||
|
||||
export async function getDPSEnrollmentKeys(
|
||||
dpsHost: string,
|
||||
sasToken: string
|
||||
): Promise<{ primaryKey: string; secondaryKey: string }> {
|
||||
const params = {
|
||||
method: 'POST',
|
||||
crossDomain: true,
|
||||
headers: {
|
||||
Authorization: sasToken,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: '*/*',
|
||||
Host: dpsHost,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: '*',
|
||||
}),
|
||||
}
|
||||
const resp = await axios.post(
|
||||
`https://${dpsHost}/enrollmentGroups/query?api-version=${API_VERSIONS.DPSData}`,
|
||||
{
|
||||
query: '*',
|
||||
},
|
||||
{
|
||||
withCredentials: false,
|
||||
headers: {
|
||||
Authorization: sasToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
const listResp = await fetch(
|
||||
`https://${dpsHost}/enrollmentGroups/query?api-version=${API_VERSIONS.DPSData}`,
|
||||
params
|
||||
)
|
||||
const groups = await listResp.json()
|
||||
const group = groups.find((g: any) => g.attestation.type === 'symmetricKey')
|
||||
if (!group) {
|
||||
throw new ApiError(
|
||||
'Failed to query DPS',
|
||||
'Cannot find an enrollment group with symmetric keys.'
|
||||
)
|
||||
}
|
||||
const enrollMentResp = await fetch(
|
||||
`https://${dpsHost}/enrollmentGroups/${group.enrollmentGroupId}attestationmechanism?api-version=${API_VERSIONS.DPSData}`,
|
||||
params
|
||||
)
|
||||
return (await enrollMentResp.json()).symmetricKey
|
||||
}
|
||||
|
||||
export async function listDPSs() {
|
||||
const armToken = await getToken(
|
||||
'https://management.azure.com/user_impersonation'
|
||||
|
@ -253,6 +222,48 @@ export async function listDPSs() {
|
|||
return data
|
||||
}
|
||||
|
||||
export async function createDpsEnrollment(
|
||||
dpsName: string,
|
||||
dpsId: string,
|
||||
enrollment: EnrollmentGroup,
|
||||
iotHubs: string[]
|
||||
) {
|
||||
const enrollmentGroupId = uuid()
|
||||
const params = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: await getDPSKeys(dpsId, 'provisioningserviceowner'),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
enrollmentGroupId,
|
||||
attestation: {
|
||||
type: 'symmetricKey',
|
||||
symmetricKey: {
|
||||
primaryKey: enrollment.primaryKey,
|
||||
secondaryKey: enrollment.secondaryKey,
|
||||
},
|
||||
},
|
||||
capabilities: {
|
||||
iotEdge: false,
|
||||
},
|
||||
reprovisionPolicy: {
|
||||
updateHubAssignment: true,
|
||||
migrateDeviceData: true,
|
||||
},
|
||||
allocationPolicy: 'hashed',
|
||||
iotHubs: [],
|
||||
}),
|
||||
}
|
||||
const resp = await fetch(
|
||||
`https://${dpsName}.azure-devices-provisioning.net/enrollmentGroups/${enrollmentGroupId}?api-version=${API_VERSIONS.DPSData}`,
|
||||
params
|
||||
)
|
||||
const data = await resp.json()
|
||||
if (!resp.ok) {
|
||||
throw new ApiError('Error working on DPS', data)
|
||||
}
|
||||
}
|
||||
export async function listHubs() {
|
||||
const armToken = await getToken(
|
||||
'https://management.azure.com/user_impersonation'
|
||||
|
@ -319,7 +330,8 @@ export async function invokeCommand(
|
|||
hubHost: string,
|
||||
sasToken: string,
|
||||
deviceId: string,
|
||||
idScope: string
|
||||
idScope: string,
|
||||
deviceTemplateId?: string
|
||||
) {
|
||||
const params = {
|
||||
method: 'POST',
|
||||
|
@ -329,7 +341,7 @@ export async function invokeCommand(
|
|||
},
|
||||
body: JSON.stringify({
|
||||
methodName: 'DeviceMove',
|
||||
payload: idScope,
|
||||
payload: JSON.stringify({ idScope, deviceTemplateId }),
|
||||
}),
|
||||
}
|
||||
const cmdResp = await fetch(
|
||||
|
@ -373,7 +385,7 @@ export async function listCentralApps() {
|
|||
return resources.flat()
|
||||
}
|
||||
|
||||
export async function listDeviceGroups(appDomain: string) {
|
||||
export async function listDeviceGroups(appSubdomain: string) {
|
||||
const centralToken = await getToken(TOKEN_AUDIENCES.Central)
|
||||
const params = {
|
||||
method: 'GET',
|
||||
|
@ -382,17 +394,18 @@ export async function listDeviceGroups(appDomain: string) {
|
|||
},
|
||||
}
|
||||
const groups = await fetch(
|
||||
`https://${appDomain}.azureiotcentral.com/api/deviceGroups?api-version=${API_VERSIONS.Central}`,
|
||||
`https://${appSubdomain}.azureiotcentral.com/api/deviceGroups?api-version=${API_VERSIONS.Central}`,
|
||||
params
|
||||
)
|
||||
return (await groups.json()).value
|
||||
}
|
||||
|
||||
export async function createCentralEnrollment(
|
||||
appDomain: string,
|
||||
appSubdomain: string,
|
||||
symmetricKey: { primaryKey: string; secondaryKey: string }
|
||||
) {
|
||||
): Promise<EnrollmentGroup> {
|
||||
const centralToken = await getToken(TOKEN_AUDIENCES.Central)
|
||||
let resp
|
||||
const params = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
|
@ -410,21 +423,53 @@ export async function createCentralEnrollment(
|
|||
}),
|
||||
}
|
||||
const enrollmentGroup = await fetch(
|
||||
`https://${appDomain}.azureiotcentral.com/api/enrollmentGroups/${uuid()}?api-version=${
|
||||
`https://${appSubdomain}.azureiotcentral.com/api/enrollmentGroups/${uuid()}?api-version=${
|
||||
API_VERSIONS.Central
|
||||
}`,
|
||||
params
|
||||
)
|
||||
if (!enrollmentGroup.ok) {
|
||||
if (enrollmentGroup.status === 409) return
|
||||
if (enrollmentGroup.status === 409) {
|
||||
// fetch the group
|
||||
const params = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${centralToken}`,
|
||||
},
|
||||
}
|
||||
const enrollmentGroups: { value: EnrollmentGroupResponse[] } =
|
||||
await (
|
||||
await fetch(
|
||||
`https://${appSubdomain}.azureiotcentral.com/api/enrollmentGroups?api-version=${API_VERSIONS.Central}`,
|
||||
params
|
||||
)
|
||||
).json()
|
||||
resp = enrollmentGroups.value.find(
|
||||
(enrl) =>
|
||||
enrl.type === 'iot' &&
|
||||
enrl.attestation.type === 'symmetricKey' &&
|
||||
enrl.attestation.symmetricKey?.primaryKey ===
|
||||
symmetricKey.primaryKey &&
|
||||
enrl.attestation.symmetricKey.secondaryKey ===
|
||||
symmetricKey.secondaryKey
|
||||
)
|
||||
if (!resp) {
|
||||
throw new ApiError(
|
||||
'IoT Central error',
|
||||
'Cannot create enrollment group on target application'
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resp = await enrollmentGroup.json()
|
||||
}
|
||||
return {
|
||||
idScope: await _getCentralIdScope(appSubdomain, centralToken),
|
||||
...resp.attestation.symmetricKey!,
|
||||
}
|
||||
return enrollmentGroup.json()
|
||||
}
|
||||
|
||||
export async function getCentralIdScope(
|
||||
appDomain: string,
|
||||
centralToken?: string
|
||||
) {
|
||||
async function _getCentralIdScope(appSubdomain: string, centralToken?: string) {
|
||||
// lack of support for getting id scope. creating a temp device to fetch it
|
||||
const bearer = centralToken || (await getToken(TOKEN_AUDIENCES.Central))
|
||||
|
||||
|
@ -442,7 +487,7 @@ export async function getCentralIdScope(
|
|||
}),
|
||||
}
|
||||
const tempDev = await fetch(
|
||||
`https://${appDomain}.azureiotcentral.com/api/devices/${tempDevId}?api-version=${API_VERSIONS.Central}`,
|
||||
`https://${appSubdomain}.azureiotcentral.com/api/devices/${tempDevId}?api-version=${API_VERSIONS.Central}`,
|
||||
createparams
|
||||
)
|
||||
if (!tempDev.ok) {
|
||||
|
@ -458,7 +503,7 @@ export async function getCentralIdScope(
|
|||
},
|
||||
}
|
||||
const creds = await fetch(
|
||||
`https://${appDomain}.azureiotcentral.com/api/devices/${tempDevId}/credentials?api-version=${API_VERSIONS.Central}`,
|
||||
`https://${appSubdomain}.azureiotcentral.com/api/devices/${tempDevId}/credentials?api-version=${API_VERSIONS.Central}`,
|
||||
getParams
|
||||
)
|
||||
if (!creds.ok) {
|
||||
|
@ -477,7 +522,7 @@ export async function getCentralIdScope(
|
|||
},
|
||||
}
|
||||
const deleteDev = await fetch(
|
||||
`https://${appDomain}.azureiotcentral.com/api/devices/${tempDevId}?api-version=${API_VERSIONS.Central}`,
|
||||
`https://${appSubdomain}.azureiotcentral.com/api/devices/${tempDevId}?api-version=${API_VERSIONS.Central}`,
|
||||
deleteparams
|
||||
)
|
||||
if (!deleteDev.ok) {
|
||||
|
@ -490,9 +535,9 @@ export async function getCentralIdScope(
|
|||
}
|
||||
|
||||
export async function getCentralEnrollment(
|
||||
appDomain: string,
|
||||
appSubdomain: string,
|
||||
centralToken?: string
|
||||
): Promise<EnrollmentGroup> {
|
||||
) {
|
||||
const bearer = centralToken || (await getToken(TOKEN_AUDIENCES.Central))
|
||||
const getParams = {
|
||||
method: 'GET',
|
||||
|
@ -501,29 +546,40 @@ export async function getCentralEnrollment(
|
|||
},
|
||||
}
|
||||
const enrollments = await fetch(
|
||||
`https://${appDomain}.azureiotcentral.com/api/enrollmentGroups?api-version=${API_VERSIONS.Central}`,
|
||||
`https://${appSubdomain}.azureiotcentral.com/api/enrollmentGroups?api-version=${API_VERSIONS.Central}`,
|
||||
getParams
|
||||
)
|
||||
if (!enrollments.ok) {
|
||||
throw new ApiError('IoT Central error', 'Cannot list enrollment groups')
|
||||
}
|
||||
const enrollmentGroups = (await enrollments.json()).value
|
||||
const attestation = enrollmentGroups.find(
|
||||
const enrollmentGroups: EnrollmentGroupResponse[] = (
|
||||
await enrollments.json()
|
||||
).value
|
||||
const group = enrollmentGroups.find(
|
||||
(e: any) => e.type === 'iot' && e.attestation.type === 'symmetricKey'
|
||||
)
|
||||
if (!attestation) {
|
||||
if (!group) {
|
||||
throw new ApiError(
|
||||
'IoT Central error',
|
||||
'Cannot find an enrollment group with symmetric keys attestation'
|
||||
)
|
||||
}
|
||||
return group.attestation.symmetricKey
|
||||
}
|
||||
|
||||
export async function getCentralCredentials(
|
||||
appSubdomain: string,
|
||||
centralToken?: string
|
||||
): Promise<EnrollmentGroup> {
|
||||
const bearer = centralToken || (await getToken(TOKEN_AUDIENCES.Central))
|
||||
const enrollment = await getCentralEnrollment(appSubdomain, bearer)
|
||||
return {
|
||||
idScope: await getCentralIdScope(appDomain, bearer),
|
||||
...attestation.symmetricKey,
|
||||
idScope: await _getCentralIdScope(appSubdomain, bearer),
|
||||
...enrollment!,
|
||||
}
|
||||
}
|
||||
|
||||
export async function listJobs(appDomain: string) {
|
||||
export async function listJobs(appSubdomain: string) {
|
||||
const centralToken = await getToken(TOKEN_AUDIENCES.Central)
|
||||
const params = {
|
||||
method: 'GET',
|
||||
|
@ -532,14 +588,14 @@ export async function listJobs(appDomain: string) {
|
|||
},
|
||||
}
|
||||
const jobs = await fetch(
|
||||
`https://${appDomain}.azureiotcentral.com/api/jobs?api-version=${API_VERSIONS.Central}`,
|
||||
`https://${appSubdomain}.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) {
|
||||
export async function listDeviceTemplates(appSubdomain: string) {
|
||||
const centralToken = await getToken(TOKEN_AUDIENCES.Central)
|
||||
const params = {
|
||||
method: 'GET',
|
||||
|
@ -548,7 +604,7 @@ export async function listDeviceTemplates(appDomain: string) {
|
|||
},
|
||||
}
|
||||
const templates = await fetch(
|
||||
`https://${appDomain}.azureiotcentral.com/api/deviceTemplates?api-version=${API_VERSIONS.Central}`,
|
||||
`https://${appSubdomain}.azureiotcentral.com/api/deviceTemplates?api-version=${API_VERSIONS.Central}`,
|
||||
params
|
||||
)
|
||||
return (await templates.json()).value
|
||||
|
@ -556,13 +612,13 @@ export async function listDeviceTemplates(appDomain: string) {
|
|||
|
||||
/**
|
||||
*
|
||||
* @param appDomain
|
||||
* @param appSubdomain
|
||||
* @param migrationData
|
||||
* @returns migrationStatus
|
||||
* @throws ApiError
|
||||
*/
|
||||
export async function createMigrationJob(
|
||||
appDomain: string,
|
||||
appSubdomain: string,
|
||||
migrationData: JobPayload
|
||||
) {
|
||||
const centralToken = await getToken(TOKEN_AUDIENCES.Central)
|
||||
|
@ -575,7 +631,7 @@ export async function createMigrationJob(
|
|||
},
|
||||
}
|
||||
const job = await fetch(
|
||||
`https://${appDomain}.azureiotcentral.com/api/jobs/${uuid()}?api-version=${
|
||||
`https://${appSubdomain}.azureiotcentral.com/api/jobs/${uuid()}?api-version=${
|
||||
API_VERSIONS.Central
|
||||
}`,
|
||||
params
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
Coachmark,
|
||||
ConstrainMode,
|
||||
DetailsList,
|
||||
DetailsListLayoutMode,
|
||||
IColumn,
|
||||
Icon,
|
||||
IconButton,
|
||||
|
@ -19,7 +20,6 @@ import { useBoolean } from '@fluentui/react-hooks'
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
createCentralEnrollment,
|
||||
getCentralIdScope,
|
||||
getHubKeys,
|
||||
invokeCommand,
|
||||
listCentralApps,
|
||||
|
@ -38,7 +38,7 @@ import enrollmentScreen from '../enrollment.png'
|
|||
|
||||
const gridStyles: Partial<IDetailsListStyles> = {
|
||||
root: {
|
||||
overflowX: 'auto',
|
||||
overflowX: 'hidden',
|
||||
selectors: {
|
||||
'& [role=grid]': {
|
||||
display: 'flex',
|
||||
|
@ -48,13 +48,8 @@ const gridStyles: Partial<IDetailsListStyles> = {
|
|||
},
|
||||
},
|
||||
},
|
||||
headerWrapper: {
|
||||
flex: '0 0 auto',
|
||||
},
|
||||
contentWrapper: {
|
||||
flex: '1 1 auto',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -82,6 +77,11 @@ const centralColumns: IColumn[] = [
|
|||
name: 'DPS',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
key: 'centralTarget',
|
||||
name: 'IoT Central target application',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
key: 'jobStatus',
|
||||
name: 'Migration Status',
|
||||
|
@ -116,8 +116,8 @@ const hubColumns: IColumn[] = [
|
|||
{
|
||||
key: 'progress',
|
||||
name: '',
|
||||
maxWidth: 200,
|
||||
minWidth: 200,
|
||||
maxWidth: 200,
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -135,14 +135,71 @@ function _onRenderCentralItemColumn(
|
|||
</a>
|
||||
)
|
||||
case 'dpsName':
|
||||
if (item?.data[0].value.dpsName) {
|
||||
return (
|
||||
<a
|
||||
href={item?.data[0].value.dpsId}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
{item?.data[0].value.dpsName}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
return null
|
||||
case 'centralTarget':
|
||||
if (
|
||||
item?.data[0].value.centralAppName &&
|
||||
item?.data[0].value.centralAppSubdomain
|
||||
) {
|
||||
return (
|
||||
<a
|
||||
href={item?.data[0].value.centralAppName}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
{`https://${item?.data[0].value.centralAppSubdomain}.azureiotcentral.com`}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
return null
|
||||
case 'jobStatus':
|
||||
const progress = item?.progress!
|
||||
if (progress.pending > 0) {
|
||||
return (
|
||||
<div className='flex horizontal between' id={item?.id}>
|
||||
<span className='spaced-right'>Pending</span>
|
||||
|
||||
<Icon
|
||||
iconName='Running'
|
||||
className='icon'
|
||||
style={{ color: 'grey' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
} else if (progress.failed > 0) {
|
||||
return (
|
||||
<div className='flex horizontal between' id={item?.id}>
|
||||
<span className='spaced-right'>Failed</span>
|
||||
|
||||
<Icon
|
||||
iconName='Error'
|
||||
className='icon'
|
||||
style={{ color: 'red' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<a
|
||||
href={item?.data[0].value.dpsId}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
{item?.data[0].value.dpsName}
|
||||
</a>
|
||||
<div className='flex horizontal between' id={item?.id}>
|
||||
<span className='spaced-right'>Completed</span>
|
||||
|
||||
<Icon
|
||||
iconName='Completed'
|
||||
className='icon'
|
||||
style={{ color: 'green' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return <span>{fieldContent}</span>
|
||||
|
@ -165,10 +222,9 @@ export default React.memo<{
|
|||
onDismiss: () => Promise<void> | void
|
||||
} | null>(null)
|
||||
|
||||
const [
|
||||
coachmarkShown,
|
||||
{ setTrue: showCoachmark, setFalse: hideCoachmark },
|
||||
] = useBoolean(!!gotoItemId)
|
||||
const [coachmarkShown, { setFalse: hideCoachmark }] = useBoolean(
|
||||
!!gotoItemId
|
||||
)
|
||||
|
||||
const progressStyles = useMemo(
|
||||
() => ({
|
||||
|
@ -222,7 +278,7 @@ export default React.memo<{
|
|||
</div>
|
||||
<div className='bottom-margin'>
|
||||
<span>
|
||||
1. Open the DPS instance page on the Azure Potal (
|
||||
1. Open the DPS instance page on the Azure Portal (
|
||||
<a
|
||||
href={hubItem.dpsLink}
|
||||
target='_blank'
|
||||
|
@ -297,8 +353,10 @@ export default React.memo<{
|
|||
)
|
||||
const devices = await listDevicesInHub(hubJob.hubHost, hubSas)
|
||||
|
||||
await createCentralEnrollment(hubJob.appHost, hubJob.enrollment!)
|
||||
const centralIdScope = await getCentralIdScope(hubJob.appHost)
|
||||
const targetEnrollment = await createCentralEnrollment(
|
||||
hubJob.appHost,
|
||||
hubJob.enrollment!
|
||||
)
|
||||
try {
|
||||
await Promise.all(
|
||||
devices.map(async (device) => {
|
||||
|
@ -306,7 +364,7 @@ export default React.memo<{
|
|||
hubJob.hubHost,
|
||||
hubSas,
|
||||
device.deviceId,
|
||||
centralIdScope
|
||||
targetEnrollment.idScope
|
||||
)
|
||||
})
|
||||
)
|
||||
|
@ -316,7 +374,7 @@ export default React.memo<{
|
|||
changeStatus(itemIdx, HubJobStatus.FAILED)
|
||||
}
|
||||
},
|
||||
[changeStatus]
|
||||
[changeStatus, setErrorMessage]
|
||||
)
|
||||
|
||||
const _onRenderHubItemColumn = useCallback(
|
||||
|
@ -413,7 +471,7 @@ export default React.memo<{
|
|||
)
|
||||
}
|
||||
},
|
||||
[changeStatus, generateContent, loadDevices, openModal]
|
||||
[changeStatus, generateContent, loadDevices, openModal, progressStyles]
|
||||
)
|
||||
|
||||
const fetchJobs = useCallback(async () => {
|
||||
|
@ -444,7 +502,7 @@ export default React.memo<{
|
|||
<h2>Migration status</h2>
|
||||
<div className='formHeader center-vertical'>
|
||||
<span>
|
||||
Watch all the IoT Central to IoT Hub migration processes.
|
||||
Watch all the migrations from IoT Central applications.
|
||||
</span>
|
||||
<IconButton
|
||||
iconProps={{ iconName: 'Sync' }}
|
||||
|
@ -454,31 +512,37 @@ export default React.memo<{
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='bottom-margin width100'>
|
||||
<ShimmeredDetailsList
|
||||
columns={centralColumns}
|
||||
items={centralItems}
|
||||
styles={gridStyles}
|
||||
skipViewportMeasures
|
||||
enableShimmer={centralItems.length === 0}
|
||||
constrainMode={ConstrainMode.unconstrained}
|
||||
selectionMode={SelectionMode.none}
|
||||
onRenderItemColumn={_onRenderCentralItemColumn}
|
||||
/>
|
||||
</div>
|
||||
<div className='formHeader center-vertical'>
|
||||
<span>Manage IoTHub to IoT Central migration processes.</span>
|
||||
</div>
|
||||
<div className='bottom-margin width100'>
|
||||
<DetailsList
|
||||
columns={hubColumns}
|
||||
items={hubItems}
|
||||
skipViewportMeasures
|
||||
selectionMode={SelectionMode.none}
|
||||
constrainMode={ConstrainMode.unconstrained}
|
||||
onRenderItemColumn={_onRenderHubItemColumn}
|
||||
styles={gridStyles}
|
||||
/>
|
||||
<div className='width90'>
|
||||
<div className='bottom-margin'>
|
||||
<DetailsList
|
||||
columns={centralColumns}
|
||||
items={centralItems}
|
||||
styles={gridStyles}
|
||||
skipViewportMeasures
|
||||
layoutMode={DetailsListLayoutMode.fixedColumns}
|
||||
// enableShimmer={centralItems.length === 0}
|
||||
constrainMode={ConstrainMode.unconstrained}
|
||||
selectionMode={SelectionMode.none}
|
||||
onRenderItemColumn={_onRenderCentralItemColumn}
|
||||
/>
|
||||
</div>
|
||||
<div className='formHeader center-vertical'>
|
||||
<span>
|
||||
Watch and manage migrations from DPS and IoTHub.
|
||||
</span>
|
||||
</div>
|
||||
<div className='bottom-margin'>
|
||||
<DetailsList
|
||||
columns={hubColumns}
|
||||
items={hubItems}
|
||||
layoutMode={DetailsListLayoutMode.fixedColumns}
|
||||
skipViewportMeasures
|
||||
selectionMode={SelectionMode.none}
|
||||
constrainMode={ConstrainMode.unconstrained}
|
||||
onRenderItemColumn={_onRenderHubItemColumn}
|
||||
styles={gridStyles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{coachmarkShown && (
|
||||
<Coachmark
|
||||
|
@ -494,10 +558,6 @@ export default React.memo<{
|
|||
{modalData?.content}
|
||||
</div>
|
||||
</Modal>
|
||||
{/* <PrimaryButton
|
||||
onClick={() => openModal({ dpsLink: '' } as any, 0)}
|
||||
text='open'
|
||||
/> */}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -24,7 +24,10 @@ import {
|
|||
listDeviceTemplates,
|
||||
createMigrationJob,
|
||||
listHubs,
|
||||
getDPSKeys,
|
||||
getCentralCredentials,
|
||||
createCentralEnrollment,
|
||||
createDpsEnrollment,
|
||||
getCentralEnrollment,
|
||||
} from '../api'
|
||||
import {
|
||||
FormValues,
|
||||
|
@ -41,8 +44,12 @@ import {
|
|||
STORAGE_ITEM,
|
||||
HubJob,
|
||||
HubJobStatus,
|
||||
CentralSourceParams,
|
||||
CentralTargetParams,
|
||||
DPSTargetParams,
|
||||
} from '../types'
|
||||
import { filterHubs, findComponent, generateSaSToken } from '../utils'
|
||||
import { filterHubs, findComponent } from '../utils'
|
||||
import enrollmentScreen from '../addgroup.png'
|
||||
|
||||
const iconButtonStyles: Partial<IButtonStyles> = {
|
||||
root: {
|
||||
|
@ -108,6 +115,7 @@ export default React.memo<{
|
|||
const dpsId = useId()
|
||||
const deviceGroupsId = useId()
|
||||
const deviceTemplateId = useId()
|
||||
const deviceTemplateId2 = useId()
|
||||
|
||||
const [values, setValues] = useState<FormValues>({
|
||||
name: '',
|
||||
|
@ -124,13 +132,20 @@ export default React.memo<{
|
|||
const [modalOpen, { setFalse: closeModal, setTrue: openModal }] =
|
||||
useBoolean(false)
|
||||
|
||||
const [modalData, setModalData] = useState<JSX.Element | null>(null)
|
||||
const [modalData, setModalData] = useState<{
|
||||
content: JSX.Element
|
||||
onDismiss: () => Promise<void> | void
|
||||
} | null>(null)
|
||||
|
||||
//#region DROPDOWN ITEMS LOADING STATE
|
||||
const [
|
||||
loadingApps,
|
||||
{ setTrue: startLoadingApps, setFalse: stopLoadingApps },
|
||||
] = useBoolean(false)
|
||||
const [
|
||||
loadingApps2,
|
||||
{ setTrue: startLoadingApps2, setFalse: stopLoadingApps2 },
|
||||
] = useBoolean(false)
|
||||
const [
|
||||
loadingDPSs,
|
||||
{ setTrue: startLoadingDPSs, setFalse: stopLoadingDPSs },
|
||||
|
@ -143,6 +158,10 @@ export default React.memo<{
|
|||
loadingTemplates,
|
||||
{ setTrue: startLoadingTemplates, setFalse: stopLoadingTemplates },
|
||||
] = useBoolean(false)
|
||||
const [
|
||||
loadingTemplates2,
|
||||
{ setTrue: startLoadingTemplates2, setFalse: stopLoadingTemplates2 },
|
||||
] = useBoolean(false)
|
||||
|
||||
const [
|
||||
loadingHubs,
|
||||
|
@ -159,6 +178,10 @@ export default React.memo<{
|
|||
const [deviceTemplates, setDeviceTemplates] = React.useState<
|
||||
IDropdownOption[]
|
||||
>([])
|
||||
|
||||
const [deviceTemplates2, setDeviceTemplates2] = React.useState<
|
||||
IDropdownOption[]
|
||||
>([])
|
||||
//#endregion
|
||||
|
||||
//#region DATA LOADING CALLBACK
|
||||
|
@ -169,6 +192,13 @@ export default React.memo<{
|
|||
)
|
||||
}, [loadingApps])
|
||||
|
||||
const onCentralAppLoading2 = React.useCallback(() => {
|
||||
return _onDataLoading(
|
||||
'Select an IoT Central application...',
|
||||
loadingApps2
|
||||
)
|
||||
}, [loadingApps2])
|
||||
|
||||
const onDPSLoading = React.useCallback(() => {
|
||||
return _onDataLoading('Select an IoT DPS instance...', loadingDPSs)
|
||||
}, [loadingDPSs])
|
||||
|
@ -180,6 +210,10 @@ export default React.memo<{
|
|||
const onTemplatesLoading = React.useCallback(() => {
|
||||
return _onDataLoading('Select a device template...', loadingTemplates)
|
||||
}, [loadingTemplates])
|
||||
|
||||
const onTemplatesLoading2 = React.useCallback(() => {
|
||||
return _onDataLoading('Select a device template...', loadingTemplates2)
|
||||
}, [loadingTemplates2])
|
||||
//#endregion
|
||||
|
||||
//#region DROPDOWNS IN OPENING STATE
|
||||
|
@ -194,6 +228,17 @@ export default React.memo<{
|
|||
}
|
||||
}, [centralApps, startLoadingApps, stopLoadingApps])
|
||||
|
||||
const onCentralDropDownOpen2 = React.useCallback(async () => {
|
||||
if (centralApps.length === 0) {
|
||||
await _onDropDownOpen(
|
||||
listCentralApps,
|
||||
setCentralApps,
|
||||
startLoadingApps2,
|
||||
stopLoadingApps2
|
||||
)
|
||||
}
|
||||
}, [centralApps, startLoadingApps2, stopLoadingApps2])
|
||||
|
||||
const onDpsDropDownOpen = React.useCallback(async () => {
|
||||
if (DPSs.length === 0) {
|
||||
await _onDropDownOpen(
|
||||
|
@ -214,22 +259,29 @@ export default React.memo<{
|
|||
startLoadingGroups,
|
||||
stopLoadingGroups
|
||||
)
|
||||
}, [startLoadingGroups, stopLoadingGroups, values.source])
|
||||
}, [startLoadingGroups, stopLoadingGroups, values.source.id])
|
||||
|
||||
const onDeviceTemplateDropDownOpen = React.useCallback(async () => {
|
||||
await _onDropDownOpen(
|
||||
() => {
|
||||
const appId =
|
||||
values.mode === MigrationMode.ToHub
|
||||
? values.source.id
|
||||
: values.target.id
|
||||
return listDeviceTemplates(appId)
|
||||
return listDeviceTemplates(values.source.id)
|
||||
},
|
||||
setDeviceTemplates,
|
||||
startLoadingTemplates,
|
||||
stopLoadingTemplates
|
||||
)
|
||||
}, [startLoadingTemplates, stopLoadingTemplates, values])
|
||||
}, [startLoadingTemplates, stopLoadingTemplates, values.source.id])
|
||||
|
||||
const onDeviceTemplateDropDownOpen2 = React.useCallback(async () => {
|
||||
await _onDropDownOpen(
|
||||
() => {
|
||||
return listDeviceTemplates(values.target.id)
|
||||
},
|
||||
setDeviceTemplates2,
|
||||
startLoadingTemplates2,
|
||||
stopLoadingTemplates2
|
||||
)
|
||||
}, [startLoadingTemplates2, stopLoadingTemplates2, values.target.id])
|
||||
|
||||
//#endregion
|
||||
|
||||
|
@ -270,24 +322,6 @@ export default React.memo<{
|
|||
[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) => ({
|
||||
|
@ -331,7 +365,7 @@ export default React.memo<{
|
|||
)
|
||||
//#endregion
|
||||
//#region GENERAL CALLBACKS
|
||||
const onDeviceTemplateSelected = React.useCallback(
|
||||
const onSourceDeviceTemplateSelected = React.useCallback(
|
||||
async (template) => {
|
||||
const { capabilityModel } = template
|
||||
const migrationComponent = findComponent(
|
||||
|
@ -393,19 +427,150 @@ export default React.memo<{
|
|||
) {
|
||||
// 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,
|
||||
},
|
||||
],
|
||||
const enrollmentGroup = await getCentralEnrollment(
|
||||
values.source.id
|
||||
)
|
||||
setModalData({
|
||||
content: (
|
||||
<div className='flex vertical center-horizontal'>
|
||||
<div className='bottom-margin'>
|
||||
<span className='bold'>
|
||||
Follow below instructions to complete job
|
||||
setup.
|
||||
</span>
|
||||
</div>
|
||||
<div className='bottom-margin'>
|
||||
<span>
|
||||
1. Open the DPS instance page on the Azure
|
||||
Portal (
|
||||
<a
|
||||
href={values.target.params.dpsId}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Click here
|
||||
</a>
|
||||
) .<br />
|
||||
2. Head to the "Manage Enrollments" section
|
||||
on the left menu.
|
||||
<br />
|
||||
3. Create a new enrollment group of
|
||||
"SymmetricKey" type and copy/paste the
|
||||
following keys.
|
||||
<br />
|
||||
4. Click "Start migration" to start the
|
||||
migration job
|
||||
<br />
|
||||
</span>
|
||||
</div>
|
||||
<Stack
|
||||
tokens={{ childrenGap: 15 }}
|
||||
className='flex vertical'
|
||||
>
|
||||
<TextField
|
||||
label='Primary Key'
|
||||
value={enrollmentGroup?.primaryKey}
|
||||
readOnly
|
||||
onRenderSuffix={() => (
|
||||
<IconButton
|
||||
iconProps={{
|
||||
iconName: 'Copy',
|
||||
}}
|
||||
styles={{
|
||||
rootHovered: {
|
||||
cursor: 'pointer',
|
||||
backgroundColor:
|
||||
'transparent',
|
||||
},
|
||||
rootPressed: {
|
||||
backgroundColor: 'white',
|
||||
height: '-webkit-fill-available',
|
||||
},
|
||||
}}
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(
|
||||
enrollmentGroup?.primaryKey!
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
label='Secondary Key'
|
||||
value={enrollmentGroup?.secondaryKey}
|
||||
readOnly
|
||||
onRenderSuffix={() => (
|
||||
<IconButton
|
||||
iconProps={{
|
||||
iconName: 'Copy',
|
||||
}}
|
||||
styles={{
|
||||
rootHovered: {
|
||||
cursor: 'pointer',
|
||||
backgroundColor:
|
||||
'transparent',
|
||||
},
|
||||
rootPressed: {
|
||||
backgroundColor: 'white',
|
||||
height: '-webkit-fill-available',
|
||||
},
|
||||
}}
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(
|
||||
enrollmentGroup?.secondaryKey!
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className='modal-image center-horizontal'>
|
||||
<img
|
||||
alt='screen'
|
||||
src={enrollmentScreen}
|
||||
className='width90'
|
||||
/>
|
||||
</div>
|
||||
<div className='center-horizontal'>
|
||||
<PrimaryButton
|
||||
className='button'
|
||||
text='Start migration'
|
||||
onClick={() => {
|
||||
closeModal()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
),
|
||||
onDismiss: async () => {
|
||||
const sourceParams = values.source
|
||||
.params as CentralSourceParams
|
||||
const targetParams = values.target
|
||||
.params as DPSTargetParams
|
||||
|
||||
await createMigrationJob(values.source.id, {
|
||||
displayName: values.name,
|
||||
group: sourceParams.groupId,
|
||||
description: JOB_DESCRIPTION,
|
||||
data: [
|
||||
{
|
||||
type: 'command',
|
||||
target: sourceParams.deviceTemplateId,
|
||||
path: `${sourceParams.componentName}.DeviceMove`,
|
||||
value: {
|
||||
idScope: targetParams.idScope,
|
||||
dpsId: targetParams.dpsId,
|
||||
dpsName: targetParams.dpsName,
|
||||
centralAppName: values.source.name,
|
||||
centralAppSubdomain: values.source.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
stopSubmit()
|
||||
},
|
||||
})
|
||||
openModal()
|
||||
} catch (err) {
|
||||
setErrorMessage(err as ApiError)
|
||||
}
|
||||
|
@ -435,38 +600,85 @@ export default React.memo<{
|
|||
})
|
||||
)
|
||||
localStorage.setItem(STORAGE_ITEM, JSON.stringify(currentStorage))
|
||||
setModalData(
|
||||
<div className='flex vertical center-horizontal'>
|
||||
<div className='center-vertical bold'>
|
||||
Ready
|
||||
<IconButton
|
||||
iconProps={{ iconName: 'Cancel' }}
|
||||
styles={iconButtonStyles}
|
||||
ariaLabel='Close popup modal'
|
||||
onClick={closeModal}
|
||||
/>
|
||||
setModalData({
|
||||
content: (
|
||||
<div className='flex vertical center-horizontal'>
|
||||
<div className='center-vertical bold'>
|
||||
Ready
|
||||
<IconButton
|
||||
iconProps={{ iconName: 'Cancel' }}
|
||||
styles={iconButtonStyles}
|
||||
ariaLabel='Close popup modal'
|
||||
onClick={closeModal}
|
||||
/>
|
||||
</div>
|
||||
<div className='bottom-margin'>
|
||||
<span>
|
||||
The migration job has been configured.
|
||||
<br /> Go to the "Migration status" tab or{' '}
|
||||
<Link
|
||||
onClick={() => {
|
||||
closeModal()
|
||||
goToStatus(id)
|
||||
}}
|
||||
>
|
||||
click here
|
||||
</Link>{' '}
|
||||
to run the job.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='bottom-margin'>
|
||||
<span>
|
||||
The migration job has been configured.<br/> Go to the
|
||||
"Migration status" tab or {' '}
|
||||
<Link
|
||||
onClick={() => {
|
||||
closeModal()
|
||||
goToStatus(id)
|
||||
}}
|
||||
>
|
||||
click here
|
||||
</Link>
|
||||
{' '} to run the job.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
onDismiss: () => {
|
||||
stopSubmit()
|
||||
},
|
||||
})
|
||||
openModal()
|
||||
} else {
|
||||
// central-to-central
|
||||
// create migration job
|
||||
try {
|
||||
const sourceParams = values.source.params as CentralSourceParams
|
||||
const targetParams = values.target.params as CentralTargetParams
|
||||
const sourceEnrollment = await getCentralCredentials(
|
||||
values.source.id
|
||||
)
|
||||
const targetEnrollment = await createCentralEnrollment(
|
||||
values.target.id,
|
||||
sourceEnrollment
|
||||
)
|
||||
await createMigrationJob(values.source.id, {
|
||||
displayName: values.name,
|
||||
group: sourceParams.groupId,
|
||||
description: JOB_DESCRIPTION,
|
||||
data: [
|
||||
{
|
||||
type: 'command',
|
||||
target: sourceParams.deviceTemplateId,
|
||||
path: `${sourceParams.componentName}.DeviceMove`,
|
||||
value: {
|
||||
idScope: targetEnrollment.idScope,
|
||||
centralAppName: values.target.name,
|
||||
centralAppSubdomain: values.target.id,
|
||||
deviceTemplateId: targetParams.deviceTemplateId,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
stopSubmit()
|
||||
} catch (err) {
|
||||
setErrorMessage(err as ApiError)
|
||||
}
|
||||
}
|
||||
stopSubmit()
|
||||
}, [values, setErrorMessage, stopSubmit])
|
||||
}, [
|
||||
values,
|
||||
setErrorMessage,
|
||||
stopSubmit,
|
||||
closeModal,
|
||||
goToStatus,
|
||||
openModal,
|
||||
setModalData,
|
||||
])
|
||||
|
||||
//#endregion
|
||||
return (
|
||||
|
@ -581,12 +793,13 @@ export default React.memo<{
|
|||
}
|
||||
onClick={onCentralDropDownOpen}
|
||||
onChange={(_, val) => {
|
||||
setCentralTargetService({
|
||||
setCentralSourceService({
|
||||
id: val?.data.properties
|
||||
.subdomain,
|
||||
name: val?.data.name,
|
||||
})
|
||||
setDeviceGroups([])
|
||||
setDeviceTemplates([])
|
||||
// setDeviceGroups([])
|
||||
// setDeviceTemplates([])
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -617,7 +830,7 @@ export default React.memo<{
|
|||
groupId: val?.data.id,
|
||||
},
|
||||
})
|
||||
setDeviceTemplates([])
|
||||
// setDeviceTemplates([])
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -641,7 +854,9 @@ export default React.memo<{
|
|||
!values.source.id || submitting
|
||||
}
|
||||
onChange={(_, val) =>
|
||||
onDeviceTemplateSelected(val?.data)
|
||||
onSourceDeviceTemplateSelected(
|
||||
val?.data
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
@ -675,6 +890,7 @@ export default React.memo<{
|
|||
target: {
|
||||
type: ServiceType.DPS,
|
||||
id: val?.key as string,
|
||||
name: val?.data.name,
|
||||
params: {
|
||||
...cur.target.params,
|
||||
idScope:
|
||||
|
@ -699,32 +915,35 @@ export default React.memo<{
|
|||
id={centralAppsId2}
|
||||
options={centralApps}
|
||||
onRenderPlaceholder={
|
||||
onCentralAppLoading
|
||||
onCentralAppLoading2
|
||||
}
|
||||
onClick={onCentralDropDownOpen}
|
||||
onClick={onCentralDropDownOpen2}
|
||||
onChange={(_, val) => {
|
||||
setCentralTargetService({
|
||||
id: val?.data.properties
|
||||
.subdomain,
|
||||
name: val?.data.name,
|
||||
})
|
||||
}}
|
||||
disabled={submitting}
|
||||
/>
|
||||
</div>
|
||||
<div className='formInput'>
|
||||
<Label htmlFor={deviceTemplateId}>
|
||||
<Label htmlFor={deviceTemplateId2}>
|
||||
Assign to template (optional)
|
||||
</Label>
|
||||
<Dropdown
|
||||
id={deviceTemplateId}
|
||||
options={deviceTemplates}
|
||||
onRenderPlaceholder={onTemplatesLoading}
|
||||
id={deviceTemplateId2}
|
||||
options={deviceTemplates2}
|
||||
onRenderPlaceholder={
|
||||
onTemplatesLoading2
|
||||
}
|
||||
onClick={async () => {
|
||||
if (
|
||||
values.target.id &&
|
||||
!submitting
|
||||
) {
|
||||
await onDeviceTemplateDropDownOpen()
|
||||
await onDeviceTemplateDropDownOpen2()
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
|
@ -734,7 +953,10 @@ export default React.memo<{
|
|||
setCentralTargetService({
|
||||
params: {
|
||||
deviceTemplateId:
|
||||
val?.data['@id'],
|
||||
val?.data
|
||||
.capabilityModel[
|
||||
'@id'
|
||||
],
|
||||
},
|
||||
})
|
||||
}}
|
||||
|
@ -760,22 +982,6 @@ export default React.memo<{
|
|||
}}
|
||||
className='spaced-right'
|
||||
/>
|
||||
<PrimaryButton
|
||||
text='Test'
|
||||
onClick={async () => {
|
||||
const keys = await getDPSKeys(
|
||||
'/subscriptions/2efa8bb6-25bf-4895-ba64-33806dd00780/resourceGroups/paas/providers/Microsoft.Devices/provisioningServices/migratordps',
|
||||
'provisioningserviceowner'
|
||||
)
|
||||
const sasToken = generateSaSToken(
|
||||
'migratordps.azure-devices-provisioning.net',
|
||||
keys.primaryKey,
|
||||
'provisioningserviceowner'
|
||||
)
|
||||
console.log(sasToken)
|
||||
}}
|
||||
className='spaced-right'
|
||||
/>
|
||||
{submitting && (
|
||||
<Spinner
|
||||
size={SpinnerSize.medium}
|
||||
|
@ -783,9 +989,12 @@ export default React.memo<{
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
<Modal isOpen={modalOpen}>
|
||||
<Modal
|
||||
isOpen={modalOpen}
|
||||
onDismissed={async () => await modalData?.onDismiss()}
|
||||
>
|
||||
<div className='flex vertical spaced-left spaced-right padding1'>
|
||||
{modalData}
|
||||
{modalData?.content}
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
|
|
18
src/types.ts
|
@ -56,6 +56,7 @@ export type DPSTargetParams = {
|
|||
|
||||
export interface ServiceBase {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface CentralSourceService extends ServiceBase {
|
||||
|
@ -83,14 +84,17 @@ export type TargetService = CentralTargetService | DPSTargetService
|
|||
|
||||
export type CommandPayload = {
|
||||
idScope: string
|
||||
dpsName: string
|
||||
dpsId: string
|
||||
dpsName?: string
|
||||
dpsId?: string
|
||||
centralAppName?: string
|
||||
centralAppSubdomain?: string
|
||||
deviceTemplateId?: string
|
||||
}
|
||||
|
||||
export type EnrollmentGroup = {
|
||||
primaryKey: string
|
||||
secondaryKey: string
|
||||
idScope?: string
|
||||
idScope: string
|
||||
}
|
||||
|
||||
export type JobPayload = {
|
||||
|
@ -118,6 +122,12 @@ export type JobResult = {
|
|||
path: string
|
||||
value: CommandPayload
|
||||
}[]
|
||||
progress: {
|
||||
total: number
|
||||
completed: number
|
||||
failed: number
|
||||
pending: number
|
||||
}
|
||||
status: string
|
||||
appName?: string
|
||||
jobLink?: string
|
||||
|
@ -131,7 +141,7 @@ export enum HubJobStatus {
|
|||
}
|
||||
|
||||
export type HubJob = {
|
||||
id:string,
|
||||
id: string
|
||||
hubName: string
|
||||
hubHost: string
|
||||
appHost: string
|
||||
|
|