From 8f26b58433ff05bd6709c7659dfb6b015e6a90dd Mon Sep 17 00:00:00 2001 From: Fernando Pauer Date: Tue, 21 Dec 2021 06:27:26 -0800 Subject: [PATCH] feat: Sitewise Resource Explorer (#21) Co-authored-by: Fernando Pauer --- packages/components/src/components.d.ts | 55 +++++-- .../iot-tree-table.spec.ts | 3 +- .../iot-resource-explorer/iot-tree-table.tsx | 44 +++--- .../sitewise-resource-explorer.spec.ts | 75 ++++++++++ .../sitewise-resource-explorer.tsx | 125 ++++++++++++++++ .../components/iot-resource-explorer/types.ts | 12 ++ .../components/iot-resource-explorer/utils.ts | 25 ++++ packages/components/src/testing/app/routes.ts | 6 +- .../iot-tree-table-demo.tsx | 101 +++++++------ .../sitewise-resource-explorer-demo.tsx | 21 +++ .../src/HOC/withUseTreeCollection.tsx | 138 ++++++------------ .../src/RelatedTable/EmptyState.tsx | 4 +- 12 files changed, 439 insertions(+), 170 deletions(-) create mode 100644 packages/components/src/components/iot-resource-explorer/sitewise-resource-explorer.spec.ts create mode 100644 packages/components/src/components/iot-resource-explorer/sitewise-resource-explorer.tsx create mode 100644 packages/components/src/components/iot-resource-explorer/types.ts create mode 100644 packages/components/src/components/iot-resource-explorer/utils.ts rename packages/components/src/testing/{iot-tree-table => resource-explorer}/iot-tree-table-demo.tsx (70%) create mode 100644 packages/components/src/testing/resource-explorer/sitewise-resource-explorer-demo.tsx diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index b085dc180..1295cd8ab 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -8,8 +8,9 @@ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; import { AnyDataStreamQuery, AssetSummaryQuery, AssetTreeSubscription, DataModule, Request, RequestConfig, SiteWiseAssetTreeQuery } from "@iot-app-kit/core"; import { DataStream, MinimalViewPortConfig } from "@synchro-charts/core"; import { TableProps } from "@awsui/components-react/table"; -import { EmptyStateProps, UseTreeCollection } from "@iot-app-kit/related-table"; -import { NonCancelableCustomEvent, TextFilterProps } from "@awsui/components-react"; +import { EmptyStateProps, ITreeNode, UseTreeCollection } from "@iot-app-kit/related-table"; +import { FilterTexts } from "./components/iot-resource-explorer/types"; +import { NonCancelableCustomEvent } from "@awsui/components-react"; export namespace Components { interface IotAssetDetails { "query": AssetSummaryQuery; @@ -86,24 +87,30 @@ export namespace Components { "collectionOptions": UseTreeCollection; "columnDefinitions": TableProps.ColumnDefinition[]; "empty": EmptyStateProps; - "filter": TextFilterProps; + "filterTexts": FilterTexts; "isItemDisabled": (item: unknown) => boolean; "items": unknown[]; "loading": boolean; "loadingText": string; - "onExpandChildren": (node: unknown) => void; + "onExpandChildren": (node: ITreeNode) => void; "onSelectionChange": (event: NonCancelableCustomEvent>) => void; "onSortingChange": (event: NonCancelableCustomEvent>) => void; "resizableColumns": boolean; - "selectedItems": unknown[]; "selectionType": TableProps.SelectionType; - "sortingColumn": TableProps.SortingColumn; - "sortingDescending": boolean; "sortingDisabled": boolean; "wrapLines": boolean; } interface IotTreeTableDemo { } + interface SitewiseResourceExplorer { + "empty"?: EmptyStateProps; + "filterTexts"?: FilterTexts; + "loadingText"?: string; + "query": SiteWiseAssetTreeQuery; + "selectionType"?: TableProps.SelectionType; + } + interface SitewiseResourceExplorerDemo { + } interface TestingGround { } } @@ -186,6 +193,18 @@ declare global { prototype: HTMLIotTreeTableDemoElement; new (): HTMLIotTreeTableDemoElement; }; + interface HTMLSitewiseResourceExplorerElement extends Components.SitewiseResourceExplorer, HTMLStencilElement { + } + var HTMLSitewiseResourceExplorerElement: { + prototype: HTMLSitewiseResourceExplorerElement; + new (): HTMLSitewiseResourceExplorerElement; + }; + interface HTMLSitewiseResourceExplorerDemoElement extends Components.SitewiseResourceExplorerDemo, HTMLStencilElement { + } + var HTMLSitewiseResourceExplorerDemoElement: { + prototype: HTMLSitewiseResourceExplorerDemoElement; + new (): HTMLSitewiseResourceExplorerDemoElement; + }; interface HTMLTestingGroundElement extends Components.TestingGround, HTMLStencilElement { } var HTMLTestingGroundElement: { @@ -206,6 +225,8 @@ declare global { "iot-test-routes": HTMLIotTestRoutesElement; "iot-tree-table": HTMLIotTreeTableElement; "iot-tree-table-demo": HTMLIotTreeTableDemoElement; + "sitewise-resource-explorer": HTMLSitewiseResourceExplorerElement; + "sitewise-resource-explorer-demo": HTMLSitewiseResourceExplorerDemoElement; "testing-ground": HTMLTestingGroundElement; } } @@ -285,24 +306,30 @@ declare namespace LocalJSX { "collectionOptions": UseTreeCollection; "columnDefinitions": TableProps.ColumnDefinition[]; "empty"?: EmptyStateProps; - "filter"?: TextFilterProps; + "filterTexts"?: FilterTexts; "isItemDisabled"?: (item: unknown) => boolean; "items": unknown[]; "loading"?: boolean; "loadingText"?: string; - "onExpandChildren"?: (node: unknown) => void; + "onExpandChildren"?: (node: ITreeNode) => void; "onSelectionChange"?: (event: NonCancelableCustomEvent>) => void; "onSortingChange"?: (event: NonCancelableCustomEvent>) => void; "resizableColumns"?: boolean; - "selectedItems"?: unknown[]; "selectionType"?: TableProps.SelectionType; - "sortingColumn"?: TableProps.SortingColumn; - "sortingDescending"?: boolean; "sortingDisabled"?: boolean; "wrapLines"?: boolean; } interface IotTreeTableDemo { } + interface SitewiseResourceExplorer { + "empty"?: EmptyStateProps; + "filterTexts"?: FilterTexts; + "loadingText"?: string; + "query"?: SiteWiseAssetTreeQuery; + "selectionType"?: TableProps.SelectionType; + } + interface SitewiseResourceExplorerDemo { + } interface TestingGround { } interface IntrinsicElements { @@ -319,6 +346,8 @@ declare namespace LocalJSX { "iot-test-routes": IotTestRoutes; "iot-tree-table": IotTreeTable; "iot-tree-table-demo": IotTreeTableDemo; + "sitewise-resource-explorer": SitewiseResourceExplorer; + "sitewise-resource-explorer-demo": SitewiseResourceExplorerDemo; "testing-ground": TestingGround; } } @@ -339,6 +368,8 @@ declare module "@stencil/core" { "iot-test-routes": LocalJSX.IotTestRoutes & JSXBase.HTMLAttributes; "iot-tree-table": LocalJSX.IotTreeTable & JSXBase.HTMLAttributes; "iot-tree-table-demo": LocalJSX.IotTreeTableDemo & JSXBase.HTMLAttributes; + "sitewise-resource-explorer": LocalJSX.SitewiseResourceExplorer & JSXBase.HTMLAttributes; + "sitewise-resource-explorer-demo": LocalJSX.SitewiseResourceExplorerDemo & JSXBase.HTMLAttributes; "testing-ground": LocalJSX.TestingGround & JSXBase.HTMLAttributes; } } diff --git a/packages/components/src/components/iot-resource-explorer/iot-tree-table.spec.ts b/packages/components/src/components/iot-resource-explorer/iot-tree-table.spec.ts index adac81a1c..e1d48eefb 100644 --- a/packages/components/src/components/iot-resource-explorer/iot-tree-table.spec.ts +++ b/packages/components/src/components/iot-resource-explorer/iot-tree-table.spec.ts @@ -61,7 +61,7 @@ const collectionOptions = { }, }; -const treeTableSpecPage = async (propOverrides: Partial = {}) => { +const treeTableSpecPage = async () => { const page = await newSpecPage({ components: [IotTreeTable], html: '
', @@ -73,7 +73,6 @@ const treeTableSpecPage = async (propOverrides: Partial collectionOptions, columnDefinitions, selectionType: 'single', - ...propOverrides, }; update(treeTable, props); page.body.appendChild(treeTable); diff --git a/packages/components/src/components/iot-resource-explorer/iot-tree-table.tsx b/packages/components/src/components/iot-resource-explorer/iot-tree-table.tsx index 7f954c531..2c6cabd10 100644 --- a/packages/components/src/components/iot-resource-explorer/iot-tree-table.tsx +++ b/packages/components/src/components/iot-resource-explorer/iot-tree-table.tsx @@ -1,4 +1,4 @@ -import { Component, Host, h, Element, Prop } from '@stencil/core'; +import { Component, Host, h, Element, Prop, State } from '@stencil/core'; import ReactDOM from 'react-dom'; import React from 'react'; import { @@ -7,15 +7,20 @@ import { RelatedTableExtendedProps, withUseTreeCollection, UseTreeCollection, + ITreeNode, } from '@iot-app-kit/related-table'; import { TableProps } from '@awsui/components-react/table'; -import { NonCancelableCustomEvent, TextFilterProps } from '@awsui/components-react'; +import { NonCancelableCustomEvent } from '@awsui/components-react'; +import { FilterTexts } from './types'; + +const RelatedTableWithCollectionHooks = withUseTreeCollection(RelatedTable); @Component({ tag: 'iot-tree-table', }) export class IotTreeTable { @Element() host: HTMLElement; + @State() selectedItems: unknown[] = []; @Prop() items!: unknown[]; @Prop() columnDefinitions!: TableProps.ColumnDefinition[]; @@ -23,23 +28,19 @@ export class IotTreeTable { @Prop() loading: boolean; @Prop() loadingText: string; + @Prop() filterTexts: FilterTexts; - @Prop() empty: EmptyStateProps; - @Prop() filter: TextFilterProps; - - @Prop() selectedItems: unknown[]; @Prop() selectionType: TableProps.SelectionType; + @Prop() empty: EmptyStateProps; @Prop() isItemDisabled: (item: unknown) => boolean; @Prop() wrapLines: boolean; @Prop() resizableColumns: boolean; - @Prop() sortingColumn: TableProps.SortingColumn; - @Prop() sortingDescending: boolean; @Prop() sortingDisabled: boolean; @Prop() ariaLabels: TableProps.AriaLabels; @Prop() onSelectionChange: (event: NonCancelableCustomEvent>) => void; - @Prop() onExpandChildren: (node: unknown) => void; + @Prop() onExpandChildren: (node: ITreeNode) => void; @Prop() onSortingChange: (event: NonCancelableCustomEvent>) => void; componentDidLoad() { @@ -47,31 +48,36 @@ export class IotTreeTable { } componentDidUpdate() { - const properties: RelatedTableExtendedProps = { + const attributes = { items: this.items, - collectionOptions: this.collectionOptions, columnDefinitions: this.columnDefinitions, - - empty: this.empty, - filter: this.filter, + collectionOptions: this.collectionOptions, loading: this.loading, loadingText: this.loadingText, + filterPlaceholder: this.filterTexts?.placeholder, selectionType: this.selectionType, selectedItems: this.selectedItems, + + empty: this.empty, isItemDisabled: this.isItemDisabled, wrapLines: this.wrapLines, resizableColumns: this.resizableColumns, - sortingColumn: this.sortingColumn, - sortingDescending: this.sortingDescending, + sortingDisabled: this.sortingDisabled, ariaLabels: this.ariaLabels, expandChildren: this.onExpandChildren, - onSelectionChange: this.onSelectionChange, onSortingChange: this.onSortingChange, - }; - ReactDOM.render(React.createElement(withUseTreeCollection(RelatedTable), properties), this.host); + onSelectionChange: (event: NonCancelableCustomEvent>) => { + this.selectedItems = event.detail.selectedItems; + if (this.onSelectionChange) { + this.onSelectionChange(event); + } + }, + } as RelatedTableExtendedProps; + + ReactDOM.render(React.createElement(RelatedTableWithCollectionHooks, attributes), this.host); } render() { diff --git a/packages/components/src/components/iot-resource-explorer/sitewise-resource-explorer.spec.ts b/packages/components/src/components/iot-resource-explorer/sitewise-resource-explorer.spec.ts new file mode 100644 index 000000000..bd4f411df --- /dev/null +++ b/packages/components/src/components/iot-resource-explorer/sitewise-resource-explorer.spec.ts @@ -0,0 +1,75 @@ +import { initialize } from '@iot-app-kit/core'; +import { MockSiteWiseAssetModule, MockSiteWiseAssetsReplayData } from '@iot-app-kit/core/asset-modules/mocks.spec'; +import * as core from '@iot-app-kit/core'; +import { newSpecPage } from '@stencil/core/testing'; +import { SitewiseResourceExplorer } from './sitewise-resource-explorer'; +import { Components } from '../../components.d'; +import { CustomHTMLElement } from '../../testing/types'; +import { update } from '../../testing/update'; + +const awsCredentials = { + accessKeyId: 'accessKeyId', + secretAccessKey: 'secretAccessKey', + sessionToken: 'sessionToken', +}; + +jest.mock('@iot-app-kit/core', () => { + const originalModule = jest.requireActual('@iot-app-kit/core'); + + return { + ...originalModule, + getSiteWiseAssetModule: () => { + return new MockSiteWiseAssetModule(new MockSiteWiseAssetsReplayData()); + }, + }; +}); + +const sitewiseResourceExplorerSpec = async (injectProps: any) => { + const page = await newSpecPage({ + components: [SitewiseResourceExplorer], + html: '
', + supportsShadowDom: false, + }); + const query: core.SiteWiseAssetTreeQuery = { rootAssetId: undefined }; + const sitewiseResourceExplorer = page.doc.createElement( + 'sitewise-resource-explorer' + ) as CustomHTMLElement; + const props: Partial = { + ...injectProps, + query, + }; + update(sitewiseResourceExplorer, props); + page.body.appendChild(sitewiseResourceExplorer); + + await page.waitForChanges(); + + return { page, sitewiseResourceExplorer }; +}; + +beforeAll(() => { + initialize({ awsCredentials, awsRegion: 'us-east-1' }); +}); + +it('renders', async () => { + const { sitewiseResourceExplorer } = await sitewiseResourceExplorerSpec(); + const elements = sitewiseResourceExplorer.querySelectorAll('iot-tree-table'); + expect(elements.length).toBe(1); +}); + +it('renders with custom copy', async () => { + const { sitewiseResourceExplorer } = await sitewiseResourceExplorerSpec({ + selectionType: 'single', + loadingText: 'loading...', + filterText: { + placeholder: 'Filter by name', + empty: 'No assets found.', + noMatch: `We can't find a match.`, + }, + empty: { + header: 'No assets', + description: `You don't have any asset.`, + }, + }); + const elements = sitewiseResourceExplorer.querySelectorAll('iot-tree-table'); + expect(elements.length).toBe(1); +}); diff --git a/packages/components/src/components/iot-resource-explorer/sitewise-resource-explorer.tsx b/packages/components/src/components/iot-resource-explorer/sitewise-resource-explorer.tsx new file mode 100644 index 000000000..cd2d3f7eb --- /dev/null +++ b/packages/components/src/components/iot-resource-explorer/sitewise-resource-explorer.tsx @@ -0,0 +1,125 @@ +import { Component, h, Prop, State } from '@stencil/core'; +import { + AssetTreeSubscription, + BranchReference, + getSiteWiseAssetModule, + SiteWiseAssetTreeModule, + SiteWiseAssetTreeQuery, + SiteWiseAssetTreeSession, +} from '@iot-app-kit/core'; +import { SitewiseAssetResource } from './types'; +import { EmptyStateProps, ITreeNode } from '@iot-app-kit/related-table'; +import { parseSitewiseAssetTree } from './utils'; +import { TableProps } from '@awsui/components-react/table'; +import { FilterTexts } from './types'; + +@Component({ + tag: 'sitewise-resource-explorer', +}) +export class SitewiseResourceExplorer { + @Prop() query: SiteWiseAssetTreeQuery; + @Prop() filterTexts?: FilterTexts; + @Prop() selectionType?: TableProps.SelectionType; + @Prop() loadingText?: string; + @Prop() empty?: EmptyStateProps; + + @State() selectItems: unknown[] = []; + @State() items: SitewiseAssetResource[] = []; + + defaults = { + selectionType: 'single' as TableProps.SelectionType, + loadingText: 'loading...', + filterText: { + placeholder: 'Filter by name', + empty: 'No assets found.', + noMatch: `We can't find a match.`, + }, + empty: { + header: 'No assets', + description: `You don't have any asset.`, + }, + }; + + columnDefinitions = [ + { + sortingField: 'name', + id: 'name', + header: 'Asset Name', + cell: ({ name }: SitewiseAssetResource) => name, + }, + { + sortingField: 'status', + id: 'status', + header: 'Status', + cell: ({ status }: SitewiseAssetResource) => status?.state, + }, + { + sortingField: 'creationDate', + id: 'creationDate', + header: 'Created', + cell: ({ creationDate }: SitewiseAssetResource) => creationDate?.toUTCString(), + }, + { + sortingField: 'lastUpdateDate', + id: 'lastUpdateDate', + header: 'Updated', + cell: ({ lastUpdateDate }: SitewiseAssetResource) => lastUpdateDate?.toUTCString(), + }, + ]; + collectionOptions = { + columnDefinitions: this.columnDefinitions, + sorting: { + defaultState: { + sortingColumn: { + sortingField: 'name', + }, + isDescending: true, + }, + }, + pagination: { pageSize: 20 }, + keyPropertyName: 'id', + parentKeyPropertyName: 'parentId', + selection: { + keepSelection: true, + }, + }; + + subscription: AssetTreeSubscription; + + componentWillLoad() { + let session: SiteWiseAssetTreeSession = new SiteWiseAssetTreeModule(getSiteWiseAssetModule()).startSession( + this.query + ); + this.subscription = session.subscribe((newTree) => { + this.items = parseSitewiseAssetTree(newTree); + }); + } + + componentWillUnmount() { + this.subscription.unsubscribe(); + } + + expandNode = (node: ITreeNode) => { + node.hierarchies?.forEach((hierarchy) => { + this.subscription.expand(new BranchReference(node.id, hierarchy.id!)); + }); + }; + + render() { + return ( + { + this.selectItems = event.detail.selectedItems; + }} + empty={this.empty || this.defaults.empty} + > + ); + } +} diff --git a/packages/components/src/components/iot-resource-explorer/types.ts b/packages/components/src/components/iot-resource-explorer/types.ts new file mode 100644 index 000000000..a6a6eb64b --- /dev/null +++ b/packages/components/src/components/iot-resource-explorer/types.ts @@ -0,0 +1,12 @@ +import { AssetSummary } from '@aws-sdk/client-iotsitewise'; + +export interface FilterTexts { + placeholder: string; + empty: string; + noMatch: string; +} + +export interface SitewiseAssetResource extends AssetSummary { + hasChildren: boolean; + parentId?: string; +} diff --git a/packages/components/src/components/iot-resource-explorer/utils.ts b/packages/components/src/components/iot-resource-explorer/utils.ts new file mode 100644 index 000000000..6ea38c809 --- /dev/null +++ b/packages/components/src/components/iot-resource-explorer/utils.ts @@ -0,0 +1,25 @@ +import { SiteWiseAssetTreeNode } from '@iot-app-kit/core'; +import { SitewiseAssetResource } from './types'; + +const recursiveParseSitewiseAssetTree = ( + flattenTree: SitewiseAssetResource[], + subTree: SiteWiseAssetTreeNode[], + parentId?: string +) => { + subTree.forEach((node) => { + flattenTree.push({ + ...node.asset, + hasChildren: node.hierarchies.size > 0, + parentId, + }); + Array.from(node.hierarchies.values()).forEach((hierarchy) => { + recursiveParseSitewiseAssetTree(flattenTree, hierarchy.children, node.asset.id); + }); + }); +}; + +export const parseSitewiseAssetTree = (tree: SiteWiseAssetTreeNode[]) => { + const flattenTree: SitewiseAssetResource[] = []; + recursiveParseSitewiseAssetTree(flattenTree, tree); + return flattenTree; +}; diff --git a/packages/components/src/testing/app/routes.ts b/packages/components/src/testing/app/routes.ts index 062ec2ec1..e04c7b8e3 100755 --- a/packages/components/src/testing/app/routes.ts +++ b/packages/components/src/testing/app/routes.ts @@ -4,7 +4,11 @@ export const routes = [ component: 'testing-ground', }, { - url: '/iot-tree-table', + url: '/resource-explorer/tree-table', component: 'iot-tree-table-demo', }, + { + url: '/resource-explorer/sitewise', + component: 'sitewise-resource-explorer-demo', + }, ]; diff --git a/packages/components/src/testing/iot-tree-table/iot-tree-table-demo.tsx b/packages/components/src/testing/resource-explorer/iot-tree-table-demo.tsx similarity index 70% rename from packages/components/src/testing/iot-tree-table/iot-tree-table-demo.tsx rename to packages/components/src/testing/resource-explorer/iot-tree-table-demo.tsx index 4ecd3de29..df8e017db 100644 --- a/packages/components/src/testing/iot-tree-table/iot-tree-table-demo.tsx +++ b/packages/components/src/testing/resource-explorer/iot-tree-table-demo.tsx @@ -1,16 +1,18 @@ import { Component, h, Prop, State } from '@stencil/core'; -import {AssetSummary} from "@aws-sdk/client-iotsitewise"; +import { AssetSummary } from '@aws-sdk/client-iotsitewise'; interface Asset extends Partial { parentId: string; + hasChildren?: true; } -const items: Partial[] = [ +const allItems: Partial[] = [ { creationDate: new Date(1605316299000), id: 'a044f631-ea39-4c2e-a8ef-631f1c0f4bc8', - lastUpdateDate: new Date(1614904229000), + lastUpdateDate: new Date(1614904229000), name: 'Root Hierarchy', + hasChildren: true, status: { state: 'ACTIVE' }, }, { @@ -34,6 +36,7 @@ const items: Partial[] = [ creationDate: new Date(1605316100000), lastUpdateDate: new Date(1614904100000), status: { state: 'ACTIVE' }, + hasChildren: true, }, { id: '7e38bf23-a2e1-4fa7-8ba8-111111', @@ -42,6 +45,7 @@ const items: Partial[] = [ creationDate: new Date(1605316202000), lastUpdateDate: new Date(1614904202000), status: { state: 'ACTIVE' }, + hasChildren: true, }, { id: '41043ebf-9912-412c-b4f9-5114e3', @@ -50,127 +54,138 @@ const items: Partial[] = [ creationDate: new Date(1605316303000), lastUpdateDate: new Date(1614904303000), status: { state: 'ACTIVE' }, + hasChildren: true, }, { id: '41043ebf-9912-412c-b4f9-511tre', parentId: '41043ebf-9912-412c-b4f9-5114e3', name: 'hierarchy_4', creationDate: new Date(1605316404000), - lastUpdateDate: new Date(1614904404000), + lastUpdateDate: new Date(1614904404000), status: { state: 'ACTIVE' }, }, { id: '51043ebf-9912-412c-b4f9-511tre', parentId: '41043ebf-9912-412c-b4f9-5114e3', name: 'hierarchy_5', - creationDate: new Date(1605316505000), - lastUpdateDate: new Date(1614904505000), + creationDate: new Date(1605316505000), + lastUpdateDate: new Date(1614904505000), status: { state: 'ACTIVE' }, }, { id: '61043ebf-9912-412c-b4f9-5114e3', parentId: '7e38bf23-a2e1-4fa7-8ba8-111111', name: 'hierarchy_6', - creationDate: new Date(1605316606000), - lastUpdateDate: new Date(1614904606000), + creationDate: new Date(1605316606000), + lastUpdateDate: new Date(1614904606000), status: { state: 'ACTIVE' }, + hasChildren: true, }, { id: '71043ebf-9912-412c-b4f9-511tre', parentId: '61043ebf-9912-412c-b4f9-5114e3', name: 'hierarchy_7', - creationDate: new Date(1605316707000), - lastUpdateDate: new Date(1614904707000), + creationDate: new Date(1605316707000), + lastUpdateDate: new Date(1614904707000), status: { state: 'ACTIVE' }, }, { id: '81043ebf-9912-412c-b4f9-511tre', parentId: '61043ebf-9912-412c-b4f9-5114e3', name: 'hierarchy_8', - creationDate: new Date(1605316808000), - lastUpdateDate: new Date(1614904808000), + creationDate: new Date(1605316808000), + lastUpdateDate: new Date(1614904808000), status: { state: 'ACTIVE' }, }, { id: '1038bf23-a2e1-4fa7-8ba8-1232313213', parentId: 'a044f631-ea39-4c2e-a8ef-631f1c0f4bc8', name: 'hierarchy_10', - creationDate: new Date(1605316100000), - lastUpdateDate: new Date(1614904100000), + creationDate: new Date(1605316100000), + lastUpdateDate: new Date(1614904100000), status: { state: 'ACTIVE' }, + hasChildren: true, }, { id: '1138bf23-a2e1-4fa7-8ba8-1232313213', parentId: '1038bf23-a2e1-4fa7-8ba8-1232313213', name: 'hierarchy_11', - creationDate: new Date(1605316110000), - lastUpdateDate: new Date(1614904110000), + creationDate: new Date(1605316110000), + lastUpdateDate: new Date(1614904110000), status: { state: 'ACTIVE' }, + hasChildren: true, }, { id: '1238bf23-a2e1-4fa7-8ba8-111111', parentId: '1138bf23-a2e1-4fa7-8ba8-1232313213', name: 'hierarchy_12', - creationDate: new Date(1605316120000), - lastUpdateDate: new Date(1614904120000), + creationDate: new Date(1605316120000), + lastUpdateDate: new Date(1614904120000), status: { state: 'ACTIVE' }, + hasChildren: true, }, { id: '13043ebf-9912-412c-b4f9-5114e3', parentId: '1238bf23-a2e1-4fa7-8ba8-111111', name: 'hierarchy_13', - creationDate: new Date(1605316130000), - lastUpdateDate: new Date(1614904130000), + creationDate: new Date(1605316130000), + lastUpdateDate: new Date(1614904130000), status: { state: 'ACTIVE' }, + hasChildren: true, }, { id: '14043ebf-9912-412c-b4f9-511tre', parentId: '13043ebf-9912-412c-b4f9-5114e3', name: 'hierarchy_14', - creationDate: new Date(1605316140000), - lastUpdateDate: new Date(1614904140000), + creationDate: new Date(1605316140000), + lastUpdateDate: new Date(1614904140000), status: { state: 'ACTIVE' }, }, { id: '15043ebf-9912-412c-b4f9-511tre', parentId: '13043ebf-9912-412c-b4f9-5114e3', name: 'hierarchy_15', - creationDate: new Date(1605316150000), - lastUpdateDate: new Date(1614904150000), + creationDate: new Date(1605316150000), + lastUpdateDate: new Date(1614904150000), status: { state: 'ACTIVE' }, }, { id: '16043ebf-9912-412c-b4f9-5114e3', parentId: '1238bf23-a2e1-4fa7-8ba8-111111', name: 'hierarchy_16', - creationDate: new Date(1605316160000), - lastUpdateDate: new Date(1614904160000), + creationDate: new Date(1605316160000), + lastUpdateDate: new Date(1614904160000), status: { state: 'ACTIVE' }, + hasChildren: true, }, { id: '17043ebf-9912-412c-b4f9-511tre', parentId: '16043ebf-9912-412c-b4f9-5114e3', name: 'hierarchy_17', - creationDate: new Date(1605316170000), - lastUpdateDate: new Date(1614904170000), + creationDate: new Date(1605316170000), + lastUpdateDate: new Date(1614904170000), status: { state: 'ACTIVE' }, }, { id: '18043ebf-9912-412c-b4f9-511tre', parentId: '16043ebf-9912-412c-b4f9-5114e3', name: 'hierarchy_18', - creationDate: new Date(1605316180000), - lastUpdateDate: new Date(1614904180000), + creationDate: new Date(1605316180000), + lastUpdateDate: new Date(1614904180000), status: { state: 'ACTIVE' }, }, ]; +const initial = allItems.filter((item) => !item.parentId); +const loaded = new Map(); + @Component({ tag: 'iot-tree-table-demo', styleUrl: '../../styles/awsui.css', }) -export class IotTreeTableTest { +export class IotTreeTableDemo { @State() selectItems: unknown[]; + @State() items: unknown[] = initial; columnDefinitions = [ { @@ -215,26 +230,28 @@ export class IotTreeTableTest { keepSelection: true, trackBy: 'id', }, - filtering: { - empty: 'No items found', - noMatch: 'No items found', - }, }; - filter = { - filteringText: '', - filteringPlaceholder: 'Filter by name', + filterTexts: { + placeholder: 'Filter by name'; + empty: 'No items found'; + noMatch: 'No items found'; }; render() { return ( {}} + filterTexts={this.filterTexts} + onExpandChildren={(node) => { + if (!loaded.has(node.id)) { + const newItems = allItems.filter((item) => item.parentId === node.id); + this.items = [...this.items, ...newItems]; + } + loaded.set(node.id, true); + }} onSelectionChange={(event) => { console.log(event); this.selectItems = event.detail.selectedItems; diff --git a/packages/components/src/testing/resource-explorer/sitewise-resource-explorer-demo.tsx b/packages/components/src/testing/resource-explorer/sitewise-resource-explorer-demo.tsx new file mode 100644 index 000000000..67e3bd76b --- /dev/null +++ b/packages/components/src/testing/resource-explorer/sitewise-resource-explorer-demo.tsx @@ -0,0 +1,21 @@ +import { Component, h } from '@stencil/core'; +import { DataModule, initialize, SiteWiseAssetTreeQuery } from '@iot-app-kit/core'; +import { getEnvCredentials } from '../testing-ground/getEnvCredentials'; + +@Component({ + tag: 'sitewise-resource-explorer-demo', + styleUrl: '../../styles/awsui.css', +}) +export class SitewiseResourceExplorerDemo { + private dataModule: DataModule; + + componentWillLoad() { + this.dataModule = initialize({ awsCredentials: getEnvCredentials(), awsRegion: 'us-east-1' }); + } + + query: SiteWiseAssetTreeQuery = { rootAssetId: undefined }; + + render() { + return ; + } +} diff --git a/packages/related-table/src/HOC/withUseTreeCollection.tsx b/packages/related-table/src/HOC/withUseTreeCollection.tsx index 0a1c5a66f..e24a1479d 100644 --- a/packages/related-table/src/HOC/withUseTreeCollection.tsx +++ b/packages/related-table/src/HOC/withUseTreeCollection.tsx @@ -1,118 +1,74 @@ import * as React from 'react'; -import { useEffect, useState } from 'react'; import TextFilter from '@awsui/components-react/text-filter'; import Pagination from '@awsui/components-react/pagination'; -import { TableProps, TextFilterProps } from '@awsui/components-react'; -import EmptyState, { EmptyStateProps } from '../RelatedTable/EmptyState'; -import SortingState = TableProps.SortingState; +import { NonCancelableCustomEvent, TableProps } from '@awsui/components-react'; +import { EmptyState, EmptyStateProps } from '../RelatedTable/EmptyState'; import { RelatedTableProps } from '../RelatedTable/RelatedTable'; import { useTreeCollection, UseTreeCollection } from '../Hooks/useTreeCollection'; import { ITreeNode } from '../Model/TreeNode'; -export interface RelatedTableActions { - setFiltering(filteringText: string): void; - setCurrentPage(pageNumber: number): void; - setSorting(state: SortingState): void; - setSelectedItems(selectedItems: ReadonlyArray): void; - reset(): void; -} - export interface RelatedTableExtendedProps extends RelatedTableProps { items: T[]; - collectionOptions: UseTreeCollection; - expandChildren: (node: T) => void; empty: EmptyStateProps; - filter?: TextFilterProps; - onReady?: (actions: RelatedTableActions) => void; + collectionOptions: UseTreeCollection; + filterPlaceholder?: string; } export const withUseTreeCollection = (RelatedTableComp: React.FC) => { - return React.forwardRef((props: RelatedTableExtendedProps, ref: any) => { - const [isReady, setReady] = useState(false); + return (wrapperProps: RelatedTableExtendedProps) => { const { items, + empty, columnDefinitions, collectionOptions, expandChildren, - empty, - filter, - selectedItems, + filterPlaceholder, onSortingChange, onSelectionChange, - onReady, - } = props; - - const { - items: nodes, - collectionProps, - filterProps, - paginationProps, - expandNode, - actions, - reset, - } = useTreeCollection(items, { + } = wrapperProps; + const { expandNode, items: tree, collectionProps, filterProps, paginationProps } = useTreeCollection(items, { ...collectionOptions, columnDefinitions, }); - const renderEmpty = () => { - if (empty) { - return ; - } - return null; - }; - - const renderFilter = () => { - if (filter) { - return ; - } - return null; - }; + const emptyComponent = React.createElement(EmptyState, empty); + const filterComponent = React.createElement(TextFilter, { + ...filterProps, + filteringPlaceholder: filterPlaceholder || '', + }); + const paginationComponent = React.createElement(Pagination, paginationProps); - const renderPagination = () => { - if (collectionOptions.pagination) { - return ; - } - return null; + const hocProps = { + ...wrapperProps, + ...collectionProps, + ...filterProps, + ...paginationProps, + columnDefinitions, + items: tree, + empty: emptyComponent, + filter: filterComponent, + pagination: paginationComponent, + expandChildren: (node: ITreeNode) => { + expandNode(node); + expandChildren(node); + }, + onSortingChange: (event: NonCancelableCustomEvent>) => { + if (onSortingChange) { + onSortingChange(event); + } + if (collectionProps.onSortingChange) { + collectionProps.onSortingChange(event); + } + }, + onSelectionChange: (event: NonCancelableCustomEvent>) => { + if (onSelectionChange) { + onSelectionChange(event); + } + if (collectionProps.onSelectionChange) { + collectionProps.onSelectionChange(event); + } + }, }; - - useEffect(() => { - if (onReady && !isReady) { - onReady({ - ...actions, - reset, - }); - setReady(true); - } - }, [actions, onReady, reset, isReady]); - - return ( - ) => { - expandNode(node); - expandChildren(node); - }} - empty={renderEmpty()} - selectedItems={selectedItems} - onSelectionChange={onSelectionChange} - onSortingChange={(event: CustomEvent>) => { - if (onSortingChange) { - onSortingChange(event); - } - if (collectionProps.onSortingChange) { - collectionProps.onSortingChange(event); - } - }} - filter={renderFilter()} - pagination={renderPagination()} - ref={ref} - /> - ); - }); + return ; + }; }; diff --git a/packages/related-table/src/RelatedTable/EmptyState.tsx b/packages/related-table/src/RelatedTable/EmptyState.tsx index 48213353f..726a22f5d 100644 --- a/packages/related-table/src/RelatedTable/EmptyState.tsx +++ b/packages/related-table/src/RelatedTable/EmptyState.tsx @@ -6,7 +6,7 @@ export interface EmptyStateProps { description?: string; } -const EmptyState = (props: EmptyStateProps) => { +export const EmptyState = (props: EmptyStateProps) => { const { header, description } = props; return ( @@ -23,5 +23,3 @@ const EmptyState = (props: EmptyStateProps) => { ); }; - -export default EmptyState;