Skip to content

Commit

Permalink
feat(dashboard): add advanced search using knowledge graph to query e…
Browse files Browse the repository at this point in the history
…ditor
  • Loading branch information
dpportet committed Aug 1, 2023
1 parent 7f38586 commit 8722b33
Show file tree
Hide file tree
Showing 8 changed files with 628 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
import { QueryEditor } from '../queryEditor';
import { type IoTSiteWiseClient } from '@aws-sdk/client-iotsitewise';
import React from 'react';
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { TwinMakerKGQueryDataModule } from '@iot-app-kit/source-iottwinmaker';

const queryClient = new QueryClient();

const Wrapper = ({ children }: React.PropsWithChildren) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

const mockResults = {
columnDescriptions: [
{
name: 'assetName',
type: 'VALUE',
},
{
name: 'propertyId',
type: 'VALUE',
},
{
name: 'propertyName',
type: 'VALUE',
},
{
name: 'displayName',
type: 'VALUE',
},
],
rows: [
{
rowData: ['asset1', 'propertyId1', 'property1', 'temperature'],
},
{
rowData: ['Asset2', 'PropertyId2', 'property2', 'speed'],
},
{
rowData: ['Asset3', 'PropertyId3', 'property3'],
},
{
rowData: ['TestAsset', null, 'sitewiseAssetId', null],
},
{
rowData: ['TestAsset', null, 'sitewiseAssetModelId', null],
},
],
};

describe('Advanced Search', () => {
afterEach(() => {
// don't cache between tests
queryClient.clear();
});

it('should display advanced search table when kGDatamodule is provided', async () => {
const mockClient = {
send: jest.fn().mockResolvedValueOnce([]),
} as unknown as IoTSiteWiseClient;

const mockDataModule = {} as TwinMakerKGQueryDataModule;
const user = userEvent.setup();

render(
<Wrapper>
<QueryEditor client={mockClient} kGDatamodule={mockDataModule} />
</Wrapper>
);

await waitFor(() => expect(screen.queryByText('Loading assets...')).not.toBeInTheDocument());
expect(screen.getByRole('tab', { name: 'Advanced search' })).toBeVisible();
await user.click(screen.getByRole('tab', { name: 'Advanced search' }));
expect(screen.getByRole('textbox', { name: 'Search Term' })).toBeVisible();
});

it('should display results as displayName when searching for a property', async () => {
const mockClient = {
send: jest.fn().mockResolvedValueOnce([]),
} as unknown as IoTSiteWiseClient;

const mockDataModule = {
executeQuery: jest.fn().mockResolvedValueOnce(mockResults),
} as TwinMakerKGQueryDataModule;
const user = userEvent.setup();

render(
<Wrapper>
<QueryEditor client={mockClient} kGDatamodule={mockDataModule} />
</Wrapper>
);

await waitFor(() => expect(screen.queryByText('Loading assets...')).not.toBeInTheDocument());
expect(screen.getByRole('tab', { name: 'Advanced search' })).toBeVisible();
await user.click(screen.getByRole('tab', { name: 'Advanced search' }));

const input = screen.getByRole('textbox');
fireEvent.change(input, { target: { value: 'test' } });

await user.click(screen.getByRole('button', { name: 'Search' }));

await waitFor(() => expect(screen.queryByText('No results')).not.toBeInTheDocument());
expect(screen.queryByText('temperature')).toBeVisible();
expect(screen.queryByText('speed')).toBeVisible();
});

it('should allow selection of multiple results with pagination', async () => {
const mockPaginatedResults = {
columnDescriptions: [
{
name: 'assetName',
type: 'VALUE',
},
{
name: 'propertyId',
type: 'VALUE',
},
{
name: 'propertyName',
type: 'VALUE',
},
{
name: 'displayName',
type: 'VALUE',
},
],
rows: [
{
rowData: ['asset1', 'propertyId1', 'propertyName1', 'temperature1'],
},
{
rowData: ['asset2', 'propertyId2', 'propertyName2', 'temperature2'],
},
{
rowData: ['asset3', 'propertyId3', 'propertyName3', 'temperature3'],
},
{
rowData: ['asset4', 'propertyId4', 'propertyName4', 'temperature4'],
},
{
rowData: ['asset5', 'propertyId5', 'propertyName5', 'temperature15'],
},
{
rowData: ['asset6', 'propertyId6', 'propertyName6', 'temperature16'],
},
{
rowData: ['asset7', 'propertyId7', 'propertyName7', 'temperature17'],
},
{
rowData: ['asset8', 'propertyId8', 'propertyName8', 'temperature18'],
},
{
rowData: ['asset9', 'propertyId9', 'propertyName9', 'temperature19'],
},
{
rowData: ['asset10', 'propertyId10', 'propertyName10', 'temperature110'],
},
{
rowData: ['asset11', 'propertyId11', 'propertyName11', 'temperature11'],
},
{
rowData: ['TestAsset', null, 'sitewiseAssetId', null],
},
{
rowData: ['TestAsset', null, 'sitewiseAssetModelId', null],
},
],
};
const mockClient = {
send: jest.fn().mockResolvedValueOnce([]),
} as unknown as IoTSiteWiseClient;

const mockDataModule = {
executeQuery: jest.fn().mockResolvedValueOnce(mockPaginatedResults),
} as TwinMakerKGQueryDataModule;
const user = userEvent.setup();

render(
<Wrapper>
<QueryEditor client={mockClient} kGDatamodule={mockDataModule} />
</Wrapper>
);

await waitFor(() => expect(screen.queryByText('Loading assets...')).not.toBeInTheDocument());
expect(screen.getByRole('tab', { name: 'Advanced search' })).toBeVisible();
await user.click(screen.getByRole('tab', { name: 'Advanced search' }));

const input = screen.getByRole('textbox');
fireEvent.change(input, { target: { value: 'test' } });

await user.click(screen.getByRole('button', { name: 'Search' }));

await waitFor(() => expect(screen.queryByText('No results')).not.toBeInTheDocument());
await user.click(screen.getByRole('checkbox', { name: 'Select property temperature1' }));
await user.click(screen.getByRole('checkbox', { name: 'Select property temperature2' }));

await user.click(screen.getByRole('button', { name: 'Next page' }));
await user.click(screen.getByRole('checkbox', { name: 'Select property temperature11' }));
});

it('should allow changing preferences like enabling propertyName column', async () => {
const mockClient = {
send: jest.fn().mockResolvedValueOnce([]),
} as unknown as IoTSiteWiseClient;

const mockDataModule = {
executeQuery: jest.fn().mockResolvedValueOnce(mockResults),
} as TwinMakerKGQueryDataModule;
const user = userEvent.setup();

render(
<Wrapper>
<QueryEditor client={mockClient} kGDatamodule={mockDataModule} />
</Wrapper>
);

await waitFor(() => expect(screen.queryByText('Loading assets...')).not.toBeInTheDocument());
expect(screen.getByRole('tab', { name: 'Advanced search' })).toBeVisible();
await user.click(screen.getByRole('tab', { name: 'Advanced search' }));
await user.click(screen.getByRole('button', { name: 'Preferences' }));
await user.click(screen.getByRole('checkbox', { name: 'Property Name' }));
await user.click(screen.getByRole('button', { name: 'Confirm' }));

const input = screen.getByRole('textbox');
fireEvent.change(input, { target: { value: 'test' } });

await user.click(screen.getByRole('button', { name: 'Search' }));

await waitFor(() => expect(screen.queryByText('No results')).not.toBeInTheDocument());

expect(screen.queryByText('property1')).toBeVisible();
expect(screen.queryByText('property2')).toBeVisible();
expect(screen.queryByText('-')).toBeVisible(); // property without DisplayName
});

it('should handle an error if KG resposne cannot be parsed', async () => {
// No assetName column should result in an error
const mockInvalidResults = {
columnDescriptions: [
{
name: 'propertyId',
type: 'VALUE',
},
{
name: 'propertyName',
type: 'VALUE',
},
{
name: 'displayName',
type: 'VALUE',
},
],
rows: [
{
rowData: ['asset1', 'propertyId1', 'property1', 'temperature'],
},
{
rowData: ['Asset2', 'PropertyId2', 'property2', 'speed'],
},
{
rowData: ['Asset3', 'PropertyId3', 'property3'],
},
{
rowData: ['TestAsset', null, 'sitewiseAssetId', null],
},
{
rowData: ['TestAsset', null, 'sitewiseAssetModelId', null],
},
],
};
const mockClient = {
send: jest.fn().mockResolvedValueOnce([]),
} as unknown as IoTSiteWiseClient;

const mockDataModule = {
executeQuery: jest.fn().mockResolvedValueOnce(mockInvalidResults),
} as TwinMakerKGQueryDataModule;
const user = userEvent.setup();

render(
<Wrapper>
<QueryEditor client={mockClient} kGDatamodule={mockDataModule} />
</Wrapper>
);

await waitFor(() => expect(screen.queryByText('Loading assets...')).not.toBeInTheDocument());
expect(screen.getByRole('tab', { name: 'Advanced search' })).toBeVisible();
await user.click(screen.getByRole('tab', { name: 'Advanced search' }));

const input = screen.getByRole('textbox');
fireEvent.change(input, { target: { value: 'test' } });

await user.click(screen.getByRole('button', { name: 'Search' }));

await waitFor(() => expect(screen.queryByText('No results')).toBeVisible());
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useState } from 'react';
import Table from '@cloudscape-design/components/table';
import Box from '@cloudscape-design/components/box';
import Pagination from '@cloudscape-design/components/pagination';
import Header from '@cloudscape-design/components/header';
import { Property } from '../model/property';
import { useCollection } from '@cloudscape-design/collection-hooks';
import { CollectionPreferences, CollectionPreferencesProps } from '@cloudscape-design/components';
import { columnDefinitions, collectionPreferencesProps, paginationLabels } from './tableConfig';

interface AdvancedResultsTableProps {
data: Property[];
onSelect: (properties: Property[]) => void;
}

export function AdvancedResultsTable({ data, onSelect }: AdvancedResultsTableProps) {
const [preferences, setPreferences] = useState<CollectionPreferencesProps.Preferences>({
pageSize: 10,
visibleContent: ['displayName', 'assetName'],
});
const [selectedProperties, setSelectedProperties] = useState<Property[]>([]);

const { items, collectionProps, paginationProps } = useCollection(data, {
pagination: { pageSize: preferences.pageSize },
sorting: {},
selection: {
keepSelection: true,
},
});

return (
<Table
{...collectionProps}
onSelectionChange={({ detail }) => {
setSelectedProperties(detail.selectedItems);
onSelect(detail.selectedItems);
}}
ariaLabels={{
itemSelectionLabel: (isNotSelected, property) =>
isNotSelected ? `Select property ${property.displayName}` : `Deselect property ${property.displayName}`,
}}
header={<Header counter={data.length ? '(' + data.length + ')' : '(0)'}>Results</Header>}
pagination={<Pagination {...paginationProps} ariaLabels={paginationLabels} />}
variant='embedded'
sortingDisabled
columnDefinitions={columnDefinitions}
visibleColumns={preferences.visibleContent}
selectedItems={selectedProperties}
items={items}
loadingText='Loading results'
selectionType='multi'
trackBy='propertyId'
preferences={
<CollectionPreferences
{...collectionPreferencesProps}
preferences={preferences}
onConfirm={({ detail }) => setPreferences(detail)}
/>
}
empty={
<Box textAlign='center' color='inherit'>
<b>No results</b>
<Box padding={{ bottom: 's' }} variant='p' color='inherit'>
No results to display.
</Box>
</Box>
}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AdvancedResultsTable } from './advancedResultsTable';

0 comments on commit 8722b33

Please sign in to comment.