From 129ebfaaa576a3c0895087f20ab281b3b83e37d3 Mon Sep 17 00:00:00 2001 From: Leck Tang Date: Mon, 15 Aug 2022 15:23:37 -0500 Subject: [PATCH 1/8] Add ecsql widget --- .../src/components/home/ViewerHome.tsx | 2 + .../src/components/app-ui/EcsqlWidget.tsx | 109 ++++++++++++++++++ .../app-ui/providers/EcsqlWidgetProvider.tsx | 44 +++++++ .../src/components/app-ui/providers/index.ts | 1 + 4 files changed, 156 insertions(+) create mode 100644 packages/modules/viewer-react/src/components/app-ui/EcsqlWidget.tsx create mode 100644 packages/modules/viewer-react/src/components/app-ui/providers/EcsqlWidgetProvider.tsx diff --git a/packages/apps/web-viewer-test/src/components/home/ViewerHome.tsx b/packages/apps/web-viewer-test/src/components/home/ViewerHome.tsx index 98f5b6e9..895ce593 100644 --- a/packages/apps/web-viewer-test/src/components/home/ViewerHome.tsx +++ b/packages/apps/web-viewer-test/src/components/home/ViewerHome.tsx @@ -24,6 +24,7 @@ import { } from "@itwin/tree-widget-react"; import type { ViewerBackstageItem } from "@itwin/web-viewer-react"; import { + EcsqlWidgetProvider, Viewer, ViewerContentToolsProvider, ViewerNavigationToolsProvider, @@ -140,6 +141,7 @@ const ViewerHome: React.FC = () => { enableCopyingPropertyText: true, }), new MeasureToolsUiItemsProvider(), + new EcsqlWidgetProvider(), ]} extensions={[ new LocalExtensionProvider({ diff --git a/packages/modules/viewer-react/src/components/app-ui/EcsqlWidget.tsx b/packages/modules/viewer-react/src/components/app-ui/EcsqlWidget.tsx new file mode 100644 index 00000000..0409dfac --- /dev/null +++ b/packages/modules/viewer-react/src/components/app-ui/EcsqlWidget.tsx @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import { UiFramework } from "@itwin/appui-react"; +import { QueryRowFormat } from "@itwin/core-common"; +import { Button, LabeledTextarea, Table } from "@itwin/itwinui-react"; +import React, { useState } from "react"; + +export default function EcsqlWidget() { + const [input, setInput] = useState(""); + const [rows, setRows] = useState[]>([]); + const [headers, setHeaders] = useState([]); + const [isloading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const getData = async () => { + try { + setRows([]); + setHeaders([]); + setLoading(true); + const results = UiFramework.getIModelConnection()!.query( + input, + undefined, + { rowFormat: QueryRowFormat.UseJsPropertyNames } + ); + const rows = []; + for await (const obj of results) { + const row = { ...obj }; + Object.keys(row) + .filter((key) => typeof row[key] === "object") + .forEach((key) => { + row[key] = JSON.stringify(obj[key]); + }); + rows.push(row); + } + setRows(rows); + setHeaders(Object.keys(rows[0])); + setLoading(false); + setError(""); + } catch (err: any) { + setLoading(false); + setError(err.message); + } + }; + + const columns = React.useMemo( + () => [ + { + Header: "Header name", + columns: headers.map((str) => ({ + id: str, + Header: str, + accessor: str, + width: 300, + })), + }, + ], + [headers] + ); + const data = rows; + + return ( +
+
+ setInput(e.target.value)} + status={error ? "negative" : undefined} + rows={1} + style={{ flexGrow: 1 }} + /> + +
+ + + ); +} diff --git a/packages/modules/viewer-react/src/components/app-ui/providers/EcsqlWidgetProvider.tsx b/packages/modules/viewer-react/src/components/app-ui/providers/EcsqlWidgetProvider.tsx new file mode 100644 index 00000000..f6a6d1ea --- /dev/null +++ b/packages/modules/viewer-react/src/components/app-ui/providers/EcsqlWidgetProvider.tsx @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import type { + AbstractWidgetProps, + UiItemsProvider, +} from "@itwin/appui-abstract"; +import { StagePanelLocation, StagePanelSection } from "@itwin/appui-abstract"; +import React from "react"; + +import EcsqlWidget from "../EcsqlWidget"; + +export class EcsqlWidgetProvider implements UiItemsProvider { + public readonly id = "EcsqlWidgetProvider"; + + public provideWidgets( + stageId: string, + stageUsage: string, + location: StagePanelLocation, + section?: StagePanelSection + ): ReadonlyArray { + const widgets: AbstractWidgetProps[] = []; + + if ( + stageId && + stageUsage && + location === StagePanelLocation.Bottom && + section === StagePanelSection.Start + ) { + const ecsqlWidget: AbstractWidgetProps = { + id: "EcsqlWidget", + label: "ECSQL", + getWidgetContent() { + return ; + }, + }; + widgets.push(ecsqlWidget); + } + + return widgets; + } +} diff --git a/packages/modules/viewer-react/src/components/app-ui/providers/index.ts b/packages/modules/viewer-react/src/components/app-ui/providers/index.ts index 48bb79f6..2f3fb982 100644 --- a/packages/modules/viewer-react/src/components/app-ui/providers/index.ts +++ b/packages/modules/viewer-react/src/components/app-ui/providers/index.ts @@ -9,3 +9,4 @@ export * from "./ViewerContentToolsProvider"; export * from "./ViewerNavigationToolsProvider"; export * from "./ViewerStatusbarItemsProvider"; export * from "./StandardFrontstageProvider"; +export * from "./EcsqlWidgetProvider"; From ca5e69d6fb0c752d0d818e6b890703449bf0fe68 Mon Sep 17 00:00:00 2001 From: Leck Tang Date: Mon, 15 Aug 2022 15:50:11 -0500 Subject: [PATCH 2/8] Rush change --- .../leck-ecsql-widget_2022-08-15-20-49.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@itwin/viewer-react/leck-ecsql-widget_2022-08-15-20-49.json diff --git a/common/changes/@itwin/viewer-react/leck-ecsql-widget_2022-08-15-20-49.json b/common/changes/@itwin/viewer-react/leck-ecsql-widget_2022-08-15-20-49.json new file mode 100644 index 00000000..34fd65f2 --- /dev/null +++ b/common/changes/@itwin/viewer-react/leck-ecsql-widget_2022-08-15-20-49.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/viewer-react", + "comment": "Add ecsql widget", + "type": "minor" + } + ], + "packageName": "@itwin/viewer-react" +} \ No newline at end of file From be65214a3d4a9b3ac867b5c81c27d1da17207de4 Mon Sep 17 00:00:00 2001 From: Leck Tang Date: Tue, 16 Aug 2022 14:19:52 -0500 Subject: [PATCH 3/8] Handle edge cases --- .../src/components/app-ui/EcsqlWidget.tsx | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/modules/viewer-react/src/components/app-ui/EcsqlWidget.tsx b/packages/modules/viewer-react/src/components/app-ui/EcsqlWidget.tsx index 0409dfac..699061c3 100644 --- a/packages/modules/viewer-react/src/components/app-ui/EcsqlWidget.tsx +++ b/packages/modules/viewer-react/src/components/app-ui/EcsqlWidget.tsx @@ -5,7 +5,12 @@ import { UiFramework } from "@itwin/appui-react"; import { QueryRowFormat } from "@itwin/core-common"; -import { Button, LabeledTextarea, Table } from "@itwin/itwinui-react"; +import { + Button, + ExpandableBlock, + LabeledTextarea, + Table, +} from "@itwin/itwinui-react"; import React, { useState } from "react"; export default function EcsqlWidget() { @@ -26,17 +31,44 @@ export default function EcsqlWidget() { { rowFormat: QueryRowFormat.UseJsPropertyNames } ); const rows = []; + const headers: string[] = []; + for await (const obj of results) { const row = { ...obj }; + // Case 1: Need to convert booleans to strings to display them in the table + Object.keys(row) + .filter((key) => typeof row[key] === "boolean") + .forEach((key) => { + row[key] = obj[key].toString(); + }); + // Case 2: Need to stringify objects to display them in the table Object.keys(row) .filter((key) => typeof row[key] === "object") .forEach((key) => { row[key] = JSON.stringify(obj[key]); }); + // Case 3: If the cell content is too long, put it inside an ExpandableBlock + Object.keys(row) + .filter( + (key) => typeof row[key] === "string" && row[key].length > 500 + ) + .forEach((key) => { + row[key] = ( + + {row[key]} + + ); + }); + rows.push(row); + for (const key of Object.keys(row)) { + if (!headers.includes(key)) { + headers.push(key); + } + } } setRows(rows); - setHeaders(Object.keys(rows[0])); + setHeaders(headers); setLoading(false); setError(""); } catch (err: any) { From c8c2ce85818b3eea4095e5e77e6a71b0defb22a8 Mon Sep 17 00:00:00 2001 From: Leck Tang Date: Tue, 16 Aug 2022 14:56:54 -0500 Subject: [PATCH 4/8] Move provider inside the test app --- .../leck-ecsql-widget_2022-08-15-20-49.json | 10 ---------- .../web-viewer-test/src/components/home/ViewerHome.tsx | 3 ++- .../web-viewer-test/src/providers}/EcsqlWidget.tsx | 0 .../src}/providers/EcsqlWidgetProvider.tsx | 2 +- .../src/components/app-ui/providers/index.ts | 1 - 5 files changed, 3 insertions(+), 13 deletions(-) delete mode 100644 common/changes/@itwin/viewer-react/leck-ecsql-widget_2022-08-15-20-49.json rename packages/{modules/viewer-react/src/components/app-ui => apps/web-viewer-test/src/providers}/EcsqlWidget.tsx (100%) rename packages/{modules/viewer-react/src/components/app-ui => apps/web-viewer-test/src}/providers/EcsqlWidgetProvider.tsx (96%) diff --git a/common/changes/@itwin/viewer-react/leck-ecsql-widget_2022-08-15-20-49.json b/common/changes/@itwin/viewer-react/leck-ecsql-widget_2022-08-15-20-49.json deleted file mode 100644 index 34fd65f2..00000000 --- a/common/changes/@itwin/viewer-react/leck-ecsql-widget_2022-08-15-20-49.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@itwin/viewer-react", - "comment": "Add ecsql widget", - "type": "minor" - } - ], - "packageName": "@itwin/viewer-react" -} \ No newline at end of file diff --git a/packages/apps/web-viewer-test/src/components/home/ViewerHome.tsx b/packages/apps/web-viewer-test/src/components/home/ViewerHome.tsx index 895ce593..5cb382a8 100644 --- a/packages/apps/web-viewer-test/src/components/home/ViewerHome.tsx +++ b/packages/apps/web-viewer-test/src/components/home/ViewerHome.tsx @@ -24,7 +24,6 @@ import { } from "@itwin/tree-widget-react"; import type { ViewerBackstageItem } from "@itwin/web-viewer-react"; import { - EcsqlWidgetProvider, Viewer, ViewerContentToolsProvider, ViewerNavigationToolsProvider, @@ -32,7 +31,9 @@ import { } from "@itwin/web-viewer-react"; import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { EcsqlWidgetProvider } from "../../providers/EcsqlWidgetProvider"; import { history } from "../routing"; + /** * Test a viewer that uses auth configuration provided at startup * @returns diff --git a/packages/modules/viewer-react/src/components/app-ui/EcsqlWidget.tsx b/packages/apps/web-viewer-test/src/providers/EcsqlWidget.tsx similarity index 100% rename from packages/modules/viewer-react/src/components/app-ui/EcsqlWidget.tsx rename to packages/apps/web-viewer-test/src/providers/EcsqlWidget.tsx diff --git a/packages/modules/viewer-react/src/components/app-ui/providers/EcsqlWidgetProvider.tsx b/packages/apps/web-viewer-test/src/providers/EcsqlWidgetProvider.tsx similarity index 96% rename from packages/modules/viewer-react/src/components/app-ui/providers/EcsqlWidgetProvider.tsx rename to packages/apps/web-viewer-test/src/providers/EcsqlWidgetProvider.tsx index f6a6d1ea..e490995d 100644 --- a/packages/modules/viewer-react/src/components/app-ui/providers/EcsqlWidgetProvider.tsx +++ b/packages/apps/web-viewer-test/src/providers/EcsqlWidgetProvider.tsx @@ -10,7 +10,7 @@ import type { import { StagePanelLocation, StagePanelSection } from "@itwin/appui-abstract"; import React from "react"; -import EcsqlWidget from "../EcsqlWidget"; +import EcsqlWidget from "./EcsqlWidget"; export class EcsqlWidgetProvider implements UiItemsProvider { public readonly id = "EcsqlWidgetProvider"; diff --git a/packages/modules/viewer-react/src/components/app-ui/providers/index.ts b/packages/modules/viewer-react/src/components/app-ui/providers/index.ts index 2f3fb982..48bb79f6 100644 --- a/packages/modules/viewer-react/src/components/app-ui/providers/index.ts +++ b/packages/modules/viewer-react/src/components/app-ui/providers/index.ts @@ -9,4 +9,3 @@ export * from "./ViewerContentToolsProvider"; export * from "./ViewerNavigationToolsProvider"; export * from "./ViewerStatusbarItemsProvider"; export * from "./StandardFrontstageProvider"; -export * from "./EcsqlWidgetProvider"; From eceba7152f1bae3beb18724745f0d509c2c4cf8d Mon Sep 17 00:00:00 2001 From: Leck Tang Date: Tue, 16 Aug 2022 16:37:25 -0500 Subject: [PATCH 5/8] Fixes --- .../src/providers/EcsqlWidget.tsx | 25 ++++++++++--------- .../src/providers/EcsqlWidgetProvider.tsx | 10 +++++--- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/apps/web-viewer-test/src/providers/EcsqlWidget.tsx b/packages/apps/web-viewer-test/src/providers/EcsqlWidget.tsx index 699061c3..ab95ac24 100644 --- a/packages/apps/web-viewer-test/src/providers/EcsqlWidget.tsx +++ b/packages/apps/web-viewer-test/src/providers/EcsqlWidget.tsx @@ -14,23 +14,25 @@ import { import React, { useState } from "react"; export default function EcsqlWidget() { - const [input, setInput] = useState(""); - const [rows, setRows] = useState[]>([]); + const [input, setInput] = useState(""); + const [data, setData] = useState[]>( + [] + ); const [headers, setHeaders] = useState([]); - const [isloading, setLoading] = useState(false); - const [error, setError] = useState(""); + const [isloading, setIsLoading] = useState(false); + const [error, setError] = useState(""); const getData = async () => { try { - setRows([]); + setData([]); setHeaders([]); - setLoading(true); + setIsLoading(true); const results = UiFramework.getIModelConnection()!.query( input, undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames } ); - const rows = []; + const rows: Record[] = []; const headers: string[] = []; for await (const obj of results) { @@ -67,12 +69,12 @@ export default function EcsqlWidget() { } } } - setRows(rows); + setData(rows); setHeaders(headers); - setLoading(false); + setIsLoading(false); setError(""); } catch (err: any) { - setLoading(false); + setIsLoading(false); setError(err.message); } }; @@ -85,13 +87,11 @@ export default function EcsqlWidget() { id: str, Header: str, accessor: str, - width: 300, })), }, ], [headers] ); - const data = rows; return (
@@ -123,6 +123,7 @@ export default function EcsqlWidget() {
Date: Wed, 17 Aug 2022 11:14:58 -0500 Subject: [PATCH 6/8] Fixes pt 2 --- .../src/providers/EcsqlWidget.tsx | 24 +++++++++---------- .../src/providers/EcsqlWidgetProvider.tsx | 3 +-- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/apps/web-viewer-test/src/providers/EcsqlWidget.tsx b/packages/apps/web-viewer-test/src/providers/EcsqlWidget.tsx index ab95ac24..4133d157 100644 --- a/packages/apps/web-viewer-test/src/providers/EcsqlWidget.tsx +++ b/packages/apps/web-viewer-test/src/providers/EcsqlWidget.tsx @@ -3,8 +3,9 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ -import { UiFramework } from "@itwin/appui-react"; +import { useActiveIModelConnection } from "@itwin/appui-react"; import { QueryRowFormat } from "@itwin/core-common"; +import type { IModelConnection } from "@itwin/core-frontend"; import { Button, ExpandableBlock, @@ -19,21 +20,20 @@ export default function EcsqlWidget() { [] ); const [headers, setHeaders] = useState([]); - const [isloading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(""); + const iModelConnection = useActiveIModelConnection() as IModelConnection; const getData = async () => { try { setData([]); setHeaders([]); setIsLoading(true); - const results = UiFramework.getIModelConnection()!.query( - input, - undefined, - { rowFormat: QueryRowFormat.UseJsPropertyNames } - ); + const results = iModelConnection.query(input, undefined, { + rowFormat: QueryRowFormat.UseJsPropertyNames, + }); const rows: Record[] = []; - const headers: string[] = []; + const columnHeaders: string[] = []; for await (const obj of results) { const row = { ...obj }; @@ -64,13 +64,13 @@ export default function EcsqlWidget() { rows.push(row); for (const key of Object.keys(row)) { - if (!headers.includes(key)) { - headers.push(key); + if (!columnHeaders.includes(key)) { + columnHeaders.push(key); } } } setData(rows); - setHeaders(headers); + setHeaders(columnHeaders); setIsLoading(false); setError(""); } catch (err: any) { @@ -130,7 +130,7 @@ export default function EcsqlWidget() { isResizable={true} density="extra-condensed" emptyTableContent="No data." - isLoading={isloading} + isLoading={isLoading} style={{ flex: "1 1 auto", height: "0em", diff --git a/packages/apps/web-viewer-test/src/providers/EcsqlWidgetProvider.tsx b/packages/apps/web-viewer-test/src/providers/EcsqlWidgetProvider.tsx index f5919a09..44b399ac 100644 --- a/packages/apps/web-viewer-test/src/providers/EcsqlWidgetProvider.tsx +++ b/packages/apps/web-viewer-test/src/providers/EcsqlWidgetProvider.tsx @@ -20,7 +20,7 @@ export class EcsqlWidgetProvider implements UiItemsProvider { public readonly id = "EcsqlWidgetProvider"; public provideWidgets( - stageId: string, + _stageId: string, stageUsage: string, location: StagePanelLocation, section?: StagePanelSection @@ -28,7 +28,6 @@ export class EcsqlWidgetProvider implements UiItemsProvider { const widgets: AbstractWidgetProps[] = []; if ( - stageId === "DefaultFrontstage" && stageUsage === StageUsage.General && location === StagePanelLocation.Bottom && section === StagePanelSection.Start From a42fe9aacdb95adeab549ac74d62837fdc77ca79 Mon Sep 17 00:00:00 2001 From: Leck Tang Date: Thu, 18 Aug 2022 16:12:36 -0500 Subject: [PATCH 7/8] Undo virtualized table for now --- .../web-viewer-test/src/providers/EcsqlWidget.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/apps/web-viewer-test/src/providers/EcsqlWidget.tsx b/packages/apps/web-viewer-test/src/providers/EcsqlWidget.tsx index 4133d157..64b20750 100644 --- a/packages/apps/web-viewer-test/src/providers/EcsqlWidget.tsx +++ b/packages/apps/web-viewer-test/src/providers/EcsqlWidget.tsx @@ -87,6 +87,8 @@ export default function EcsqlWidget() { id: str, Header: str, accessor: str, + width: 300, + minWidth: 100, })), }, ], @@ -106,10 +108,14 @@ export default function EcsqlWidget() { > setInput(e.target.value)} - status={error ? "negative" : undefined} + status={error && !isLoading ? "negative" : undefined} rows={1} style={{ flexGrow: 1 }} /> @@ -123,7 +129,6 @@ export default function EcsqlWidget() {
Date: Thu, 18 Aug 2022 20:47:46 -0500 Subject: [PATCH 8/8] Fixes pt 3 --- .../apps/web-viewer-test/src/providers/EcsqlWidget.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/apps/web-viewer-test/src/providers/EcsqlWidget.tsx b/packages/apps/web-viewer-test/src/providers/EcsqlWidget.tsx index 64b20750..9a139d3c 100644 --- a/packages/apps/web-viewer-test/src/providers/EcsqlWidget.tsx +++ b/packages/apps/web-viewer-test/src/providers/EcsqlWidget.tsx @@ -5,7 +5,6 @@ import { useActiveIModelConnection } from "@itwin/appui-react"; import { QueryRowFormat } from "@itwin/core-common"; -import type { IModelConnection } from "@itwin/core-frontend"; import { Button, ExpandableBlock, @@ -22,9 +21,12 @@ export default function EcsqlWidget() { const [headers, setHeaders] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(""); - const iModelConnection = useActiveIModelConnection() as IModelConnection; + const iModelConnection = useActiveIModelConnection(); const getData = async () => { + if (!iModelConnection) { + return; + } try { setData([]); setHeaders([]); @@ -71,11 +73,11 @@ export default function EcsqlWidget() { } setData(rows); setHeaders(columnHeaders); - setIsLoading(false); setError(""); } catch (err: any) { - setIsLoading(false); setError(err.message); + } finally { + setIsLoading(false); } };