Skip to content

Commit

Permalink
feat(Knowledge Graph): adding KG parent component, search and explore…
Browse files Browse the repository at this point in the history
… logic
  • Loading branch information
MO-Elmu committed May 23, 2023
1 parent e8faf6e commit 95cc307
Show file tree
Hide file tree
Showing 40 changed files with 566 additions and 487 deletions.
434 changes: 132 additions & 302 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion packages/react-components/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ module.exports = {
overrides: [
{
// Disabling explicit any rule for graph-view component since types are defined in 3p component.
files: ['**/src/components/graph/graph-view.tsx'],
files: [
'**/src/components/knowledge-graph/graph/graph-view.tsx',
'**/src/components/knowledge-graph/responseParser.tsx',
],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
],
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/react-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"lodash.omitby": "^4.6.0",
"parse-duration": "^1.0.3",
"react-cytoscapejs": "^2.0.0",
"react-intl": "6.4.2",
"styled-components": "^5.3.10",
"uuid": "^9.0.0",
"video.js": "8.3.0"
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { useEffect, useCallback, useMemo, useState } from 'react';
import { IntlProvider, FormattedMessage } from 'react-intl';
import { ElementDefinition } from 'cytoscape';
import { Button, Container, Header, Input, SpaceBetween } from '@cloudscape-design/components';
import { TwinMakerKGQueryDataModule } from '@iot-app-kit/source-iottwinmaker';
import { Graph } from './graph';
import StateManager, { useKnowledgeGraphState } from './StateManager';
import { createKnowledgeGraphQueryClient } from './KnowledgeGraphQueries';
import { ResponseParser } from './responseParser';
import { getElementsDefinition } from './utils';

interface KnowledgeGraphInterface {
kgDataSource: TwinMakerKGQueryDataModule;
}
const MAX_NUMBER_HOPS = 10;

const KnowledgeGraphContainer: React.FC<KnowledgeGraphInterface> = ({ kgDataSource }) => {
const { selectedGraphNodeEntityId, setQueryResult, queryResult, clearGraphResults } = useKnowledgeGraphState();
const [searchTerm, setSearchTerm] = useState('');
const [elements, setElements] = useState<ElementDefinition[]>([]);

const knowledgeGraphQueryClient = useMemo(() => {
return createKnowledgeGraphQueryClient(kgDataSource, setQueryResult);
}, [kgDataSource, setQueryResult]);

const onSearchClicked = useCallback(() => {
if (searchTerm) {
knowledgeGraphQueryClient.findEntitiesByName(searchTerm);
}
}, [knowledgeGraphQueryClient, searchTerm]);

const onExploreClicked = useCallback(() => {
if (selectedGraphNodeEntityId) {
knowledgeGraphQueryClient.findRelatedEntities(selectedGraphNodeEntityId, MAX_NUMBER_HOPS);
}
}, [selectedGraphNodeEntityId, knowledgeGraphQueryClient]);

const onClearClicked = useCallback(() => {
clearGraphResults(true);
}, [clearGraphResults]);

useEffect(() => {
if (queryResult) {
console.log('queryResults: ', queryResult);
const { nodeData, edgeData } = ResponseParser.parse(queryResult['rows'], queryResult['columnDescriptions']);
setElements(getElementsDefinition([...nodeData.values()], [...edgeData.values()]));
} else {
setElements([]);
setSearchTerm('');
}
}, [queryResult]);
return (
<Container header={<Header variant='h3'>Knowledge Graph</Header>}>
<SpaceBetween direction='vertical' size='s'>
<SpaceBetween direction='horizontal' size='s'>
<Input
type='text'
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.detail.value);
}}
></Input>
<Button onClick={onSearchClicked}>
{/* eventually will move to auto-generated IDs */}
<FormattedMessage
id='KnowledgeGraphPanel.button.search'
defaultMessage='Search'
description='Search button text'
/>
</Button>
</SpaceBetween>
{/* inline styling here for testing only this will be fixed in the next PR */}
<Graph elements={elements} style={{ width: '1000px', height: '1000px' }} />
<SpaceBetween direction='horizontal' size='s'>
<Button disabled={selectedGraphNodeEntityId ? false : true} onClick={onExploreClicked}>
<FormattedMessage
id='KnowledgeGraphPanel.button.explore'
defaultMessage='Explore'
description='Explore button text'
/>
</Button>
{queryResult ? (
<Button onClick={onClearClicked}>Clear</Button>
) : (
<Button disabled onClick={onClearClicked}>
<FormattedMessage
id='KnowledgeGraphPanel.button.clear'
defaultMessage='Clear'
description='Clear button text'
/>
</Button>
)}
</SpaceBetween>
</SpaceBetween>
</Container>
);
};
export const KnowledgeGraph: React.FC<KnowledgeGraphInterface> = (props) => {
return (
<StateManager>
{/* For the moment we're setting it to a fixed language,
later we will determine the user's locale by evaluating the language request sent by the browser */}
<IntlProvider locale='en' defaultLocale='en'>
<KnowledgeGraphContainer {...props} />
</IntlProvider>
</StateManager>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { TwinMakerKGQueryDataModule } from '@iot-app-kit/source-iottwinmaker';
import { ExecuteQueryCommandOutput } from '@aws-sdk/client-iottwinmaker';
export interface KnowledgeGraphQueryInterface {
findEntitiesByName(name: string): Promise<void>;
findRelatedEntities(entityId: string, numberOfHops: number): Promise<void>;
}
export const createKnowledgeGraphQueryClient = function (
dataSource: TwinMakerKGQueryDataModule,
updateQueryResults: (result: ExecuteQueryCommandOutput) => void
) {
const knowledgeGraphQuery: KnowledgeGraphQueryInterface = {
findEntitiesByName: async (name: string): Promise<void> => {
const result = await dataSource.executeQuery({
queryStatement: `SELECT e FROM EntityGraph MATCH (e) WHERE e.entityName like '%${name}%'`,
});
updateQueryResults(result);
},
findRelatedEntities: async (entityId: string, numberOfHops: number): Promise<void> => {
const result = await dataSource.executeQuery({
queryStatement: `SELECT e1 FROM EntityGraph MATCH (e)-[]-{1,${numberOfHops}}(e1) WHERE e.entityId = '${entityId}'`,
});
updateQueryResults(result);
},
};
return knowledgeGraphQuery;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { ReactNode, createContext, useContext, useState } from 'react';
import { ExecuteQueryCommandOutput } from '@aws-sdk/client-iottwinmaker';
interface KnowledgeGraphContext {
selectedGraphNodeEntityId?: string;
setSelectedGraphNodeEntityId: (entityId?: string) => void;
queryStatement?: string;
setQueryStatement: (query: string) => void;
queryResult?: ExecuteQueryCommandOutput;
setQueryResult: (result: ExecuteQueryCommandOutput) => void;
clearGraphResults: (clear: boolean) => void;
}
export interface StateManagerProps {
children: ReactNode;
}

const context = createContext<KnowledgeGraphContext>({} as KnowledgeGraphContext);
export function useKnowledgeGraphState() {
return useContext(context);
}
const StateManager: React.FC<StateManagerProps> = ({ children }) => {
const [selectedGraphNodeEntityId, setSelectedGraphNodeEntityId] = useState<string>();
const [queryStatement, setQueryStatement] = useState<string>();
const [queryResult, setQueryResult] = useState<ExecuteQueryCommandOutput>();
const clearGraphResults = (clear: boolean) => {
if (clear) setQueryResult(undefined);
};
return (
<context.Provider
value={{
selectedGraphNodeEntityId,
setSelectedGraphNodeEntityId,
queryStatement,
setQueryStatement,
queryResult,
setQueryResult,
clearGraphResults,
}}
>
{children}
</context.Provider>
);
};
export default StateManager;
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<graph /> renders default elements 1`] = `
<div>
<div
class="iot-app-kit-graph"
>
<div
data-mocked="CytoscapeComponent"
layout="[object Object]"
style="width: 100%; height: 100%;"
stylesheet="[object Object],[object Object],[object Object],[object Object]"
>
[]
</div>
<ul
class="iot-app-kit-graph-controls"
>
<li
class="iot-app-kit-graph-control-item"
>
<button
class="iot-app-kit-graph-button awsui_button_vjswe_12zyy_101 awsui_variant-icon_vjswe_12zyy_166 awsui_button-no-text_vjswe_12zyy_885"
data-testid="fit-button"
type="submit"
>
<span
class="awsui_icon_vjswe_12zyy_905 awsui_icon-left_vjswe_12zyy_905 awsui_icon_h11ix_ahfiu_98 awsui_size-normal-mapped-height_h11ix_ahfiu_151 awsui_size-normal_h11ix_ahfiu_147 awsui_variant-normal_h11ix_ahfiu_219"
>
<svg
aria-hidden="true"
focusable="false"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14 11v3h-4M2 11v3h4M2 5V2h4M14 5V2h-4M10 6H6v4h4V6Z"
/>
</svg>
</span>
</button>
</li>
<li
class="iot-app-kit-graph-control-item"
>
<button
class="iot-app-kit-graph-button awsui_button_vjswe_12zyy_101 awsui_variant-icon_vjswe_12zyy_166 awsui_button-no-text_vjswe_12zyy_885"
data-testid="center-button"
type="submit"
>
<span
class="awsui_icon_vjswe_12zyy_905 awsui_icon-left_vjswe_12zyy_905 awsui_icon_h11ix_ahfiu_98 awsui_size-normal-mapped-height_h11ix_ahfiu_151 awsui_size-normal_h11ix_ahfiu_147 awsui_variant-normal_h11ix_ahfiu_219"
>
<svg
aria-hidden="true"
focusable="false"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9 2h5v5M7 2H2v5M7 14H2V9M9 14h5V9M2 2l12 12M14 2 2 14"
/>
</svg>
</span>
</button>
</li>
<li
class="iot-app-kit-graph-control-item"
>
<button
class="iot-app-kit-graph-button awsui_button_vjswe_12zyy_101 awsui_variant-icon_vjswe_12zyy_166 awsui_button-no-text_vjswe_12zyy_885"
data-testid="zoom-in-button"
type="submit"
>
<span
class="awsui_icon_vjswe_12zyy_905 awsui_icon-left_vjswe_12zyy_905 awsui_icon_h11ix_ahfiu_98 awsui_size-normal-mapped-height_h11ix_ahfiu_151 awsui_size-normal_h11ix_ahfiu_147 awsui_variant-normal_h11ix_ahfiu_219"
>
<svg
aria-hidden="true"
focusable="false"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="6.885"
cy="6.885"
r="5.385"
/>
<path
d="m14.5 14.5-3.846-3.846M7 4v6M10 7H4"
/>
</svg>
</span>
</button>
</li>
<li
class="iot-app-kit-graph-control-item"
>
<button
class="iot-app-kit-graph-button awsui_button_vjswe_12zyy_101 awsui_variant-icon_vjswe_12zyy_166 awsui_button-no-text_vjswe_12zyy_885"
data-testid="zoom-out-button"
type="submit"
>
<span
class="awsui_icon_vjswe_12zyy_905 awsui_icon-left_vjswe_12zyy_905 awsui_icon_h11ix_ahfiu_98 awsui_size-normal-mapped-height_h11ix_ahfiu_151 awsui_size-normal_h11ix_ahfiu_147 awsui_variant-normal_h11ix_ahfiu_219"
>
<svg
aria-hidden="true"
focusable="false"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="6.885"
cy="6.885"
r="5.385"
/>
<path
d="m14.5 14.5-3.846-3.846M10 7H4"
/>
</svg>
</span>
</button>
</li>
</ul>
</div>
</div>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jest.mock('./hooks/useCyEvent');

describe('<graph />', () => {
it('renders default elements', () => {
const { container } = render(<Graph />);
const { container } = render(<Graph elements={[]} />);
expect(container).toMatchSnapshot();
});

Expand All @@ -49,7 +49,7 @@ describe('<graph />', () => {

useRefMock.mockReturnValueOnce(fakeCy);

render(<Graph />);
render(<Graph elements={[]} />);

expect(fakeCy.current.resize).toHaveBeenCalled();
expect(fakeCy.current.center).toHaveBeenCalled();
Expand All @@ -69,7 +69,7 @@ describe('<graph />', () => {

useRefMock.mockReturnValueOnce(fakeCy);

const { findByTestId } = render(<Graph />);
const { findByTestId } = render(<Graph elements={[]} />);
const sut = await findByTestId('fit-button');

fireEvent.click(sut);
Expand All @@ -91,7 +91,7 @@ describe('<graph />', () => {

useRefMock.mockReturnValueOnce(fakeCy);

const { findByTestId } = render(<Graph />);
const { findByTestId } = render(<Graph elements={[]} />);
const sut = await findByTestId('center-button');

fireEvent.click(sut);
Expand All @@ -113,7 +113,7 @@ describe('<graph />', () => {

useRefMock.mockReturnValueOnce(fakeCy);

const { findByTestId } = render(<Graph />);
const { findByTestId } = render(<Graph elements={[]} />);
const sut = await findByTestId('zoom-in-button');

fireEvent.click(sut);
Expand All @@ -135,7 +135,7 @@ describe('<graph />', () => {

useRefMock.mockReturnValueOnce(fakeCy);

const { findByTestId } = render(<Graph />);
const { findByTestId } = render(<Graph elements={[]} />);
const sut = await findByTestId('zoom-out-button');

fireEvent.click(sut);
Expand Down
Loading

0 comments on commit 95cc307

Please sign in to comment.