├── docs ├── header.png └── oidc.md ├── test ├── electron │ ├── mocha.opts │ └── basic.test.ts ├── end-to-end │ ├── mocha.opts │ ├── views │ │ ├── OpenIModel.test.ts │ │ ├── SignIn.test.ts │ │ └── Content.test.ts │ ├── setupTests.ts │ └── helpers.ts ├── integration │ ├── test-data │ │ └── Properties_60InstancesWithUrl2.ibim │ ├── mocha.opts │ ├── setupTests.js │ ├── Tree.test.ts │ └── Tree.test.snap └── unit │ ├── mocha.opts │ ├── setupTests.js │ ├── index.test.ts │ └── frontend │ └── components │ ├── Properties.test.tsx │ └── Tree.test.tsx ├── public ├── appicon.ico ├── favicon.ico ├── locales │ └── en │ │ └── SimpleViewer.json └── manifest.json ├── tslint.json ├── .gitignore ├── .vscode ├── extensions.json ├── settings.json └── launch.json ├── tsconfig.json ├── src ├── frontend │ ├── index.css │ ├── index.tsx │ ├── components │ │ ├── Components.scss │ │ ├── App.css │ │ ├── Viewport.tsx │ │ ├── Toolbar.tsx │ │ ├── Table.tsx │ │ ├── Tree.tsx │ │ ├── Properties.tsx │ │ └── App.tsx │ ├── api │ │ ├── rpc.ts │ │ ├── logging.ts │ │ └── SimpleViewerApp.ts │ └── index.html ├── common │ ├── config.json │ ├── rpcs.ts │ └── configuration.ts └── backend │ ├── web │ └── BackendServer.ts │ ├── electron │ └── main.ts │ └── main.ts ├── LICENSE.md ├── README.md ├── package.json └── assets └── presentation_rules └── Default.PresentationRuleSet.xml /docs/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imodeljs/simple-viewer-app/HEAD/docs/header.png -------------------------------------------------------------------------------- /test/electron/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --check-leaks 3 | --no-timeouts 4 | -------------------------------------------------------------------------------- /test/end-to-end/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --check-leaks 3 | --no-timeouts 4 | -------------------------------------------------------------------------------- /public/appicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imodeljs/simple-viewer-app/HEAD/public/appicon.ico -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imodeljs/simple-viewer-app/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@bentley/build-tools/tslint.json", 3 | "linterOptions": { 4 | "exclude": [ 5 | "**/*.json" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/integration/test-data/Properties_60InstancesWithUrl2.ibim: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imodeljs/simple-viewer-app/HEAD/test/integration/test-data/Properties_60InstancesWithUrl2.ibim -------------------------------------------------------------------------------- /test/unit/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --require jsdom-global/register 3 | --require ignore-styles 4 | --file ./test/unit/setupTests.js 5 | --check-leaks 6 | --no-timeouts 7 | -------------------------------------------------------------------------------- /test/integration/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --require jsdom-global/register 3 | --require ignore-styles 4 | --file ./test/integration/setupTests.js 5 | --check-leaks 6 | --no-timeouts 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # production 5 | /lib 6 | 7 | # misc 8 | .DS_Store 9 | .env.local* 10 | .env.development.local 11 | .env.test.local* 12 | .env.production.local 13 | *.TileCache* 14 | *.Tiles 15 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 6 | "eg2.tslint", 7 | "msjsdiag.debugger-for-chrome" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /public/locales/en/SimpleViewer.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome-message": "Welcome to Simple Viewer App", 3 | "signing-in": "Signing-in", 4 | "components": { 5 | "imodel-picker": { 6 | "open-imodel": "Open Sample iModel" 7 | }, 8 | "properties": "Properties", 9 | "table": "Table", 10 | "tree": "Tree" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "simple-viewer-app", 3 | "name": "Simple Viewer App", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@bentley/build-tools/tsconfig-base.json", 3 | "compilerOptions": { 4 | "skipLibCheck": true, 5 | "resolveJsonModule": true, 6 | "baseUrl": "./node_modules", 7 | "outDir": "./lib" 8 | }, 9 | "include": [ 10 | "./**/*.ts", 11 | "./**/*.tsx" 12 | ], 13 | "exclude": [ 14 | "lib", 15 | "node_modules", 16 | "test" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/frontend/index.css: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | font-family: sans-serif; 10 | } 11 | 12 | h3 { 13 | margin-bottom: 0.3em; 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "editor.detectIndentation": false, 4 | "editor.insertSpaces": true, 5 | "editor.tabSize": 2, 6 | "editor.trimAutoWhitespace": true, 7 | "files.associations": { 8 | "*.snap": "javascript" 9 | }, 10 | "files.exclude": { 11 | "**/lib/**": true, 12 | "**/node_modules/**": true, 13 | "**/*.TileCache*": true, 14 | }, 15 | "files.insertFinalNewline": true, 16 | "files.trimFinalNewlines": true, 17 | "files.trimTrailingWhitespace": true 18 | } 19 | -------------------------------------------------------------------------------- /src/common/config.json: -------------------------------------------------------------------------------- 1 | { 2 | // ----------------------------------------------------------------------------------------------------------- 3 | // Project and iModel (REQUIRED) 4 | // Must un-comment the lines below and set these variables before testing - create a new project and 5 | // iModel with the developer registration procedure here - https://git.io/fx8YP 6 | // ----------------------------------------------------------------------------------------------------------- 7 | // "imjs_test_project" : "", // Set this to the name of the sample project 8 | // "imjs_test_imodel" : "" // Set this to the name of the sample iModel 9 | } 10 | -------------------------------------------------------------------------------- /test/unit/setupTests.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | // A workaround to react-testing-library {dom-testing-library {wait-for-expect}} breaking somewhere, 6 | // because somewhere (most likely in jsdom) window.Date becomes undefined. 7 | // Similar issue mentioned in https://github.com/vuejs/vue-test-utils/issues/936 8 | require('jsdom-global')(); 9 | window.Date = Date; 10 | -------------------------------------------------------------------------------- /test/end-to-end/views/OpenIModel.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { page } from "../setupTests"; 7 | import { signIn } from "../helpers"; 8 | 9 | describe("Open iModel view", () => { 10 | 11 | it("renders after sign in", async () => { 12 | await signIn(page); 13 | await page.waitForSelector(".button-open-imodel", {timeout: 5000}); 14 | }); 15 | 16 | }); 17 | -------------------------------------------------------------------------------- /src/common/rpcs.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import { RpcInterfaceDefinition, IModelReadRpcInterface, IModelTileRpcInterface, SnapshotIModelRpcInterface } from "@bentley/imodeljs-common"; 6 | import { PresentationRpcInterface } from "@bentley/presentation-common"; 7 | 8 | /** 9 | * Returns a list of RPCs supported by this application 10 | */ 11 | export default function getSupportedRpcs(): RpcInterfaceDefinition[] { 12 | return [ 13 | IModelReadRpcInterface, 14 | IModelTileRpcInterface, 15 | PresentationRpcInterface, 16 | SnapshotIModelRpcInterface, 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /test/end-to-end/views/SignIn.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { page } from "../setupTests"; 7 | 8 | describe("Sign in view", () => { 9 | 10 | it("renders initially", async () => { 11 | await page.waitForSelector(".components-signin-button"); 12 | 13 | // Verify that welcome message exists 14 | await page.waitForSelector(".components-signin-prompt"); 15 | 16 | // Verify that "Register" link exists 17 | await page.waitForSelector(".components-signin-register a"); 18 | 19 | // Verify that "Work offline" link exists 20 | await page.waitForSelector(".components-signin-offline"); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /src/frontend/index.tsx: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import * as React from "react"; 6 | import * as ReactDOM from "react-dom"; 7 | import { SimpleViewerApp } from "./api/SimpleViewerApp"; 8 | import App from "./components/App"; 9 | import "./index.css"; 10 | import setupEnv from "../common/configuration"; 11 | 12 | // setup environment 13 | setupEnv(); 14 | 15 | // initialize the application 16 | SimpleViewerApp.startup(); 17 | 18 | // tslint:disable-next-line:no-floating-promises 19 | SimpleViewerApp.ready.then(() => { 20 | // when initialization is complete, render 21 | ReactDOM.render( 22 | , 23 | document.getElementById("root") as HTMLElement, 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /test/end-to-end/setupTests.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as Puppeteer from "puppeteer"; 7 | export let browser: Puppeteer.Browser; 8 | 9 | before(async () => { 10 | browser = await Puppeteer.launch(); 11 | }); 12 | 13 | after(async () => { 14 | await browser.close(); 15 | }); 16 | 17 | export let page: Puppeteer.Page; 18 | 19 | beforeEach(async () => { 20 | page = await browser.newPage(); 21 | await page.setViewport({ height: 1080, width: 1920 }); 22 | await page.goto("http://localhost:3000"); 23 | await page.setCacheEnabled(false); 24 | await page.target().createCDPSession().then((session) => session.send("Network.clearBrowserCookies")); 25 | }); 26 | 27 | afterEach(async () => { 28 | await page.close(); 29 | }); 30 | -------------------------------------------------------------------------------- /src/frontend/components/Components.scss: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | .toolbar { 7 | float: right; 8 | background-origin: border-box; 9 | top: 3%; 10 | right: 1%; 11 | position: absolute; 12 | -webkit-transition: all 500ms ease; 13 | -o-transition: all 500ms ease; 14 | transition: all 500ms ease; 15 | cursor: pointer; 16 | }; 17 | 18 | .toolbar a { 19 | padding: 12px; 20 | color: black; 21 | text-decoration: none; 22 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0)), to(rgba(0, 0, 0, 0.22))); 23 | background-color: #f9f9f9; 24 | border: 1px #4d575f solid; 25 | } 26 | 27 | .toolbar a:hover { 28 | color: #0072b8 29 | } 30 | 31 | .icon { 32 | background: none; 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright © 2019 Bentley Systems, Inc. 4 | 5 | All rights reserved. 6 | 7 | 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: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | 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. 12 | -------------------------------------------------------------------------------- /src/backend/web/BackendServer.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | // tslint:disable:no-console 6 | import { RpcInterfaceDefinition, BentleyCloudRpcManager } from "@bentley/imodeljs-common"; 7 | import { IModelJsExpressServer } from "@bentley/express-server"; 8 | 9 | /** 10 | * Initializes Web Server backend 11 | */ 12 | export default async function initialize(rpcs: RpcInterfaceDefinition[]) { 13 | // tell BentleyCloudRpcManager which RPC interfaces to handle 14 | const rpcConfig = BentleyCloudRpcManager.initializeImpl({ info: { title: "simple-viewer-app", version: "v1.0" } }, rpcs); 15 | 16 | const port = Number(process.env.PORT || 3001); 17 | const server = new IModelJsExpressServer(rpcConfig.protocol); 18 | await server.initialize(port); 19 | console.log("RPC backend for simple-viewer-app listening on port " + port); 20 | } 21 | -------------------------------------------------------------------------------- /src/backend/electron/main.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import * as path from "path"; 6 | import { RpcInterfaceDefinition, ElectronRpcManager } from "@bentley/imodeljs-common"; 7 | import { IModelJsElectronManager } from "@bentley/electron-manager"; 8 | /** 9 | * Initializes Electron backend 10 | */ 11 | export default function initialize(rpcs: RpcInterfaceDefinition[]) { 12 | (async () => { // tslint:disable-line:no-floating-promises 13 | const manager = new IModelJsElectronManager(path.join(__dirname, "..", "..", "webresources")); 14 | await manager.initialize({ 15 | width: 1280, 16 | height: 800, 17 | webPreferences: { 18 | experimentalFeatures: true, // Needed for CSS Grid support 19 | }, 20 | autoHideMenuBar: true, 21 | show: false, 22 | }); 23 | // tell ElectronRpcManager which RPC interfaces to handle 24 | ElectronRpcManager.initializeImpl({}, rpcs); 25 | if (manager.mainWindow) { 26 | manager.mainWindow.show(); 27 | } 28 | }) (); 29 | } 30 | -------------------------------------------------------------------------------- /test/integration/setupTests.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | // A workaround to react-testing-library {dom-testing-library {wait-for-expect}} breaking somewhere, 6 | // because somewhere (most likely in jsdom) window.Date becomes undefined. 7 | // Similar issue mentioned in https://github.com/vuejs/vue-test-utils/issues/936 8 | require('jsdom-global')(); 9 | window.Date = Date; 10 | 11 | const chai = require("chai"); 12 | const chaiJestSnapshot = require("chai-jest-snapshot"); 13 | 14 | chai.use(chaiJestSnapshot); 15 | 16 | beforeEach(function () { 17 | const sourceFilePath = this.currentTest.file.replace("lib\\test", "src\\test").replace(/\.(jsx?|tsx?)$/, ""); 18 | const snapPath = sourceFilePath + ".snap"; 19 | 20 | chaiJestSnapshot.setFilename(snapPath); 21 | chaiJestSnapshot.setTestName(this.currentTest.fullTitle()); 22 | }); 23 | 24 | 25 | // This is required by our I18n module (specifically the i18next package). 26 | global.XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest; 27 | -------------------------------------------------------------------------------- /src/frontend/api/rpc.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import { 6 | BentleyCloudRpcManager, BentleyCloudRpcParams, 7 | ElectronRpcManager, ElectronRpcConfiguration, 8 | RpcConfiguration, 9 | } from "@bentley/imodeljs-common"; 10 | import getSupportedRpcs from "../../common/rpcs"; 11 | 12 | /** 13 | * Initializes RPC communication based on the platform 14 | */ 15 | export default function initRpc(rpcParams?: BentleyCloudRpcParams): RpcConfiguration { 16 | let config: RpcConfiguration; 17 | const rpcInterfaces = getSupportedRpcs(); 18 | if (ElectronRpcConfiguration.isElectron) { 19 | // initializes RPC for Electron 20 | config = ElectronRpcManager.initializeClient({}, rpcInterfaces); 21 | } else { 22 | // initialize RPC for web apps 23 | if (!rpcParams) 24 | rpcParams = { info: { title: "simple-viewer-app", version: "v1.0" }, uriPrefix: "http://localhost:3001" }; 25 | config = BentleyCloudRpcManager.initializeClient(rpcParams, rpcInterfaces); 26 | } 27 | return config; 28 | } 29 | -------------------------------------------------------------------------------- /src/frontend/api/logging.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | // tslint:disable:no-console 6 | import { LogFunction, Logger } from "@bentley/bentleyjs-core"; 7 | import { GetMetaDataFunction } from "@bentley/imodeljs-common"; 8 | 9 | export default function init() { 10 | // map between iModelJs LogFunction signature and console logger 11 | const errorLogger: LogFunction = (_category: string, message: string, getMetaData?: GetMetaDataFunction): void => console.log("Error: " + message + (getMetaData ? " " + JSON.stringify(getMetaData()) : "")); 12 | const warningLogger: LogFunction = (_category: string, message: string, getMetaData?: GetMetaDataFunction): void => console.log("Warning: " + message + (getMetaData ? " " + JSON.stringify(getMetaData()) : "")); 13 | const infoLogger: LogFunction = (_category: string, message: string, getMetaData?: GetMetaDataFunction): void => console.log("Info: " + message + (getMetaData ? " " + JSON.stringify(getMetaData()) : "")); 14 | Logger.initialize(errorLogger, warningLogger, infoLogger); 15 | } 16 | -------------------------------------------------------------------------------- /test/integration/Tree.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { expect } from "chai"; 7 | import { initialize, terminate, HierarchyBuilder} from "@bentley/presentation-testing"; 8 | import { IModelConnection } from "@bentley/imodeljs-frontend"; 9 | 10 | before(() => { 11 | initialize({ rulesetDirectories: ["assets/presentation_rules"] }); 12 | }); 13 | 14 | after(() => { 15 | terminate(); 16 | }); 17 | 18 | describe("Tree", () => { 19 | let imodel: IModelConnection; 20 | let builder: HierarchyBuilder; 21 | const imodelPath = "test/integration/test-data/Properties_60InstancesWithUrl2.ibim"; 22 | 23 | beforeEach(async () => { 24 | imodel = await IModelConnection.openSnapshot(imodelPath); 25 | builder = new HierarchyBuilder(imodel); 26 | }); 27 | 28 | afterEach(async () => { 29 | await imodel.closeSnapshot(); 30 | }); 31 | 32 | it("generates correct hierarchy for 'Default' ruleset", async () => { 33 | const hierarchy = await builder.createHierarchy("Default"); 34 | expect(hierarchy).to.matchSnapshot(); 35 | }); 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /src/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | Simple Viewer App 15 | 16 | 17 | 18 | 19 | 20 | 23 |
24 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/electron/basic.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import { Application } from "spectron"; 6 | import { assert } from "chai"; 7 | import * as path from "path"; 8 | import * as app from "electron"; 9 | 10 | describe("Application launch", () => { 11 | let testApp: Application; 12 | beforeEach(async () => { 13 | testApp = new Application({ 14 | path: app as any, 15 | args: [path.join(__dirname, "..", "..", "lib/backend/main.js")], 16 | }); 17 | return testApp.start(); 18 | }); 19 | 20 | afterEach(async () => { 21 | if (testApp && testApp.isRunning()) { 22 | return testApp.stop(); 23 | } else { 24 | return testApp; 25 | } 26 | }); 27 | 28 | it("shows an initial window", async () => { 29 | return testApp.client.getWindowCount().then((count) => { 30 | assert.equal(count, 1); 31 | // Please note that getWindowCount() will return 2 if `dev tools` are opened. 32 | // assert.equal(count, 2) 33 | }); 34 | }); 35 | 36 | it("sign page", async () => { 37 | testApp.client.waitForExist(".components-signin-offline").then((t) => { 38 | assert.isTrue(t); 39 | }).waitForExist(".components-signin-button").then((t) => { 40 | assert.isTrue(t); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/frontend/components/App.css: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | .app { 7 | display: flex; 8 | flex-flow: column; 9 | height: 100vh; 10 | overflow: hidden; 11 | } 12 | 13 | .app-header { 14 | background-color: #222; 15 | color: white; 16 | padding-left: 20px; 17 | } 18 | 19 | .app .button-open-imodel { 20 | width: 200px; 21 | margin: 30vh auto auto auto; 22 | padding-left: 0; 23 | padding-right: 0; 24 | } 25 | 26 | .app-content { 27 | position: relative; 28 | height: 100%; 29 | width: 100%; 30 | } 31 | 32 | .app-content .top-left { 33 | position: absolute; 34 | top: 0; 35 | left: 0; 36 | height: 70%; 37 | width: 70%; 38 | } 39 | 40 | .app-content .bottom { 41 | position: absolute; 42 | bottom: 0; 43 | left: 0; 44 | height: 30%; 45 | width: 70%; 46 | } 47 | 48 | .app-content .right { 49 | position: absolute; 50 | top: 0; 51 | right: 0; 52 | height: 100%; 53 | width: 30%; 54 | } 55 | 56 | .app-content .right .top { 57 | position: absolute; 58 | top: 0; 59 | height: 50%; 60 | width: 100%; 61 | overflow: hidden; 62 | display: flex; 63 | flex-direction: column; 64 | } 65 | 66 | .app-content .right .bottom { 67 | position: absolute; 68 | bottom: 0; 69 | height: 50%; 70 | width: 100%; 71 | overflow: hidden; 72 | display: flex; 73 | flex-direction: column; 74 | } 75 | -------------------------------------------------------------------------------- /src/frontend/components/Viewport.tsx: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import * as React from "react"; 6 | import { Id64String } from "@bentley/bentleyjs-core"; 7 | import { IModelConnection } from "@bentley/imodeljs-frontend"; 8 | import { ViewportComponent } from "@bentley/ui-components"; 9 | import { viewWithUnifiedSelection } from "@bentley/presentation-components"; 10 | import Toolbar from "./Toolbar"; 11 | 12 | // create a HOC viewport component that supports unified selection 13 | // tslint:disable-next-line:variable-name 14 | const SimpleViewport = viewWithUnifiedSelection(ViewportComponent); 15 | 16 | /** React properties for the viewport component */ 17 | export interface Props { 18 | /** iModel whose contents should be displayed in the viewport */ 19 | imodel: IModelConnection; 20 | /** View definition to use when the viewport is first loaded */ 21 | viewDefinitionId: Id64String; 22 | /** ID of the presentation rule set to use for unified selection */ 23 | rulesetId: string; 24 | } 25 | 26 | /** Viewport component for the viewer app */ 27 | export default class SimpleViewportComponent extends React.Component { 28 | public render() { 29 | return ( 30 | <> 31 | 36 | 37 | 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/backend/main.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import * as path from "path"; 6 | import { app as electron } from "electron"; 7 | import { Logger } from "@bentley/bentleyjs-core"; 8 | import { IModelHost } from "@bentley/imodeljs-backend"; 9 | import { Presentation } from "@bentley/presentation-backend"; 10 | import getSupportedRpcs from "../common/rpcs"; 11 | import { RpcInterfaceDefinition } from "@bentley/imodeljs-common"; 12 | import setupEnv from "../common/configuration"; 13 | // setup environment 14 | setupEnv(); 15 | 16 | // initialize logging 17 | Logger.initializeToConsole(); 18 | 19 | // initialize imodeljs-backend 20 | IModelHost.startup(); 21 | 22 | // initialize presentation-backend 23 | Presentation.initialize({ 24 | // Specify location of where application's presentation rule sets are located. 25 | // May be omitted if application doesn't have any presentation rules. 26 | rulesetDirectories: [path.join("assets", "presentation_rules")], 27 | }); 28 | 29 | // invoke platform-specific initialization 30 | // tslint:disable-next-line:no-floating-promises 31 | (async () => { 32 | // get platform-specific initialization function 33 | let init: (rpcs: RpcInterfaceDefinition[]) => void; 34 | if (electron) { 35 | init = (await import("./electron/main")).default; 36 | } else { 37 | init = (await import("./web/BackendServer")).default; 38 | } 39 | // get RPCs supported by this backend 40 | const rpcs = getSupportedRpcs(); 41 | // do initialize 42 | init(rpcs); 43 | })(); 44 | -------------------------------------------------------------------------------- /test/unit/index.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import * as path from "path"; 6 | import * as typemoq from "typemoq"; 7 | import { IModelApp, NoRenderApp } from "@bentley/imodeljs-frontend"; 8 | import { UiCore } from "@bentley/ui-core"; 9 | import { UiComponents } from "@bentley/ui-components"; 10 | import { cleanup } from "react-testing-library"; 11 | import { Presentation, SelectionManager } from "@bentley/presentation-frontend"; 12 | import { SelectionScopesManager } from "@bentley/presentation-frontend/lib/selection/SelectionScopesManager"; 13 | import { I18NOptions } from "@bentley/imodeljs-i18n"; 14 | 15 | 16 | function supplyI18NOptions(): I18NOptions { 17 | const urlTemplate = "file://" + path.join(path.resolve("lib/webresources/locales"), "{{lng}}/{{ns}}.json").replace(/\\/g, "/"); 18 | return { urlTemplate }; 19 | } 20 | 21 | before(async () => { 22 | NoRenderApp.startup({ i18n: supplyI18NOptions() }); 23 | 24 | await UiCore.initialize(IModelApp.i18n); 25 | await UiComponents.initialize(IModelApp.i18n); 26 | Presentation.initialize(); 27 | 28 | // Presentation.selection needs to be set, because WithUnifiedSelection requires a SelectionHandler. 29 | // If selection handler is not provided through props, the HOC creates a new SelectionHandler by 30 | // using Presentation.selection 31 | Presentation.selection = new SelectionManager({ scopes: typemoq.Mock.ofType().object }); 32 | }); 33 | 34 | after(() => { 35 | UiCore.terminate(); 36 | UiComponents.terminate(); 37 | }); 38 | 39 | afterEach(() => { 40 | cleanup(); 41 | }); 42 | -------------------------------------------------------------------------------- /test/unit/frontend/components/Properties.test.tsx: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as React from "react"; 7 | import * as moq from "typemoq"; 8 | import { render } from "react-testing-library"; 9 | import { expect } from "chai"; 10 | import PropertiesComponent from "../../../../src/frontend/components/Properties"; 11 | import { IModelApp, IModelConnection } from "@bentley/imodeljs-frontend"; 12 | import { IPresentationPropertyDataProvider } from "@bentley/presentation-components"; 13 | import { PropertyData, PropertyDataChangeEvent } from "@bentley/ui-components"; 14 | import { KeySet } from "@bentley/presentation-common"; 15 | 16 | const iModelConnectionMock = moq.Mock.ofType(); 17 | 18 | class EmptyPropertyDataProvider implements IPresentationPropertyDataProvider { 19 | public displayType = "test"; 20 | public keys = new KeySet(); 21 | public selectionInfo = undefined; 22 | public imodel = iModelConnectionMock.object; 23 | public rulesetId = ""; 24 | 25 | protected _data: PropertyData = { 26 | label: "Empty data", 27 | categories: [], 28 | records: { test: [] }, 29 | }; 30 | 31 | public dispose() { } 32 | 33 | public getContentDescriptor = async () => undefined; 34 | public getContentSetSize = async () => 0; 35 | public getContent = async () => undefined; 36 | 37 | public getData = async () => this._data; 38 | public onDataChanged = new PropertyDataChangeEvent(); 39 | } 40 | 41 | describe("Properties", () => { 42 | 43 | it("renders header", () => { 44 | const renderWrapper = render(); 45 | const header = renderWrapper.getByTestId("property-pane-component-header"); 46 | expect(header.innerHTML).to.equal(IModelApp.i18n.translate("SimpleViewer:components.properties")); 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /src/frontend/components/Toolbar.tsx: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as React from "react"; 7 | import { 8 | IModelApp, 9 | ZoomViewTool, PanViewTool, RotateViewTool, SelectionTool, FitViewTool, 10 | } from "@bentley/imodeljs-frontend"; 11 | 12 | import "./Components.scss"; 13 | 14 | /** Toolbar containing simple navigation tools */ 15 | const toolbar = () => { 16 | return ( 17 |
18 | 19 | 20 | 21 | 22 | 23 |
24 | ); 25 | }; 26 | 27 | /** 28 | * See the https://imodeljs.github.io/iModelJs-docs-output/learning/frontend/tools/ 29 | * for more details and available tools. 30 | */ 31 | 32 | const select = () => { 33 | IModelApp.tools.run(SelectionTool.toolId); 34 | }; 35 | 36 | const fitView = () => { 37 | IModelApp.tools.run(FitViewTool.toolId, IModelApp.viewManager.selectedView); 38 | }; 39 | 40 | const rotate = () => { 41 | IModelApp.tools.run(RotateViewTool.toolId, IModelApp.viewManager.selectedView); 42 | }; 43 | 44 | const pan = () => { 45 | IModelApp.tools.run(PanViewTool.toolId, IModelApp.viewManager.selectedView); 46 | }; 47 | 48 | const zoom = () => { 49 | IModelApp.tools.run(ZoomViewTool.toolId, IModelApp.viewManager.selectedView); 50 | }; 51 | 52 | export default toolbar; 53 | -------------------------------------------------------------------------------- /test/end-to-end/helpers.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import { Config } from "@bentley/imodeljs-clients"; 6 | import * as Puppeteer from "puppeteer"; 7 | 8 | /** Wait for the specified text to appear on the page */ 9 | export async function waitForText(page: Puppeteer.Page, text: string, options?: Puppeteer.WaitForSelectorOptions) { 10 | await page.waitForXPath(`//text()[contains(., '${text}')]`, { visible: true, ...options }); 11 | } 12 | 13 | /** Find an element in the DOM by specified text */ 14 | export async function findByText(element: Puppeteer.Page | Puppeteer.ElementHandle, text: string) { 15 | const elements = await element.$x(`//text()[contains(., '${text}')]/..`); 16 | 17 | if (elements.length) 18 | return elements[0]; 19 | 20 | throw Error(`Element "${text}" not found!`); 21 | } 22 | 23 | /** Sign in to the main page using test credentials */ 24 | export async function signIn(page: Puppeteer.Page) { 25 | await page.waitForSelector(".components-signin-button"); 26 | await page.click(".components-signin-button"); 27 | 28 | await fillInSignin(page); 29 | } 30 | 31 | /** Fill in sign in form with test credentials and submit */ 32 | export async function fillInSignin(page: Puppeteer.Page) { 33 | await page.waitForSelector("#submitLogon"); 34 | 35 | await page.type("#EmailAddress", Config.App.getString("imjs_test_regular_user_name")); 36 | await page.type("#Password", Config.App.getString("imjs_test_regular_user_password")); 37 | 38 | await page.click("#submitLogon"); 39 | 40 | // Try to catch failed logins 41 | try { 42 | await page.waitForSelector("#messageControlDiv", { visible: true, timeout: 1000 }).then(() => { throw new Error(`Failed login to ${page.url()} for ${Config.App.getString("imjs_test_regular_user_name")} using ${Config.App.getString("IMJS_BUDDI_RESOLVE_URL_USING_REGION")}`); }); 43 | } catch (e) { 44 | // Ignore Timeout errors 45 | if (!e.name.includes("Timeout")) { 46 | throw e; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/frontend/components/Table.tsx: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import * as React from "react"; 6 | import { IModelConnection } from "@bentley/imodeljs-frontend"; 7 | import { Table } from "@bentley/ui-components"; 8 | import { 9 | IPresentationTableDataProvider, 10 | PresentationTableDataProvider, 11 | tableWithUnifiedSelection, 12 | } from "@bentley/presentation-components"; 13 | 14 | // create a HOC table component that supports unified selection 15 | // tslint:disable-next-line:variable-name 16 | const SimpleTable = tableWithUnifiedSelection(Table); 17 | 18 | /** React properties for the table component, that accepts an iModel connection with ruleset id */ 19 | export interface IModelConnectionProps { 20 | /** iModel whose contents should be displayed in the table */ 21 | imodel: IModelConnection; 22 | /** ID of the presentation rule set to use for creating the content displayed in the table */ 23 | rulesetId: string; 24 | } 25 | 26 | /** React properties for the table component, that accepts a data provider */ 27 | export interface DataProviderProps { 28 | /** Custom property pane data provider. */ 29 | dataProvider: IPresentationTableDataProvider; 30 | } 31 | 32 | /** React properties for the table component */ 33 | export type Props = IModelConnectionProps | DataProviderProps; 34 | 35 | /** Table component for the viewer app */ 36 | export default class SimpleTableComponent extends React.PureComponent { 37 | private getDataProvider(props: Props) { 38 | if ((props as any).dataProvider) { 39 | const providerProps = props as DataProviderProps; 40 | return providerProps.dataProvider; 41 | } else { 42 | const imodelProps = props as IModelConnectionProps; 43 | return new PresentationTableDataProvider({ imodel: imodelProps.imodel, ruleset: imodelProps.rulesetId }); 44 | } 45 | } 46 | 47 | public render() { 48 | return ( 49 |
50 | 51 |
52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /docs/oidc.md: -------------------------------------------------------------------------------- 1 | # Implementing OIDC Sign-In 2 | 3 | To access iModelHub services consumers need an access token. The token may be acquired through an OIDC sign-in process whose implementation is described below. 4 | 5 | ## Implementation 6 | 7 | OIDC sign-in implementation consists of 3 parts: 8 | 9 | - OidcBrowserClient wrapper component that wraps `oidc-client` package and acts as a helper. 10 | 11 | - Application-wide `OidcBrowserClient` instance created in `IModelApp.onStartup()` callback (*see [SimpleViewerApp.ts](../src/frontend/api/SimpleViewerApp.ts)*): 12 | ```ts 13 | this._oidcClient = new OidcBrowserClient(); 14 | ``` 15 | 16 | - Changes in React component (*see [App.tsx](../src/frontend/components/App.tsx)*): 17 | 18 | 1. Add sign-in state into component's state in component's constructor. `OidcClient` created in previous step can be used to get initial values: 19 | ```ts 20 | this.state = { 21 | user: { 22 | isLoading: SimpleViewerApp.oidcClient.isLoading, 23 | accessToken: SimpleViewerApp.oidcClient.accessToken, 24 | }, 25 | }; 26 | ``` 27 | 28 | 2. Subscribe for `onUserStateChanged` callback in `componentDidMount`: 29 | ```ts 30 | SimpleViewerApp.oidcClient.onUserStateChanged.addListener(this._onUserStateChanged); 31 | ``` 32 | 33 | 3. Unsubscribe from `onUserStateChanged` callback in `componentWillUnmount`: 34 | ```ts 35 | SimpleViewerApp.oidcClient.onUserStateChanged.removeListener(this._onUserStateChanged); 36 | ``` 37 | 38 | 4. Implement the `onUserStateChanged` event handler to update component state: 39 | ```ts 40 | private _onUserStateChanged = (accessToken: AccessToken | undefined) => { 41 | this.setState((prev) => ({ user: { ...prev.user, accessToken } })); 42 | } 43 | ``` 44 | 45 | 5. In the component's `render` callback, if we don't have an access token, we want to show either a sign-in dialog or report the sign-in process. To initiate the sign-in process, `OidcClient.signIn()` should be called. 46 | ```ts 47 | if (this.state.user.isLoading) { 48 | ui = `${IModelApp.i18n.translate("SimpleViewer:signing-in")}...`; 49 | } else if (!this.state.user.accessToken) { 50 | ui = ( SimpleViewerApp.oidcClient.signIn(new ActivityLoggingContext(Guid.createValue()))} />); 51 | } else { 52 | // user is logged in, render application UI 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /src/frontend/components/Tree.tsx: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import * as React from "react"; 6 | import { IModelApp, IModelConnection } from "@bentley/imodeljs-frontend"; 7 | import { Tree } from "@bentley/ui-components"; 8 | import { 9 | IPresentationTreeDataProvider, 10 | PresentationTreeDataProvider, 11 | treeWithUnifiedSelection, 12 | } from "@bentley/presentation-components"; 13 | 14 | // create a HOC tree component that supports unified selection 15 | // tslint:disable-next-line:variable-name 16 | const SimpleTree = treeWithUnifiedSelection(Tree); 17 | 18 | /** React properties for the tree component, that accepts an iModel connection with ruleset id */ 19 | export interface IModelConnectionProps { 20 | /** iModel whose contents should be displayed in the tree */ 21 | imodel: IModelConnection; 22 | /** ID of the presentation rule set to use for creating the hierarchy in the tree */ 23 | rulesetId: string; 24 | } 25 | 26 | /** React properties for the tree component, that accepts a data provider */ 27 | export interface DataProviderProps { 28 | /** Custom tree data provider. */ 29 | dataProvider: IPresentationTreeDataProvider; 30 | } 31 | 32 | /** React properties for the tree component */ 33 | export type Props = IModelConnectionProps | DataProviderProps; 34 | 35 | /** Tree component for the viewer app */ 36 | export default class SimpleTreeComponent extends React.PureComponent { 37 | private getDataProvider(props: Props) { 38 | if ((props as any).dataProvider) { 39 | const providerProps = props as DataProviderProps; 40 | return providerProps.dataProvider; 41 | } else { 42 | const imodelProps = props as IModelConnectionProps; 43 | return new PresentationTreeDataProvider(imodelProps.imodel, imodelProps.rulesetId); 44 | } 45 | } 46 | 47 | public render() { 48 | return ( 49 | <> 50 |

{IModelApp.i18n.translate("SimpleViewer:components.tree")}

51 |
52 | 53 |
54 | 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/frontend/components/Properties.tsx: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import * as React from "react"; 6 | import { IModelApp, IModelConnection } from "@bentley/imodeljs-frontend"; 7 | import { Orientation } from "@bentley/ui-core"; 8 | import { PropertyGrid } from "@bentley/ui-components"; 9 | import { 10 | IPresentationPropertyDataProvider, 11 | PresentationPropertyDataProvider, 12 | propertyGridWithUnifiedSelection, 13 | } from "@bentley/presentation-components"; 14 | 15 | // create a HOC property grid component that supports unified selection 16 | // tslint:disable-next-line:variable-name 17 | const SimplePropertyGrid = propertyGridWithUnifiedSelection(PropertyGrid); 18 | 19 | /** React properties for the property pane component, that accepts an iModel connection with ruleset id */ 20 | export interface IModelConnectionProps { 21 | /** iModel whose contents should be displayed in the property pane */ 22 | imodel: IModelConnection; 23 | /** ID of the presentation rule set to use for creating the hierarchy in the property pane */ 24 | rulesetId: string; 25 | } 26 | 27 | /** React properties for the property pane component, that accepts a data provider */ 28 | export interface DataProviderProps { 29 | /** Custom property pane data provider. */ 30 | dataProvider: IPresentationPropertyDataProvider; 31 | } 32 | 33 | /** React properties for the property pane component */ 34 | export type Props = IModelConnectionProps | DataProviderProps; 35 | 36 | /** Property grid component for the viewer app */ 37 | export default class SimplePropertiesComponent extends React.PureComponent { 38 | private getDataProvider(props: Props) { 39 | if ((props as any).dataProvider) { 40 | const providerProps = props as DataProviderProps; 41 | return providerProps.dataProvider; 42 | } else { 43 | const imodelProps = props as IModelConnectionProps; 44 | return new PresentationPropertyDataProvider(imodelProps.imodel, imodelProps.rulesetId); 45 | } 46 | } 47 | 48 | public render() { 49 | return ( 50 | <> 51 |

{IModelApp.i18n.translate("SimpleViewer:components.properties")}

52 |
53 | 57 |
58 | 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/common/configuration.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import { Config } from "@bentley/imodeljs-clients"; 6 | 7 | /** 8 | * List of possible backends that simple-viewer-app can use 9 | */ 10 | export const enum UseBackend { 11 | /** Use local simple-viewer-app backend */ 12 | Local = 0, 13 | 14 | /** Use deployed Navigator backend */ 15 | Navigator = 1, 16 | } 17 | 18 | /** 19 | * Setup configuration for the application 20 | * 21 | * **Note:** this part of configuration is shared between both the application itself and 22 | * the tests. Each of them also have unique configuration stored in: 23 | * - App: `src/common/config.json` 24 | * - Tests: `test/end-to-end/config.json` 25 | */ 26 | export default function setupEnv() { 27 | Config.App.merge({ 28 | // ----------------------------------------------------------------------------------------------------------- 29 | // Client registration (RECOMMENDED but OPTIONAL) 30 | // Must set these variables before deployment, but the supplied defaults can be used for testing on localhost. 31 | // Create a client registration using the procedure here - https://git.io/fx8YP (Developer registration). For the purpose 32 | // of running this sample on localhost, ensure your registration includes http://localhost:3000/signin-callback as a 33 | // valid redirect URI. 34 | // ----------------------------------------------------------------------------------------------------------- 35 | 36 | // Set this to the registered clientId 37 | // Note: "imodeljs-spa-test-2686" is setup to work with the (default) localhost redirect URI below 38 | imjs_browser_test_client_id: "imodeljs-spa-test-2686", 39 | 40 | // Use this client id when running electron app 41 | imjs_electron_test_client_id: "spa-nhNhyPAwoeFnwIhOwvnekjy7W", 42 | 43 | // Set this to be the registered redirect URI 44 | // Note: "http://localhost:3000/signin-callback" is setup to work with the (default) web clientId above 45 | imjs_browser_test_redirect_uri: "http://localhost:3000/signin-callback", 46 | 47 | // This redirect uri is set up to be used with the electron clientId above 48 | imjs_electron_test_redirect_uri: "electron://frontend/signin-callback", 49 | 50 | // Set this to be the scopes of services the application needs to access 51 | // Note: The default value set above ensures the minimal working of the application 52 | imjs_browser_test_scope: "openid email profile organization imodelhub context-registry-service:read-only", 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /test/unit/frontend/components/Tree.test.tsx: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as React from "react"; 7 | import * as moq from "typemoq"; 8 | import { render, waitForElement } from "react-testing-library"; 9 | import { expect } from "chai"; 10 | import TreeComponent from "../../../../src/frontend/components/Tree"; 11 | import { IModelApp, IModelConnection } from "@bentley/imodeljs-frontend"; 12 | import { TreeNodeItem } from "@bentley/ui-components"; 13 | import { IPresentationTreeDataProvider } from "@bentley/presentation-components"; 14 | 15 | const iModelConnectionMock = moq.Mock.ofType(); 16 | 17 | class EmptyTreeDataProvider implements IPresentationTreeDataProvider { 18 | protected _nodes: TreeNodeItem[] = []; 19 | public get imodel() { return iModelConnectionMock.object; } 20 | public getFilteredNodePaths = async () => []; 21 | public get onTreeNodeChanged() { return undefined; } 22 | public get rulesetId() { return ""; } 23 | public getNodeKey = () => ({ type: "testType", pathFromRoot: ["root"] }); 24 | public getNodes = async () => this._nodes; 25 | public getNodesCount = async () => this._nodes.length; 26 | } 27 | 28 | describe("Tree", () => { 29 | 30 | it("renders header and tree component", () => { 31 | const renderWrapper = render(); 32 | const header = renderWrapper.getByTestId("tree-component-header"); 33 | expect(header.innerHTML).to.be.equal(IModelApp.i18n.translate("SimpleViewer:components.tree")); 34 | expect(renderWrapper.container.querySelector(".components-tree-loader")).to.not.be.empty; 35 | }); 36 | 37 | describe("Tree content", () => { 38 | 39 | class DataProvider extends EmptyTreeDataProvider { 40 | protected _nodes: TreeNodeItem[] = [{ id: "1", label: "Node 1" }, { id: "2", label: "Node 2" }]; 41 | } 42 | 43 | before(() => { 44 | // note: this is needed for AutoSizer used by the Tree to 45 | // have non-zero size and render the virtualized list 46 | Object.defineProperties(HTMLElement.prototype, { 47 | offsetHeight: { get: () => 200 }, 48 | offsetWidth: { get: () => 200 }, 49 | }); 50 | }); 51 | 52 | after(() => { 53 | Object.defineProperties(HTMLElement.prototype, { 54 | offsetHeight: { get: () => 0 }, 55 | offsetWidth: { get: () => 0 }, 56 | }); 57 | }); 58 | 59 | it("renders 'no data' when data provider is empty", async () => { 60 | const renderWrapper = render(); 61 | expect(renderWrapper.container.querySelector(".components-tree-loader")).to.not.be.empty; 62 | const noDataLabel = await waitForElement(() => renderWrapper.getByText(IModelApp.i18n.translate("UiComponents:general.noData"))); 63 | expect(noDataLabel).to.not.be.undefined; 64 | }); 65 | 66 | it("renders all nodes from data provider when it's not empty", async () => { 67 | const renderWrapper = render(); 68 | expect(renderWrapper.container.querySelector(".components-tree-loader")).to.not.be.empty; 69 | 70 | const nodes = await waitForElement(() => renderWrapper.getAllByTestId("tree-node")); 71 | expect(nodes.length).to.equal(2); 72 | expect(renderWrapper.getByText("Node 1")).to.not.be.undefined; 73 | }); 74 | 75 | }); 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /src/frontend/api/SimpleViewerApp.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import { BentleyCloudRpcParams, ElectronRpcConfiguration } from "@bentley/imodeljs-common"; 6 | import { Config, UrlDiscoveryClient, OidcFrontendClientConfiguration, IOidcFrontendClient } from "@bentley/imodeljs-clients"; 7 | import { IModelApp, OidcBrowserClient, FrontendRequestContext } from "@bentley/imodeljs-frontend"; 8 | import { Presentation } from "@bentley/presentation-frontend"; 9 | import { UiCore } from "@bentley/ui-core"; 10 | import { UiComponents } from "@bentley/ui-components"; 11 | import { UseBackend } from "../../common/configuration"; 12 | import initLogging from "./logging"; 13 | import initRpc from "./rpc"; 14 | 15 | // initialize logging 16 | initLogging(); 17 | 18 | // subclass of IModelApp needed to use imodeljs-frontend 19 | export class SimpleViewerApp { 20 | 21 | private static _isReady: Promise; 22 | private static _oidcClient: IOidcFrontendClient; 23 | 24 | public static get oidcClient() { return this._oidcClient; } 25 | 26 | public static get ready(): Promise { return this._isReady; } 27 | 28 | public static startup() { 29 | IModelApp.startup(); 30 | 31 | // contains various initialization promises which need 32 | // to be fulfilled before the app is ready 33 | const initPromises = new Array>(); 34 | 35 | // initialize localization for the app 36 | initPromises.push(IModelApp.i18n.registerNamespace("SimpleViewer").readFinished); 37 | 38 | // initialize UiCore 39 | initPromises.push(UiCore.initialize(IModelApp.i18n)); 40 | 41 | // initialize UiComponents 42 | initPromises.push(UiComponents.initialize(IModelApp.i18n)); 43 | 44 | // initialize Presentation 45 | Presentation.initialize({ 46 | activeLocale: IModelApp.i18n.languageList()[0], 47 | }); 48 | 49 | // initialize RPC communication 50 | initPromises.push(SimpleViewerApp.initializeRpc()); 51 | 52 | // initialize OIDC 53 | initPromises.push(SimpleViewerApp.initializeOidc()); 54 | 55 | // the app is ready when all initialization promises are fulfilled 56 | this._isReady = Promise.all(initPromises).then(() => { }); 57 | } 58 | 59 | private static async initializeRpc(): Promise { 60 | const rpcParams = await this.getConnectionInfo(); 61 | initRpc(rpcParams); 62 | } 63 | 64 | private static async initializeOidc() { 65 | let clientId, redirectUri; 66 | if (ElectronRpcConfiguration.isElectron) { 67 | // We are running in an electron context 68 | clientId = Config.App.get("imjs_electron_test_client_id"); 69 | redirectUri = Config.App.get("imjs_electron_test_redirect_uri"); 70 | } else { 71 | // We are running in a web context 72 | clientId = Config.App.get("imjs_browser_test_client_id"); 73 | redirectUri = Config.App.getString("imjs_browser_test_redirect_uri"); 74 | } 75 | const scope = Config.App.getString("imjs_browser_test_scope"); 76 | const oidcConfig: OidcFrontendClientConfiguration = { clientId, redirectUri, scope }; 77 | 78 | this._oidcClient = new OidcBrowserClient(oidcConfig); 79 | 80 | const requestContext = new FrontendRequestContext(); 81 | await this._oidcClient.initialize(requestContext); 82 | 83 | IModelApp.authorizationClient = this._oidcClient; 84 | } 85 | 86 | public static shutdown() { 87 | this._oidcClient.dispose(); 88 | IModelApp.shutdown(); 89 | } 90 | 91 | private static async getConnectionInfo(): Promise { 92 | const usedBackend = Config.App.getNumber("imjs_backend", UseBackend.Local); 93 | 94 | if (usedBackend === UseBackend.Navigator) { 95 | const urlClient = new UrlDiscoveryClient(); 96 | const requestContext = new FrontendRequestContext(); 97 | const orchestratorUrl = await urlClient.discoverUrl(requestContext, "iModelJsOrchestrator.SF", undefined); 98 | return { info: { title: "navigator-backend", version: "v1.0" }, uriPrefix: orchestratorUrl }; 99 | } 100 | 101 | if (usedBackend === UseBackend.Local) 102 | return undefined; 103 | 104 | throw new Error(`Invalid backend "${usedBackend}" specified in configuration`); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /test/end-to-end/views/Content.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as Puppeteer from "puppeteer"; 7 | import { expect } from "chai"; 8 | import { page } from "../setupTests"; 9 | import { signIn, findByText } from "../helpers"; 10 | import { Config } from "@bentley/imodeljs-clients"; 11 | 12 | async function openIModel() { 13 | // Wait for "Open iModel" button to appear 14 | await page.waitForSelector(".button-open-imodel"); 15 | 16 | // Set up a promise for alert popup 17 | const dialogPromise = new Promise((response) => page.on("dialog", response)); 18 | 19 | // Open iModel 20 | await page.click(".button-open-imodel"); 21 | 22 | // If there's an alert with an error message, catch it earlier than 5 mins 23 | await Promise.race([ 24 | dialogPromise.then((dialog) => { throw new Error((dialog as Puppeteer.Dialog).message()); }), 25 | page.waitFor(5000), 26 | ]); 27 | 28 | // Wait for at least one node to show up in the tree 29 | await page.waitForSelector(`[data-testid="tree-node"]`, {timeout: 300000}); // 5 min. timeout, IModel may take a long time to load 30 | } 31 | 32 | async function findNode(text: string) { 33 | const selector = `//div[contains(@data-testid, "tree-node") and contains(., "${text}")]`; 34 | 35 | // Wait for Node to render 36 | await page.waitForXPath(selector, { visible: true }); 37 | 38 | // Find Node 39 | const elementHandles = await page.$x(selector); 40 | if (!elementHandles[0]) 41 | throw Error(`Node "${text}" not found!`); 42 | 43 | return elementHandles[0]; 44 | } 45 | 46 | async function findAndExpandNode(text: string) { 47 | const nodeHandle = await findNode(text); 48 | 49 | // Expand it 50 | const expansionHandle = await nodeHandle.$(`[data-testid="tree-node-expansion-toggle"]`); 51 | if (!expansionHandle) 52 | throw Error(`Expansion handle for node "${text}" not found!`); 53 | await expansionHandle.click(); 54 | } 55 | 56 | describe("Content view", () => { 57 | 58 | it("renders after loading iModel", async () => { 59 | await signIn(page); 60 | await openIModel(); 61 | 62 | // Make sure that iModel canvas has appeared 63 | await page.waitForSelector("canvas"); 64 | 65 | // Make sure that property pane is rendered 66 | await page.waitForSelector(".components-property-grid"); 67 | 68 | // Make sure that table is rendered 69 | await page.waitForSelector(".components-table"); 70 | 71 | // Make sure that toolbar is rendered 72 | await page.waitForSelector(".toolbar"); 73 | }); 74 | 75 | it("loads table and property data after clicking on a tree node", async () => { 76 | await signIn(page); 77 | await openIModel(); 78 | 79 | // Make sure that neither table nor property pane renders before data is loaded 80 | expect(async () => page.$(".components-table .components-table-cell")).to.throw; 81 | expect(async () => page.$(".components-property-grid .components-property-category-block")).to.throw; 82 | 83 | // Expand nodes 84 | await findAndExpandNode(Config.App.getString("imjs_test_project")); 85 | await findAndExpandNode("BisCore.DictionaryModel"); 86 | await findAndExpandNode("Line Style"); 87 | 88 | // Find and select 'lc1' node 89 | const nodeHandle = await findNode("lc1"); 90 | await nodeHandle.hover(); // Ensure that mouse is over the node 91 | await nodeHandle.click(); 92 | 93 | // Wait for table to load 94 | await page.waitForSelector(".components-table .components-table-cell"); 95 | 96 | // Limit search to table 97 | const tableHandle = await page.$(".components-table"); 98 | expect(tableHandle, "Table wrapper not found!").to.exist; 99 | // Find "lc1" in table 100 | await findByText(tableHandle!, "lc1"); 101 | 102 | // Expand properties 103 | await page.waitFor(".uicore-expandable-blocks-block .title"); 104 | const expanderHandle = await page.$$(".uicore-expandable-blocks-block .title"); 105 | expect(expanderHandle[0], "Property Pane block not found!").to.exist; 106 | await expanderHandle[0]!.click(); 107 | 108 | // Limit search to properties 109 | const propertiesHandle = await page.$(".components-property-grid-wrapper"); 110 | expect(propertiesHandle, "Property Pane wrapper not found!").to.exist; 111 | // Find "lc1" in properties 112 | await findByText(propertiesHandle!, "lc1"); 113 | }); 114 | 115 | }); 116 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "compounds": [ 7 | { 8 | "name": "Web: All", 9 | "configurations": [ 10 | "Make config", 11 | "Web: Server (webserver)", 12 | "Web: Chrome (frontend)", 13 | ] 14 | }, 15 | { 16 | "name": "Electron: All", 17 | "configurations": [ 18 | "Electron: Main (backend)" 19 | ] 20 | } 21 | ], 22 | "configurations": [ 23 | { 24 | "type": "node", 25 | "request": "launch", 26 | "name": "Web: Server (webserver)", 27 | "protocol": "inspector", 28 | "program": "${workspaceFolder}/node_modules/env-cmd/bin/env-cmd.js", 29 | "cwd": "${workspaceFolder}", 30 | "showAsyncStacks": true, 31 | "smartStep": true, 32 | "restart": true, 33 | "args": [ 34 | "./lib/webresources/config.json", 35 | "node", 36 | "${workspaceFolder}/node_modules/npm-run-all/bin/run-p/index.js", 37 | "start:webserver", 38 | "start:backend" 39 | ] 40 | }, 41 | { 42 | "type": "node", 43 | "request": "launch", 44 | "name": "Web: Server (backend)", 45 | "protocol": "inspector", 46 | "program": "${workspaceFolder}/lib/backend/main.js", 47 | "cwd": "${workspaceFolder}", 48 | "showAsyncStacks": true, 49 | "smartStep": true, 50 | "restart": true 51 | }, 52 | { 53 | "name": "Make config", 54 | "type": "node", 55 | "request": "launch", 56 | "protocol": "inspector", 57 | "program": "${workspaceFolder}/node_modules/strip-json-comments-cli/cli.js", 58 | "cwd": "${workspaceFolder}", 59 | "args": [ 60 | "--no-whitespace", 61 | "${workspaceFolder}/src/common/config.json", 62 | ">", 63 | "${workspaceFolder}/lib/webresources/config.json", 64 | "${workspaceFolder}/test/unit/**/*.test.ts*" 65 | ] 66 | }, 67 | { 68 | "type": "node", 69 | "request": "launch", 70 | "name": "Web: Server (backend) for tests", 71 | "protocol": "inspector", 72 | "program": "${workspaceFolder}/lib/backend/main.js", 73 | "cwd": "${workspaceFolder}", 74 | "env": { 75 | "imjs_test_project": "Retail Building Sample QA", 76 | "imjs_test_imodel": "Retail Building Sample QA", 77 | "imjs_buddi_resolve_url_using_region": "102" 78 | }, 79 | "showAsyncStacks": true, 80 | "smartStep": true, 81 | "restart": true 82 | }, 83 | { 84 | "name": "Web: Chrome (frontend)", 85 | "type": "chrome", 86 | "request": "launch", 87 | "url": "http://localhost:3000/", 88 | "webRoot": "${workspaceFolder}/src", 89 | "sourceMapPathOverrides": { 90 | "webpack:///src/*": "${webRoot}/*" 91 | }, 92 | "smartStep": true 93 | }, 94 | { 95 | "type": "node", 96 | "request": "launch", 97 | "name": "Electron: Main (backend)", 98 | "protocol": "inspector", 99 | "cwd": "${workspaceFolder}", 100 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 101 | "windows": { 102 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" 103 | }, 104 | "env": { 105 | "BROWSER": "none" 106 | }, 107 | "args": [ 108 | "lib/backend/main.js" 109 | ], 110 | }, 111 | { 112 | "name": "E2E Tests only", 113 | "type": "node", 114 | "request": "launch", 115 | "protocol": "inspector", 116 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 117 | "cwd": "${workspaceFolder}/test/end-to-end", 118 | "args": [ 119 | "--opts", 120 | "${workspaceFolder}/test/end-to-end/mocha.opts", 121 | "${workspaceFolder}/test/end-to-end/**/*.test.ts*" 122 | ] 123 | }, 124 | { 125 | "name": "Unit Tests only", 126 | "type": "node", 127 | "request": "launch", 128 | "protocol": "inspector", 129 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 130 | "cwd": "${workspaceFolder}", 131 | "args": [ 132 | "--opts", 133 | "${workspaceFolder}/test/unit/mocha.opts", 134 | "${workspaceFolder}/test/unit/**/*.test.ts*" 135 | ] 136 | }, 137 | { 138 | "name": "Electron test", 139 | "type": "node", 140 | "request": "launch", 141 | "protocol": "inspector", 142 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 143 | "cwd": "${workspaceFolder}", 144 | "args": [ 145 | "--opts", 146 | "${workspaceFolder}/test/electron/mocha.opts", 147 | "${workspaceFolder}/test/electron/**/*.test.ts*" 148 | ] 149 | } 150 | ] 151 | } 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Viewer App 2 | 3 | > NOTE: This repository has been archived and all future updates will be made in the [iModel.js Samples](https://github.com/imodeljs/imodeljs-samples) repository. 4 | 5 | Copyright © 2019 Bentley Systems, Incorporated. All rights reserved. 6 | 7 | An iModel.js sample application that demonstrates opening an iModel and viewing its data. The data is presented using the following components: 8 | 9 | * _Viewport_: Renders geometric data onto an HTMLCanvasElement. 10 | * _Tree_: Displays a hierarchical view of iModel contents. 11 | * _Property Grid_: Displays properties of selected element(s). 12 | * _Table_: Displays element properties in a tabular format. 13 | 14 | This app serves as a guide on how you can embed one or more of these components into your own application. 15 | See http://imodeljs.org for comprehensive documentation on the iModel.js API and the various constructs used in this sample. 16 | 17 | ## Development Setup 18 | 19 | 1. Get the [required tools](https://imodeljs.github.io/iModelJs-docs-output/getting-started/#1-get-the-tools) and ensure you have _Node.js 10.x LTS_ installed on your machine. 20 | 21 | 2. (Optional) Create a sample project using the procedure at [Developer Registration](https://imodeljs.github.io/iModelJs-docs-output/getting-started/#developer-registration). This step is not needed if you already have a project to test with. 22 | 23 | 3. (Recommended) Register your application at [Developer Registration](https://imodeljs.github.io/iModelJs-docs-output/getting-started/#developer-registration). 24 | 25 | For the purpose of running this sample on localhost, ensure your registration includes http://localhost:3000/signin-callback as a valid redirect URI. 26 | 27 | If you would like to run this sample in electron, another application should be registered which includes [electron://frontend/signin-callback]() as a valid redirect URI. 28 | 29 | Note: If you are just testing on localhost you can use the default registration included in this sample. However, it's recommended that you complete the registration, especially since registration is a requirement before the application can be deployed. For more information, see the section on [authorization](https://imodeljs.github.io/iModelJs-docs-output/learning/common/accesstoken/). 30 | 31 | 4. Edit [src/common/configuration.ts](./src/common/configuration.ts) and [src/common/config.json](./src/common/config.json) to set the values you obtain from the registration process. 32 | 33 | 5. Install the dependencies 34 | 35 | ```sh 36 | npm install 37 | ``` 38 | 39 | 6. Build the application 40 | 41 | ```sh 42 | npm run build 43 | ``` 44 | 45 | 7. There are two servers, a web server that delivers the static web resources (the frontend Javascript, localizable strings, fonts, cursors, etc.), and the backend RPC server that opens the iModel on behalf of the application. Start them both running locally: 46 | 47 | ```sh 48 | npm run start:servers 49 | ``` 50 | 51 | 8. Open a web browser (e.g., Chrome or Edge), and browse to localhost:3000. 52 | 53 | [//]: # (Commented out until Electron version fixed. Note: The Electron version is meant to run on desktops, but will currently not work within a virtual machine.) 54 | 55 | ![Screenshot of the application](./docs/header.png) 56 | 57 | ## Testing 58 | 59 | Run both e2e and unit tests with `npm test` 60 | 61 | ### End-to-end tests 62 | 63 | You can run just end-to-end tests with `npm run test:e2e`. But it takes a while 64 | to build and start the tests, so if want to actively change something within them, 65 | first launch the app with `npm run test:e2e:start-app` and when it's done `npm run test:e2e:test-app` 66 | 67 | If you want to see what tests do behind the scenes, you can launch them in non 68 | headless mode. Edit the file in *./test/end-to-end/setupTests.ts* and add 69 | 70 | ```js 71 | { headless: false } 72 | ``` 73 | 74 | to puppeteer launch options. Like this 75 | 76 | ```ts 77 | before(async () => { 78 | browser = await Puppeteer.launch({ headless: false }); 79 | }); 80 | ``` 81 | 82 | ### Unit tests 83 | 84 | Run with `npm run test:unit` 85 | 86 | ## Purpose 87 | 88 | The purpose of this application is to demonstrate the following: 89 | 90 | * [Dependencies](./package.json) required for iModel.js-based frontend applications. 91 | * [Scripts](./package.json) recommended to build and run iModel.js-based applications. 92 | * How to set up a simple backend for 93 | [web](./src/backend/web/BackendServer.ts) and 94 | [electron](./src/backend/electron/main.ts). 95 | * How to set up a simple [frontend for web and electron](./src/frontend/api/SimpleViewerApp.ts). 96 | * How to [implement OIDC sign-in](./docs/oidc.md) to get access to iModels on iModelHub. 97 | * How to [consume](./src/frontend/components/App.tsx) iModel.js React components. 98 | * How to implement unified selection between a 99 | [viewport](./src/frontend/components/Viewport.tsx), 100 | [tree](./src/frontend/components/Tree.tsx), 101 | [property grid](./src/frontend/components/Properties.tsx) and a 102 | [table](./src/frontend/components/Table.tsx). 103 | * How to include 104 | [tools](./src/frontend/components/Toolbar.tsx) in a 105 | [viewport](./src/frontend/components/Viewport.tsx). 106 | 107 | ## Contributing 108 | 109 | [Contributing to iModel.js](https://github.com/imodeljs/imodeljs/blob/master/CONTRIBUTING.md) 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bentley/simple-viewer-app", 3 | "description": "Simple Viewer App", 4 | "license": "MIT", 5 | "author": { 6 | "name": "Bentley Systems, Inc.", 7 | "url": "http://www.bentley.com" 8 | }, 9 | "os": [ 10 | "win32", 11 | "linux" 12 | ], 13 | "version": "0.0.0", 14 | "private": true, 15 | "scripts": { 16 | "build": "buildIModelJsModule", 17 | "start:webserver": "node ./node_modules/@bentley/imodeljs-webserver/lib/WebServer.js --port=3000 --resources=./lib/webresources/", 18 | "start:backend": "node lib/backend/main.js", 19 | "start:servers": "env-cmd ./lib/webresources/config.json run-p \"start:webserver\" \"start:backend\"", 20 | "electron": "electron lib/backend/main.js", 21 | "clean": "rimraf lib", 22 | "test": "npm run test:unit && npm run test:integration && npm run test:e2e", 23 | "test:unit": "mocha --opts ./test/unit/mocha.opts \"test/unit/**/*.test.ts*\"", 24 | "test:integration": "mocha --opts ./test/integration/mocha.opts \"test/integration/**/*.test.ts*\"", 25 | "test:e2e": "start-server-and-test test:e2e:start-app http-get://localhost:3000 test:e2e:test-app", 26 | "test:e2e:start-app": "env-cmd ./lib/webresources/config.json run-p \"start:webserver\" \"start:backend\"", 27 | "test:e2e:test-app": "mocha --opts ./test/end-to-end/mocha.opts \"test/end-to-end/**/*.test.ts*\"", 28 | "test:electron": "mocha --opts ./test/electron/mocha.opts \"test/electron/**/*.test.ts*\"" 29 | }, 30 | "iModelJs": { 31 | "buildModule": { 32 | "type": "application", 33 | "sourceResources": [ 34 | { 35 | "source": "./src/**/*.scss", 36 | "dest": "./lib" 37 | }, 38 | { 39 | "source": "./src/**/*.css", 40 | "dest": "./lib" 41 | }, 42 | { 43 | "source": "./public/**/*", 44 | "dest": "./lib/webresources" 45 | }, 46 | { 47 | "source": "./src/frontend/plugins/public/**/*", 48 | "dest": "./lib/webresources" 49 | } 50 | ], 51 | "webpack": { 52 | "dest": "./lib/webresources", 53 | "entry": "./lib/frontend/index.js", 54 | "bundleName": "main", 55 | "styleSheets": true, 56 | "htmlTemplate": "./src/frontend/index.html" 57 | }, 58 | "makeConfig": { 59 | "dest": "./lib/webresources/config.json", 60 | "sources": [ 61 | { 62 | "file": "process.env", 63 | "filter": "^(i|I)(m|M)(j|J)(s|S)_" 64 | }, 65 | { 66 | "file": "./src/common/config.json", 67 | "filter": "^(i|I)(m|M)(j|J)(s|S)_" 68 | } 69 | ] 70 | } 71 | } 72 | }, 73 | "dependencies": { 74 | "@bentley/bentleyjs-core": "1.4.0", 75 | "@bentley/config-loader": "1.4.0", 76 | "@bentley/electron-manager": "1.4.0", 77 | "@bentley/express-server": "1.4.0", 78 | "@bentley/geometry-core": "1.4.0", 79 | "@bentley/icons-generic-webfont": "^0.0.31", 80 | "@bentley/imodeljs-backend": "1.4.0", 81 | "@bentley/imodeljs-clients": "1.4.0", 82 | "@bentley/imodeljs-clients-backend": "1.4.0", 83 | "@bentley/imodeljs-common": "1.4.0", 84 | "@bentley/imodeljs-frontend": "1.4.0", 85 | "@bentley/imodeljs-i18n": "1.4.0", 86 | "@bentley/imodeljs-quantity": "1.4.0", 87 | "@bentley/imodeljs-webserver": "1.4.0", 88 | "@bentley/presentation-backend": "1.4.0", 89 | "@bentley/presentation-common": "1.4.0", 90 | "@bentley/presentation-components": "1.4.0", 91 | "@bentley/presentation-frontend": "1.4.0", 92 | "@bentley/presentation-testing": "1.4.0", 93 | "@bentley/ui-components": "1.4.0", 94 | "@bentley/ui-core": "1.4.0", 95 | "body-parser": "^1.18", 96 | "bunyan": "^1.8.12", 97 | "chai-jest-snapshot": "^2.0.0", 98 | "express": "^4", 99 | "inspire-tree": "^5.0.1", 100 | "lodash": "^4.17.10", 101 | "react": "^16.4.2", 102 | "react-dnd": "^5.0.0", 103 | "react-dnd-html5-backend": "^5.0.1", 104 | "react-dom": "^16.4.2", 105 | "react-redux": "^5.0.7", 106 | "redux": "^4.0.0" 107 | }, 108 | "devDependencies": { 109 | "@bentley/build-tools": "1.4.0", 110 | "@bentley/webpack-tools": "1.4.0", 111 | "@types/body-parser": "^1.17", 112 | "@types/bunyan": "^1.8", 113 | "@types/chai": "^4.1.7", 114 | "@types/express": "^4", 115 | "@types/jsdom": "^12.2.0", 116 | "@types/mocha": "^5.2.5", 117 | "@types/puppeteer": "^1.10.0", 118 | "@types/react": "^16.4.14", 119 | "@types/react-dom": "16.0.7", 120 | "chai": "^4.2.0", 121 | "chromedriver": "^2.34.1", 122 | "electron": "^4.0.1", 123 | "electron-chromedriver": "^3.0.0", 124 | "env-cmd": "^8.0.2", 125 | "ignore-styles": "^5.0.1", 126 | "jsdom": "^13.0.0", 127 | "jsdom-global": "^3.0.2", 128 | "mocha": "^5.2.0", 129 | "nodemon": "^1.18.4", 130 | "npm-run-all": "^4.1.3", 131 | "puppeteer": "^1.10.0", 132 | "react-testing-library": "^5.3.2", 133 | "rimraf": "^2.6.2", 134 | "sinon": "^7.2.0", 135 | "spectron": "^5.0.0", 136 | "start-server-and-test": "^1.7.11", 137 | "strip-json-comments-cli": "^1.0.1", 138 | "ts-node": "^7.0.1", 139 | "typemoq": "^2.1.0", 140 | "typescript": "^3.5.0", 141 | "webpack": "^4.20.2", 142 | "webpack-cli": "^3.1.0", 143 | "xmlhttprequest": "^1.8.0" 144 | }, 145 | "homepage": "http://localhost:3000/", 146 | "proxy": "http://localhost:5000" 147 | } 148 | -------------------------------------------------------------------------------- /assets/presentation_rules/Default.PresentationRuleSet.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/frontend/components/App.tsx: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2019 Bentley Systems, Incorporated. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license terms. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import * as React from "react"; 6 | import { Id64String, OpenMode } from "@bentley/bentleyjs-core"; 7 | import { AccessToken, ConnectClient, IModelQuery, Project, Config } from "@bentley/imodeljs-clients"; 8 | import { IModelApp, IModelConnection, FrontendRequestContext, AuthorizedFrontendRequestContext } from "@bentley/imodeljs-frontend"; 9 | import { Presentation, SelectionChangeEventArgs, ISelectionProvider } from "@bentley/presentation-frontend"; 10 | import { Button, ButtonSize, ButtonType, Spinner, SpinnerSize } from "@bentley/ui-core"; 11 | import { SignIn } from "@bentley/ui-components"; 12 | import { SimpleViewerApp } from "../api/SimpleViewerApp"; 13 | import PropertiesWidget from "./Properties"; 14 | import GridWidget from "./Table"; 15 | import TreeWidget from "./Tree"; 16 | import ViewportContentControl from "./Viewport"; 17 | import "@bentley/icons-generic-webfont/dist/bentley-icons-generic-webfont.css"; 18 | import "./App.css"; 19 | 20 | // tslint:disable: no-console 21 | // cSpell:ignore imodels 22 | 23 | /** React state of the App component */ 24 | export interface AppState { 25 | user: { 26 | accessToken?: AccessToken; 27 | isLoading?: boolean; 28 | }; 29 | offlineIModel: boolean; 30 | imodel?: IModelConnection; 31 | viewDefinitionId?: Id64String; 32 | } 33 | 34 | /** A component the renders the whole application UI */ 35 | export default class App extends React.Component<{}, AppState> { 36 | 37 | /** Creates an App instance */ 38 | constructor(props?: any, context?: any) { 39 | super(props, context); 40 | this.state = { 41 | user: { 42 | isLoading: false, 43 | accessToken: undefined, 44 | }, 45 | offlineIModel: false, 46 | }; 47 | } 48 | 49 | public componentDidMount() { 50 | // subscribe for unified selection changes 51 | Presentation.selection.selectionChange.addListener(this._onSelectionChanged); 52 | 53 | // Initialize authorization state, and add listener to changes 54 | SimpleViewerApp.oidcClient.onUserStateChanged.addListener(this._onUserStateChanged); 55 | if (SimpleViewerApp.oidcClient.isAuthorized) { 56 | SimpleViewerApp.oidcClient.getAccessToken(new FrontendRequestContext()) // tslint:disable-line: no-floating-promises 57 | .then((accessToken: AccessToken | undefined) => { 58 | this.setState((prev) => ({ user: { ...prev.user, accessToken, isLoading: false } })); 59 | }); 60 | } 61 | } 62 | 63 | public componentWillUnmount() { 64 | // unsubscribe from unified selection changes 65 | Presentation.selection.selectionChange.removeListener(this._onSelectionChanged); 66 | // unsubscribe from user state changes 67 | SimpleViewerApp.oidcClient.onUserStateChanged.removeListener(this._onUserStateChanged); 68 | } 69 | 70 | private _onSelectionChanged = (evt: SelectionChangeEventArgs, selectionProvider: ISelectionProvider) => { 71 | const selection = selectionProvider.getSelection(evt.imodel, evt.level); 72 | if (selection.isEmpty) { 73 | console.log("========== Selection cleared =========="); 74 | } else { 75 | console.log("========== Selection change ==========="); 76 | if (selection.instanceKeys.size !== 0) { 77 | // log all selected ECInstance ids grouped by ECClass name 78 | console.log("ECInstances:"); 79 | selection.instanceKeys.forEach((ids, ecclass) => { 80 | console.log(`${ecclass}: [${[...ids].join(",")}]`); 81 | }); 82 | } 83 | if (selection.nodeKeys.size !== 0) { 84 | // log all selected node keys 85 | console.log("Nodes:"); 86 | selection.nodeKeys.forEach((key) => console.log(JSON.stringify(key))); 87 | } 88 | console.log("======================================="); 89 | } 90 | } 91 | 92 | private _onRegister = () => { 93 | window.open("https://imodeljs.github.io/iModelJs-docs-output/getting-started/#developer-registration", "_blank"); 94 | } 95 | 96 | private _onOffline = () => { 97 | this.setState((prev) => ({ user: { ...prev.user, isLoading: false }, offlineIModel: true })); 98 | } 99 | 100 | private _onStartSignin = async () => { 101 | this.setState((prev) => ({ user: { ...prev.user, isLoading: true } })); 102 | await SimpleViewerApp.oidcClient.signIn(new FrontendRequestContext()); 103 | } 104 | 105 | private _onUserStateChanged = (accessToken: AccessToken | undefined) => { 106 | this.setState((prev) => ({ user: { ...prev.user, accessToken, isLoading: false } })); 107 | } 108 | 109 | /** Pick the first available spatial view definition in the imodel */ 110 | private async getFirstViewDefinitionId(imodel: IModelConnection): Promise { 111 | const viewSpecs = await imodel.views.queryProps({}); 112 | const acceptedViewClasses = [ 113 | "BisCore:SpatialViewDefinition", 114 | "BisCore:DrawingViewDefinition", 115 | ]; 116 | const acceptedViewSpecs = viewSpecs.filter((spec) => (-1 !== acceptedViewClasses.indexOf(spec.classFullName))); 117 | if (0 === acceptedViewSpecs.length) 118 | throw new Error("No valid view definitions in imodel"); 119 | 120 | // Prefer spatial view over drawing. 121 | const spatialViews = acceptedViewSpecs.filter((v) => { 122 | return v.classFullName === "BisCore:SpatialViewDefinition"; 123 | }); 124 | 125 | if (spatialViews.length > 0) 126 | return spatialViews[0].id!; 127 | 128 | return acceptedViewSpecs[0].id!; 129 | } 130 | 131 | /** Handle iModel open event */ 132 | private _onIModelSelected = async (imodel: IModelConnection | undefined) => { 133 | if (!imodel) { 134 | // reset the state when imodel is closed 135 | this.setState({ imodel: undefined, viewDefinitionId: undefined }); 136 | return; 137 | } 138 | try { 139 | // attempt to get a view definition 140 | const viewDefinitionId = imodel ? await this.getFirstViewDefinitionId(imodel) : undefined; 141 | this.setState({ imodel, viewDefinitionId }); 142 | } catch (e) { 143 | // if failed, close the imodel and reset the state 144 | if (this.state.offlineIModel) { 145 | await imodel.closeSnapshot(); 146 | } else { 147 | await imodel.close(); 148 | } 149 | this.setState({ imodel: undefined, viewDefinitionId: undefined }); 150 | alert(e.message); 151 | } 152 | } 153 | 154 | private get _signInRedirectUri() { 155 | const split = (Config.App.get("imjs_browser_test_redirect_uri") as string).split("://"); 156 | return split[split.length - 1]; 157 | } 158 | 159 | /** The component's render method */ 160 | public render() { 161 | let ui: React.ReactNode; 162 | 163 | if (this.state.user.isLoading || window.location.href.includes(this._signInRedirectUri)) { 164 | // if user is currently being loaded, just tell that 165 | ui = `${IModelApp.i18n.translate("SimpleViewer:signing-in")}...`; 166 | } else if (!this.state.user.accessToken && !this.state.offlineIModel) { 167 | // if user doesn't have and access token, show sign in page 168 | ui = (); 169 | } else if (!this.state.imodel || !this.state.viewDefinitionId) { 170 | // if we don't have an imodel / view definition id - render a button that initiates imodel open 171 | ui = (); 172 | } else { 173 | // if we do have an imodel and view definition id - render imodel components 174 | ui = (); 175 | } 176 | 177 | // render the app 178 | return ( 179 |
180 |
181 |

{IModelApp.i18n.translate("SimpleViewer:welcome-message")}

182 |
183 | {ui} 184 |
185 | ); 186 | } 187 | } 188 | 189 | /** React props for [[OpenIModelButton]] component */ 190 | interface OpenIModelButtonProps { 191 | accessToken: AccessToken | undefined; 192 | offlineIModel: boolean; 193 | onIModelSelected: (imodel: IModelConnection | undefined) => void; 194 | } 195 | /** React state for [[OpenIModelButton]] component */ 196 | interface OpenIModelButtonState { 197 | isLoading: boolean; 198 | } 199 | /** Renders a button that opens an iModel identified in configuration */ 200 | class OpenIModelButton extends React.PureComponent { 201 | public state = { isLoading: false }; 202 | 203 | /** Finds project and imodel ids using their names */ 204 | private async getIModelInfo(): Promise<{ projectId: string, imodelId: string }> { 205 | const projectName = Config.App.get("imjs_test_project"); 206 | const imodelName = Config.App.get("imjs_test_imodel"); 207 | 208 | const requestContext: AuthorizedFrontendRequestContext = await AuthorizedFrontendRequestContext.create(); 209 | 210 | const connectClient = new ConnectClient(); 211 | let project: Project; 212 | try { 213 | project = await connectClient.getProject(requestContext, { $filter: `Name+eq+'${projectName}'` }); 214 | } catch (e) { 215 | throw new Error(`Project with name "${projectName}" does not exist`); 216 | } 217 | 218 | const imodelQuery = new IModelQuery(); 219 | imodelQuery.byName(imodelName); 220 | const imodels = await IModelApp.iModelClient.iModels.get(requestContext, project.wsgId, imodelQuery); 221 | if (imodels.length === 0) 222 | throw new Error(`iModel with name "${imodelName}" does not exist in project "${projectName}"`); 223 | return { projectId: project.wsgId, imodelId: imodels[0].wsgId }; 224 | } 225 | 226 | /** Handle iModel open event */ 227 | private async onIModelSelected(imodel: IModelConnection | undefined) { 228 | this.props.onIModelSelected(imodel); 229 | this.setState({ isLoading: false }); 230 | } 231 | 232 | private _onClick = async () => { 233 | this.setState({ isLoading: true }); 234 | let imodel: IModelConnection | undefined; 235 | try { 236 | // attempt to open the imodel 237 | if (this.props.offlineIModel) { 238 | const offlineIModel = Config.App.getString("imjs_offline_imodel"); 239 | imodel = await IModelConnection.openSnapshot(offlineIModel); 240 | } else { 241 | const info = await this.getIModelInfo(); 242 | imodel = await IModelConnection.open(info.projectId, info.imodelId, OpenMode.Readonly); 243 | } 244 | } catch (e) { 245 | alert(e.message); 246 | } 247 | await this.onIModelSelected(imodel); 248 | } 249 | 250 | public render() { 251 | return ( 252 | 256 | ); 257 | } 258 | } 259 | 260 | /** React props for [[IModelComponents]] component */ 261 | interface IModelComponentsProps { 262 | imodel: IModelConnection; 263 | viewDefinitionId: Id64String; 264 | } 265 | /** Renders a viewport, a tree, a property grid and a table */ 266 | class IModelComponents extends React.PureComponent { 267 | public render() { 268 | // ID of the presentation ruleset used by all of the controls; the ruleset 269 | // can be found at `assets/presentation_rules/Default.PresentationRuleSet.xml` 270 | const rulesetId = "Default"; 271 | return ( 272 |
273 |
274 | 275 |
276 |
277 |
278 | 279 |
280 |
281 | 282 |
283 |
284 |
285 | 286 |
287 |
288 | ); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /test/integration/Tree.test.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`RulesetTesting generates correct hierarchy for 'Items' ruleset 1`] = ` 4 | Array [ 5 | Object { 6 | "children": Array [ 7 | Object { 8 | "children": Array [ 9 | Object { 10 | "children": Array [ 11 | Object { 12 | "children": Array [ 13 | Object { 14 | "children": Array [ 15 | Object { 16 | "children": Array [ 17 | Object { 18 | "icon": "ECInstanceImage://Generic:PhysicalObject", 19 | "label": "Physical Object [0-38]", 20 | }, 21 | Object { 22 | "icon": "ECInstanceImage://Generic:PhysicalObject", 23 | "label": "Physical Object [0-39]", 24 | }, 25 | Object { 26 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 27 | "label": "TestClass [0-1K]", 28 | }, 29 | Object { 30 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 31 | "label": "TestClass [0-1L]", 32 | }, 33 | Object { 34 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 35 | "label": "TestClass [0-1M]", 36 | }, 37 | Object { 38 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 39 | "label": "TestClass [0-1N]", 40 | }, 41 | Object { 42 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 43 | "label": "TestClass [0-1O]", 44 | }, 45 | Object { 46 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 47 | "label": "TestClass [0-1P]", 48 | }, 49 | Object { 50 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 51 | "label": "TestClass [0-1Q]", 52 | }, 53 | Object { 54 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 55 | "label": "TestClass [0-1R]", 56 | }, 57 | Object { 58 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 59 | "label": "TestClass [0-1S]", 60 | }, 61 | Object { 62 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 63 | "label": "TestClass [0-1T]", 64 | }, 65 | Object { 66 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 67 | "label": "TestClass [0-1U]", 68 | }, 69 | Object { 70 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 71 | "label": "TestClass [0-1V]", 72 | }, 73 | Object { 74 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 75 | "label": "TestClass [0-1W]", 76 | }, 77 | Object { 78 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 79 | "label": "TestClass [0-1X]", 80 | }, 81 | Object { 82 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 83 | "label": "TestClass [0-1Y]", 84 | }, 85 | Object { 86 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 87 | "label": "TestClass [0-1Z]", 88 | }, 89 | Object { 90 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 91 | "label": "TestClass [0-2A]", 92 | }, 93 | Object { 94 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 95 | "label": "TestClass [0-2B]", 96 | }, 97 | Object { 98 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 99 | "label": "TestClass [0-2C]", 100 | }, 101 | Object { 102 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 103 | "label": "TestClass [0-2D]", 104 | }, 105 | Object { 106 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 107 | "label": "TestClass [0-2E]", 108 | }, 109 | Object { 110 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 111 | "label": "TestClass [0-2F]", 112 | }, 113 | Object { 114 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 115 | "label": "TestClass [0-2G]", 116 | }, 117 | Object { 118 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 119 | "label": "TestClass [0-2H]", 120 | }, 121 | Object { 122 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 123 | "label": "TestClass [0-2I]", 124 | }, 125 | Object { 126 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 127 | "label": "TestClass [0-2J]", 128 | }, 129 | Object { 130 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 131 | "label": "TestClass [0-2K]", 132 | }, 133 | Object { 134 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 135 | "label": "TestClass [0-2L]", 136 | }, 137 | Object { 138 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 139 | "label": "TestClass [0-2M]", 140 | }, 141 | Object { 142 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 143 | "label": "TestClass [0-2N]", 144 | }, 145 | Object { 146 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 147 | "label": "TestClass [0-2O]", 148 | }, 149 | Object { 150 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 151 | "label": "TestClass [0-2P]", 152 | }, 153 | Object { 154 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 155 | "label": "TestClass [0-2Q]", 156 | }, 157 | Object { 158 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 159 | "label": "TestClass [0-2R]", 160 | }, 161 | Object { 162 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 163 | "label": "TestClass [0-2S]", 164 | }, 165 | Object { 166 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 167 | "label": "TestClass [0-2T]", 168 | }, 169 | Object { 170 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 171 | "label": "TestClass [0-2U]", 172 | }, 173 | Object { 174 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 175 | "label": "TestClass [0-2V]", 176 | }, 177 | Object { 178 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 179 | "label": "TestClass [0-2W]", 180 | }, 181 | Object { 182 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 183 | "label": "TestClass [0-2X]", 184 | }, 185 | Object { 186 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 187 | "label": "TestClass [0-2Y]", 188 | }, 189 | Object { 190 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 191 | "label": "TestClass [0-2Z]", 192 | }, 193 | Object { 194 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 195 | "label": "TestClass [0-20]", 196 | }, 197 | Object { 198 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 199 | "label": "TestClass [0-21]", 200 | }, 201 | Object { 202 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 203 | "label": "TestClass [0-22]", 204 | }, 205 | Object { 206 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 207 | "label": "TestClass [0-23]", 208 | }, 209 | Object { 210 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 211 | "label": "TestClass [0-24]", 212 | }, 213 | Object { 214 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 215 | "label": "TestClass [0-25]", 216 | }, 217 | Object { 218 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 219 | "label": "TestClass [0-26]", 220 | }, 221 | Object { 222 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 223 | "label": "TestClass [0-27]", 224 | }, 225 | Object { 226 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 227 | "label": "TestClass [0-28]", 228 | }, 229 | Object { 230 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 231 | "label": "TestClass [0-29]", 232 | }, 233 | Object { 234 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 235 | "label": "TestClass [0-30]", 236 | }, 237 | Object { 238 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 239 | "label": "TestClass [0-31]", 240 | }, 241 | Object { 242 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 243 | "label": "TestClass [0-32]", 244 | }, 245 | Object { 246 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 247 | "label": "TestClass [0-33]", 248 | }, 249 | Object { 250 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 251 | "label": "TestClass [0-34]", 252 | }, 253 | Object { 254 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 255 | "label": "TestClass [0-35]", 256 | }, 257 | Object { 258 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 259 | "label": "TestClass [0-36]", 260 | }, 261 | Object { 262 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 263 | "label": "TestClass [0-37]", 264 | }, 265 | ], 266 | "description": "The Spatial Category used to categorize the 3D Geometric Element", 267 | "hasChildren": true, 268 | "icon": "ECPropertyImage://BisCore:GeometricElement3d--Category", 269 | "label": "Uncategorized", 270 | }, 271 | ], 272 | "hasChildren": true, 273 | "icon": "ECInstanceImage://BisCore:PhysicalPartition", 274 | "label": "Properties_60InstancesWithUrl2", 275 | }, 276 | ], 277 | "hasChildren": true, 278 | "icon": "ECInstanceImage://BisCore:Subject", 279 | "label": "Properties_60InstancesWithUrl2", 280 | }, 281 | Object { 282 | "icon": "ECInstanceImage://BisCore:DocumentPartition", 283 | "label": "Converted Drawings", 284 | }, 285 | Object { 286 | "icon": "ECInstanceImage://BisCore:GroupInformationPartition", 287 | "label": "Converted Groups", 288 | }, 289 | Object { 290 | "icon": "ECInstanceImage://BisCore:DocumentPartition", 291 | "label": "Converted Sheets", 292 | }, 293 | Object { 294 | "children": Array [ 295 | Object { 296 | "children": Array [ 297 | Object { 298 | "icon": "ECInstanceImage://BisCore:DisplayStyle3d", 299 | "label": "Default - View 2", 300 | }, 301 | Object { 302 | "icon": "ECInstanceImage://BisCore:DisplayStyle3d", 303 | "label": "Default - View 3", 304 | }, 305 | Object { 306 | "icon": "ECInstanceImage://BisCore:DisplayStyle3d", 307 | "label": "Default - View 4", 308 | }, 309 | Object { 310 | "icon": "ECInstanceImage://BisCore:DisplayStyle3d", 311 | "label": "Illustration", 312 | }, 313 | ], 314 | "hasChildren": true, 315 | "icon": "ECClassImage://BisCore:DisplayStyle3d", 316 | "label": "3D Display Style", 317 | }, 318 | Object { 319 | "children": Array [ 320 | Object { 321 | "icon": "ECInstanceImage://BisCore:CategorySelector", 322 | "label": "Default - View 1", 323 | }, 324 | Object { 325 | "icon": "ECInstanceImage://BisCore:CategorySelector", 326 | "label": "Default - View 2", 327 | }, 328 | Object { 329 | "icon": "ECInstanceImage://BisCore:CategorySelector", 330 | "label": "Default - View 3", 331 | }, 332 | Object { 333 | "icon": "ECInstanceImage://BisCore:CategorySelector", 334 | "label": "Default - View 4", 335 | }, 336 | ], 337 | "hasChildren": true, 338 | "icon": "ECClassImage://BisCore:CategorySelector", 339 | "label": "Category Selector", 340 | }, 341 | Object { 342 | "children": Array [ 343 | Object { 344 | "children": Array [ 345 | Object { 346 | "icon": "ECInstanceImage://BisCore:SubCategory", 347 | "label": "Uncategorized", 348 | }, 349 | ], 350 | "hasChildren": true, 351 | "icon": "ECInstanceImage://BisCore:DrawingCategory", 352 | "label": "Uncategorized", 353 | }, 354 | ], 355 | "hasChildren": true, 356 | "icon": "ECClassImage://BisCore:DrawingCategory", 357 | "label": "Drawing Category", 358 | }, 359 | Object { 360 | "children": Array [ 361 | Object { 362 | "icon": "ECInstanceImage://BisCore:ModelSelector", 363 | "label": "Default - View 1", 364 | }, 365 | Object { 366 | "icon": "ECInstanceImage://BisCore:ModelSelector", 367 | "label": "Default - View 2", 368 | }, 369 | Object { 370 | "icon": "ECInstanceImage://BisCore:ModelSelector", 371 | "label": "Default - View 3", 372 | }, 373 | Object { 374 | "icon": "ECInstanceImage://BisCore:ModelSelector", 375 | "label": "Default - View 4", 376 | }, 377 | ], 378 | "hasChildren": true, 379 | "icon": "ECClassImage://BisCore:ModelSelector", 380 | "label": "Model Selector", 381 | }, 382 | Object { 383 | "children": Array [ 384 | Object { 385 | "icon": "ECInstanceImage://BisCore:AuxCoordSystemSpatial", 386 | "label": "Default - View 1", 387 | }, 388 | Object { 389 | "icon": "ECInstanceImage://BisCore:AuxCoordSystemSpatial", 390 | "label": "Default - View 2", 391 | }, 392 | Object { 393 | "icon": "ECInstanceImage://BisCore:AuxCoordSystemSpatial", 394 | "label": "Default - View 3", 395 | }, 396 | Object { 397 | "icon": "ECInstanceImage://BisCore:AuxCoordSystemSpatial", 398 | "label": "Default - View 4", 399 | }, 400 | ], 401 | "description": "A spatial coordinate system.", 402 | "hasChildren": true, 403 | "icon": "ECClassImage://BisCore:AuxCoordSystemSpatial", 404 | "label": "Spatial Auxiliary Coordinate System", 405 | }, 406 | Object { 407 | "children": Array [ 408 | Object { 409 | "children": Array [ 410 | Object { 411 | "icon": "ECInstanceImage://BisCore:SubCategory", 412 | "label": "Uncategorized", 413 | }, 414 | ], 415 | "hasChildren": true, 416 | "icon": "ECInstanceImage://BisCore:SpatialCategory", 417 | "label": "Uncategorized", 418 | }, 419 | ], 420 | "hasChildren": true, 421 | "icon": "ECClassImage://BisCore:SpatialCategory", 422 | "label": "Spatial Category", 423 | }, 424 | Object { 425 | "children": Array [ 426 | Object { 427 | "icon": "ECInstanceImage://BisCore:SpatialViewDefinition", 428 | "label": "Default - View 1", 429 | }, 430 | Object { 431 | "icon": "ECInstanceImage://BisCore:SpatialViewDefinition", 432 | "label": "Default - View 2", 433 | }, 434 | Object { 435 | "icon": "ECInstanceImage://BisCore:SpatialViewDefinition", 436 | "label": "Default - View 3", 437 | }, 438 | Object { 439 | "icon": "ECInstanceImage://BisCore:SpatialViewDefinition", 440 | "label": "Default - View 4", 441 | }, 442 | ], 443 | "description": "A view of a spatially located volume.", 444 | "hasChildren": true, 445 | "icon": "ECClassImage://BisCore:SpatialViewDefinition", 446 | "label": "Spatial View Definition", 447 | }, 448 | ], 449 | "hasChildren": true, 450 | "icon": "ECInstanceImage://BisCore:DefinitionPartition", 451 | "label": "Definition Model For DgnV8Bridge:D:\\\\Temp\\\\Properties_60InstancesWithUrl2.dgn, Default", 452 | }, 453 | ], 454 | "hasChildren": true, 455 | "icon": "ECInstanceImage://BisCore:Subject", 456 | "label": "DgnV8Bridge:D:\\\\Temp\\\\Properties_60InstancesWithUrl2.dgn, Default", 457 | }, 458 | Object { 459 | "children": Array [ 460 | Object { 461 | "children": Array [ 462 | Object { 463 | "icon": "ECInstanceImage://BisCore:LineStyle", 464 | "label": "lc1", 465 | }, 466 | Object { 467 | "icon": "ECInstanceImage://BisCore:LineStyle", 468 | "label": "lc2", 469 | }, 470 | Object { 471 | "icon": "ECInstanceImage://BisCore:LineStyle", 472 | "label": "lc3", 473 | }, 474 | Object { 475 | "icon": "ECInstanceImage://BisCore:LineStyle", 476 | "label": "lc4", 477 | }, 478 | Object { 479 | "icon": "ECInstanceImage://BisCore:LineStyle", 480 | "label": "lc5", 481 | }, 482 | Object { 483 | "icon": "ECInstanceImage://BisCore:LineStyle", 484 | "label": "lc6", 485 | }, 486 | Object { 487 | "icon": "ECInstanceImage://BisCore:LineStyle", 488 | "label": "lc7", 489 | }, 490 | ], 491 | "hasChildren": true, 492 | "icon": "ECClassImage://BisCore:LineStyle", 493 | "label": "Line Style", 494 | }, 495 | ], 496 | "hasChildren": true, 497 | "icon": "ECInstanceImage://BisCore:DefinitionPartition", 498 | "label": "BisCore.DictionaryModel", 499 | }, 500 | Object { 501 | "icon": "ECInstanceImage://BisCore:LinkPartition", 502 | "label": "BisCore.RealityDataSources", 503 | }, 504 | ], 505 | "hasChildren": true, 506 | "icon": "ECInstanceImage://BisCore:Subject", 507 | "label": "DgnV8Bridge", 508 | }, 509 | ], 510 | "description": "test nested value", 511 | "hasChildren": true, 512 | "icon": "subjects", 513 | "label": "test value", 514 | }, 515 | ] 516 | `; 517 | 518 | exports[`RulesetTesting generates correct hierarchy for 'default' ruleset 1`] = ` 519 | Array [ 520 | Object { 521 | "label": "root", 522 | }, 523 | ] 524 | `; 525 | 526 | exports[`Tree generates correct hierarchy for 'Default' ruleset 1`] = ` 527 | Array [ 528 | Object { 529 | "children": Array [ 530 | Object { 531 | "children": Array [ 532 | Object { 533 | "children": Array [ 534 | Object { 535 | "children": Array [ 536 | Object { 537 | "children": Array [ 538 | Object { 539 | "icon": "ECInstanceImage://Generic:PhysicalObject", 540 | "label": "Physical Object [0-38]", 541 | }, 542 | Object { 543 | "icon": "ECInstanceImage://Generic:PhysicalObject", 544 | "label": "Physical Object [0-39]", 545 | }, 546 | Object { 547 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 548 | "label": "TestClass [0-1K]", 549 | }, 550 | Object { 551 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 552 | "label": "TestClass [0-1L]", 553 | }, 554 | Object { 555 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 556 | "label": "TestClass [0-1M]", 557 | }, 558 | Object { 559 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 560 | "label": "TestClass [0-1N]", 561 | }, 562 | Object { 563 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 564 | "label": "TestClass [0-1O]", 565 | }, 566 | Object { 567 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 568 | "label": "TestClass [0-1P]", 569 | }, 570 | Object { 571 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 572 | "label": "TestClass [0-1Q]", 573 | }, 574 | Object { 575 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 576 | "label": "TestClass [0-1R]", 577 | }, 578 | Object { 579 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 580 | "label": "TestClass [0-1S]", 581 | }, 582 | Object { 583 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 584 | "label": "TestClass [0-1T]", 585 | }, 586 | Object { 587 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 588 | "label": "TestClass [0-1U]", 589 | }, 590 | Object { 591 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 592 | "label": "TestClass [0-1V]", 593 | }, 594 | Object { 595 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 596 | "label": "TestClass [0-1W]", 597 | }, 598 | Object { 599 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 600 | "label": "TestClass [0-1X]", 601 | }, 602 | Object { 603 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 604 | "label": "TestClass [0-1Y]", 605 | }, 606 | Object { 607 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 608 | "label": "TestClass [0-1Z]", 609 | }, 610 | Object { 611 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 612 | "label": "TestClass [0-2A]", 613 | }, 614 | Object { 615 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 616 | "label": "TestClass [0-2B]", 617 | }, 618 | Object { 619 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 620 | "label": "TestClass [0-2C]", 621 | }, 622 | Object { 623 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 624 | "label": "TestClass [0-2D]", 625 | }, 626 | Object { 627 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 628 | "label": "TestClass [0-2E]", 629 | }, 630 | Object { 631 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 632 | "label": "TestClass [0-2F]", 633 | }, 634 | Object { 635 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 636 | "label": "TestClass [0-2G]", 637 | }, 638 | Object { 639 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 640 | "label": "TestClass [0-2H]", 641 | }, 642 | Object { 643 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 644 | "label": "TestClass [0-2I]", 645 | }, 646 | Object { 647 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 648 | "label": "TestClass [0-2J]", 649 | }, 650 | Object { 651 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 652 | "label": "TestClass [0-2K]", 653 | }, 654 | Object { 655 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 656 | "label": "TestClass [0-2L]", 657 | }, 658 | Object { 659 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 660 | "label": "TestClass [0-2M]", 661 | }, 662 | Object { 663 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 664 | "label": "TestClass [0-2N]", 665 | }, 666 | Object { 667 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 668 | "label": "TestClass [0-2O]", 669 | }, 670 | Object { 671 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 672 | "label": "TestClass [0-2P]", 673 | }, 674 | Object { 675 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 676 | "label": "TestClass [0-2Q]", 677 | }, 678 | Object { 679 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 680 | "label": "TestClass [0-2R]", 681 | }, 682 | Object { 683 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 684 | "label": "TestClass [0-2S]", 685 | }, 686 | Object { 687 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 688 | "label": "TestClass [0-2T]", 689 | }, 690 | Object { 691 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 692 | "label": "TestClass [0-2U]", 693 | }, 694 | Object { 695 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 696 | "label": "TestClass [0-2V]", 697 | }, 698 | Object { 699 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 700 | "label": "TestClass [0-2W]", 701 | }, 702 | Object { 703 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 704 | "label": "TestClass [0-2X]", 705 | }, 706 | Object { 707 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 708 | "label": "TestClass [0-2Y]", 709 | }, 710 | Object { 711 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 712 | "label": "TestClass [0-2Z]", 713 | }, 714 | Object { 715 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 716 | "label": "TestClass [0-20]", 717 | }, 718 | Object { 719 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 720 | "label": "TestClass [0-21]", 721 | }, 722 | Object { 723 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 724 | "label": "TestClass [0-22]", 725 | }, 726 | Object { 727 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 728 | "label": "TestClass [0-23]", 729 | }, 730 | Object { 731 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 732 | "label": "TestClass [0-24]", 733 | }, 734 | Object { 735 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 736 | "label": "TestClass [0-25]", 737 | }, 738 | Object { 739 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 740 | "label": "TestClass [0-26]", 741 | }, 742 | Object { 743 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 744 | "label": "TestClass [0-27]", 745 | }, 746 | Object { 747 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 748 | "label": "TestClass [0-28]", 749 | }, 750 | Object { 751 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 752 | "label": "TestClass [0-29]", 753 | }, 754 | Object { 755 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 756 | "label": "TestClass [0-30]", 757 | }, 758 | Object { 759 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 760 | "label": "TestClass [0-31]", 761 | }, 762 | Object { 763 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 764 | "label": "TestClass [0-32]", 765 | }, 766 | Object { 767 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 768 | "label": "TestClass [0-33]", 769 | }, 770 | Object { 771 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 772 | "label": "TestClass [0-34]", 773 | }, 774 | Object { 775 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 776 | "label": "TestClass [0-35]", 777 | }, 778 | Object { 779 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 780 | "label": "TestClass [0-36]", 781 | }, 782 | Object { 783 | "icon": "ECInstanceImage://PCJ_TestSchema:TestClass", 784 | "label": "TestClass [0-37]", 785 | }, 786 | ], 787 | "description": "The Spatial Category used to categorize the 3D Geometric Element", 788 | "hasChildren": true, 789 | "icon": "ECPropertyImage://BisCore:GeometricElement3d--Category", 790 | "label": "Uncategorized", 791 | }, 792 | ], 793 | "hasChildren": true, 794 | "icon": "ECInstanceImage://BisCore:PhysicalPartition", 795 | "label": "Properties_60InstancesWithUrl2", 796 | }, 797 | ], 798 | "hasChildren": true, 799 | "icon": "ECInstanceImage://BisCore:Subject", 800 | "label": "Properties_60InstancesWithUrl2", 801 | }, 802 | Object { 803 | "icon": "ECInstanceImage://BisCore:DocumentPartition", 804 | "label": "Converted Drawings", 805 | }, 806 | Object { 807 | "icon": "ECInstanceImage://BisCore:GroupInformationPartition", 808 | "label": "Converted Groups", 809 | }, 810 | Object { 811 | "icon": "ECInstanceImage://BisCore:DocumentPartition", 812 | "label": "Converted Sheets", 813 | }, 814 | Object { 815 | "children": Array [ 816 | Object { 817 | "children": Array [ 818 | Object { 819 | "icon": "ECInstanceImage://BisCore:DisplayStyle3d", 820 | "label": "Default - View 2", 821 | }, 822 | Object { 823 | "icon": "ECInstanceImage://BisCore:DisplayStyle3d", 824 | "label": "Default - View 3", 825 | }, 826 | Object { 827 | "icon": "ECInstanceImage://BisCore:DisplayStyle3d", 828 | "label": "Default - View 4", 829 | }, 830 | Object { 831 | "icon": "ECInstanceImage://BisCore:DisplayStyle3d", 832 | "label": "Illustration", 833 | }, 834 | ], 835 | "hasChildren": true, 836 | "icon": "ECClassImage://BisCore:DisplayStyle3d", 837 | "label": "3D Display Style", 838 | }, 839 | Object { 840 | "children": Array [ 841 | Object { 842 | "icon": "ECInstanceImage://BisCore:CategorySelector", 843 | "label": "Default - View 1", 844 | }, 845 | Object { 846 | "icon": "ECInstanceImage://BisCore:CategorySelector", 847 | "label": "Default - View 2", 848 | }, 849 | Object { 850 | "icon": "ECInstanceImage://BisCore:CategorySelector", 851 | "label": "Default - View 3", 852 | }, 853 | Object { 854 | "icon": "ECInstanceImage://BisCore:CategorySelector", 855 | "label": "Default - View 4", 856 | }, 857 | ], 858 | "hasChildren": true, 859 | "icon": "ECClassImage://BisCore:CategorySelector", 860 | "label": "Category Selector", 861 | }, 862 | Object { 863 | "children": Array [ 864 | Object { 865 | "children": Array [ 866 | Object { 867 | "icon": "ECInstanceImage://BisCore:SubCategory", 868 | "label": "Uncategorized", 869 | }, 870 | ], 871 | "hasChildren": true, 872 | "icon": "ECInstanceImage://BisCore:DrawingCategory", 873 | "label": "Uncategorized", 874 | }, 875 | ], 876 | "hasChildren": true, 877 | "icon": "ECClassImage://BisCore:DrawingCategory", 878 | "label": "Drawing Category", 879 | }, 880 | Object { 881 | "children": Array [ 882 | Object { 883 | "icon": "ECInstanceImage://BisCore:ModelSelector", 884 | "label": "Default - View 1", 885 | }, 886 | Object { 887 | "icon": "ECInstanceImage://BisCore:ModelSelector", 888 | "label": "Default - View 2", 889 | }, 890 | Object { 891 | "icon": "ECInstanceImage://BisCore:ModelSelector", 892 | "label": "Default - View 3", 893 | }, 894 | Object { 895 | "icon": "ECInstanceImage://BisCore:ModelSelector", 896 | "label": "Default - View 4", 897 | }, 898 | ], 899 | "hasChildren": true, 900 | "icon": "ECClassImage://BisCore:ModelSelector", 901 | "label": "Model Selector", 902 | }, 903 | Object { 904 | "children": Array [ 905 | Object { 906 | "icon": "ECInstanceImage://BisCore:AuxCoordSystemSpatial", 907 | "label": "Default - View 1", 908 | }, 909 | Object { 910 | "icon": "ECInstanceImage://BisCore:AuxCoordSystemSpatial", 911 | "label": "Default - View 2", 912 | }, 913 | Object { 914 | "icon": "ECInstanceImage://BisCore:AuxCoordSystemSpatial", 915 | "label": "Default - View 3", 916 | }, 917 | Object { 918 | "icon": "ECInstanceImage://BisCore:AuxCoordSystemSpatial", 919 | "label": "Default - View 4", 920 | }, 921 | ], 922 | "description": "A spatial coordinate system.", 923 | "hasChildren": true, 924 | "icon": "ECClassImage://BisCore:AuxCoordSystemSpatial", 925 | "label": "Spatial Auxiliary Coordinate System", 926 | }, 927 | Object { 928 | "children": Array [ 929 | Object { 930 | "children": Array [ 931 | Object { 932 | "icon": "ECInstanceImage://BisCore:SubCategory", 933 | "label": "Uncategorized", 934 | }, 935 | ], 936 | "hasChildren": true, 937 | "icon": "ECInstanceImage://BisCore:SpatialCategory", 938 | "label": "Uncategorized", 939 | }, 940 | ], 941 | "hasChildren": true, 942 | "icon": "ECClassImage://BisCore:SpatialCategory", 943 | "label": "Spatial Category", 944 | }, 945 | Object { 946 | "children": Array [ 947 | Object { 948 | "icon": "ECInstanceImage://BisCore:SpatialViewDefinition", 949 | "label": "Default - View 1", 950 | }, 951 | Object { 952 | "icon": "ECInstanceImage://BisCore:SpatialViewDefinition", 953 | "label": "Default - View 2", 954 | }, 955 | Object { 956 | "icon": "ECInstanceImage://BisCore:SpatialViewDefinition", 957 | "label": "Default - View 3", 958 | }, 959 | Object { 960 | "icon": "ECInstanceImage://BisCore:SpatialViewDefinition", 961 | "label": "Default - View 4", 962 | }, 963 | ], 964 | "description": "A view of a spatially located volume.", 965 | "hasChildren": true, 966 | "icon": "ECClassImage://BisCore:SpatialViewDefinition", 967 | "label": "Spatial View Definition", 968 | }, 969 | ], 970 | "hasChildren": true, 971 | "icon": "ECInstanceImage://BisCore:DefinitionPartition", 972 | "label": "Definition Model For DgnV8Bridge:D:\\\\Temp\\\\Properties_60InstancesWithUrl2.dgn, Default", 973 | }, 974 | ], 975 | "hasChildren": true, 976 | "icon": "ECInstanceImage://BisCore:Subject", 977 | "label": "DgnV8Bridge:D:\\\\Temp\\\\Properties_60InstancesWithUrl2.dgn, Default", 978 | }, 979 | Object { 980 | "children": Array [ 981 | Object { 982 | "children": Array [ 983 | Object { 984 | "icon": "ECInstanceImage://BisCore:LineStyle", 985 | "label": "lc1", 986 | }, 987 | Object { 988 | "icon": "ECInstanceImage://BisCore:LineStyle", 989 | "label": "lc2", 990 | }, 991 | Object { 992 | "icon": "ECInstanceImage://BisCore:LineStyle", 993 | "label": "lc3", 994 | }, 995 | Object { 996 | "icon": "ECInstanceImage://BisCore:LineStyle", 997 | "label": "lc4", 998 | }, 999 | Object { 1000 | "icon": "ECInstanceImage://BisCore:LineStyle", 1001 | "label": "lc5", 1002 | }, 1003 | Object { 1004 | "icon": "ECInstanceImage://BisCore:LineStyle", 1005 | "label": "lc6", 1006 | }, 1007 | Object { 1008 | "icon": "ECInstanceImage://BisCore:LineStyle", 1009 | "label": "lc7", 1010 | }, 1011 | ], 1012 | "hasChildren": true, 1013 | "icon": "ECClassImage://BisCore:LineStyle", 1014 | "label": "Line Style", 1015 | }, 1016 | ], 1017 | "hasChildren": true, 1018 | "icon": "ECInstanceImage://BisCore:DefinitionPartition", 1019 | "label": "BisCore.DictionaryModel", 1020 | }, 1021 | Object { 1022 | "icon": "ECInstanceImage://BisCore:LinkPartition", 1023 | "label": "BisCore.RealityDataSources", 1024 | }, 1025 | ], 1026 | "hasChildren": true, 1027 | "icon": "ECInstanceImage://BisCore:Subject", 1028 | "label": "DgnV8Bridge", 1029 | }, 1030 | ] 1031 | `; 1032 | --------------------------------------------------------------------------------