update readme
This commit is contained in:
Родитель
43aba9d81b
Коммит
764fd2c5a6
|
@ -0,0 +1,35 @@
|
|||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
/Windows ML Dashboard*
|
||||
|
||||
# installer
|
||||
/release
|
||||
/installer
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# generated static files
|
||||
public/*
|
||||
!public/favicon.ico
|
||||
!public/index.html
|
||||
!public/manifest.json
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
yarn.lock
|
||||
package-lock.json
|
|
@ -0,0 +1,22 @@
|
|||
# Contributing
|
||||
|
||||
## Repository layout
|
||||
|
||||
Files in `public/` are copied as-is to the final build output. Any files that should be packaged with the application can be put there.
|
||||
|
||||
Files in `src/` are processed and transpiled.
|
||||
|
||||
## Source code organization
|
||||
|
||||
Inside `src/`, there are:
|
||||
|
||||
* `components`: Reusable UI components
|
||||
* `datastore`: Datastore for data shared among multiple components. Uses [redux](https://redux.js.org/)
|
||||
* `fonts`: Autogenerated fonts; see [src/fonts/README.md](src/fonts/README.md)
|
||||
* `native`: Code interfacing with native (non-web) features
|
||||
* `schema`: Schemas used for input validation and autocomplete
|
||||
* `view`: UI views
|
||||
* `App.{css,tsx}`: Root React component
|
||||
* `electronMain.js`: Electron entry point
|
||||
* `index.{css,tsx}`: Index component (will be loaded with the page)
|
||||
* `registerServiceWorker.js`: Service worker that caches content offline so resources can be accessed on a browser without internet connection
|
|
@ -0,0 +1,55 @@
|
|||
# Windows ML Dashboard
|
||||
|
||||
Developer dashboard for Windows ML and ONNX.
|
||||
|
||||
## Building from source
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
|Requirements|Version|Download|Command to check|
|
||||
|------------|-------|--------|----------------|
|
||||
|Yarn |latest |[here](https://yarnpkg.com/en/docs/install)|`yarn --version`|
|
||||
|Node.js |latest |[here](https://nodejs.org/en/)|`node --version`|
|
||||
|Python3 |3.4+ |[here](https://www.python.org/)|`python --version`|
|
||||
|
||||
#### Building
|
||||
|
||||
1. Git clone [Microsoft/Windows-Machine-Learning](https://github.com/Microsoft/Windows-Machine-Learning) repo.
|
||||
|
||||
2. `cd Tools/WinMLDashboard`
|
||||
3. Run `yarn` to download dependencies.
|
||||
4. Then, run one of the following:
|
||||
|
||||
* `yarn start` to build and start the web application
|
||||
* `yarn electron-prod` to build and start the desktop application
|
||||
|
||||
> All available commands can be seen at [package.json](package.json).
|
||||
|
||||
### Debugging
|
||||
|
||||
Chromium's dev tools pane can be used for debugging. To open it in the web view, press F12. To open it in the Electron app, run it with flag `--dev-tools`, or select *View -> Toggle Dev Tools* in the application menu, or press *Ctrl+Shift+I*.
|
||||
|
||||
### Distribution
|
||||
|
||||
To distribute the application, you can copy the whole `build` folder and the files `src/electronMain.js` and `package.json` to `node_modules/electron/dist/resources/app`, and distribute the folder `node_modules/electron/dist`. The final directory structure in `node_modules/electron/dist/resources` should be:
|
||||
|
||||
```
|
||||
app/
|
||||
├───build/
|
||||
├───────...
|
||||
├───src/
|
||||
│ └───electronMain.js
|
||||
└───package.json
|
||||
```
|
||||
|
||||
[Electron builder](https://github.com/electron-userland/electron-builder) can be used to generate installers. [See the documentation here](https://www.electron.build/).
|
||||
|
||||
### Built with
|
||||
|
||||
* [Electron](https://electronjs.org/)
|
||||
* [React](https://reactjs.org/)
|
||||
* [Redux](https://redux.js.org/)
|
||||
|
||||
### License
|
||||
|
||||
This tool is under the MIT license. The license can be found at the root of this repository.
|
|
@ -0,0 +1,411 @@
|
|||
THIRD PARTY SOFTWARE NOTICES AND INFORMATION
|
||||
Do Not Translate or Localize
|
||||
|
||||
This software incorporates material from third parties. Microsoft makes certain
|
||||
open source code available at http://3rdpartysource.microsoft.com, or you may
|
||||
send a check or money order for US $5.00, including the product name, the open
|
||||
source component name, and version number, to:
|
||||
|
||||
Source Code Compliance Team
|
||||
Microsoft Corporation
|
||||
One Microsoft Way
|
||||
Redmond, WA 98052
|
||||
USA
|
||||
|
||||
Notwithstanding any other terms, you may reverse engineer this software to the
|
||||
extent required to debug changes to any libraries licensed under the GNU Lesser
|
||||
General Public License.
|
||||
|
||||
|
||||
-------------------------START OF OPEN SOURCE LICENSES ------------------
|
||||
|
||||
|
||||
================================================
|
||||
react 16.4.1
|
||||
react-dom 16.4.1
|
||||
=====
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) Facebook, Inc. and its affiliates.
|
||||
|
||||
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.
|
||||
================================================
|
||||
|
||||
|
||||
================================================
|
||||
office-ui-fabric-react 6.39.2
|
||||
=====
|
||||
|
||||
Office UI Fabric React
|
||||
|
||||
Copyright (c) Microsoft Corporation
|
||||
|
||||
All rights reserved.
|
||||
|
||||
MIT License
|
||||
|
||||
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.
|
||||
|
||||
Note: Usage of the fonts and icons referenced in Office UI Fabric is subject to the terms listed at http://aka.ms/fabric-assets-license
|
||||
================================================
|
||||
|
||||
================================================
|
||||
ajv 6.5.2
|
||||
=====
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 Evgeny Poberezkin
|
||||
|
||||
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.
|
||||
================================================
|
||||
|
||||
|
||||
================================================
|
||||
react-markdown 3.3.4
|
||||
=====
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Espen Hovlandsdal
|
||||
|
||||
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.
|
||||
================================================
|
||||
|
||||
================================================
|
||||
react-redux 5.0.7
|
||||
redux 4.0.0
|
||||
=====
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Dan Abramov
|
||||
|
||||
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.
|
||||
================================================
|
||||
|
||||
|
||||
================================================
|
||||
yauzl 2.9.2
|
||||
=====
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Josh Wolfe
|
||||
|
||||
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.
|
||||
================================================
|
||||
|
||||
|
||||
================================================
|
||||
yauzl 2.9.2
|
||||
=====
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Josh Wolfe
|
||||
|
||||
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.
|
||||
================================================
|
||||
|
||||
================================================
|
||||
babel-minify 0.4.3
|
||||
=====
|
||||
|
||||
Copyright (c) 2015-2016 Amjad Masad <amjad.masad@gmail.com>
|
||||
|
||||
MIT License
|
||||
|
||||
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.
|
||||
================================================
|
||||
|
||||
================================================
|
||||
electron 2.0.4
|
||||
Copyright (c) 2013-2018 GitHub Inc.
|
||||
|
||||
electron-winstaller 2.6.4
|
||||
Copyright (c) 2015 GitHub Inc.
|
||||
=====
|
||||
|
||||
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.
|
||||
|
||||
================================================
|
||||
|
||||
================================================
|
||||
electron-packager 12.1.2
|
||||
=====
|
||||
Copyright (c) 2015 Max Ogden and other contributors
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
================================================
|
||||
|
||||
================================================
|
||||
react-app-rewired 1.5.2
|
||||
=====
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Tim Arney
|
||||
|
||||
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.
|
||||
|
||||
================================================
|
||||
|
||||
|
||||
================================================
|
||||
npm-font-open-sans 1.1.0
|
||||
=====
|
||||
|
||||
Copyright (c) 2015 Enrico Hoffmann
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
================================================
|
||||
|
||||
================================================
|
||||
Netron v2.1.1
|
||||
=====
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) Lutz Roeder
|
||||
|
||||
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.
|
||||
|
||||
================================================
|
||||
|
||||
|
||||
================================================
|
||||
typescript 2.9.2
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
electron-squirrel-startup 1.0.0
|
||||
Copyright (c) 2004 Lucas Hrabovsky
|
||||
=====
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
this file except in compliance with the License. You may obtain a copy of the
|
||||
License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
|
||||
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
|
||||
MERCHANTABLITY OR NON-INFRINGEMENT.
|
||||
|
||||
See the Apache Version 2.0 License for specific language governing permissions
|
||||
and limitations under the License.
|
||||
================================================
|
||||
|
||||
|
||||
================================================
|
||||
|
||||
@types/jest 23.3.1
|
||||
@types/node 10.5.2
|
||||
@types/prop-types 15.5.4
|
||||
@types/react 16.4.6
|
||||
@types/react-dom 16.0.6
|
||||
@types/react-redux 6.0.4
|
||||
@types/yauzl 2.9.0
|
||||
=====
|
||||
|
||||
This project is licensed under the MIT license.
|
||||
Copyrights are respective of each contributor listed at the beginning of each definition file.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
================================================
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
var electronInstaller = require('electron-winstaller');
|
||||
var path = require("path");
|
||||
|
||||
resultPromise = electronInstaller.createWindowsInstaller({
|
||||
appDirectory: path.join('./release/WinMLDashboard-win32-x64'),
|
||||
authors: 'Microsoft Corporation',
|
||||
exe: 'WinMLDashboard.exe',
|
||||
outputDirectory: path.join('./installer'),
|
||||
setupExe: 'WinMLDashboard_setup.exe',
|
||||
setupMsi: 'WinMLDashboard_setup.msi',
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
// tslint:disable-next-line:no-console
|
||||
resultPromise.then(() => console.log("It worked!"), (e) => console.log(`No dice: ${e.message}`));
|
|
@ -0,0 +1,18 @@
|
|||
module.exports = function override(config, env) {
|
||||
config.node = {
|
||||
__dirname: false,
|
||||
__filename: false,
|
||||
|
||||
Buffer: false,
|
||||
child_process: false,
|
||||
dgram: false,
|
||||
fs: false,
|
||||
global: false,
|
||||
net: false,
|
||||
process: false,
|
||||
setImmediate: false,
|
||||
tls: false,
|
||||
};
|
||||
config.target = 'electron-renderer';
|
||||
return config;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 950c9324c185fd4bc60be4b2434c378af4fc5f8e
|
|
@ -0,0 +1,92 @@
|
|||
#!/usr/bin/env python3
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
|
||||
@patch('setuptools.setup')
|
||||
def get_package_data(mock_setup):
|
||||
import Netron.setup as netron_setup
|
||||
package_data = mock_setup.call_args[1]['package_data']['netron']
|
||||
return (package_data, netron_setup.node_dependencies[0][1])
|
||||
|
||||
|
||||
def get_netron_static_scripts(src_path):
|
||||
with open(src_path / 'view-browser.html') as f:
|
||||
scripts = []
|
||||
regex = re.compile("<script type='text/javascript' src='(.*)'></script>")
|
||||
for line in f.readlines():
|
||||
match = re.match(regex, line)
|
||||
if match:
|
||||
scripts.append(match.group(1))
|
||||
return scripts
|
||||
|
||||
|
||||
def rebuild_needed(sources, destination):
|
||||
try:
|
||||
destination_mtime = os.path.getmtime(destination)
|
||||
except FileNotFoundError:
|
||||
return True
|
||||
return any(os.path.getmtime(f) >= destination_mtime for f in sources)
|
||||
|
||||
|
||||
def bundle_scripts(files):
|
||||
bundle = []
|
||||
for script in files:
|
||||
with script.open('rb') as f:
|
||||
bundle.append(f.read())
|
||||
return b'\n'.join(bundle)
|
||||
|
||||
|
||||
def minify(input_file, output_file):
|
||||
subprocess.check_call([shutil.which('yarn'), 'minify', str(input_file), '-o', str(output_file)])
|
||||
|
||||
|
||||
def main():
|
||||
netron = Path('deps/Netron')
|
||||
src = netron / 'src'
|
||||
package_data, node_dependencies = get_package_data()
|
||||
print('Netron package files:\n{}'.format(' '.join(package_data)))
|
||||
print('Netron Node dependencies:\n{}'.format(' '.join(node_dependencies)))
|
||||
|
||||
static_scripts = get_netron_static_scripts(src)
|
||||
print('These scripts will be bundled:\n{}'.format(' '.join(static_scripts)))
|
||||
# Update script paths to point to paths before installation
|
||||
for i, script in enumerate(static_scripts):
|
||||
if script in package_data:
|
||||
static_scripts[i] = src / script
|
||||
else:
|
||||
for filename in node_dependencies:
|
||||
path = Path(filename)
|
||||
if script == path.name:
|
||||
static_scripts[i] = netron / path
|
||||
|
||||
package_data = set(package_data) - set(static_scripts)
|
||||
|
||||
public = Path('public')
|
||||
ignored_extensions = ['.css', '.html', '.ico']
|
||||
package_data = [src / filename for filename in package_data if not Path(filename).suffix in ignored_extensions]
|
||||
for package_file in package_data:
|
||||
try:
|
||||
os.link(package_file, public / package_file.name)
|
||||
except FileExistsError:
|
||||
pass
|
||||
except FileNotFoundError:
|
||||
print("Warning: Got FileNotFoundError linking {} -> {}. "
|
||||
"Netron's setup might be declaring files that are missing in their repository."
|
||||
.format(public / package_file.name, package_file))
|
||||
|
||||
bundle_destination = public / 'netron_bundle.js'
|
||||
if rebuild_needed(static_scripts, bundle_destination):
|
||||
import tempfile
|
||||
with tempfile.NamedTemporaryFile() as f:
|
||||
f.write(bundle_scripts(static_scripts))
|
||||
minify(f.name, bundle_destination)
|
||||
else:
|
||||
print('Bundle is already up to date')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,3 @@
|
|||
declare module '*.svg'
|
||||
declare module '*.png'
|
||||
declare module '*.jpg'
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"name": "WinMLDashboard",
|
||||
"version": "1.0.0",
|
||||
"description": "Dashboard for development in ONNX using Windows ML",
|
||||
"author": "Microsoft Corporation",
|
||||
"homepage": ".",
|
||||
"main": "src/electronMain.js",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"ajv": "^6.5.2",
|
||||
"electron-packager": "^12.1.2",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"npm-font-open-sans": "^1.1.0",
|
||||
"office-ui-fabric-react": "^6.40.0",
|
||||
"react": "^16.4.1",
|
||||
"react-dom": "^16.4.1",
|
||||
"react-markdown": "^3.4.1",
|
||||
"react-redux": "^5.0.7",
|
||||
"redux": "^4.0.0",
|
||||
"yauzl": "^2.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts-ts start",
|
||||
"build": "react-scripts-ts build",
|
||||
"test": "react-scripts-ts test --env=jsdom",
|
||||
"eject": "react-scripts-ts eject",
|
||||
"postinstall": "node scripts/postinstall.js && yarn copy-netron",
|
||||
"copy-netron": "yarn --cwd deps/Netron && python deps/copy_netron.py",
|
||||
"build-electron": "react-app-rewired build --scripts-version react-scripts-ts",
|
||||
"electron-dev": "electron . http://localhost:3000",
|
||||
"electron-prod": "yarn build-electron && electron .",
|
||||
"packager": "yarn build-electron && electron-packager . WinMLDashboard --overwrite --win=x32 --out ./release --arch=x64 --app-version=1.0.0 --electron-version=2.0.6",
|
||||
"installer": "yarn packager && node build.js"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.microsoft.dashboard.winml",
|
||||
"mac": {
|
||||
"category": "public.app-category.developer-tools"
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
"msi"
|
||||
]
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^23.3.0",
|
||||
"@types/node": "^10.5.3",
|
||||
"@types/prop-types": "^15.5.4",
|
||||
"@types/react": "^16.4.7",
|
||||
"@types/react-dom": "^16.0.6",
|
||||
"@types/react-redux": "^6.0.5",
|
||||
"@types/yauzl": "^2.9.0",
|
||||
"babel-minify": "^0.4.3",
|
||||
"electron": "^2.0.6",
|
||||
"electron-winstaller": "^2.6.4",
|
||||
"react-app-rewired": "^1.5.2",
|
||||
"react-scripts-ts": "^2.16.0",
|
||||
"typescript": "^2.9.2"
|
||||
}
|
||||
}
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 3.8 KiB |
|
@ -0,0 +1,40 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is added to the
|
||||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
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>WinML Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"short_name": "WinML Dashboard",
|
||||
"name": "Windows ML Dashboard",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
for (directory of ['node_modules/netron', 'node_modules/netron/src']) {
|
||||
if (!fs.existsSync(directory))
|
||||
fs.mkdirSync(directory);
|
||||
}
|
||||
|
||||
for (source of ['view-render.css', 'view-sidebar.css', 'view.css']) {
|
||||
destination = path.join('node_modules/netron/src', source);
|
||||
if (!fs.existsSync(destination))
|
||||
fs.linkSync(path.join('deps/Netron/src', source), path.join('node_modules/netron/src', source));
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
.App {
|
||||
text-align: center;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.MainView {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.HiddenTab {
|
||||
visibility: hidden;
|
||||
overflow: hidden;
|
||||
height: 0px;
|
||||
line-height: 0px;
|
||||
}
|
||||
|
||||
.DisplayFlex {
|
||||
display: flex;
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import { Pivot, PivotItem } from 'office-ui-fabric-react/lib/Pivot';
|
||||
import * as React from 'react';
|
||||
|
||||
import { initializeIcons } from './fonts';
|
||||
import ConvertView from './view/convert/View';
|
||||
import EditView from './view/edit/View';
|
||||
|
||||
import './App.css';
|
||||
|
||||
interface IComponentState {
|
||||
tab: string,
|
||||
}
|
||||
|
||||
const views = {
|
||||
Edit: <EditView />,
|
||||
// tslint:disable-next-line:object-literal-sort-keys
|
||||
Convert: <ConvertView />,
|
||||
};
|
||||
|
||||
class App extends React.Component<{}, IComponentState> {
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
initializeIcons();
|
||||
this.state = {
|
||||
tab: 'Edit',
|
||||
};
|
||||
}
|
||||
|
||||
public render() {
|
||||
const pivotItems = Object.keys(views).map(x => <PivotItem headerText={x} key={x} />);
|
||||
const mainView = Object.entries(views).map(([k, v]) =>
|
||||
<div className={k === this.state.tab ? 'MainView' : 'HiddenTab'} key={k} >
|
||||
{v}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='App'>
|
||||
<Pivot onLinkClick={this.onLinkClick}>
|
||||
{pivotItems}
|
||||
</Pivot>
|
||||
{mainView}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onLinkClick = (item?: PivotItem, ev?: React.MouseEvent<HTMLElement>) => {
|
||||
if (item) {
|
||||
this.setState({ tab: item.props.headerText! });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,27 @@
|
|||
.Label {
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
border: 1px lightgray;
|
||||
border-style: solid none;
|
||||
padding: 5px 5px 10px;
|
||||
}
|
||||
|
||||
.Content {
|
||||
padding: 5px;
|
||||
transition: 200ms;
|
||||
transition-timing-function: ease-out;
|
||||
max-height: 2000px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: inline;
|
||||
margin: 5px;
|
||||
font-size: 18px;
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import { Label } from 'office-ui-fabric-react/lib/Label';
|
||||
import * as React from 'react';
|
||||
|
||||
import './Collapsible.css';
|
||||
|
||||
interface IComponentProperties {
|
||||
label: string,
|
||||
}
|
||||
interface IComponentState {
|
||||
visible: boolean,
|
||||
}
|
||||
|
||||
export default class Collapsible extends React.Component<IComponentProperties, IComponentState> {
|
||||
constructor(props: IComponentProperties) {
|
||||
super(props);
|
||||
this.state = {
|
||||
visible: true,
|
||||
};
|
||||
}
|
||||
|
||||
public render() {
|
||||
const style: React.CSSProperties = {};
|
||||
if (!this.state.visible) {
|
||||
style.maxHeight = '0px';
|
||||
style.visibility = 'hidden';
|
||||
style.padding = '0px';
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Label className='Label' onClick={this.toggle}><pre>{this.state.visible && '-' || '+'}</pre>{this.props.label}</Label>
|
||||
<div className='Content' style={style}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private toggle = () => {
|
||||
this.setState(previousState => ({
|
||||
visible: !previousState.visible,
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
.KeyValueEditorButton {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.KeyValueEditorButtonContainer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.KeyValueBox {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.KeyValueSeparator {
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
.RemoveIcon {
|
||||
cursor: pointer;
|
||||
padding: 10px 4px 0px 0px;
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
import * as Ajv from 'ajv';
|
||||
import { DefaultButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { ComboBox, IComboBoxOption } from 'office-ui-fabric-react/lib/ComboBox';
|
||||
import { Icon } from 'office-ui-fabric-react/lib/Icon';
|
||||
import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import IState from '../datastore/state';
|
||||
|
||||
import './KeyValueEditor.css';
|
||||
|
||||
const ajv = new Ajv({ allErrors: true, verbose: true });
|
||||
|
||||
interface IComponentProperties {
|
||||
actionCreator?: (keyValueObject: { [key: string]: string }) => {},
|
||||
getState: (state: IState) => { [key: string]: string },
|
||||
schema: any,
|
||||
|
||||
// Redux properties
|
||||
keyValueObject?: { [key: string]: string },
|
||||
updateKeyValueObject?: (keyValueObject: { [key: string]: string }) => void,
|
||||
}
|
||||
|
||||
interface IComponentState {
|
||||
caseInsensitiveSchema: any,
|
||||
keyErrors: { [key: string]: string },
|
||||
}
|
||||
|
||||
class KeyValueEditor extends React.Component<IComponentProperties, IComponentState> {
|
||||
constructor(props: IComponentProperties) {
|
||||
super(props);
|
||||
this.state = {
|
||||
caseInsensitiveSchema: this.toLowerCaseSchema(props.schema),
|
||||
keyErrors: {},
|
||||
};
|
||||
}
|
||||
|
||||
public render() {
|
||||
const properties = this.props.schema.properties || {};
|
||||
const knownKeys = Object.keys(properties).filter(
|
||||
// Don't suggesting adding keys that already exist
|
||||
(x) => Object.keys(this.props.keyValueObject || {}).every((key) => !this.areSameKeys(key, x)));
|
||||
const options = knownKeys.map((key: string) => ({ key, text: key }));
|
||||
let rows: Array<React.ReactElement<HTMLDivElement>> = [];
|
||||
|
||||
if (this.props.keyValueObject) {
|
||||
const [keyErrors, valueErrors] = this.validateSchema(this.state.caseInsensitiveSchema, this.props.keyValueObject);
|
||||
const schemaEntries = Object.entries(properties);
|
||||
|
||||
rows = Object.keys(this.props.keyValueObject!).map((x: string) => {
|
||||
const lowerCaseKey = x.toLowerCase();
|
||||
const keyChangedCallback = (option?: IComboBoxOption, index?: number, value?: string) => {
|
||||
const key = value || option!.text;
|
||||
if (Object.keys(this.props.keyValueObject!).includes(key)) {
|
||||
this.setState((prevState: IComponentState, props: IComponentProperties) => (
|
||||
{
|
||||
keyErrors: {
|
||||
[lowerCaseKey]: prevState.keyErrors[lowerCaseKey] ? `${prevState.keyErrors[lowerCaseKey]} ` : 'duplicate key'
|
||||
},
|
||||
}
|
||||
));
|
||||
} else {
|
||||
this.props.updateKeyValueObject!(this.copyRenameKey(this.props.keyValueObject, x, key));
|
||||
}
|
||||
};
|
||||
const valueChangedCallback = (option?: IComboBoxOption, index?: number, value?: string) => {
|
||||
const newValue = value || option!.text;
|
||||
this.props.updateKeyValueObject!({ ...this.props.keyValueObject!, [x]: newValue });
|
||||
};
|
||||
const removeCallback = () => {
|
||||
const newObject = { ...this.props.keyValueObject! };
|
||||
delete newObject[x];
|
||||
this.props.updateKeyValueObject!(newObject);
|
||||
}
|
||||
|
||||
let knownValues = [];
|
||||
const property = schemaEntries.find((prop) => this.areSameKeys(prop[0], x)) as [string, any];
|
||||
if (property && property[1].enum) {
|
||||
knownValues = property[1].enum.map((key: string) => ({ key, text: key }));
|
||||
}
|
||||
|
||||
const keyErrorsState = this.state.keyErrors[lowerCaseKey];
|
||||
return (
|
||||
<div key={`${x}${keyErrorsState}`} className='DisplayFlex'>
|
||||
{ this.props.actionCreator &&
|
||||
<Icon className='RemoveIcon' iconName='Cancel' onClick={removeCallback} />
|
||||
}
|
||||
<ComboBox
|
||||
className='KeyValueBox'
|
||||
allowFreeform={true}
|
||||
text={x}
|
||||
errorMessage={keyErrorsState || keyErrors[lowerCaseKey]}
|
||||
options={options}
|
||||
disabled={!this.props.actionCreator}
|
||||
onChanged={keyChangedCallback}
|
||||
/>
|
||||
<span className='KeyValueSeparator'>=</span>
|
||||
<ComboBox
|
||||
className='KeyValueBox'
|
||||
allowFreeform={true}
|
||||
text={this.props.keyValueObject![x]}
|
||||
errorMessage={valueErrors[lowerCaseKey]}
|
||||
options={knownValues}
|
||||
disabled={!this.props.actionCreator}
|
||||
onChanged={valueChangedCallback}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const addButtonDisabled = !(this.props.keyValueObject && Object.keys(this.props.keyValueObject).every((x) => !!x));
|
||||
const getSplitButtonClassNames = () => ({ splitButtonContainer: '.KeyValueEditorButtonContainer' });
|
||||
|
||||
return (
|
||||
<div>
|
||||
{rows}
|
||||
{ this.props.actionCreator &&
|
||||
<DefaultButton
|
||||
className='KeyValueEditorButtonContainer'
|
||||
iconProps={{ iconName: 'Add' }}
|
||||
disabled={addButtonDisabled}
|
||||
onClick={this.addProp}
|
||||
split={!!knownKeys.length}
|
||||
menuProps={knownKeys.length ? {
|
||||
items: options,
|
||||
onItemClick: this.addProp,
|
||||
} : undefined}
|
||||
// The Office UI includes a spam element in it when split = true, which is not of display type block
|
||||
// and ignores the parent width. We do a hack to get it to 100% of the parent width.
|
||||
getSplitButtonClassNames={getSplitButtonClassNames}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private validateSchema = (schema: any, data: { [key: string]: string }) => {
|
||||
const keyErrors = {};
|
||||
const valueErrors = {};
|
||||
if (!ajv.validate(schema, this.toLowerCaseObject(data))) {
|
||||
ajv.errors!.forEach((error: Ajv.ErrorObject) => {
|
||||
if (error.schemaPath.startsWith('#/properties/')) {
|
||||
valueErrors[error.schemaPath.split('/', 3)[2]] = error.message!;
|
||||
} else if (error.keyword === 'additionalProperties') {
|
||||
const params = error.params as Ajv.AdditionalPropertiesParams;
|
||||
keyErrors[params.additionalProperty] = 'unknown property';
|
||||
}
|
||||
});
|
||||
}
|
||||
return [keyErrors, valueErrors];
|
||||
}
|
||||
|
||||
private areSameKeys = (a: string, b: string) => {
|
||||
return a.toLowerCase() === b.toLowerCase();
|
||||
}
|
||||
|
||||
private copyRenameKey = (obj: any, oldKey: string, newKey: string) => {
|
||||
return Object.entries(obj).reduce((acc: any[], keyValue: [string, string]) => {
|
||||
const [key, value] = keyValue;
|
||||
acc[key === oldKey ? newKey : key] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
private renameKey = (obj: any, oldKey: string, newKey: string) => {
|
||||
if (oldKey !== newKey) {
|
||||
obj[newKey] = obj[oldKey];
|
||||
delete obj[oldKey];
|
||||
}
|
||||
}
|
||||
|
||||
private toLowerCaseSchema = (schema: any) => {
|
||||
// Hacky way of performing case insensitive validation. JSON schema has no case insensitive enum,
|
||||
// so we convert the schema and object being validated to lowercase before validating.
|
||||
// This code assumes the object schema has a "properties" property.
|
||||
const lowerCaseSchema = JSON.parse(JSON.stringify(schema));
|
||||
const properties = lowerCaseSchema.properties || {};
|
||||
Object.keys(properties).forEach((key: string) => {
|
||||
const propertyEnum = properties[key].enum;
|
||||
if (propertyEnum) {
|
||||
propertyEnum.forEach((x: string, i: number, arr: any[]) => arr[i] = x.toLowerCase());
|
||||
}
|
||||
this.renameKey(properties, key, key.toLowerCase());
|
||||
});
|
||||
return lowerCaseSchema;
|
||||
}
|
||||
|
||||
private toLowerCaseObject = (obj: { [key: string]: string }) => {
|
||||
return Object.entries(obj).reduce((acc: {}, keyValue: [string, any]) => {
|
||||
const [key, value] = keyValue;
|
||||
acc[key.toLowerCase()] = value.toString().toLowerCase();
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
private addProp = (event: any, item?: any) => {
|
||||
const key = item && item.key || '';
|
||||
if (this.props.keyValueObject) {
|
||||
this.props.updateKeyValueObject!({ ...this.props.keyValueObject, [key]: ''});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IState, ownProps: IComponentProperties) => ({
|
||||
keyValueObject: ownProps.getState(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: any, ownProps: IComponentProperties) => ({
|
||||
updateKeyValueObject: (keyValueObject: { [key: string]: string }) => dispatch(ownProps.actionCreator!(keyValueObject)),
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(KeyValueEditor as any);
|
|
@ -0,0 +1,21 @@
|
|||
.Resizable {
|
||||
position: relative;
|
||||
border: 2px lightgray;
|
||||
border-style: none solid;
|
||||
overflow: auto;
|
||||
transition: width 200ms;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.drag {
|
||||
position: relative;
|
||||
width: 4px;
|
||||
cursor: w-resize;
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import './Resizable.css';
|
||||
|
||||
|
||||
interface IComponentProperties {
|
||||
isRightPanel?: boolean,
|
||||
visible?: boolean,
|
||||
}
|
||||
|
||||
interface IComponentStates {
|
||||
isResizing: boolean,
|
||||
resizeWidth: number,
|
||||
};
|
||||
|
||||
export default class Resizable extends React.Component<IComponentProperties, IComponentStates> {
|
||||
public static defaultProps: Partial<IComponentProperties> = {
|
||||
isRightPanel: false,
|
||||
visible: true,
|
||||
};
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isResizing: false,
|
||||
resizeWidth: 350,
|
||||
}
|
||||
}
|
||||
public render() {
|
||||
// TODO Have a better resizable box (that can be resized by clicking and
|
||||
// draging the corners) instead of CSS' resize: horizontal property
|
||||
// TODO Have a direction (horizontal/vertical) option
|
||||
const style: React.CSSProperties = {};
|
||||
if (!this.props.visible) {
|
||||
style.width = style.minWidth = '0px';
|
||||
style.pointerEvents = 'none';
|
||||
}
|
||||
else {
|
||||
style.width = this.state.resizeWidth;
|
||||
}
|
||||
return (
|
||||
<div className="container" style={style} >
|
||||
<div className="drag"
|
||||
onMouseDown= {this.whenMouseDown}
|
||||
/>
|
||||
<div className='Resizable' >
|
||||
{this.props.children}
|
||||
</div>
|
||||
<div className="drag"
|
||||
onMouseDown= {this.whenMouseDownRight}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
private whenMouseDown = () => {
|
||||
this.setState({isResizing: true});
|
||||
window.addEventListener('mousemove', this.whenMouseMove, false);
|
||||
window.addEventListener('mouseup', this.whenMouseUp, false);
|
||||
}
|
||||
private whenMouseUp = () => {
|
||||
this.setState({isResizing: false});
|
||||
window.removeEventListener('mousemove', this.whenMouseMove, false);
|
||||
window.removeEventListener('mouseup', this.whenMouseUp, false);
|
||||
}
|
||||
private whenMouseMove = (e: MouseEvent) => {
|
||||
if(!(this.state.isResizing && this.props.isRightPanel)){
|
||||
return;
|
||||
}
|
||||
else {
|
||||
this.setState({resizeWidth: window.innerWidth - e.clientX});
|
||||
}
|
||||
}
|
||||
|
||||
private whenMouseDownRight = () => {
|
||||
this.setState({isResizing: true});
|
||||
window.addEventListener('mousemove', this.whenMouseMoveRight, false);
|
||||
window.addEventListener('mouseup', this.whenMouseUpRight, false);
|
||||
}
|
||||
|
||||
private whenMouseUpRight = () => {
|
||||
this.setState({isResizing: false});
|
||||
window.removeEventListener('mousemove', this.whenMouseMoveRight, false);
|
||||
window.removeEventListener('mouseup', this.whenMouseUpRight, false);
|
||||
}
|
||||
|
||||
private whenMouseMoveRight = (e: MouseEvent) => {
|
||||
if(!(this.state.isResizing && !this.props.isRightPanel)){
|
||||
return;
|
||||
}
|
||||
else{
|
||||
this.setState({resizeWidth: e.clientX});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import * as actions from './actions';
|
||||
import { IMetadataProps, IProperties } from './state';
|
||||
|
||||
export const setFile = (file?: File) => ({
|
||||
file,
|
||||
type: actions.SET_FILE,
|
||||
})
|
||||
|
||||
export const setInputs = (inputs?: { [key: string]: any }) => ({
|
||||
inputs,
|
||||
type: actions.SET_INPUTS,
|
||||
})
|
||||
|
||||
export const setMetadataProps = (metadataProps: IMetadataProps) => ({
|
||||
metadataProps,
|
||||
type: actions.SET_METADATA_PROPS,
|
||||
})
|
||||
|
||||
export const setModelInputs = (modelInputs?: string[]) => ({
|
||||
modelInputs,
|
||||
type: actions.SET_MODEL_INPUTS,
|
||||
})
|
||||
|
||||
export const setModelOutputs = (modelOutputs?: string[]) => ({
|
||||
modelOutputs,
|
||||
type: actions.SET_MODEL_OUTPUTS,
|
||||
})
|
||||
|
||||
export const setNodes = (nodes?: { [key: string]: any }) => ({
|
||||
nodes,
|
||||
type: actions.SET_NODES,
|
||||
})
|
||||
|
||||
export const setOutputs = (outputs?: { [key: string]: any }) => ({
|
||||
outputs,
|
||||
type: actions.SET_OUTPUTS,
|
||||
})
|
||||
|
||||
export const setProperties = (properties: IProperties) => ({
|
||||
properties,
|
||||
type: actions.SET_PROPERTIES,
|
||||
})
|
||||
|
||||
export const setSaveFileName = (saveFileName?: string) => ({
|
||||
saveFileName,
|
||||
type: actions.SET_SAVE_FILE_NAME,
|
||||
})
|
||||
|
||||
export const setSelectedNode = (selectedNode?: string) => ({
|
||||
selectedNode,
|
||||
type: actions.SET_SELECTED_NODE,
|
||||
})
|
|
@ -0,0 +1,29 @@
|
|||
import { IMetadataProps, IProperties } from './state';
|
||||
|
||||
export const SET_FILE = 'SET_FILE';
|
||||
export const SET_INPUTS = 'SET_INPUTS';
|
||||
export const SET_METADATA_PROPS = 'SET_METADATA_PROPS';
|
||||
export const SET_MODEL_INPUTS = 'SET_MODEL_INPUTS';
|
||||
export const SET_MODEL_OUTPUTS = 'SET_MODEL_OUTPUTS';
|
||||
export const SET_NODES = 'SET_NODES';
|
||||
export const SET_OUTPUTS = 'SET_OUTPUTS';
|
||||
export const SET_PROPERTIES = 'SET_PROPERTIES';
|
||||
export const SET_SAVE_FILE_NAME = 'SET_SAVE_FILE_NAME';
|
||||
export const SET_SELECTED_NODE = 'SET_SELECTED_NODE';
|
||||
|
||||
export interface IAction {
|
||||
file: File,
|
||||
nodes: { [key: string]: any },
|
||||
saveFileName: string,
|
||||
type: string,
|
||||
|
||||
inputs: any[],
|
||||
outputs: any[],
|
||||
|
||||
modelInputs: string[],
|
||||
modelOutputs: string[],
|
||||
|
||||
metadataProps: IMetadataProps,
|
||||
properties: IProperties,
|
||||
selectedNode: string,
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { IAction, SET_INPUTS, SET_METADATA_PROPS, SET_OUTPUTS } from '../actions';
|
||||
import { ModelProtoSingleton } from './modelProto';
|
||||
|
||||
export const protoMiddleware = (store: any) => (next: (action: IAction) => any) => (action: IAction) => {
|
||||
switch (action.type) {
|
||||
case SET_INPUTS:
|
||||
ModelProtoSingleton.setInputs(action.inputs);
|
||||
break;
|
||||
case SET_METADATA_PROPS:
|
||||
ModelProtoSingleton.setMetadata(action.metadataProps);
|
||||
break;
|
||||
case SET_OUTPUTS:
|
||||
ModelProtoSingleton.setOutputs(action.outputs);
|
||||
break;
|
||||
}
|
||||
return next(action);
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import { IMetadataProps } from '../state';
|
||||
import { Proto } from './proto';
|
||||
|
||||
|
||||
class ModelProto extends Proto {
|
||||
public setInputs(inputs: { [key: string]: any }) {
|
||||
if (!Proto.getOnnx() || !this.proto) {
|
||||
return;
|
||||
}
|
||||
this.proto.graph.input = Object.keys(inputs).map((name: string) => ({ name, ...inputs[name] }));
|
||||
}
|
||||
|
||||
public setMetadata(metadata: IMetadataProps) {
|
||||
if (!Proto.getOnnx() || !this.proto) {
|
||||
return;
|
||||
}
|
||||
this.proto.metadataProps = Object.keys(metadata).map((x: string) => {
|
||||
const entry = new Proto.types.StringStringEntryProto();
|
||||
entry.key = x;
|
||||
entry.value = metadata[x];
|
||||
return entry;
|
||||
});
|
||||
}
|
||||
public setOutputs(outputs: { [key: string]: any }) {
|
||||
if (!Proto.getOnnx() || !this.proto) {
|
||||
return;
|
||||
}
|
||||
this.proto.graph.output = Object.keys(outputs).map((name: string) => ({ name, ...outputs[name] }));
|
||||
}
|
||||
|
||||
public serialize() {
|
||||
Proto.getOnnx();
|
||||
const clone = Proto.types.ModelProto.fromObject(this.proto);
|
||||
const writer = Proto.types.ModelProto.encode(clone);
|
||||
return writer.finish();
|
||||
}
|
||||
}
|
||||
|
||||
export const ModelProtoSingleton = new ModelProto();
|
|
@ -0,0 +1,27 @@
|
|||
const browserGlobal = window as any;
|
||||
|
||||
export class Proto {
|
||||
public static types: {
|
||||
ModelProto: any,
|
||||
StringStringEntryProto: any,
|
||||
TypeProto: any,
|
||||
ValueInfoProto: any,
|
||||
};
|
||||
|
||||
public static getOnnx() {
|
||||
if (!this.onnx) {
|
||||
if (!browserGlobal.protobuf) {
|
||||
throw new Error("Can't find protobuf module");
|
||||
}
|
||||
if (!browserGlobal.protobuf.roots.onnx) {
|
||||
return;
|
||||
}
|
||||
this.onnx = browserGlobal.protobuf.roots.onnx.onnx;
|
||||
this.types = this.onnx;
|
||||
}
|
||||
return this.onnx;
|
||||
}
|
||||
|
||||
private static onnx: any;
|
||||
public proto: any;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import * as actions from './actions';
|
||||
import IState from './state';
|
||||
|
||||
export function rootReducer(state: IState, action: actions.IAction) {
|
||||
state = state || {};
|
||||
switch (action.type) {
|
||||
case actions.SET_FILE:
|
||||
return { ...state, file: action.file };
|
||||
case actions.SET_INPUTS:
|
||||
return { ...state, inputs: action.inputs };
|
||||
case actions.SET_OUTPUTS:
|
||||
return { ...state, outputs: action.outputs };
|
||||
case actions.SET_METADATA_PROPS:
|
||||
return { ...state, metadataProps: action.metadataProps };
|
||||
case actions.SET_MODEL_INPUTS:
|
||||
return { ...state, modelInputs: action.modelInputs };
|
||||
case actions.SET_MODEL_OUTPUTS:
|
||||
return { ...state, modelOutputs: action.modelOutputs };
|
||||
case actions.SET_NODES:
|
||||
return { ...state, nodes: action.nodes };
|
||||
case actions.SET_PROPERTIES:
|
||||
return { ...state, properties: action.properties };
|
||||
case actions.SET_SAVE_FILE_NAME:
|
||||
return { ...state, saveFileName: action.saveFileName };
|
||||
case actions.SET_SELECTED_NODE:
|
||||
return { ...state, selectedNode: action.selectedNode };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
export interface IMetadataProps {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export interface IProperties {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export default interface IState {
|
||||
file: File,
|
||||
saveFileName: string,
|
||||
|
||||
inputs: { [key: string]: any },
|
||||
metadataProps: IMetadataProps,
|
||||
modelInputs: string[],
|
||||
modelOutputs: string[],
|
||||
nodes: { [key: string]: any },
|
||||
outputs: { [key: string]: any },
|
||||
properties: IProperties,
|
||||
selectedNode: string,
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { applyMiddleware, createStore } from 'redux'
|
||||
|
||||
import { protoMiddleware } from './proto/middleware';
|
||||
import { rootReducer } from './reducers';
|
||||
|
||||
const browserGlobal = window as any;
|
||||
|
||||
const store = createStore(rootReducer, browserGlobal.__REDUX_DEVTOOLS_EXTENSION__ && browserGlobal.__REDUX_DEVTOOLS_EXTENSION__(), applyMiddleware(protoMiddleware));
|
||||
export default store;
|
|
@ -0,0 +1,92 @@
|
|||
const {app, protocol, BrowserWindow} = require('electron');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
|
||||
let mainWindow;
|
||||
|
||||
if(require('electron-squirrel-startup')) {
|
||||
return;
|
||||
}
|
||||
function interceptFileProtocol() {
|
||||
// Intercept the file protocol so that references to folders return its index.html file
|
||||
const fileProtocol = 'file';
|
||||
const cwd = process.cwd();
|
||||
protocol.interceptFileProtocol(fileProtocol, (request, callback) => {
|
||||
const fileUrl = new url.URL(request.url);
|
||||
const hostname = decodeURI(fileUrl.hostname);
|
||||
const filePath = decodeURI(fileUrl.pathname);
|
||||
let resolvedPath = path.normalize(filePath);
|
||||
if (resolvedPath[0] === '\\') {
|
||||
// Remove URL host to pathname separator
|
||||
resolvedPath = resolvedPath.substr(1);
|
||||
}
|
||||
if (hostname) {
|
||||
resolvedPath = path.join(hostname, resolvedPath);
|
||||
if (process.platform === 'win32') { // File is on a share
|
||||
resolvedPath = `\\\\${resolvedPath}`;
|
||||
}
|
||||
}
|
||||
resolvedPath = path.relative(cwd, resolvedPath);
|
||||
try {
|
||||
if (fs.statSync(resolvedPath).isDirectory) {
|
||||
let index = path.posix.join(resolvedPath, 'index.html');
|
||||
if (fs.existsSync(index)) {
|
||||
resolvedPath = index;
|
||||
}
|
||||
}
|
||||
} catch(_) {
|
||||
// Use path as is if it can't be accessed
|
||||
}
|
||||
callback({
|
||||
path: resolvedPath,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
interceptFileProtocol();
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
height: 600,
|
||||
width: 800,
|
||||
});
|
||||
global.mainWindow = mainWindow;
|
||||
|
||||
let pageUrl;
|
||||
for (const arg of process.argv.slice(1)) {
|
||||
if (arg.includes('://')) {
|
||||
pageUrl = arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pageUrl === undefined) {
|
||||
pageUrl = url.format({
|
||||
pathname: path.join(__dirname, '../build/'),
|
||||
protocol: 'file',
|
||||
});
|
||||
}
|
||||
mainWindow.loadURL(pageUrl);
|
||||
|
||||
if (process.argv.includes('--dev-tools')) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
app.on('ready', createWindow);
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (mainWindow === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
**Files in this folder and public/fabric-icons.woff are auto generated from https://uifabricicons.azurewebsites.net/**
|
||||
|
||||
If you want to add icons, regenerate the font file in the above link.
|
||||
|
||||
The icons currently in use are:
|
||||
|
||||
* E70D
|
||||
* E710
|
||||
* E711
|
||||
* EA39
|
|
@ -0,0 +1,36 @@
|
|||
// Your use of the content in the files referenced here is subject to the terms of the license at https://aka.ms/fabric-assets-license
|
||||
|
||||
// tslint:disable:max-line-length
|
||||
|
||||
import {
|
||||
IIconOptions,
|
||||
IIconSubset,
|
||||
registerIcons
|
||||
} from '@uifabric/styling';
|
||||
|
||||
export function initializeIcons(
|
||||
baseUrl: string = '',
|
||||
options?: IIconOptions
|
||||
): void {
|
||||
const subset: IIconSubset = {
|
||||
fontFace: {
|
||||
fontFamily: `"FabricMDL2Icons"`,
|
||||
src: `url('${baseUrl}fabric-icons.woff') format('woff')`
|
||||
},
|
||||
icons: {
|
||||
'Add': '\uE710',
|
||||
'Cancel': '\uE711',
|
||||
'ChevronDown': '\uE70D',
|
||||
'ErrorBadge': '\uEA39',
|
||||
},
|
||||
style: {
|
||||
MozOsxFontSmoothing: 'grayscale',
|
||||
WebkitFontSmoothing: 'antialiased',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: 'normal',
|
||||
speak: 'none'
|
||||
},
|
||||
};
|
||||
|
||||
registerIcons(subset, options);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { registerIconAlias } from '@uifabric/styling';
|
||||
|
||||
export const registerIconAliases = () => {
|
||||
registerIconAlias('trash', 'delete');
|
||||
registerIconAlias('onedrive', 'onedrivelogo');
|
||||
registerIconAlias('alertsolid12', 'eventdatemissed12');
|
||||
}
|
||||
|
||||
export default registerIconAliases;
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,18 @@
|
|||
import { initializeIcons as i } from './fabric-icons';
|
||||
|
||||
import { IIconOptions } from '@uifabric/styling';
|
||||
import { registerIconAliases } from './iconAliases';
|
||||
const DEFAULT_BASE_URL = '';
|
||||
|
||||
export function initializeIcons(
|
||||
baseUrl: string = DEFAULT_BASE_URL,
|
||||
options?: IIconOptions
|
||||
): void {
|
||||
[i].forEach(
|
||||
(initialize: (url: string, options?: IIconOptions) => void) => initialize(baseUrl, options)
|
||||
);
|
||||
|
||||
registerIconAliases();
|
||||
}
|
||||
|
||||
export { IconNames } from './iconNames';
|
|
@ -0,0 +1,5 @@
|
|||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
import App from './App';
|
||||
import store from './datastore/store';
|
||||
import './index.css';
|
||||
import { registerKeyboardShurtcuts } from './native/menu';
|
||||
import registerServiceWorker from './registerServiceWorker';
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
registerKeyboardShurtcuts();
|
||||
registerServiceWorker();
|
|
@ -0,0 +1,17 @@
|
|||
# Python
|
||||
|
||||
Python is used to run the ONNX converters.
|
||||
|
||||
## How is it installed
|
||||
|
||||
The dashboard needs a Python distribution to install the converter and its dependencies. An isolated Python environment is used to avoid interactions with the system-wide Python distribution.
|
||||
|
||||
There are two possibilities to create a Python environment. On the first run of the converter, it will ask the user to choose one:
|
||||
|
||||
* Use an existing Python installation. The dashboard looks for executables `python3`, `py` and `python` and, for any of them with `venv` support, lists it as a candidate for installation.
|
||||
* If this option is chosen, a virtual environment is created using `venv`. All packages are installed in the environment and system-wide packages are not acessible.
|
||||
* Download an embedded version of Python (Windows only)
|
||||
* If this option is chosen, the embeddable zip file release of Python is downloaded from the [Windows releases page](https://www.python.org/downloads/windows/). This version is a zip file with Python and its standard libraries. It's only available for Windows (due to its lack of a standardized package manager to install Python from).
|
||||
* The file is unzipped. The embedded version comes without `pip`, so we install `pip` into it manually. We also change the Python library search path to include pip packages, so that packages can be imported from scripts run with this interpreter.
|
||||
|
||||
After either of the above choices, `pip install -r requirements.txt` is run to install the packages specified in `requirements.txt`.
|
|
@ -0,0 +1,24 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const nodeProcess = process;
|
||||
const env = nodeProcess.env;
|
||||
|
||||
export const appData = env.APPDATA ||
|
||||
path.join(env.HOME || '/tmp', process.platform === 'darwin' ? 'Library/Preferences' : '.local/share');
|
||||
|
||||
export function mkdir(...directory: string[]) {
|
||||
const joined = path.join(...directory);
|
||||
if (fs.exists && !fs.existsSync(joined)) { // skips if running in the web
|
||||
fs.mkdirSync(joined);
|
||||
}
|
||||
return joined;
|
||||
}
|
||||
|
||||
export function packagedFile(...filePath: string[]) {
|
||||
// Return a path to a file packaged in the application
|
||||
return path.join(__filename, ...filePath);
|
||||
}
|
||||
|
||||
// Point to the root if running in the web
|
||||
export const winmlDataFolder = fs.exists ? mkdir(appData, 'WinMLDashboard') : '/';
|
|
@ -0,0 +1,9 @@
|
|||
import { fileFilterToAccept } from "./dialog";
|
||||
|
||||
it('converts Array<FileFilter> to HTML accept field', () => {
|
||||
expect(fileFilterToAccept([
|
||||
{ name: 'CoreML model', extensions: [ 'mlmodel' ] },
|
||||
{ name: 'Keras model', extensions: [ 'keras', 'h5' ] },
|
||||
{ name: 'ONNX model', extensions: [ 'onnx' ] },
|
||||
])).toBe('.mlmodel,.keras,.h5,.onnx');
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* Provide dialogs, abstracting whether in web or Electron.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { getElectron } from './util';
|
||||
|
||||
export function fileFilterToAccept(filters: Electron.FileFilter[]) {
|
||||
/**
|
||||
* Convert Electron FileFilter[] to HTML accept field.
|
||||
*/
|
||||
return filters.map(x => x.extensions.map(extension => `.${extension}`).join(',')).join(',');
|
||||
}
|
||||
|
||||
export function showWebOpenDialog(accept: string) {
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('type', 'file');
|
||||
input.setAttribute('accept', accept);
|
||||
return new Promise<FileList>(resolve => {
|
||||
input.addEventListener('change', () => resolve(input.files || undefined));
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
||||
export function showNativeOpenDialog(options: Electron.OpenDialogOptions) {
|
||||
const { dialog } = getElectron().remote;
|
||||
return new Promise<string[]>(resolve => dialog.showOpenDialog(options, resolve));
|
||||
}
|
||||
|
||||
export function populateFileFields(fileLikeObject: any, filePath: string) {
|
||||
fileLikeObject.lastModified = 0;
|
||||
fileLikeObject.name = path.basename(filePath);
|
||||
fileLikeObject.path = path.resolve(filePath);
|
||||
return fileLikeObject as File;
|
||||
}
|
||||
|
||||
export function fileFromPath(filePath: string) {
|
||||
// Make the native dialogs return an object that acts like the File object returned in HTML forms.
|
||||
// This way, all functions that use dialogs can have a single code path instead of one for the web
|
||||
// and one for Electron.
|
||||
// "new File(fs.readFileSync(x), path.basename(x))" followed by "file.path = x" doesn't work because
|
||||
// the path field is read-only. Instead, we create a Blob and manually add the remaining fields (per
|
||||
// https://www.w3.org/TR/FileAPI/#dfn-file and an extra "path" field, containing the real path).
|
||||
const file = new Blob([fs.readFileSync(filePath)]);
|
||||
return populateFileFields(file, filePath);
|
||||
}
|
||||
|
||||
export async function showOpenDialog(filters: Electron.FileFilter[]) {
|
||||
if (getElectron()) {
|
||||
const paths = await showNativeOpenDialog({ filters });
|
||||
return paths.map(fileFromPath);
|
||||
}
|
||||
return showWebOpenDialog(fileFilterToAccept(filters));
|
||||
}
|
||||
|
||||
export function showWebSaveDialog(data: Uint8Array, filename: string) {
|
||||
// This function could support more flags (e.g. multiple and directory selections):
|
||||
// https://stackoverflow.com/questions/12942436/how-to-get-folder-directory-from-html-input-type-file-or-any-other-way
|
||||
const blob = new Blob([data], {type: 'application/octet-stream'});
|
||||
const anchor = document.createElement('a');
|
||||
anchor.download = filename;
|
||||
|
||||
anchor.href = URL.createObjectURL(blob);
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
URL.revokeObjectURL(anchor.href);
|
||||
}
|
||||
|
||||
export function showNativeSaveDialog(options: Electron.SaveDialogOptions) {
|
||||
const { dialog } = getElectron().remote;
|
||||
return new Promise<string>(resolve => dialog.showSaveDialog(options, resolve));
|
||||
}
|
||||
|
||||
export async function save(data: Uint8Array, filename: string, filters: Electron.FileFilter[]) {
|
||||
if (getElectron()) {
|
||||
if (!path.isAbsolute(filename)) {
|
||||
// If a suggested filename is given instead of a full path, show a save dialog
|
||||
filename = await showNativeSaveDialog({ defaultPath: filename, filters });
|
||||
if (!filename) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Save contents
|
||||
fs.writeFileSync(filename, new Buffer(data));
|
||||
return filename;
|
||||
}
|
||||
showWebSaveDialog(data, filename);
|
||||
return;
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
/**
|
||||
* Provide native menus and shortcuts. In the web, installs shortcuts only.
|
||||
*/
|
||||
|
||||
import { setFile, setSaveFileName } from "../datastore/actionCreators";
|
||||
import { ModelProtoSingleton } from "../datastore/proto/modelProto";
|
||||
import store from "../datastore/store";
|
||||
import { save, showOpenDialog } from "./dialog";
|
||||
import { isWeb } from "./util";
|
||||
|
||||
export function createMenu(electron: typeof Electron) {
|
||||
const { Menu } = electron.remote;
|
||||
const template = [
|
||||
{
|
||||
label: 'File',
|
||||
submenu: [
|
||||
{ label: 'Open', accelerator: 'Ctrl+O', click: onOpen },
|
||||
{ label: 'Save', accelerator: 'Ctrl+S', click: onSave, enabled: false },
|
||||
],
|
||||
}, {
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{ role: 'undo' },
|
||||
{ role: 'redo' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'cut' },
|
||||
{ role: 'copy' },
|
||||
{ role: 'paste' },
|
||||
{ role: 'pasteandmatchstyle' },
|
||||
{ role: 'delete' },
|
||||
{ role: 'selectall '},
|
||||
],
|
||||
}, {
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{ role: 'toggledevtools' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetzoom' },
|
||||
{ role: 'zoomin' },
|
||||
{ role: 'zoomout' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen '},
|
||||
],
|
||||
}, {
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Third Party Notice',
|
||||
click() {
|
||||
const path = require('path');
|
||||
require('electron').shell.openItem(path.join('file://', process.cwd(), 'ThirdPartyNotice.txt'));
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
];
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
template.unshift({
|
||||
label: 'WinML Dashboard',
|
||||
submenu: [
|
||||
{ role: 'about' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'services', submenu: [] },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideothers' },
|
||||
{ role: 'unhide' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit '},
|
||||
],
|
||||
} as any);
|
||||
}
|
||||
|
||||
const menu = Menu.buildFromTemplate(template as any);
|
||||
store.subscribe(() => (menu.items[0] as any).submenu.items[1].enabled = !!store.getState().nodes);
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
|
||||
export function registerKeyboardShurtcuts() {
|
||||
if (isWeb()) { // Electron's accelerators are used outside web builds
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.ctrlKey) {
|
||||
switch (event.code) {
|
||||
case 'KeyS':
|
||||
onSave();
|
||||
break;
|
||||
|
||||
case 'KeyO':
|
||||
onOpen();
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (!ModelProtoSingleton.proto) {
|
||||
return;
|
||||
}
|
||||
const suggestedPath = store.getState().saveFileName || 'model.onnx';
|
||||
const selectedPath = await save(ModelProtoSingleton.serialize(), suggestedPath,
|
||||
[{ name: 'ONNX model', extensions: [ 'onnx', 'prototxt' ] }]);
|
||||
if (selectedPath && selectedPath !== suggestedPath) {
|
||||
store.dispatch(setSaveFileName(suggestedPath));
|
||||
}
|
||||
}
|
||||
|
||||
async function onOpen() {
|
||||
const files = await showOpenDialog([
|
||||
{ name: 'ONNX Model', extensions: [ 'onnx', 'pb' ] },
|
||||
{ name: 'Keras Model', extensions: [ 'h5', 'json', 'keras' ] },
|
||||
{ name: 'CoreML Model', extensions: [ 'mlmodel' ] },
|
||||
{ name: 'Caffe Model', extensions: [ 'caffemodel' ] },
|
||||
{ name: 'Caffe2 Model', extensions: [ 'pb' ] },
|
||||
{ name: 'MXNet Model', extensions: [ 'model', 'json' ] },
|
||||
{ name: 'TensorFlow Graph', extensions: [ 'pb', 'meta' ] },
|
||||
{ name: 'TensorFlow Saved Model', extensions: [ 'pb' ] },
|
||||
{ name: 'TensorFlow Lite Model', extensions: [ 'tflite' ] }
|
||||
]);
|
||||
if (files) {
|
||||
store.dispatch(setFile(files[0]));
|
||||
store.dispatch(setSaveFileName(files[0].path || files[0].name));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
import { execFile, ExecFileOptions, execFileSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as https from 'https';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as yauzl from 'yauzl';
|
||||
|
||||
import { mkdir, winmlDataFolder } from '../native/appData';
|
||||
|
||||
const localPython = mkdir(winmlDataFolder, 'python');
|
||||
const embeddedPythonBinary = path.join(localPython, 'python.exe');
|
||||
|
||||
function filterPythonBinaries(binaries: string[]) {
|
||||
return [...new Set(binaries.reduce((acc, x) => {
|
||||
if (x === embeddedPythonBinary && fs.existsSync(x)) { // Install directly (without a venv) in embedded Python installations
|
||||
acc.push(x);
|
||||
} else {
|
||||
try {
|
||||
const binary = execFileSync(x, ['-c', 'import venv; import sys; print(sys.executable)'], { encoding: 'utf8' });
|
||||
acc.push(binary.trim());
|
||||
} catch(_) {
|
||||
// Ignore binary if unavailable or no venv support
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]))];
|
||||
}
|
||||
|
||||
export function getPythonBinaries() {
|
||||
const binaries = filterPythonBinaries(['python3', 'py', 'python']) as Array<string | null>;
|
||||
if (process.platform === 'win32') {
|
||||
binaries.push(null); // use null to represent local, embedded version
|
||||
}
|
||||
return binaries;
|
||||
}
|
||||
|
||||
async function unzip(buffer: Buffer, directory: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
yauzl.fromBuffer(buffer, (err, zipfile) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
zipfile!.once('end', resolve);
|
||||
zipfile!.once('error', reject);
|
||||
zipfile!.on('entry', entry => {
|
||||
const destination = path.join(directory, entry.fileName);
|
||||
if (entry.fileName.endsWith('/')) {
|
||||
if (fs.existsSync(destination)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
return fs.mkdirSync(destination);
|
||||
} catch (e) {
|
||||
return reject(e);
|
||||
}
|
||||
}
|
||||
zipfile!.openReadStream(entry, (error, readStream) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
readStream!.once('error', reject);
|
||||
readStream!.pipe(fs.createWriteStream(destination));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadBinaryFile(url: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const data: Buffer[] = [];
|
||||
https.get(url, response => {
|
||||
response
|
||||
.on('data', (x: string) => data.push(Buffer.from(x, 'binary')))
|
||||
.on('end', async () => {
|
||||
resolve(Buffer.concat(data));
|
||||
})
|
||||
.on('error', reject);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Change the PTH to discover modules from Lib/site-packages, so that pip modules can be found
|
||||
const pythonPth = `Lib/site-packages
|
||||
python36.zip
|
||||
.
|
||||
# Uncomment to run site.main() automatically
|
||||
import site`;
|
||||
|
||||
export async function downloadPython() {
|
||||
if (process.platform !== 'win32') {
|
||||
throw Error('Unsupported platform');
|
||||
}
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const data = await downloadBinaryFile('https://www.python.org/ftp/python/3.6.6/python-3.6.6-embed-amd64.zip') as Buffer;
|
||||
await unzip(data, localPython);
|
||||
fs.writeFileSync(path.join(localPython, 'python36._pth'), pythonPth);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
export async function downloadPip(listener?: IOutputListener) {
|
||||
// Python embedded distribution for Windows doesn't have pip
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const installer = path.join(localPython, 'get-pip.py');
|
||||
try {
|
||||
const data = await downloadBinaryFile('https://bootstrap.pypa.io/get-pip.py') as Buffer;
|
||||
fs.writeFileSync(installer, data);
|
||||
await python([installer], {}, listener);
|
||||
} catch (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
export function getLocalPython() { // Get the local (embedded or venv) Python
|
||||
return filterPythonBinaries([embeddedPythonBinary, path.join(localPython, 'Scripts/python'), path.join(localPython, 'bin/python')])[0];
|
||||
}
|
||||
|
||||
interface IOutputListener {
|
||||
stdout: (output: string) => void,
|
||||
stderr: (output: string) => void,
|
||||
}
|
||||
|
||||
async function execFilePromise(file: string, args: string[], options?: ExecFileOptions, listener?: IOutputListener) {
|
||||
const run = async () => new Promise((resolve, reject) => {
|
||||
const childProcess = execFile(file, args, {...options});
|
||||
if (listener) {
|
||||
childProcess.stdout.on('data', listener.stdout);
|
||||
childProcess.stderr.on('data', listener.stderr);
|
||||
}
|
||||
childProcess.on('exit', (code, signal) => {
|
||||
if (code !== 0) {
|
||||
return reject(Error(`Child process ${file} ${code !== null ? `exited with code ${code}` : `killed by signal ${signal}`}`));
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
for (let i = 0; i < 10; i++) {
|
||||
try {
|
||||
return await run();
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') { // ignoring possible spurious ENOENT in child process invocation
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
return run();
|
||||
}
|
||||
|
||||
export async function python(command: string[], options?: ExecFileOptions, listener?: IOutputListener) {
|
||||
const binary = getLocalPython();
|
||||
if (!binary) {
|
||||
throw Error('Failed to find local Python');
|
||||
}
|
||||
return execFilePromise(binary, command, options, listener);
|
||||
}
|
||||
|
||||
export async function pip(command: string[], listener?: IOutputListener) {
|
||||
let options;
|
||||
if (getLocalPython() === embeddedPythonBinary) {
|
||||
options = { cwd: os.tmpdir() }; // without this workaround, pip downloads packages to whatever the current working directory is
|
||||
}
|
||||
return python(['-m', 'pip', ...command], options, listener);
|
||||
}
|
||||
|
||||
export async function installVenv(targetPython: string, listener?: IOutputListener) {
|
||||
await execFilePromise(targetPython, ['-m', 'venv', localPython], {}, listener);
|
||||
await pip(['install', '-U', 'pip'], listener);
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import * as fs from 'fs';
|
||||
|
||||
import { createMenu } from './menu';
|
||||
|
||||
export function isWeb() {
|
||||
return !fs.exists || typeof it === 'function';
|
||||
}
|
||||
|
||||
let electron: typeof Electron;
|
||||
if (!isWeb()) {
|
||||
import('electron')
|
||||
.then(mod => { electron = mod; createMenu(electron); })
|
||||
// tslint:disable-next-line:no-console
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
export function getElectron() {
|
||||
return electron;
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
// In production, we register a service worker to serve assets from local cache.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on the "N+1" visit to a page, since previously
|
||||
// cached resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
|
||||
// This link also includes instructions on opting out of this behavior.
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
export default function register() {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Lets check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://goo.gl/SC7cgQ'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not local host. Just register service worker
|
||||
registerValidSW(swUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the old content will have been purged and
|
||||
// the fresh content will have been added to the cache.
|
||||
// It's the perfect time to display a "New content is
|
||||
// available; please refresh." message in your web app.
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('New content is available; please refresh.');
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Content is cached for offline use.');
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl)
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
if (
|
||||
response.status === 404 ||
|
||||
response.headers.get('content-type').indexOf('javascript') === -1
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
export default {
|
||||
// https://github.com/onnx/onnx/blob/master/docs/MetadataProps.md
|
||||
type: 'object',
|
||||
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
'Image.BitmapPixelFormat': {
|
||||
enum: [
|
||||
'Gray8',
|
||||
'Rgb8',
|
||||
'Bgr8',
|
||||
'Rgba8',
|
||||
'Bgra8',
|
||||
],
|
||||
},
|
||||
'Image.ColorSpaceGamma': {
|
||||
enum: [
|
||||
'Linear',
|
||||
'SRGB',
|
||||
],
|
||||
},
|
||||
'Image.NominalPixelRange': {
|
||||
enum: [
|
||||
'NominalRange_0_255',
|
||||
'Normalized_0_1',
|
||||
'Normalized_1_1',
|
||||
'NominalRange_16_235',
|
||||
],
|
||||
},
|
||||
|
||||
author: {
|
||||
type: 'string',
|
||||
},
|
||||
company: {
|
||||
type: 'string',
|
||||
},
|
||||
converted_from: {
|
||||
type: 'string',
|
||||
},
|
||||
licence: {
|
||||
type: 'string',
|
||||
},
|
||||
licence_url: {
|
||||
format: 'uri',
|
||||
type: 'string',
|
||||
},
|
||||
model_author: {
|
||||
type: 'string',
|
||||
},
|
||||
model_license: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
.ConvertView {
|
||||
margin: 0px 10px;
|
||||
text-align: initial;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ModelConvertBrowser {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ms-TextField, .ConvertViewControls {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ConvertViewControls, .ConverterViewConsole {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ConverterViewConsole {
|
||||
display: block;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
#ConverterModelInputBrowse {
|
||||
margin: 24px 5px 0px;
|
||||
}
|
||||
|
||||
#ConvertButton {
|
||||
margin: 5px 0px;
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { DefaultButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { ChoiceGroup, IChoiceGroupOption } from 'office-ui-fabric-react/lib/ChoiceGroup';
|
||||
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
|
||||
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import { TextField } from 'office-ui-fabric-react/lib/TextField';
|
||||
|
||||
import Collapsible from '../../components/Collapsible';
|
||||
import { setFile, setSaveFileName } from '../../datastore/actionCreators';
|
||||
import IState from '../../datastore/state';
|
||||
import { packagedFile } from '../../native/appData';
|
||||
import { fileFromPath, showNativeOpenDialog, showNativeSaveDialog } from '../../native/dialog';
|
||||
import { downloadPip, downloadPython, getLocalPython, getPythonBinaries, installVenv, pip, python } from '../../native/python';
|
||||
import { isWeb } from '../../native/util';
|
||||
|
||||
|
||||
import './View.css';
|
||||
|
||||
enum Step {
|
||||
Idle,
|
||||
Downloading,
|
||||
GetPip,
|
||||
CreatingVenv,
|
||||
InstallingRequirements,
|
||||
Converting,
|
||||
}
|
||||
|
||||
interface IComponentProperties {
|
||||
// Redux properties
|
||||
file: File,
|
||||
setFile: typeof setFile,
|
||||
setSaveFileName: typeof setSaveFileName,
|
||||
}
|
||||
|
||||
interface IComponentState {
|
||||
console: string,
|
||||
currentStep: Step,
|
||||
error?: Error | string,
|
||||
source?: string,
|
||||
}
|
||||
|
||||
class ConvertView extends React.Component<IComponentProperties, IComponentState> {
|
||||
private localPython?: string;
|
||||
|
||||
constructor(props: IComponentProperties) {
|
||||
super(props);
|
||||
const error = isWeb() ? "The converter can't be run in the web interface" : undefined;
|
||||
this.state = { console: '', error, currentStep: Step.Idle };
|
||||
}
|
||||
|
||||
public render() {
|
||||
const collabsibleRef: React.RefObject<Collapsible> = React.createRef();
|
||||
return (
|
||||
<div className='ConvertView'>
|
||||
<div className='ConvertViewControls'>
|
||||
{this.getView()}
|
||||
</div>
|
||||
{ this.state.console &&
|
||||
<Collapsible ref={collabsibleRef} label='Console output'>
|
||||
<pre className='ConverterViewConsole'>
|
||||
{this.state.console}
|
||||
</pre>
|
||||
</Collapsible>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private getView() {
|
||||
const { error } = this.state;
|
||||
if (error) {
|
||||
const message = typeof error === 'string' ? error : (`${error.stack ? `${error.stack}: ` : ''}${error.message}`);
|
||||
return <MessageBar messageBarType={MessageBarType.error}>{message}</MessageBar>
|
||||
}
|
||||
switch (this.state.currentStep) {
|
||||
case Step.Downloading:
|
||||
return <Spinner label="Downloading Python..." />;
|
||||
case Step.GetPip:
|
||||
return <Spinner label="Getting pip in embedded Python..." />;
|
||||
case Step.CreatingVenv:
|
||||
return <Spinner label="Creating virtual environment..." />;
|
||||
case Step.InstallingRequirements:
|
||||
return <Spinner label="Downloading and installing requirements..." />;
|
||||
case Step.Converting:
|
||||
return <Spinner label="Converting..." />;
|
||||
}
|
||||
this.localPython = this.localPython || getLocalPython();
|
||||
if (!this.localPython) {
|
||||
return this.pythonChooser();
|
||||
}
|
||||
return this.converterView();
|
||||
}
|
||||
|
||||
private printMessage = (message: string) => {
|
||||
this.setState((prevState) => ({
|
||||
...prevState,
|
||||
console: prevState.console.concat(message),
|
||||
}))
|
||||
}
|
||||
|
||||
private printError = (error: string | Error) => {
|
||||
this.setState({ currentStep: Step.Idle, error });
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:member-ordering
|
||||
private outputListener = {
|
||||
stderr: this.printMessage,
|
||||
stdout: this.printMessage,
|
||||
};
|
||||
|
||||
private pythonChooser = () => {
|
||||
const binaries = getPythonBinaries();
|
||||
const options = binaries.map((key) => key ? { key, text: key } : { key: '__download', text: 'Download a new Python binary to be used exclusively by the WinML Dashboard' });
|
||||
const onChange = async (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption) => {
|
||||
try {
|
||||
if (option.key === '__download') {
|
||||
this.setState({ currentStep: Step.Downloading });
|
||||
await downloadPython();
|
||||
this.setState({ currentStep: Step.GetPip });
|
||||
await downloadPip(this.outputListener);
|
||||
} else {
|
||||
this.setState({ currentStep: Step.CreatingVenv });
|
||||
await installVenv(option.key, this.outputListener);
|
||||
}
|
||||
this.setState({ currentStep: Step.InstallingRequirements });
|
||||
await pip(['install', '-r', packagedFile('requirements.txt')], this.outputListener);
|
||||
this.setState({ currentStep: Step.Idle });
|
||||
} catch (error) {
|
||||
this.printError(error);
|
||||
}
|
||||
}
|
||||
// TODO Options to reinstall environment or update dependencies
|
||||
return (
|
||||
<ChoiceGroup
|
||||
options={options}
|
||||
label={binaries[0] ? 'Suitable Python versions were found in the system. Pick one to be used by the converter.' : 'No suitable Python versions were found in the system.'}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private converterView = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className='DisplayFlex ModelConvertBrowser'>
|
||||
<TextField placeholder='Path' value={this.state.source || this.props.file && this.props.file.path} label='Model to convert' onChanged={this.setSource} />
|
||||
<DefaultButton id='ConverterModelInputBrowse' text='Browse' onClick={this.browseSource}/>
|
||||
</div>
|
||||
<DefaultButton id='ConvertButton' text='Convert' disabled={!this.state.source} onClick={this.convert}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private setSource = (source?: string) => {
|
||||
this.setState({ source })
|
||||
}
|
||||
|
||||
private browseSource = () => {
|
||||
const openDialogOptions = {
|
||||
filters: [
|
||||
{ name: 'CoreML model', extensions: [ 'mlmodel' ] },
|
||||
{ name: 'Keras model', extensions: [ 'keras', 'h5' ] },
|
||||
{ name: 'ONNX model', extensions: [ 'onnx' ] },
|
||||
],
|
||||
properties: Array<'openFile'>('openFile'),
|
||||
};
|
||||
showNativeOpenDialog(openDialogOptions)
|
||||
.then((filePaths) => {
|
||||
if (filePaths) {
|
||||
this.setSource(filePaths[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private convert = async () => {
|
||||
const source = this.state.source!;
|
||||
const destination = await showNativeSaveDialog({ filters: [{ name: 'ONNX model', extensions: ['onnx'] }, { name: 'ONNX text protobuf', extensions: ['prototxt'] }] });
|
||||
if (!destination) {
|
||||
return;
|
||||
}
|
||||
this.setState({ currentStep: Step.Converting });
|
||||
try {
|
||||
await python([packagedFile('convert.py'), source, destination], {}, this.outputListener);
|
||||
} catch (e) {
|
||||
this.printError(e);
|
||||
}
|
||||
this.setState({ currentStep: Step.Idle, source: undefined });
|
||||
// TODO Show dialog (https://developer.microsoft.com/en-us/fabric#/components/dialog) asking whether we should open the converted model
|
||||
this.props.setFile(fileFromPath(destination));
|
||||
this.props.setSaveFileName(destination);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IState) => ({
|
||||
file: state.file,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setFile,
|
||||
setSaveFileName,
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ConvertView);
|
|
@ -0,0 +1,241 @@
|
|||
import { ComboBox, IComboBoxOption } from 'office-ui-fabric-react/lib/ComboBox';
|
||||
import { ExpandingCardMode, HoverCard, IExpandingCardProps } from 'office-ui-fabric-react/lib/HoverCard';
|
||||
import { Label } from 'office-ui-fabric-react/lib/Label';
|
||||
import { TextField } from 'office-ui-fabric-react/lib/TextField';
|
||||
import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Collapsible from '../../components/Collapsible';
|
||||
import Resizable from '../../components/Resizable';
|
||||
import { setInputs, setOutputs } from '../../datastore/actionCreators';
|
||||
import { Proto } from '../../datastore/proto/proto';
|
||||
import IState from '../../datastore/state';
|
||||
|
||||
import './Panel.css';
|
||||
|
||||
interface IComponentProperties {
|
||||
// Redux properties
|
||||
inputs: { [key: string]: any },
|
||||
modelInputs: string[];
|
||||
modelOutputs: string[];
|
||||
nodes: { [key: string]: any },
|
||||
outputs: { [key: string]: any },
|
||||
selectedNode: string,
|
||||
setInputs: typeof setInputs,
|
||||
setOutputs: typeof setOutputs,
|
||||
}
|
||||
|
||||
const denotationOptions = ['', 'IMAGE', 'AUDIO', 'TEXT', 'TENSOR'].map((key: string) => ({ key, text: key }));
|
||||
const dimensionDenotationOptions = [
|
||||
'DATA_BATCH',
|
||||
'DATA_CHANNEL',
|
||||
'DATA_TIME',
|
||||
'DATA_FEATURE',
|
||||
'FILTER_IN_CHANNEL',
|
||||
'FILTER_OUT_CHANNEL',
|
||||
'FILTER_SPATIAL',
|
||||
].map((key) => ({ key, text: key }));
|
||||
|
||||
const tensorProtoDataType = [
|
||||
'UNDEFINED',
|
||||
'float',
|
||||
'uint8',
|
||||
'int8',
|
||||
'uint16',
|
||||
'int16',
|
||||
'int32',
|
||||
'int64',
|
||||
'string',
|
||||
'bool',
|
||||
'float16',
|
||||
'double',
|
||||
'uint32',
|
||||
'uint64',
|
||||
'complex64',
|
||||
'complex128',
|
||||
]
|
||||
|
||||
const getFullType = (typeProto: any): string => {
|
||||
if (typeProto.tensorType) {
|
||||
return `Tensor<${tensorProtoDataType[typeProto.tensorType.elemType]}>`;
|
||||
} else if (typeProto.sequenceType) {
|
||||
return `List<${getFullType(typeProto.sequenceType.elemType)}>`;
|
||||
} else if (typeProto.mapType) {
|
||||
return `Map<${tensorProtoDataType[typeProto.mapType.keyType]}, ${getFullType(typeProto.mapType.valueType)}>`;
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
class LeftPanel extends React.Component<IComponentProperties, {}> {
|
||||
public render() {
|
||||
return (
|
||||
<div className="Unselectable">
|
||||
<Resizable visible={this.props.selectedNode !== undefined}>
|
||||
{this.props.selectedNode !== undefined && this.getContent()}
|
||||
</Resizable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private getContent() {
|
||||
let name = 'Model Properties';
|
||||
const modelPropertiesSelected = this.props.selectedNode === name;
|
||||
let input: any[];
|
||||
let output: any[];
|
||||
if (modelPropertiesSelected) {
|
||||
input = this.props.modelInputs;
|
||||
output = this.props.modelOutputs;
|
||||
} else {
|
||||
const node = this.props.nodes[this.props.selectedNode];
|
||||
({ input, output, name } = node);
|
||||
name = `Node: ${name ? `${name} (${node.opType})` : node.opType}`;
|
||||
}
|
||||
|
||||
const inputsForm = this.buildConnectionList(input);
|
||||
const outputsForm = this.buildConnectionList(output);
|
||||
return (
|
||||
<div>
|
||||
<Label className='PanelName'>{name}</Label>
|
||||
<div className='Panel'>
|
||||
<Collapsible label='Inputs'>
|
||||
{inputsForm}
|
||||
</Collapsible>
|
||||
<Collapsible label='Outputs'>
|
||||
{outputsForm}
|
||||
</Collapsible>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private buildConnectionList = (connections: any[]) => {
|
||||
return connections.map((x: any) => {
|
||||
const valueInfoProto = this.props.inputs[x] || this.props.outputs[x];
|
||||
if (!valueInfoProto) {
|
||||
return (
|
||||
<div key={x}>
|
||||
<Label className='TensorName' disabled={true}>{`${x} (type: internal connection)`}</Label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let tensorName = (
|
||||
<div className={valueInfoProto.docString ? 'TensorDocumentationHover' : 'TensorName'}>
|
||||
<b>{x}</b><span>{` (type: ${getFullType(valueInfoProto.type)})`}</span>
|
||||
</div>
|
||||
);
|
||||
if (valueInfoProto.docString) {
|
||||
const expandingCardProps: IExpandingCardProps = {
|
||||
compactCardHeight: 100,
|
||||
mode: ExpandingCardMode.compact,
|
||||
onRenderCompactCard: (item: any): JSX.Element => (
|
||||
<p className='DocString'>{valueInfoProto.docString}</p>
|
||||
),
|
||||
};
|
||||
tensorName = (
|
||||
<HoverCard expandingCardProps={expandingCardProps} instantOpenOnClick={true}>
|
||||
{tensorName}
|
||||
</HoverCard>
|
||||
);
|
||||
}
|
||||
|
||||
const isModelInput = this.props.modelInputs.includes(x)
|
||||
const isModelOutput = this.props.modelOutputs.includes(x);
|
||||
const disabled = !isModelInput && !isModelOutput;
|
||||
const valueInfoProtoCopy = () => Proto.types.ValueInfoProto.fromObject(Proto.types.ValueInfoProto.toObject(valueInfoProto));
|
||||
const tensorDenotationChanged = (option?: IComboBoxOption, index?: number, value?: string) => {
|
||||
const nextValueInfoProto = valueInfoProtoCopy();
|
||||
nextValueInfoProto.type.denotation = value || option!.text;
|
||||
this.props[isModelInput ? 'setInputs' : 'setOutputs']({
|
||||
...this.props[isModelInput ? 'inputs' : 'outputs'],
|
||||
[x]: nextValueInfoProto,
|
||||
});
|
||||
};
|
||||
|
||||
let shapeEditor;
|
||||
if (valueInfoProto.type.tensorType) {
|
||||
const getValueInfoDimensions = (valueInfo: any) => valueInfo.type.tensorType.shape.dim;
|
||||
shapeEditor = getValueInfoDimensions(valueInfoProto).map((dim: any, index: number) => {
|
||||
const dimensionChanged = (value: string) => {
|
||||
const nextValueInfoProto = valueInfoProtoCopy();
|
||||
const dimension = getValueInfoDimensions(nextValueInfoProto)[index]
|
||||
if (value) {
|
||||
dimension.dimValue = +value;
|
||||
delete dimension.dimParam;
|
||||
} else {
|
||||
dimension.dimParam = 'None'
|
||||
delete dimension.dimValue;
|
||||
}
|
||||
this.props[isModelInput ? 'setInputs' : 'setOutputs']({
|
||||
...this.props[isModelInput ? 'inputs' : 'outputs'],
|
||||
[x]: nextValueInfoProto,
|
||||
});
|
||||
}
|
||||
const dimensionDenotationChanged = (option?: IComboBoxOption) => {
|
||||
const nextValueInfoProto = valueInfoProtoCopy();
|
||||
getValueInfoDimensions(nextValueInfoProto)[index].denotation = option!.text;
|
||||
this.props[isModelInput ? 'setInputs' : 'setOutputs']({
|
||||
...this.props[isModelInput ? 'inputs' : 'outputs'],
|
||||
[x]: nextValueInfoProto,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div key={index}>
|
||||
<div className='DisplayFlex'>
|
||||
<TextField
|
||||
className='DenotationLabel'
|
||||
label={`Dimension [${index}]`}
|
||||
value={dim.dimParam === 'None' ? undefined : dim.dimValue}
|
||||
inputMode='numeric'
|
||||
type='number'
|
||||
placeholder='None'
|
||||
disabled={disabled}
|
||||
onChanged={dimensionChanged} />
|
||||
<ComboBox
|
||||
label='Denotation'
|
||||
defaultSelectedKey={dim.denotation}
|
||||
className='DenotationComboBox'
|
||||
options={dimensionDenotationOptions}
|
||||
disabled={disabled}
|
||||
onChanged={dimensionDenotationChanged} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={x}>
|
||||
{tensorName}
|
||||
<div className='DenotationDiv'>
|
||||
<ComboBox
|
||||
className='DenotationComboBox'
|
||||
label='Type denotation'
|
||||
allowFreeform={true}
|
||||
text={valueInfoProto.type.denotation}
|
||||
options={denotationOptions}
|
||||
disabled={!isModelInput && !isModelOutput}
|
||||
onChanged={tensorDenotationChanged} />
|
||||
</div>
|
||||
{shapeEditor}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IState) => ({
|
||||
inputs: state.inputs,
|
||||
modelInputs: state.modelInputs,
|
||||
modelOutputs: state.modelOutputs,
|
||||
nodes: state.nodes,
|
||||
outputs: state.outputs,
|
||||
selectedNode: state.selectedNode,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setInputs,
|
||||
setOutputs,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LeftPanel);
|
|
@ -0,0 +1,50 @@
|
|||
.Unselectable {
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.Panel {
|
||||
text-align: left;
|
||||
min-width: 290px;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.PanelName {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.TensorName, .TensorDocumentationHover {
|
||||
display: block;
|
||||
margin: 5px 5px;
|
||||
}
|
||||
|
||||
.TensorDocumentationHover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.DenotationLabel {
|
||||
margin: 2px !important;
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.DenotationComboBox {
|
||||
margin: 2px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.DocString {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
height: 100%;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.FormatIsNotOnnx {
|
||||
margin: 10px;
|
||||
font-size: medium;
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import { Label } from 'office-ui-fabric-react/lib/Label';
|
||||
import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Collapsible from '../../components/Collapsible';
|
||||
import KeyValueEditor from '../../components/KeyValueEditor'
|
||||
import Resizable from '../../components/Resizable';
|
||||
import { setMetadataProps } from '../../datastore/actionCreators';
|
||||
import IState, { IMetadataProps } from '../../datastore/state';
|
||||
import MetadataSchema from '../../schema/Metadata';
|
||||
|
||||
import './Panel.css';
|
||||
|
||||
interface IComponentProperties {
|
||||
// Redux properties
|
||||
nodes: any,
|
||||
metadataProps: IMetadataProps,
|
||||
setMetadataProps: typeof setMetadataProps,
|
||||
}
|
||||
|
||||
class RightPanel extends React.Component<IComponentProperties, {}> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
}
|
||||
public render() {
|
||||
if (this.props.metadataProps !== undefined && !this.props.nodes) {
|
||||
return (
|
||||
// TODO Make it a button to navigate to the Convert tab
|
||||
<Label className='FormatIsNotOnnx'>To support editing, convert the model to ONNX first.</Label>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Unselectable">
|
||||
<Resizable isRightPanel={true}>
|
||||
<Label >Model</Label>
|
||||
<div className='Panel'>
|
||||
<Collapsible label='Properties'>
|
||||
<KeyValueEditor getState={this.getPropertiesFromState} schema={{ type: 'object' }} />
|
||||
</Collapsible>
|
||||
<Collapsible label='Metadata properties'>
|
||||
<KeyValueEditor actionCreator={setMetadataProps} getState={this.getMetadataPropsFromState} schema={MetadataSchema} />
|
||||
</Collapsible>
|
||||
</div>
|
||||
</Resizable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private getMetadataPropsFromState = (state: IState) => state.metadataProps;
|
||||
private getPropertiesFromState = (state: IState) => state.properties;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IState) => ({
|
||||
metadataProps: state.metadataProps,
|
||||
nodes: state.nodes,
|
||||
properties: state.properties,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setMetadataProps,
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(RightPanel);
|
|
@ -0,0 +1,4 @@
|
|||
#EditView {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import LeftPanel from './LeftPanel';
|
||||
import Netron from './netron/Netron';
|
||||
import RightPanel from './RightPanel';
|
||||
|
||||
import './View.css';
|
||||
|
||||
export default class EditView extends React.Component {
|
||||
public render() {
|
||||
return (
|
||||
<div id='EditView'>
|
||||
<LeftPanel />
|
||||
<Netron />
|
||||
<RightPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,278 @@
|
|||
import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import 'netron/src/view-render.css';
|
||||
import 'netron/src/view-sidebar.css';
|
||||
import 'netron/src/view.css';
|
||||
import 'npm-font-open-sans/open-sans.css';
|
||||
|
||||
import { setInputs, setMetadataProps, setModelInputs, setModelOutputs, setNodes, setOutputs, setProperties, setSelectedNode } from '../../../datastore/actionCreators';
|
||||
import { ModelProtoSingleton } from '../../../datastore/proto/modelProto';
|
||||
import IState from '../../../datastore/state';
|
||||
import './fixed-position-override.css';
|
||||
|
||||
const browserGlobal = window as any;
|
||||
|
||||
interface IComponentProperties {
|
||||
// Redux properties
|
||||
file: File,
|
||||
nodes: { [key: string]: any },
|
||||
setInputs: typeof setInputs,
|
||||
setMetadataProps: typeof setMetadataProps,
|
||||
setModelInputs: typeof setModelInputs,
|
||||
setModelOutputs: typeof setModelOutputs,
|
||||
setNodes: typeof setNodes,
|
||||
setOutputs: typeof setOutputs,
|
||||
setProperties: typeof setProperties,
|
||||
setSelectedNode: typeof setSelectedNode,
|
||||
}
|
||||
|
||||
interface IComponentState {
|
||||
graph: any,
|
||||
metadataProps: { [key: string]: string },
|
||||
properties: { [key: string]: string },
|
||||
}
|
||||
|
||||
class Netron extends React.Component<IComponentProperties, IComponentState> {
|
||||
private proxiesRevoke: Array<() => void> = [];
|
||||
|
||||
constructor(props: IComponentProperties) {
|
||||
super(props);
|
||||
this.state = {
|
||||
graph: {},
|
||||
metadataProps: {},
|
||||
properties: {},
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
// Netron must be run after rendering the HTML
|
||||
if (!browserGlobal.host) {
|
||||
const s = document.createElement('script');
|
||||
s.src = "netron_bundle.js";
|
||||
s.async = true;
|
||||
s.onload = this.onNetronInitialized;
|
||||
document.body.appendChild(s);
|
||||
} else {
|
||||
this.onNetronInitialized();
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
for (const revoke of this.proxiesRevoke) {
|
||||
revoke();
|
||||
}
|
||||
}
|
||||
|
||||
public UNSAFE_componentWillReceiveProps(nextProps: IComponentProperties) {
|
||||
if (!browserGlobal.host) {
|
||||
return;
|
||||
}
|
||||
if (nextProps.file && nextProps.file !== this.props.file) {
|
||||
browserGlobal.host._openFile(nextProps.file);
|
||||
}
|
||||
}
|
||||
|
||||
public shouldComponentUpdate(nextProps: IComponentProperties, nextState: IComponentState) {
|
||||
return false; // Netron is a static page and all updates are handled by its JavaScript code
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
// Instead of hardcoding the page, a div with dangerouslySetInnerHTML could be used to include Netron's content
|
||||
<div className='netron-root'>
|
||||
<div id='welcome' className='background' style={{display: 'block'}}>
|
||||
<div className='center logo'>
|
||||
<img className='logo absolute' src='logo.svg' />
|
||||
<img id='spinner' className='spinner logo absolute' src='spinner.svg' style={{display: 'none'}} />
|
||||
</div>
|
||||
<button id='open-file-button' className='center' style={{top: '200px', width: '125px', opacity: 0}}>Open Model...</button>
|
||||
<input type="file" id="open-file-dialog" style={{display: 'none'}} multiple={false} accept=".onnx, .pb, .meta, .tflite, .keras, .h5, .json, .mlmodel, .caffemodel" />
|
||||
<div style={{fontWeight: 'normal', color: '#e6e6e6', userSelect: 'none'}}>.</div>
|
||||
<div style={{fontWeight: 600, color: '#e6e6e6', userSelect: 'none'}}>.</div>
|
||||
<div style={{fontWeight: 'bold', color: '#e6e6e6', userSelect: 'none'}}>.</div>
|
||||
</div>
|
||||
<svg id='graph' className='graph' preserveAspectRatio='xMidYMid meet' width='100%' height='100%' />
|
||||
<div id='toolbar' className='toolbar' style={{position: 'absolute', top: '10px', left: '10px', display: 'none',}}>
|
||||
<button id='model-properties-button' className='xxx' title='Model Properties'>
|
||||
<svg viewBox="0 0 100 100" width="24" height="24">
|
||||
<rect x="12" y="12" width="76" height="76" rx="16" ry="16" strokeWidth="8" stroke="#fff" />
|
||||
<line x1="30" y1="37" x2="70" y2="37" strokeWidth="8" strokeLinecap="round" stroke="#fff" />
|
||||
<line x1="30" y1="50" x2="70" y2="50" strokeWidth="8" strokeLinecap="round" stroke="#fff" />
|
||||
<line x1="30" y1="63" x2="70" y2="63" strokeWidth="8" strokeLinecap="round" stroke="#fff" />
|
||||
<rect x="12" y="12" width="76" height="76" rx="16" ry="16" strokeWidth="4" />
|
||||
<line x1="30" y1="37" x2="70" y2="37" strokeWidth="4" strokeLinecap="round" />
|
||||
<line x1="30" y1="50" x2="70" y2="50" strokeWidth="4" strokeLinecap="round" />
|
||||
<line x1="30" y1="63" x2="70" y2="63" strokeWidth="4" strokeLinecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button id='zoom-in-button' className='icon' title='Zoom In'>
|
||||
<svg viewBox="0 0 100 100" width="24" height="24">
|
||||
<circle cx="50" cy="50" r="35" strokeWidth="8" stroke="#fff" />
|
||||
<line x1="50" y1="38" x2="50" y2="62" strokeWidth="8" strokeLinecap="round" stroke="#fff" />
|
||||
<line x1="38" y1="50" x2="62" y2="50" strokeWidth="8" strokeLinecap="round" stroke="#fff" />
|
||||
<line x1="78" y1="78" x2="82" y2="82" strokeWidth="12" strokeLinecap="square" stroke="#fff" />
|
||||
<circle cx="50" cy="50" r="35" strokeWidth="4" />
|
||||
<line x1="50" y1="38" x2="50" y2="62" strokeWidth="4" strokeLinecap="round" />
|
||||
<line x1="38" y1="50" x2="62" y2="50" strokeWidth="4" strokeLinecap="round" />
|
||||
<line x1="78" y1="78" x2="82" y2="82" strokeWidth="8" strokeLinecap="square" />
|
||||
</svg>
|
||||
</button>
|
||||
<button id='zoom-out-button' className='icon' title='Zoom Out'>
|
||||
<svg viewBox="0 0 100 100" width="24" height="24">
|
||||
<circle cx="50" cy="50" r="35" strokeWidth="8" stroke="#fff" />
|
||||
<line x1="38" y1="50" x2="62" y2="50" strokeWidth="8" strokeLinecap="round" stroke="#fff" />
|
||||
<line x1="78" y1="78" x2="82" y2="82" strokeWidth="12" strokeLinecap="square" stroke="#fff" />
|
||||
<circle cx="50" cy="50" r="35" strokeWidth="4" />
|
||||
<line x1="38" y1="50" x2="62" y2="50" strokeWidth="4" strokeLinecap="round" />
|
||||
<line x1="78" y1="78" x2="82" y2="82" strokeWidth="8" strokeLinecap="square" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id='sidebar' className='sidebar'>
|
||||
<h1 id='sidebar-title' className='sidebar-title' />
|
||||
<a href='javascript:void(0)' id='sidebar-closebutton' className='sidebar-closebutton'>×</a>
|
||||
<div id='sidebar-content' className='sidebar-content' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private propsToObject(props: any) {
|
||||
return props.reduce((acc: { [key: string]: string }, x: any) => {
|
||||
// metadataProps uses key, while model.properties uses name
|
||||
acc[x.key || x.name] = x.value;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
private valueListToObject(values: any) {
|
||||
return values.reduce((acc: { [key: string]: any }, x: any) => {
|
||||
const {name, ...props} = x;
|
||||
acc[name] = props;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
private onNetronInitialized = () => {
|
||||
// Reset document overflow property
|
||||
document.documentElement.style.overflow = 'initial';
|
||||
this.installProxies();
|
||||
}
|
||||
|
||||
private installProxies = () => {
|
||||
// Install proxy on browserGlobal.view.loadBuffer and update the data store
|
||||
const loadBufferHandler = {
|
||||
apply: (target: any, thisArg: any, args: any) => {
|
||||
const [buffer, identifier, callback] = args;
|
||||
// Patch the callback to update our data store first
|
||||
return target.call(thisArg, buffer, identifier, (err: Error, model: any) => {
|
||||
if (!err) {
|
||||
this.updateDataStore(model);
|
||||
}
|
||||
return callback(err, model);
|
||||
});
|
||||
},
|
||||
};
|
||||
const loadBufferProxy = Proxy.revocable(browserGlobal.view.loadBuffer, loadBufferHandler);
|
||||
browserGlobal.view.loadBuffer = loadBufferProxy.proxy;
|
||||
this.proxiesRevoke.push(loadBufferProxy.revoke);
|
||||
|
||||
const panelOpenHandler = {
|
||||
apply: (target: any, thisArg: any, args: any) => {
|
||||
if (this.props.nodes) {
|
||||
const title = args[1];
|
||||
if (title === 'Model Properties') {
|
||||
this.props.setSelectedNode(title);
|
||||
return;
|
||||
} else {
|
||||
this.props.setSelectedNode(undefined);
|
||||
}
|
||||
}
|
||||
return target.apply(thisArg, args);
|
||||
},
|
||||
};
|
||||
const panelOpenProxy = Proxy.revocable(browserGlobal.view._sidebar.open, panelOpenHandler);
|
||||
browserGlobal.view._sidebar.open = panelOpenProxy.proxy;
|
||||
this.proxiesRevoke.push(panelOpenProxy.revoke);
|
||||
|
||||
const panelCloseHandler = {
|
||||
apply: (target: any, thisArg: any, args: any) => {
|
||||
this.props.setSelectedNode(undefined);
|
||||
return target.apply(thisArg, args);
|
||||
},
|
||||
};
|
||||
const panelCloseProxy = Proxy.revocable(browserGlobal.view._sidebar.open, panelCloseHandler);
|
||||
browserGlobal.view._sidebar.open = panelCloseProxy.proxy;
|
||||
this.proxiesRevoke.push(panelCloseProxy.revoke);
|
||||
|
||||
browserGlobal.view.showNodeProperties = (node: any) => this.props.setSelectedNode(node.graph.nodes.indexOf(node));
|
||||
}
|
||||
|
||||
private getModelProperties(modelProto: any) {
|
||||
const opsetImport = (modelProto.opsetImport as Array<{domain: string, version: number}>)
|
||||
.reduce((acc, x) => {
|
||||
const opset = `opsetImport.${x.domain || 'ai.onnx'}`;
|
||||
return {
|
||||
...acc,
|
||||
[opset]: x.version,
|
||||
};
|
||||
}, {});
|
||||
const properties = {
|
||||
...opsetImport,
|
||||
docString: modelProto.docString,
|
||||
domain: modelProto.domain,
|
||||
irVersion: modelProto.irVersion,
|
||||
modelVersion: modelProto.modelVersion,
|
||||
producerName: modelProto.producerName,
|
||||
producerVersion: modelProto.producerVersion,
|
||||
}
|
||||
Object.entries(properties).forEach(([key, value]) => value || delete properties[key]); // filter empty properties
|
||||
return properties;
|
||||
}
|
||||
|
||||
private updateDataStore = (model: any) => {
|
||||
const proto = model._model
|
||||
ModelProtoSingleton.proto = null;
|
||||
// FIXME What to do when model has multiple graphs?
|
||||
const graph = model.graphs[0];
|
||||
if (graph.constructor.name === 'OnnxGraph') {
|
||||
const getNames = (list: any[]): string[] => list.map((x: any) => x.name);
|
||||
const inputs = getNames(graph.inputs);
|
||||
const outputs = getNames(graph.outputs);
|
||||
this.props.setModelInputs(inputs);
|
||||
this.props.setModelOutputs(outputs);
|
||||
this.props.setInputs(this.valueListToObject(proto.graph.input));
|
||||
this.props.setOutputs(this.valueListToObject(proto.graph.output));
|
||||
this.props.setNodes(proto.graph.node.filter((x: any) => x.opType !== 'Constant'));
|
||||
this.props.setMetadataProps(this.propsToObject(proto.metadataProps));
|
||||
this.props.setProperties(this.getModelProperties(proto));
|
||||
} else {
|
||||
this.props.setModelInputs(undefined);
|
||||
this.props.setModelOutputs(undefined);
|
||||
this.props.setNodes(undefined);
|
||||
this.props.setMetadataProps({});
|
||||
this.props.setProperties({});
|
||||
}
|
||||
this.props.setSelectedNode(undefined);
|
||||
ModelProtoSingleton.proto = proto;
|
||||
};
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IState) => ({
|
||||
file: state.file,
|
||||
nodes: state.nodes,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setInputs,
|
||||
setMetadataProps,
|
||||
setModelInputs,
|
||||
setModelOutputs,
|
||||
setNodes,
|
||||
setOutputs,
|
||||
setProperties,
|
||||
setSelectedNode,
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Netron);
|
|
@ -0,0 +1,20 @@
|
|||
/* Overrides fixed elements in Netron */
|
||||
.netron-root {
|
||||
all: initial;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.background {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.center {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: absolute;
|
||||
overflow: auto;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "build/dist",
|
||||
"module": "esnext",
|
||||
"target": "es5",
|
||||
"downlevelIteration": true,
|
||||
"lib": ["es6", "es2016", "es2017.object", "dom"],
|
||||
"sourceMap": true,
|
||||
"allowJs": true,
|
||||
"jsx": "react",
|
||||
"moduleResolution": "node",
|
||||
"rootDir": "src",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"noUnusedLocals": true
|
||||
},
|
||||
"exclude": [
|
||||
"acceptance-tests",
|
||||
"build",
|
||||
"deps",
|
||||
"jest",
|
||||
"node_modules",
|
||||
"public",
|
||||
"scripts",
|
||||
"src/setupTests.ts",
|
||||
"webpack",
|
||||
"Windows ML Dashboard*",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "./tsconfig.json"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
|
||||
"linterOptions": {
|
||||
"exclude": [
|
||||
"config/**/*.js",
|
||||
"deps/**/*.js",
|
||||
"node_modules/**/*.ts"
|
||||
]
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче