diff --git a/packages/motif/src/containers/SidePanel/Header/Header.tsx b/packages/motif/src/containers/SidePanel/Header/Header.tsx index eba1a799a..5cf299f3b 100644 --- a/packages/motif/src/containers/SidePanel/Header/Header.tsx +++ b/packages/motif/src/containers/SidePanel/Header/Header.tsx @@ -9,6 +9,7 @@ import { StyleOptions, GraphList, GraphData, + FilterOptions, } from '../../../redux/graph'; import * as Icon from '../../../components/Icons'; import Editable from '../../../components/Editable'; @@ -28,6 +29,10 @@ const Header = () => { const styleOptions: StyleOptions = useSelector((state) => GraphSelectors.getStyleOptions(state), ); + + const filterOptions: FilterOptions = useSelector((state) => + GraphSelectors.getFilterOptions(state), + ); const dispatch = useDispatch(); const onChangeName = useCallback( @@ -78,6 +83,7 @@ const Header = () => { graphList={graphList} styleOptions={styleOptions} graphFlatten={graphFlatten} + filterOptions={filterOptions} /> )} @@ -91,6 +97,7 @@ const Header = () => { styleOptions, graphList, graphFlatten, + filterOptions, ], ); }; diff --git a/packages/motif/src/containers/SidePanel/Header/SaveButton/Menu.tsx b/packages/motif/src/containers/SidePanel/Header/SaveButton/Menu.tsx index 62e835772..4a353000a 100644 --- a/packages/motif/src/containers/SidePanel/Header/SaveButton/Menu.tsx +++ b/packages/motif/src/containers/SidePanel/Header/SaveButton/Menu.tsx @@ -12,18 +12,17 @@ const SaveChoicesMenu: FC = ({ graphList, styleOptions, graphFlatten, + filterOptions, onExportExternal, }) => { const [, theme] = useStyletron(); - const exportJSON = ( - graphList: GraphT.GraphList, - styleOptions: GraphT.StyleOptions, - ) => { + const exportJSON = () => { const positionGraphList = setGraphListPosition(graphList, graphFlatten); const exportData: GraphT.TLoadFormat = { data: positionGraphList, style: styleOptions, + filter: filterOptions, }; const contentType = 'application/json;charset=utf-8;'; @@ -36,11 +35,12 @@ const SaveChoicesMenu: FC = ({ document.body.removeChild(file); }; - const saveToCloud = (graphList, styleOptions) => { + const saveToCloud = () => { const positionGraphList = setGraphListPosition(graphList, graphFlatten); const exportData: GraphT.TLoadFormat = { data: positionGraphList, style: styleOptions, + filter: filterOptions, }; onExportExternal(exportData); @@ -50,19 +50,19 @@ const SaveChoicesMenu: FC = ({ const items = [ { label: , - onClick: () => exportJSON(graphList, styleOptions), + onClick: () => exportJSON(), }, ]; if (onExportExternal) { items.push({ label: , - onClick: () => saveToCloud(graphList, styleOptions), + onClick: () => saveToCloud(), }); } return items; - }, [graphList, styleOptions, exportJSON, theme, graphFlatten]); + }, [graphList, styleOptions, exportJSON, theme, graphFlatten, filterOptions]); return ( { return ( )} diff --git a/packages/motif/src/containers/SidePanel/Header/SaveButton/types.ts b/packages/motif/src/containers/SidePanel/Header/SaveButton/types.ts index ac6242a03..b8bbe5b58 100644 --- a/packages/motif/src/containers/SidePanel/Header/SaveButton/types.ts +++ b/packages/motif/src/containers/SidePanel/Header/SaveButton/types.ts @@ -7,6 +7,7 @@ export type SaveButtonProps = { graphList: GraphT.GraphList; graphFlatten: GraphT.GraphData; styleOptions: GraphT.StyleOptions; + filterOptions: GraphT.FilterOptions; onExportExternal?: ExplorerContextProps['onExportExternal']; }; export type LabelProps = { theme: Theme }; diff --git a/packages/motif/src/redux/graph/slice.ts b/packages/motif/src/redux/graph/slice.ts index 3e7f1860d..26941254b 100644 --- a/packages/motif/src/redux/graph/slice.ts +++ b/packages/motif/src/redux/graph/slice.ts @@ -232,6 +232,9 @@ const graph = createSlice({ updateStyleOption(state, action: PayloadAction) { Object.assign(state.styleOptions, action.payload); }, + updateFilterOption(state, action: PayloadAction) { + Object.assign(state.filterOptions, action.payload); + }, changeLayout(state, action: PayloadAction): void { const { id, ...options } = action.payload.layout; const defaultOptions = LAYOUT.OPTIONS.find((x) => x.type === id); @@ -522,6 +525,7 @@ export const { updateLastGroupEdgeIds, overwriteEdgeSelection, updateNodePosition, + updateFilterOption, } = graph.actions; export default graph.reducer; diff --git a/packages/motif/src/redux/graph/tests/constants/positive/edgeListCsv.ts b/packages/motif/src/redux/graph/tests/constants/positive/edgeListCsv.ts new file mode 100644 index 000000000..e34fbf2c6 --- /dev/null +++ b/packages/motif/src/redux/graph/tests/constants/positive/edgeListCsv.ts @@ -0,0 +1,7 @@ +export const firstEdgeListCsv = + 'id,relation,source,target\ntxn1,works,jason,cylynx\ntxn3,abc,cylynx,timothy\ntxn4,says hi to,swan,cylynx'; + +export const secondEdgeListCsv = 'id,source,target\n123,x,y\n456,y,z\n789,z,x'; + +export const quotesHeaderCsv = + '"id","relation","source","target"\ntxn1,works,jason,cylynx\ntxn3,abc,cylynx,timothy\ntxn4,says hi to,swan,cylynx'; diff --git a/packages/motif/src/redux/graph/tests/constants/positive.ts b/packages/motif/src/redux/graph/tests/constants/positive/json.ts similarity index 60% rename from packages/motif/src/redux/graph/tests/constants/positive.ts rename to packages/motif/src/redux/graph/tests/constants/positive/json.ts index 8a782330a..c7fdafd6b 100644 --- a/packages/motif/src/redux/graph/tests/constants/positive.ts +++ b/packages/motif/src/redux/graph/tests/constants/positive/json.ts @@ -1,37 +1,93 @@ -import * as LAYOUT from '../../../../constants/layout-options'; -import { DEFAULT_NODE_STYLE } from '../../../../constants/graph-shapes'; -import { GraphData } from '../..'; +import * as LAYOUT from '../../../../../constants/layout-options'; +import { DEFAULT_NODE_STYLE } from '../../../../../constants/graph-shapes'; +import { GraphData } from '../../..'; -export const whitespaceNodeEdge = { - edgeCsv: [ +export const graphWithGroupEdge = { + nodes: [{ id: 'a' }, { id: 'b' }], + edges: [ { - fileName: 'test-1.csv', - content: - 'id,relation,source,target\ntxn1,hello, a,b \ntxn2,works,b ,c \ntxn3,abc,c , a', + id: 'a-b1', + source: 'a', + target: 'b', + Value: 300000, + TokenName: 'Tether USD', }, - ], - nodeCsv: [ { - fileName: 'test-2.csv', - content: 'id,value,score\n a,20,80\nb ,40,100\n c,60,123', + id: 'a-b2', + source: 'a', + target: 'b', + Value: 300000, + TokenName: 'Tether USD', }, - ], -}; - -export const numericAccessorsNodeEdge = { - edgeCsv: [ { - fileName: 'numeric-accessors-1.csv', - content: - 'id,relation,numeric_source,numeric_target\ntxn1,hello,1,2\ntxn2,works,2,3\ntxn3,abc,3,1\n', + id: 'a-b3', + source: 'a', + target: 'b', + Value: 147000, + TokenName: 'ETH', }, - ], - nodeCsv: [ { - fileName: 'numeric-accessors-2.csv', - content: 'custom_id,value,score\n1,20,80\n2,40,100\n3,60,123', + id: 'a-b4', + source: 'a', + target: 'b', + Value: 38000, + TokenName: 'Tether USD', + }, + { + id: 'a-b5', + source: 'a', + target: 'b', + Value: 31000, + TokenName: 'Tether USD', }, ], + metadata: { + fields: { + // @ts-ignore + nodes: [], + edges: [ + { + name: 'source', + format: '', + type: 'string', + analyzerType: 'STRING', + }, + { + name: 'target', + format: '', + type: 'string', + analyzerType: 'STRING', + }, + { + name: 'Value', + format: '', + type: 'integer', + analyzerType: 'INT', + }, + { + name: 'TokenName', + format: '', + type: 'string', + analyzerType: 'STRING', + }, + ], + }, + groupEdges: { + toggle: true, + availability: true, + type: 'TokenName', + fields: { + Or2fv2L2W: { + field: 'Value', + aggregation: ['count', 'sum', 'max'], + }, + Sfdjksdf2: { + field: 'TokenName', + aggregation: ['first', 'last', 'most_frequent'], + }, + }, + }, + }, }; export const sampleJson1 = { @@ -310,114 +366,132 @@ export const sampleGraphFlatten: GraphData = { }, }; -export const graphWithGroupEdge = { - nodes: [{ id: 'a' }, { id: 'b' }], - edges: [ - { - id: 'a-b1', - source: 'a', - target: 'b', - Value: 300000, - TokenName: 'Tether USD', - }, - { - id: 'a-b2', - source: 'a', - target: 'b', - Value: 300000, - TokenName: 'Tether USD', - }, +export const graphWithFilter = { + data: [ { - id: 'a-b3', - source: 'a', - target: 'b', - Value: 147000, - TokenName: 'ETH', + nodes: [ + { id: 'node-0', label: 'node-0' }, + { id: 'node-1', label: 'node-1' }, + { id: 'node-2', label: 'node-2' }, + { id: 'node-3', label: 'node-3' }, + { id: 'node-4', label: 'node-4' }, + { id: 'node-5', label: 'node-5' }, + { id: 'node-6', label: 'node-6' }, + { id: 'node-7', label: 'node-7' }, + { id: 'node-8', label: 'node-8' }, + { id: 'node-9', label: 'node-9' }, + ], + edges: [ + { source: 'node-0', target: 'node-0', numeric: 2, id: 'l5O4mnNhR5' }, + { source: 'node-0', target: 'node-1', numeric: 10, id: '64l4IOP-qf' }, + { source: 'node-0', target: 'node-2', numeric: 7, id: 'lj01TmTzLy' }, + { source: 'node-0', target: 'node-3', numeric: 9, id: 'mCpGU2GpGl' }, + { source: 'node-0', target: 'node-4', numeric: 6, id: 'YzhGFuNL_c' }, + { source: 'node-0', target: 'node-5', numeric: 4, id: 'M_xjfLXSsH' }, + { source: 'node-0', target: 'node-6', numeric: 6, id: 'Puxu6Q0ZIW' }, + { source: 'node-0', target: 'node-7', numeric: 1, id: 'BJb1lYPq6_' }, + { source: 'node-0', target: 'node-8', numeric: 9, id: 'IbLAWDjY-T' }, + { source: 'node-1', target: 'node-0', numeric: 8, id: 'Q-3JBeWh_I' }, + { source: 'node-2', target: 'node-0', numeric: 5, id: 'ic19cuNzLI' }, + { source: 'node-3', target: 'node-0', numeric: 6, id: 'UYWF5odnoI' }, + { source: 'node-4', target: 'node-0', numeric: 2, id: '4IWQ5BJ-p9' }, + { source: 'node-5', target: 'node-0', numeric: 4, id: 'uB6DGIRXSw' }, + { source: 'node-6', target: 'node-0', numeric: 3, id: 'G8LkkiRv5X' }, + { source: 'node-7', target: 'node-0', numeric: 7, id: 'tZzIzcOeErQ' }, + { source: 'node-8', target: 'node-0', numeric: 4, id: 'xsW6KdHI41O' }, + { source: 'node-9', target: 'node-0', numeric: 7, id: 'CtUMCSLt1HH' }, + ], + metadata: { + title: 'Circle Data', + fields: { + nodes: [ + { name: 'id', format: '', type: 'string', analyzerType: 'STRING' }, + { + name: 'label', + format: '', + type: 'string', + analyzerType: 'STRING', + }, + ], + edges: [ + { + name: 'source', + format: '', + type: 'string', + analyzerType: 'STRING', + }, + { + name: 'target', + format: '', + type: 'string', + analyzerType: 'STRING', + }, + { + name: 'numeric', + format: '', + type: 'integer', + analyzerType: 'INT', + }, + { name: 'id', format: '', type: 'string', analyzerType: 'STRING' }, + ], + }, + key: 'ius8Qbjao', + groupEdges: { toggle: false, availability: false }, + }, }, - { - id: 'a-b4', - source: 'a', - target: 'b', - Value: 38000, - TokenName: 'Tether USD', + ], + style: { + layout: { + type: 'concentric', + minNodeSpacing: 50, + sortBy: 'degree', + preventOverlap: true, + workerEnabled: true, }, - { - id: 'a-b5', - source: 'a', - target: 'b', - Value: 31000, - TokenName: 'Tether USD', + nodeStyle: { + color: { value: 'green', id: 'fixed' }, + size: { id: 'fixed', value: 20 }, }, - ], - metadata: { - fields: { - // @ts-ignore - nodes: [], - edges: [ - { - name: 'source', - format: '', - type: 'string', - analyzerType: 'STRING', - }, + edgeStyle: { width: { id: 'fixed', value: 1 } }, + }, + filter: { + '6gTFk8X0Q': { + id: 'id', + from: 'nodes', + selection: [ { - name: 'target', - format: '', + id: 'id', + label: 'id', type: 'string', analyzerType: 'STRING', - }, - { - name: 'Value', format: '', - type: 'integer', - analyzerType: 'INT', - }, - { - name: 'TokenName', - format: '', - type: 'string', - analyzerType: 'STRING', + from: 'nodes', + optionKey: 'nodes-id', + __optgroup: 'Nodes', }, ], - }, - groupEdges: { - toggle: true, - availability: true, - type: 'TokenName', - fields: { - Or2fv2L2W: { - field: 'Value', - aggregation: ['count', 'sum', 'max'], - }, - Sfdjksdf2: { - field: 'TokenName', - aggregation: ['first', 'last', 'most_frequent'], - }, - }, + analyzerType: 'STRING', + isFilterReady: true, + stringOptions: [ + { id: 'node-0', label: 'node-0' }, + { id: 'node-1', label: 'node-1' }, + { id: 'node-2', label: 'node-2' }, + { id: 'node-3', label: 'node-3' }, + { id: 'node-4', label: 'node-4' }, + { id: 'node-5', label: 'node-5' }, + { id: 'node-6', label: 'node-6' }, + { id: 'node-7', label: 'node-7' }, + { id: 'node-8', label: 'node-8' }, + { id: 'node-9', label: 'node-9' }, + ], + caseSearch: [ + { id: 'node-1', label: 'node-1' }, + { id: 'node-0', label: 'node-0' }, + { id: 'node-3', label: 'node-3' }, + { id: 'node-4', label: 'node-4' }, + { id: 'node-5', label: 'node-5' }, + { id: 'node-2', label: 'node-2' }, + ], }, }, }; - -export const sampleNodeEdgeData = { - edgeCsv: [ - { - fileName: 'test-1.csv', - content: - 'id,relation,source,target\ntxn1,hello,a,b\ntxn2,works,b,c\ntxn3,abc,c,a', - }, - ], - nodeCsv: [ - { - fileName: 'test-2.csv', - content: 'id,value,score\na,20,80\nb,40,100\nc,60,123', - }, - ], -}; - -export const firstEdgeListCsv = - 'id,relation,source,target\ntxn1,works,jason,cylynx\ntxn3,abc,cylynx,timothy\ntxn4,says hi to,swan,cylynx'; - -export const secondEdgeListCsv = 'id,source,target\n123,x,y\n456,y,z\n789,z,x'; - -export const quotesHeaderCsv = - '"id","relation","source","target"\ntxn1,works,jason,cylynx\ntxn3,abc,cylynx,timothy\ntxn4,says hi to,swan,cylynx'; diff --git a/packages/motif/src/redux/graph/tests/constants/positive/nodeEdgeCsv.ts b/packages/motif/src/redux/graph/tests/constants/positive/nodeEdgeCsv.ts new file mode 100644 index 000000000..e1bd0c2d8 --- /dev/null +++ b/packages/motif/src/redux/graph/tests/constants/positive/nodeEdgeCsv.ts @@ -0,0 +1,47 @@ +export const sampleNodeEdgeData = { + edgeCsv: [ + { + fileName: 'test-1.csv', + content: + 'id,relation,source,target\ntxn1,hello,a,b\ntxn2,works,b,c\ntxn3,abc,c,a', + }, + ], + nodeCsv: [ + { + fileName: 'test-2.csv', + content: 'id,value,score\na,20,80\nb,40,100\nc,60,123', + }, + ], +}; + +export const whitespaceNodeEdge = { + edgeCsv: [ + { + fileName: 'test-1.csv', + content: + 'id,relation,source,target\ntxn1,hello, a,b \ntxn2,works,b ,c \ntxn3,abc,c , a', + }, + ], + nodeCsv: [ + { + fileName: 'test-2.csv', + content: 'id,value,score\n a,20,80\nb ,40,100\n c,60,123', + }, + ], +}; + +export const numericAccessorsNodeEdge = { + edgeCsv: [ + { + fileName: 'numeric-accessors-1.csv', + content: + 'id,relation,numeric_source,numeric_target\ntxn1,hello,1,2\ntxn2,works,2,3\ntxn3,abc,3,1\n', + }, + ], + nodeCsv: [ + { + fileName: 'numeric-accessors-2.csv', + content: 'custom_id,value,score\n1,20,80\n2,40,100\n3,60,123', + }, + ], +}; diff --git a/packages/motif/src/redux/graph/tests/group-edge-aggregation.test.tsx b/packages/motif/src/redux/graph/tests/group-edge-aggregation.test.tsx index 08e4f21e3..74e80633b 100644 --- a/packages/motif/src/redux/graph/tests/group-edge-aggregation.test.tsx +++ b/packages/motif/src/redux/graph/tests/group-edge-aggregation.test.tsx @@ -6,27 +6,12 @@ import { flatten } from 'underscore'; import React from 'react'; import { render } from '@testing-library/react'; import { ToasterContainer } from 'baseui/toast'; -import { - addQuery, - initialState, - processGraphResponse, - overwriteEdgeSelection, -} from '../slice'; +import * as GraphSlice from '../slice'; import { resetState } from '../../import/fileUpload/slice'; -import * as Constant from './constants/positive'; +import * as Json from './constants/positive/json'; import { importJson } from '../processors/import'; -import { - clearError, - closeModal, - fetchBegin, - fetchDone, - updateToast, -} from '../../ui/slice'; -import { - groupEdgesWithAggregation, - importJsonData, - computeEdgeSelection, -} from '../thunk'; +import * as UiSlice from '../../ui/slice'; +import * as GraphThunk from '../thunk'; import { Field, GraphData, Selection } from '../types'; import { groupEdgesForImportation, @@ -36,7 +21,7 @@ import { getGraph } from '../selectors'; const mockStore = configureStore([thunk]); const getStore = () => { - const graphState = cloneDeep(initialState); + const graphState = cloneDeep(GraphSlice.initialState); const store = { investigate: { ui: {}, @@ -63,13 +48,17 @@ describe('Group Edges', () => { describe('Group Edge For Importation', () => { it('should perform group edge during importation', async (done) => { // arrange - const importDataArr = [Constant.simpleGraphWithGroupEdge]; + const importDataArr = [Json.simpleGraphWithGroupEdge]; const groupEdgeToggle = true; // act const batchDataPromises = importDataArr.map((graphData) => { const { data } = graphData; - return importJson(data, initialState.accessors, groupEdgeToggle); + return importJson( + data, + GraphSlice.initialState.accessors, + groupEdgeToggle, + ); }); const graphDataArr = await Promise.all(batchDataPromises); @@ -91,26 +80,26 @@ describe('Group Edges', () => { // expected results const expectedActions = [ - fetchBegin(), - addQuery([firstGraphData]), - processGraphResponse({ + UiSlice.fetchBegin(), + GraphSlice.addQuery([firstGraphData]), + GraphSlice.processGraphResponse({ data: modData, - accessors: initialState.accessors, + accessors: GraphSlice.initialState.accessors, }), - updateToast('toast-0'), + UiSlice.updateToast('toast-0'), resetState(), - clearError(), - fetchDone(), - closeModal(), + UiSlice.clearError(), + UiSlice.fetchDone(), + UiSlice.closeModal(), ]; // assertions return store .dispatch( - importJsonData( + GraphThunk.importJsonData( importDataArr as any, groupEdgeToggle, - initialState.accessors, + GraphSlice.initialState.accessors, ), ) .then(() => { @@ -129,7 +118,7 @@ describe('Group Edges', () => { describe('Group Edges With Aggregation', () => { describe('Group By All', () => { - const simpleGraphWithGroupEdge = Constant.graphWithGroupEdge; + const simpleGraphWithGroupEdge = Json.graphWithGroupEdge; const importedGraphState = () => { const store = { @@ -172,7 +161,9 @@ describe('Group Edges', () => { }); it('should display the correct title and visibility', async () => { - await store.dispatch(groupEdgesWithAggregation(graphIndex) as any); + await store.dispatch( + GraphThunk.groupEdgesWithAggregation(graphIndex) as any, + ); store.getActions().forEach((actions) => { const { payload, type } = actions; @@ -189,7 +180,9 @@ describe('Group Edges', () => { }); it('should compute the correct grouped edges', async () => { - await store.dispatch(groupEdgesWithAggregation(graphIndex) as any); + await store.dispatch( + GraphThunk.groupEdgesWithAggregation(graphIndex) as any, + ); store.getActions().forEach((actions) => { const { payload, type } = actions; @@ -202,7 +195,9 @@ describe('Group Edges', () => { }); it('should derive the correct group edge configurations', async () => { - await store.dispatch(groupEdgesWithAggregation(graphIndex) as any); + await store.dispatch( + GraphThunk.groupEdgesWithAggregation(graphIndex) as any, + ); store.getActions().forEach((actions) => { const { payload } = actions; @@ -216,7 +211,9 @@ describe('Group Edges', () => { }); it('should display the correct edge properties', async () => { - await store.dispatch(groupEdgesWithAggregation(graphIndex) as any); + await store.dispatch( + GraphThunk.groupEdgesWithAggregation(graphIndex) as any, + ); const aggregatedEdgeFields: Field[] = [ { @@ -310,7 +307,9 @@ describe('Group Edges', () => { }); it('should update valid group edge ids', async () => { - await store.dispatch(groupEdgesWithAggregation(graphIndex) as any); + await store.dispatch( + GraphThunk.groupEdgesWithAggregation(graphIndex) as any, + ); store.getActions().forEach((actions) => { const { payload, type } = actions; @@ -323,7 +322,7 @@ describe('Group Edges', () => { }); describe('Group By Types', () => { - const { graphWithGroupEdge } = Constant; + const { graphWithGroupEdge } = Json; const importedGraphState = () => { const store = { @@ -366,7 +365,9 @@ describe('Group Edges', () => { }); it('should compute the correct grouped edges', async () => { - await store.dispatch(groupEdgesWithAggregation(graphIndex) as any); + await store.dispatch( + GraphThunk.groupEdgesWithAggregation(graphIndex) as any, + ); store.getActions().forEach((actions) => { const { payload, type } = actions; @@ -379,7 +380,9 @@ describe('Group Edges', () => { }); it('should derive correct group edge configuration', async () => { - await store.dispatch(groupEdgesWithAggregation(graphIndex) as any); + await store.dispatch( + GraphThunk.groupEdgesWithAggregation(graphIndex) as any, + ); store.getActions().forEach((actions) => { const { payload, type } = actions; @@ -393,7 +396,9 @@ describe('Group Edges', () => { }); it('should update valid group edge ids', async () => { - await store.dispatch(groupEdgesWithAggregation(graphIndex) as any); + await store.dispatch( + GraphThunk.groupEdgesWithAggregation(graphIndex) as any, + ); store.getActions().forEach((actions) => { const { payload, type } = actions; @@ -407,7 +412,7 @@ describe('Group Edges', () => { }); describe('computeEdgeSelection', () => { - const { sampleGraphFlatten } = Constant; + const { sampleGraphFlatten } = Json; const aggregatedEdgeFields: Field[] = [ { name: 'id', @@ -556,7 +561,7 @@ describe('Group Edges', () => { const store = mockStore(importedGraphState()); beforeEach(() => { - store.dispatch(computeEdgeSelection() as any); + store.dispatch(GraphThunk.computeEdgeSelection() as any); }); afterEach(() => { @@ -582,7 +587,9 @@ describe('Group Edges', () => { }; }); - const expectedActions = [overwriteEdgeSelection(computedEdgeSelection)]; + const expectedActions = [ + GraphSlice.overwriteEdgeSelection(computedEdgeSelection), + ]; expect(store.getActions()).toEqual(expectedActions); }); diff --git a/packages/motif/src/redux/graph/tests/import-edge-list.test.tsx b/packages/motif/src/redux/graph/tests/import-edge-list.test.tsx index 773cf3e0a..eb34efadc 100644 --- a/packages/motif/src/redux/graph/tests/import-edge-list.test.tsx +++ b/packages/motif/src/redux/graph/tests/import-edge-list.test.tsx @@ -5,16 +5,10 @@ import { render } from '@testing-library/react'; import React from 'react'; import { ToasterContainer } from 'baseui/toast'; import { addQuery, initialState, processGraphResponse } from '../slice'; -import { - clearError, - closeModal, - fetchBegin, - fetchDone, - updateToast, -} from '../../ui/slice'; +import * as UISlices from '../../ui/slice'; import { resetState } from '../../import/fileUpload/slice'; -import * as Constant from './constants/positive'; +import * as EdgeListCsv from './constants/positive/edgeListCsv'; import { importEdgeListCsv } from '../processors/import'; import { importEdgeListData } from '../thunk'; import { combineGraphs } from '../../../utils/graph-utils/utils'; @@ -47,7 +41,7 @@ describe('Import Edge List Data', () => { it('should import header with quotes successfully', async (done) => { const store = mockStore(getStore()); - const importDataArr = [Constant.quotesHeaderCsv]; + const importDataArr = [EdgeListCsv.quotesHeaderCsv]; const groupEdgeToggle = false; // processes @@ -66,17 +60,17 @@ describe('Import Edge List Data', () => { Object.assign(graphData.metadata.groupEdges, groupEdgeConfig); const expectedActions = [ - fetchBegin(), + UISlices.fetchBegin(), addQuery([graphData]), processGraphResponse({ data: graphData, accessors: initialState.accessors, }), - updateToast('toast-0'), + UISlices.updateToast('toast-0'), resetState(), - clearError(), - fetchDone(), - closeModal(), + UISlices.clearError(), + UISlices.fetchDone(), + UISlices.closeModal(), ]; // assertions @@ -101,8 +95,8 @@ describe('Import Edge List Data', () => { // input const importDataArr = [ - Constant.firstEdgeListCsv, - Constant.secondEdgeListCsv, + EdgeListCsv.firstEdgeListCsv, + EdgeListCsv.secondEdgeListCsv, ]; const groupEdgeToggle = false; @@ -127,18 +121,18 @@ describe('Import Edge List Data', () => { // expected results const expectedActions = [ - fetchBegin(), + UISlices.fetchBegin(), addQuery([firstGraphData, secondGraphData]), processGraphResponse({ data: mergedGraph, accessors: initialState.accessors, }), - updateToast('toast-0'), + UISlices.updateToast('toast-0'), resetState(), - clearError(), - fetchDone(), - closeModal(), + UISlices.clearError(), + UISlices.fetchDone(), + UISlices.closeModal(), ]; // assertions diff --git a/packages/motif/src/redux/graph/tests/import-json.test.tsx b/packages/motif/src/redux/graph/tests/import-json.test.tsx index abdd8b71e..33c62a795 100644 --- a/packages/motif/src/redux/graph/tests/import-json.test.tsx +++ b/packages/motif/src/redux/graph/tests/import-json.test.tsx @@ -6,30 +6,19 @@ import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import cloneDeep from 'lodash/cloneDeep'; import flatten from 'lodash/flatten'; -import { - initialState, - addQuery, - processGraphResponse, - updateStyleOption, -} from '../slice'; import { importJson } from '../processors/import'; import { Accessors } from '../types'; - -import * as Constant from './constants/positive'; +import * as Json from './constants/positive/json'; import { importJsonData } from '../thunk'; -import { - clearError, - fetchBegin, - fetchDone, - updateToast, - closeModal, -} from '../../ui/slice'; import { resetState } from '../../import/fileUpload/slice'; import { combineGraphs } from '../../../utils/graph-utils/utils'; +import * as GraphSlice from '../slice'; +import * as UiSlice from '../../ui/slice'; +import * as GraphThunk from '../thunk'; const mockStore = configureStore([thunk]); const getStore = () => { - const graphState = cloneDeep(initialState); + const graphState = cloneDeep(GraphSlice.initialState); const store = { investigate: { ui: {}, @@ -53,521 +42,630 @@ describe('Import JSON', () => { store.clearActions(); }); - it('should process custom accessors with numeric values accurately', async (done) => { - const importDataArr = [Constant.simpleGraphThree]; - const groupEdgeToggle = false; - - const customAccessors: Accessors = { - nodeID: 'custom_id', - edgeID: 'id', - edgeSource: 'custom_source', - edgeTarget: 'custom_target', - }; - - // processes - const batchDataPromises = importDataArr.map((graphData) => { - const { data } = graphData; - return importJson(data as any, customAccessors, groupEdgeToggle); - }); + describe('Normal Import', () => { + it('should determine whether graph possess duplicate connectivity', async (done) => { + // arrange + const importDataArr = [Json.simpleGraphWithGroupEdge]; + const groupEdgeToggle = false; + + // act + const batchDataPromises = importDataArr.map((graphData) => { + const { data } = graphData; + return importJson( + data, + GraphSlice.initialState.accessors, + groupEdgeToggle, + ); + }); + + const graphDataArr = await Promise.all(batchDataPromises); + const [firstGraphData] = flatten(graphDataArr); + + // group edge configuration arrangements + const groupEdgeConfig = { + availability: true, + toggle: groupEdgeToggle, + }; + firstGraphData.metadata.groupEdges = groupEdgeConfig; + + // expected results + const expectedActions = [ + UiSlice.fetchBegin(), + GraphSlice.addQuery([firstGraphData]), + GraphSlice.processGraphResponse({ + data: firstGraphData, + accessors: GraphSlice.initialState.accessors, + }), + UiSlice.updateToast('toast-0'), + resetState(), + UiSlice.clearError(), + UiSlice.fetchDone(), + UiSlice.closeModal(), + ]; + + const executions = GraphThunk.importJsonData( + importDataArr as any, + groupEdgeToggle, + GraphSlice.initialState.accessors, + ) as any; - const graphDataArr = await Promise.all(batchDataPromises); - const [graphData] = flatten(graphDataArr); - - // group edge configuration arrangements - const groupEdgeConfig = { availability: false, toggle: groupEdgeToggle }; - Object.assign(graphData.metadata.groupEdges, groupEdgeConfig); - - const expectedActions = [ - fetchBegin(), - addQuery([graphData]), - processGraphResponse({ - data: graphData, - accessors: customAccessors, - }), - updateToast('toast-0'), - resetState(), - clearError(), - fetchDone(), - closeModal(), - ]; - - return store - .dispatch( - importJsonData(importDataArr as any, groupEdgeToggle, customAccessors), - ) - .then(() => { + // assertions + return store.dispatch(executions).then(() => { setTimeout(() => { expect(store.getActions()).toEqual(expectedActions); done(); }, 50); }); - }); - - it('should receive array of importData and process graph responses accurately', async (done) => { - // input - const importDataArr = [Constant.jsonDataOne, Constant.jsonDataTwo]; - const groupEdgeToggle = false; + }); + it('should process custom accessors with numeric values accurately', async (done) => { + const importDataArr = [Json.simpleGraphThree]; + const groupEdgeToggle = false; + + const customAccessors: Accessors = { + nodeID: 'custom_id', + edgeID: 'id', + edgeSource: 'custom_source', + edgeTarget: 'custom_target', + }; + + // processes + const batchDataPromises = importDataArr.map((graphData) => { + const { data } = graphData; + return importJson(data as any, customAccessors, groupEdgeToggle); + }); - // processes - const batchDataPromises = importDataArr.map((graphData) => { - const { data } = graphData; - return importJson(data as any, initialState.accessors, groupEdgeToggle); + const graphDataArr = await Promise.all(batchDataPromises); + const [graphData] = flatten(graphDataArr); + + // group edge configuration arrangements + const groupEdgeConfig = { availability: false, toggle: groupEdgeToggle }; + Object.assign(graphData.metadata.groupEdges, groupEdgeConfig); + + const expectedActions = [ + UiSlice.fetchBegin(), + GraphSlice.addQuery([graphData]), + GraphSlice.processGraphResponse({ + data: graphData, + accessors: customAccessors, + }), + UiSlice.updateToast('toast-0'), + resetState(), + UiSlice.clearError(), + UiSlice.fetchDone(), + UiSlice.closeModal(), + ]; + + return store + .dispatch( + importJsonData( + importDataArr as any, + groupEdgeToggle, + customAccessors, + ), + ) + .then(() => { + setTimeout(() => { + expect(store.getActions()).toEqual(expectedActions); + done(); + }, 50); + }); }); - const graphDataArr = await Promise.all(batchDataPromises); - const [firstGraphData, secondGraphData] = flatten(graphDataArr); - - // group edge configuration arrangements - const groupEdgeConfig = { availability: false, toggle: groupEdgeToggle }; - Object.assign(firstGraphData.metadata.groupEdges, groupEdgeConfig); - Object.assign(secondGraphData.metadata.groupEdges, groupEdgeConfig); - - const combinedGraph = combineGraphs([firstGraphData, secondGraphData]); - - // expected results - const expectedActions = [ - fetchBegin(), - addQuery([firstGraphData, secondGraphData]), - processGraphResponse({ - data: combinedGraph, - accessors: initialState.accessors, - }), - updateToast('toast-0'), - resetState(), - clearError(), - fetchDone(), - closeModal(), - ]; - - // assertions - return store - .dispatch( - importJsonData( - importDataArr as any, + it('should receive array of importData and process graph responses accurately', async (done) => { + // input + const importDataArr = [Json.jsonDataOne, Json.jsonDataTwo]; + const groupEdgeToggle = false; + + // processes + const batchDataPromises = importDataArr.map((graphData) => { + const { data } = graphData; + return importJson( + data as any, + GraphSlice.initialState.accessors, groupEdgeToggle, - initialState.accessors, - ), - ) - .then(() => { - setTimeout(() => { - expect(store.getActions()).toEqual(expectedActions); - done(); - }, 50); + ); }); - }); - it('should display different success message in toast with filter applied', async (done) => { - const currentStore = getStore(); - Object.assign(currentStore.investigate.graph.present.filterOptions, { - value: 'something', + const graphDataArr = await Promise.all(batchDataPromises); + const [firstGraphData, secondGraphData] = flatten(graphDataArr); + + // group edge configuration arrangements + const groupEdgeConfig = { availability: false, toggle: groupEdgeToggle }; + Object.assign(firstGraphData.metadata.groupEdges, groupEdgeConfig); + Object.assign(secondGraphData.metadata.groupEdges, groupEdgeConfig); + + const combinedGraph = combineGraphs([firstGraphData, secondGraphData]); + + // expected results + const expectedActions = [ + UiSlice.fetchBegin(), + GraphSlice.addQuery([firstGraphData, secondGraphData]), + GraphSlice.processGraphResponse({ + data: combinedGraph, + accessors: GraphSlice.initialState.accessors, + }), + UiSlice.updateToast('toast-0'), + resetState(), + UiSlice.clearError(), + UiSlice.fetchDone(), + UiSlice.closeModal(), + ]; + + // assertions + return store + .dispatch( + importJsonData( + importDataArr as any, + groupEdgeToggle, + GraphSlice.initialState.accessors, + ), + ) + .then(() => { + setTimeout(() => { + expect(store.getActions()).toEqual(expectedActions); + done(); + }, 50); + }); }); - const modifiedStore = mockStore(currentStore) as any; + it('should display different success message in toast with existing filter applied', async (done) => { + const currentStore = getStore(); + Object.assign(currentStore.investigate.graph.present.filterOptions, { + value: 'something', + }); - // input - const importDataArr = [Constant.jsonDataOne, Constant.jsonDataTwo]; - const groupEdgeToggle = false; + const modifiedStore = mockStore(currentStore) as any; - // processes - const batchDataPromises = importDataArr.map((graphData) => { - const { data } = graphData; - return importJson(data as any, initialState.accessors, groupEdgeToggle); - }); + // input + const importDataArr = [Json.jsonDataOne, Json.jsonDataTwo]; + const groupEdgeToggle = false; - const graphDataArr = await Promise.all(batchDataPromises); - const [firstGraphData, secondGraphData] = flatten(graphDataArr); - - // group edge configuration arrangements - const groupEdgeConfig = { availability: false, toggle: groupEdgeToggle }; - firstGraphData.metadata.groupEdges = groupEdgeConfig; - secondGraphData.metadata.groupEdges = groupEdgeConfig; - - const mergedGraph = combineGraphs([firstGraphData, secondGraphData]); - - // expected results - const expectedActions = [ - fetchBegin(), - addQuery([firstGraphData, secondGraphData]), - processGraphResponse({ - data: mergedGraph, - accessors: initialState.accessors, - }), - updateToast('toast-0'), - resetState(), - clearError(), - fetchDone(), - closeModal(), - ]; - - // @ts-ignore - return modifiedStore - .dispatch( - importJsonData( - importDataArr as any, + // processes + const batchDataPromises = importDataArr.map((graphData) => { + const { data } = graphData; + return importJson( + data as any, + GraphSlice.initialState.accessors, groupEdgeToggle, - initialState.accessors, - ), - ) - .then(() => { - setTimeout(() => { - expect(modifiedStore.getActions()).toEqual(expectedActions); - done(); - }, 300); + ); }); - }); - it('should overwrite styles with the last file', async (done) => { - // input - const importDataArr = [Constant.jsonDataOne, Constant.jsonDataTwo]; - let { styleOptions } = initialState; - const groupEdgeToggle = false; + const graphDataArr = await Promise.all(batchDataPromises); + const [firstGraphData, secondGraphData] = flatten(graphDataArr); + + // group edge configuration arrangements + const groupEdgeConfig = { availability: false, toggle: groupEdgeToggle }; + firstGraphData.metadata.groupEdges = groupEdgeConfig; + secondGraphData.metadata.groupEdges = groupEdgeConfig; + + const mergedGraph = combineGraphs([firstGraphData, secondGraphData]); + + // expected results + const expectedActions = [ + UiSlice.fetchBegin(), + GraphSlice.addQuery([firstGraphData, secondGraphData]), + GraphSlice.processGraphResponse({ + data: mergedGraph, + accessors: GraphSlice.initialState.accessors, + }), + UiSlice.updateToast('toast-0'), + resetState(), + UiSlice.clearError(), + UiSlice.fetchDone(), + UiSlice.closeModal(), + ]; + + // @ts-ignore + return modifiedStore + .dispatch( + importJsonData( + importDataArr as any, + groupEdgeToggle, + GraphSlice.initialState.accessors, + ), + ) + .then(() => { + setTimeout(() => { + expect(modifiedStore.getActions()).toEqual(expectedActions); + done(); + }, 300); + }); + }); - // processes - const batchDataPromises = importDataArr.map((graphData) => { - const { data, style } = graphData as any; + it('should import successfully without metadata', async (done) => { + const graphWithoutMetadata = Json.simpleGraphOne; + delete graphWithoutMetadata.data.metadata; + const groupEdgeToggle = false; - if (style) { - styleOptions = style; - } + const importDataArr = [graphWithoutMetadata]; - return importJson(data, initialState.accessors, groupEdgeToggle); - }); + // processes + const batchDataPromises = importDataArr.map((graphData) => { + const { data } = graphData; + return importJson( + data as any, + GraphSlice.initialState.accessors, + groupEdgeToggle, + ); + }); - const graphDataArr = await Promise.all(batchDataPromises); - const [firstGraphData, secondGraphData] = flatten(graphDataArr); - - // group edge configuration arrangements - const groupEdgeConfig = { availability: false, toggle: groupEdgeToggle }; - firstGraphData.metadata.groupEdges = groupEdgeConfig; - secondGraphData.metadata.groupEdges = groupEdgeConfig; - - const mergedGraph = combineGraphs([firstGraphData, secondGraphData]); - - // expected results - const expectedActions = [ - fetchBegin(), - updateStyleOption(styleOptions), - addQuery([firstGraphData, secondGraphData]), - processGraphResponse({ - data: mergedGraph, - accessors: initialState.accessors, - }), - updateToast('toast-0'), - resetState(), - clearError(), - fetchDone(), - closeModal(), - ]; - - // assertions - return store - .dispatch( + const graphDataArr = await Promise.all(batchDataPromises); + const [graphData] = flatten(graphDataArr); + + // group edge configuration arrangements + const groupEdgeConfig = { + availability: false, + toggle: groupEdgeToggle, + }; + graphData.metadata.groupEdges = groupEdgeConfig; + + // expected results + const expectedActions = [ + UiSlice.fetchBegin(), + GraphSlice.addQuery([graphData]), + GraphSlice.processGraphResponse({ + data: graphData, + accessors: GraphSlice.initialState.accessors, + }), + UiSlice.updateToast('toast-0'), + resetState(), + UiSlice.clearError(), + UiSlice.fetchDone(), + UiSlice.closeModal(), + ]; + + // assertions + await store.dispatch( importJsonData( importDataArr as any, groupEdgeToggle, - initialState.accessors, - true, + GraphSlice.initialState.accessors, ), - ) - .then(() => { - setTimeout(() => { - expect(store.getActions()).toEqual(expectedActions); - done(); - }, 300); - }); - }); + ); - it('should import successfully without metadata', async (done) => { - const graphWithoutMetadata = Constant.simpleGraphOne; - delete graphWithoutMetadata.data.metadata; - const groupEdgeToggle = false; + setTimeout(() => { + expect(store.getActions()).toEqual(expectedActions); + done(); + }, 300); + }); - const importDataArr = [graphWithoutMetadata]; + it('should import with single file contains two graph lists', async (done) => { + // input + const importDataArr = [Json.simpleGraphTwo]; + const groupEdgeToggle = false; + + // processes + const batchDataPromises = importDataArr.map((graphData) => { + const { data } = graphData; + return importJson( + data, + GraphSlice.initialState.accessors, + groupEdgeToggle, + ); + }); - // processes - const batchDataPromises = importDataArr.map((graphData) => { - const { data } = graphData; - return importJson(data as any, initialState.accessors, groupEdgeToggle); + const graphDataArr = await Promise.all(batchDataPromises); + const [firstGraphData, secondGraphData] = flatten(graphDataArr); + + // group edge configuration arrangements + const groupEdgeConfig = { + availability: false, + toggle: groupEdgeToggle, + }; + firstGraphData.metadata.groupEdges = groupEdgeConfig; + secondGraphData.metadata.groupEdges = groupEdgeConfig; + + const mergedGraph = combineGraphs([firstGraphData, secondGraphData]); + + // expected results + const expectedActions = [ + UiSlice.fetchBegin(), + GraphSlice.addQuery([firstGraphData, secondGraphData]), + GraphSlice.processGraphResponse({ + data: mergedGraph, + accessors: GraphSlice.initialState.accessors, + }), + + UiSlice.updateToast('toast-0'), + resetState(), + UiSlice.clearError(), + UiSlice.fetchDone(), + UiSlice.closeModal(), + ]; + + // assertions + return store + .dispatch( + importJsonData( + importDataArr as any, + groupEdgeToggle, + GraphSlice.initialState.accessors, + ), + ) + .then(() => { + setTimeout(() => { + expect(store.getActions()).toEqual(expectedActions); + done(); + }, 300); + }); }); - const graphDataArr = await Promise.all(batchDataPromises); - const [graphData] = flatten(graphDataArr); - - // group edge configuration arrangements - const groupEdgeConfig = { - availability: false, - toggle: groupEdgeToggle, - }; - graphData.metadata.groupEdges = groupEdgeConfig; - - // expected results - const expectedActions = [ - fetchBegin(), - addQuery([graphData]), - processGraphResponse({ - data: graphData, - accessors: initialState.accessors, - }), - updateToast('toast-0'), - resetState(), - clearError(), - fetchDone(), - closeModal(), - ]; - - // assertions - await store.dispatch( - importJsonData( - importDataArr as any, - groupEdgeToggle, - initialState.accessors, - ), - ); - - setTimeout(() => { - expect(store.getActions()).toEqual(expectedActions); - done(); - }, 300); - }); - - it('should not overwrite styles if data has no style', async (done) => { - // input - const jsonOneWithoutStyle = { - data: Constant.jsonDataOne.data, - }; + it('should import with two files contain three graph lists', async (done) => { + // input + const importDataArr = [Json.simpleGraphOne, Json.simpleGraphTwo]; + const groupEdgeToggle = false; + + // processes + const batchDataPromises = importDataArr.map((graphData) => { + const { data } = graphData; + return importJson( + data, + GraphSlice.initialState.accessors, + groupEdgeToggle, + ); + }); - const jsonTwoWithoutStyle = { - data: Constant.jsonDataTwo.data, - }; + const graphDataArr = await Promise.all(batchDataPromises); + const [firstGraphData, secondGraphData, thirdGraphData] = + flatten(graphDataArr); + + // group edge configuration arrangements + const groupEdgeConfig = { + availability: false, + toggle: groupEdgeToggle, + }; + firstGraphData.metadata.groupEdges = groupEdgeConfig; + secondGraphData.metadata.groupEdges = groupEdgeConfig; + thirdGraphData.metadata.groupEdges = groupEdgeConfig; + + const mergedGraph = combineGraphs([ + firstGraphData, + secondGraphData, + thirdGraphData, + ]); + + // expected results + const expectedActions = [ + UiSlice.fetchBegin(), + GraphSlice.addQuery([firstGraphData, secondGraphData, thirdGraphData]), + GraphSlice.processGraphResponse({ + data: mergedGraph, + accessors: GraphSlice.initialState.accessors, + }), + UiSlice.updateToast('toast-0'), + resetState(), + UiSlice.clearError(), + UiSlice.fetchDone(), + UiSlice.closeModal(), + ]; + + // assertions + return store + .dispatch( + importJsonData( + importDataArr as any, + groupEdgeToggle, + GraphSlice.initialState.accessors, + ), + ) + .then(() => { + setTimeout(() => { + expect(store.getActions()).toEqual(expectedActions); + done(); + }, 300); + }); + }); + }); - const groupEdgeToggle = false; + describe('Style Override Imports', () => { + it('should overwrite styles with the last file', async (done) => { + // input + const importDataArr = [Json.jsonDataOne, Json.jsonDataTwo]; + let { styleOptions } = GraphSlice.initialState; + const groupEdgeToggle = false; - const importDataArr = [jsonOneWithoutStyle, jsonTwoWithoutStyle]; - let { styleOptions } = initialState; + // processes + const batchDataPromises = importDataArr.map((graphData) => { + const { data, style } = graphData as any; - // processes - const batchDataPromises = importDataArr.map((graphData) => { - const { data, style } = graphData as any; + if (style) { + styleOptions = style; + } - if (style) { - styleOptions = style; - } + return importJson( + data, + GraphSlice.initialState.accessors, + groupEdgeToggle, + ); + }); - return importJson(data, initialState.accessors, groupEdgeToggle); + const graphDataArr = await Promise.all(batchDataPromises); + const [firstGraphData, secondGraphData] = flatten(graphDataArr); + + // group edge configuration arrangements + const groupEdgeConfig = { availability: false, toggle: groupEdgeToggle }; + firstGraphData.metadata.groupEdges = groupEdgeConfig; + secondGraphData.metadata.groupEdges = groupEdgeConfig; + + const mergedGraph = combineGraphs([firstGraphData, secondGraphData]); + + // expected results + const expectedActions = [ + UiSlice.fetchBegin(), + GraphSlice.updateStyleOption(styleOptions), + GraphSlice.addQuery([firstGraphData, secondGraphData]), + GraphSlice.processGraphResponse({ + data: mergedGraph, + accessors: GraphSlice.initialState.accessors, + }), + UiSlice.updateToast('toast-0'), + resetState(), + UiSlice.clearError(), + UiSlice.fetchDone(), + UiSlice.closeModal(), + ]; + + // assertions + return store + .dispatch( + importJsonData( + importDataArr as any, + groupEdgeToggle, + GraphSlice.initialState.accessors, + true, + ), + ) + .then(() => { + setTimeout(() => { + expect(store.getActions()).toEqual(expectedActions); + done(); + }, 300); + }); }); - const graphDataArr = await Promise.all(batchDataPromises); - const [firstGraphData, secondGraphData] = flatten(graphDataArr); - - // group edge configuration arrangements - const groupEdgeConfig = { availability: false, toggle: groupEdgeToggle }; - firstGraphData.metadata.groupEdges = groupEdgeConfig; - secondGraphData.metadata.groupEdges = groupEdgeConfig; - - const mergedGraph = combineGraphs([firstGraphData, secondGraphData]); - - // expected results - const expectedActions = [ - fetchBegin(), - addQuery([firstGraphData, secondGraphData]), - processGraphResponse({ - data: mergedGraph, - accessors: initialState.accessors, - }), - updateToast('toast-0'), - resetState(), - clearError(), - fetchDone(), - closeModal(), - ]; - - // assertions - return store - .dispatch( - importJsonData( - importDataArr as any, - groupEdgeToggle, - initialState.accessors, - true, - ), - ) - .then(() => { - setTimeout(() => { - expect(store.getActions()).toEqual(expectedActions); - done(); - }, 300); - }); - }); + it('should not overwrite styles if data has no style', async (done) => { + // input + const jsonOneWithoutStyle = { + data: Json.jsonDataOne.data, + }; - it('should import with single file contains two graph lists', async (done) => { - // input - const importDataArr = [Constant.simpleGraphTwo]; - const groupEdgeToggle = false; + const jsonTwoWithoutStyle = { + data: Json.jsonDataTwo.data, + }; - // processes - const batchDataPromises = importDataArr.map((graphData) => { - const { data } = graphData; - return importJson(data, initialState.accessors, groupEdgeToggle); - }); + const groupEdgeToggle = false; - const graphDataArr = await Promise.all(batchDataPromises); - const [firstGraphData, secondGraphData] = flatten(graphDataArr); - - // group edge configuration arrangements - const groupEdgeConfig = { - availability: false, - toggle: groupEdgeToggle, - }; - firstGraphData.metadata.groupEdges = groupEdgeConfig; - secondGraphData.metadata.groupEdges = groupEdgeConfig; - - const mergedGraph = combineGraphs([firstGraphData, secondGraphData]); - - // expected results - const expectedActions = [ - fetchBegin(), - addQuery([firstGraphData, secondGraphData]), - processGraphResponse({ - data: mergedGraph, - accessors: initialState.accessors, - }), - - updateToast('toast-0'), - resetState(), - clearError(), - fetchDone(), - closeModal(), - ]; - - // assertions - return store - .dispatch( - importJsonData( - importDataArr as any, - groupEdgeToggle, - initialState.accessors, - ), - ) - .then(() => { - setTimeout(() => { - expect(store.getActions()).toEqual(expectedActions); - done(); - }, 300); - }); - }); + const importDataArr = [jsonOneWithoutStyle, jsonTwoWithoutStyle]; + // eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental + let { styleOptions } = GraphSlice.initialState; - it('should import with two files contain three graph lists', async (done) => { - // input - const importDataArr = [Constant.simpleGraphOne, Constant.simpleGraphTwo]; - const groupEdgeToggle = false; + // processes + const batchDataPromises = importDataArr.map((graphData) => { + const { data, style } = graphData as any; - // processes - const batchDataPromises = importDataArr.map((graphData) => { - const { data } = graphData; - return importJson(data, initialState.accessors, groupEdgeToggle); - }); + if (style) { + styleOptions = style; + } - const graphDataArr = await Promise.all(batchDataPromises); - const [firstGraphData, secondGraphData, thirdGraphData] = - flatten(graphDataArr); - - // group edge configuration arrangements - const groupEdgeConfig = { - availability: false, - toggle: groupEdgeToggle, - }; - firstGraphData.metadata.groupEdges = groupEdgeConfig; - secondGraphData.metadata.groupEdges = groupEdgeConfig; - thirdGraphData.metadata.groupEdges = groupEdgeConfig; - - const mergedGraph = combineGraphs([ - firstGraphData, - secondGraphData, - thirdGraphData, - ]); - - // expected results - const expectedActions = [ - fetchBegin(), - addQuery([firstGraphData, secondGraphData, thirdGraphData]), - processGraphResponse({ - data: mergedGraph, - accessors: initialState.accessors, - }), - updateToast('toast-0'), - resetState(), - clearError(), - fetchDone(), - closeModal(), - ]; - - // assertions - return store - .dispatch( - importJsonData( - importDataArr as any, + return importJson( + data, + GraphSlice.initialState.accessors, groupEdgeToggle, - initialState.accessors, - ), - ) - .then(() => { - setTimeout(() => { - expect(store.getActions()).toEqual(expectedActions); - done(); - }, 300); + ); }); + + const graphDataArr = await Promise.all(batchDataPromises); + const [firstGraphData, secondGraphData] = flatten(graphDataArr); + + // group edge configuration arrangements + const groupEdgeConfig = { availability: false, toggle: groupEdgeToggle }; + firstGraphData.metadata.groupEdges = groupEdgeConfig; + secondGraphData.metadata.groupEdges = groupEdgeConfig; + + const mergedGraph = combineGraphs([firstGraphData, secondGraphData]); + + // expected results + const expectedActions = [ + UiSlice.fetchBegin(), + GraphSlice.addQuery([firstGraphData, secondGraphData]), + GraphSlice.processGraphResponse({ + data: mergedGraph, + accessors: GraphSlice.initialState.accessors, + }), + UiSlice.updateToast('toast-0'), + resetState(), + UiSlice.clearError(), + UiSlice.fetchDone(), + UiSlice.closeModal(), + ]; + + // assertions + return store + .dispatch( + importJsonData( + importDataArr as any, + groupEdgeToggle, + GraphSlice.initialState.accessors, + true, + ), + ) + .then(() => { + setTimeout(() => { + expect(store.getActions()).toEqual(expectedActions); + done(); + }, 300); + }); + }); }); - it('should determine whether graph possess duplicate connectivity', async (done) => { - // arrange - const importDataArr = [Constant.simpleGraphWithGroupEdge]; - const groupEdgeToggle = false; + describe('Import With Filters', () => { + it('should override both filter and style attributes', async (done) => { + const groupEdgeToggle = false; + const importDataArr = [Json.graphWithFilter]; + // eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental + let { styleOptions, filterOptions } = GraphSlice.initialState; - // act - const batchDataPromises = importDataArr.map((graphData) => { - const { data } = graphData; - return importJson(data, initialState.accessors, groupEdgeToggle); - }); + // processes + const batchDataPromises = importDataArr.map((graphData) => { + const { data, style, filter } = graphData as any; - const graphDataArr = await Promise.all(batchDataPromises); - const [firstGraphData] = flatten(graphDataArr); - - // group edge configuration arrangements - const groupEdgeConfig = { - availability: true, - toggle: groupEdgeToggle, - }; - firstGraphData.metadata.groupEdges = groupEdgeConfig; - - // expected results - const expectedActions = [ - fetchBegin(), - addQuery([firstGraphData]), - processGraphResponse({ - data: firstGraphData, - accessors: initialState.accessors, - }), - updateToast('toast-0'), - resetState(), - clearError(), - fetchDone(), - closeModal(), - ]; - - // assertions - return store - .dispatch( - importJsonData( - importDataArr as any, + if (style) { + styleOptions = style; + } + + if (filter) { + filterOptions = filter; + } + + return importJson( + data, + GraphSlice.initialState.accessors, groupEdgeToggle, - initialState.accessors, - ), - ) - .then(() => { - setTimeout(() => { - expect(store.getActions()).toEqual(expectedActions); - done(); - }, 50); + ); }); + + const graphDataArr = await Promise.all(batchDataPromises); + const [firstGraphData] = flatten(graphDataArr); + + // group edge configuration arrangements + const groupEdgeConfig = { availability: false, toggle: groupEdgeToggle }; + firstGraphData.metadata.groupEdges = groupEdgeConfig; + + const mergedGraph = combineGraphs([firstGraphData]); + + // expected results + const expectedActions = [ + UiSlice.fetchBegin(), + GraphSlice.updateStyleOption(styleOptions), + GraphSlice.updateFilterOption(filterOptions), + GraphSlice.addQuery([firstGraphData]), + GraphSlice.processGraphResponse({ + data: mergedGraph, + accessors: GraphSlice.initialState.accessors, + }), + UiSlice.updateToast('toast-0'), + resetState(), + UiSlice.clearError(), + UiSlice.fetchDone(), + UiSlice.closeModal(), + ]; + + // assertions + return store + .dispatch( + importJsonData( + importDataArr as any, + groupEdgeToggle, + GraphSlice.initialState.accessors, + true, + ), + ) + .then(() => { + setTimeout(() => { + expect(store.getActions()).toEqual(expectedActions); + done(); + }, 300); + }); + }); }); }); diff --git a/packages/motif/src/redux/graph/tests/import-node-edge.test.tsx b/packages/motif/src/redux/graph/tests/import-node-edge.test.tsx index 5c4572ca6..8cb3e1b80 100644 --- a/packages/motif/src/redux/graph/tests/import-node-edge.test.tsx +++ b/packages/motif/src/redux/graph/tests/import-node-edge.test.tsx @@ -5,10 +5,10 @@ import cloneDeep from 'lodash/cloneDeep'; import { render } from '@testing-library/react'; import { ToasterContainer } from 'baseui/toast'; import React from 'react'; -import { addQuery, initialState, processGraphResponse } from '../slice'; +import * as GraphSlices from '../slice'; import { resetState } from '../../import/fileUpload/slice'; -import * as Constant from './constants/positive'; +import * as NodeEdgeCsv from './constants/positive/nodeEdgeCsv'; import { TFileContent } from '../../import/fileUpload'; import { importNodeEdgeCsv } from '../processors/import'; import { closeModal, fetchBegin, fetchDone, updateToast } from '../../ui/slice'; @@ -16,7 +16,7 @@ import { importNodeEdgeData } from '../thunk'; const mockStore = configureStore([thunk]); const getStore = () => { - const graphState = cloneDeep(initialState); + const graphState = cloneDeep(GraphSlices.initialState); const store = { investigate: { ui: {}, @@ -41,8 +41,8 @@ describe('Import Node Edge Data', () => { }); it('should receive nodeCsv and edgeCsv as array and process graph responses accurately', async (done) => { const store = mockStore(getStore()); - const { nodeCsv, edgeCsv } = Constant.sampleNodeEdgeData; - const { accessors } = initialState; + const { nodeCsv, edgeCsv } = NodeEdgeCsv.sampleNodeEdgeData; + const { accessors } = GraphSlices.initialState; const metadataKey = '123'; const groupEdgeToggle = false; @@ -70,8 +70,8 @@ describe('Import Node Edge Data', () => { const expectedActions = [ fetchBegin(), - addQuery([data]), - processGraphResponse({ + GraphSlices.addQuery([data]), + GraphSlices.processGraphResponse({ data, accessors, }), @@ -84,7 +84,7 @@ describe('Import Node Edge Data', () => { return store .dispatch( importNodeEdgeData( - Constant.sampleNodeEdgeData, + NodeEdgeCsv.sampleNodeEdgeData, groupEdgeToggle, accessors, metadataKey, @@ -100,8 +100,8 @@ describe('Import Node Edge Data', () => { it('should receive nodeCsv and edgeCsv as array and process graph responses accurately', async (done) => { const store = mockStore(getStore()); - const { nodeCsv, edgeCsv } = Constant.sampleNodeEdgeData; - const { accessors } = initialState; + const { nodeCsv, edgeCsv } = NodeEdgeCsv.sampleNodeEdgeData; + const { accessors } = GraphSlices.initialState; const metadataKey = '123'; const groupEdgeToggle = false; @@ -129,8 +129,8 @@ describe('Import Node Edge Data', () => { const expectedActions = [ fetchBegin(), - addQuery([data]), - processGraphResponse({ + GraphSlices.addQuery([data]), + GraphSlices.processGraphResponse({ data, accessors, }), @@ -143,7 +143,7 @@ describe('Import Node Edge Data', () => { return store .dispatch( importNodeEdgeData( - Constant.sampleNodeEdgeData, + NodeEdgeCsv.sampleNodeEdgeData, groupEdgeToggle, accessors, metadataKey, @@ -159,8 +159,8 @@ describe('Import Node Edge Data', () => { it('should import source and target with whitespace successfully', async (done) => { const store = mockStore(getStore()); - const { nodeCsv, edgeCsv } = Constant.whitespaceNodeEdge; - const { accessors } = initialState; + const { nodeCsv, edgeCsv } = NodeEdgeCsv.whitespaceNodeEdge; + const { accessors } = GraphSlices.initialState; const metadataKey = '123'; const groupEdgeToggle = false; @@ -185,8 +185,8 @@ describe('Import Node Edge Data', () => { const expectedActions = [ fetchBegin(), - addQuery([data]), - processGraphResponse({ + GraphSlices.addQuery([data]), + GraphSlices.processGraphResponse({ data, accessors, }), @@ -199,7 +199,7 @@ describe('Import Node Edge Data', () => { return store .dispatch( importNodeEdgeData( - Constant.whitespaceNodeEdge, + NodeEdgeCsv.whitespaceNodeEdge, groupEdgeToggle, accessors, metadataKey, @@ -215,7 +215,7 @@ describe('Import Node Edge Data', () => { it('should process numeric custom nodeID, edgeSource and edgeTarget successfully', async (done) => { const store = mockStore(getStore()); - const { nodeCsv, edgeCsv } = Constant.numericAccessorsNodeEdge; + const { nodeCsv, edgeCsv } = NodeEdgeCsv.numericAccessorsNodeEdge; const accessors = { nodeID: 'custom_id', edgeID: 'id', @@ -247,8 +247,8 @@ describe('Import Node Edge Data', () => { const expectedActions = [ fetchBegin(), - addQuery([data]), - processGraphResponse({ + GraphSlices.addQuery([data]), + GraphSlices.processGraphResponse({ data, accessors, }), @@ -261,7 +261,7 @@ describe('Import Node Edge Data', () => { return store .dispatch( importNodeEdgeData( - Constant.numericAccessorsNodeEdge, + NodeEdgeCsv.numericAccessorsNodeEdge, groupEdgeToggle, accessors, metadataKey, diff --git a/packages/motif/src/redux/graph/thunk.ts b/packages/motif/src/redux/graph/thunk.ts index 3c17ef5c1..04b64decf 100644 --- a/packages/motif/src/redux/graph/thunk.ts +++ b/packages/motif/src/redux/graph/thunk.ts @@ -2,14 +2,7 @@ import { flatten, isEmpty, cloneDeep, uniqBy } from 'lodash'; import { combineGraphs } from '../../utils/graph-utils/utils'; import { getFilterOptions, getGraph, getStyleOptions } from './selectors'; -import { - updateGraphFlatten, - addQuery, - processGraphResponse, - updateStyleOption, - overwriteEdgeSelection, - updateGroupEdgeIds, -} from './slice'; +import * as GraphSlices from './slice'; import { importEdgeListCsv, importNodeEdgeCsv, @@ -55,11 +48,11 @@ const processResponse = ( }, ); - dispatch(addQuery(modifiedGraphList)); + dispatch(GraphSlices.addQuery(modifiedGraphList)); const mergedGraph = combineGraphs(groupedEdgeGraphList); // combine new graph data with existing graph data to form graph flattens. - dispatch(processGraphResponse({ data: mergedGraph, accessors })); + dispatch(GraphSlices.processGraphResponse({ data: mergedGraph, accessors })); }; /** @@ -159,21 +152,28 @@ export const importJsonData = ? mainAccessors : (importAccessors as ImportAccessors); - const filterOptions: T.FilterOptions = getFilterOptions(getState()); - let isDataPossessStyle = false; + let filterOptions: T.FilterOptions = getFilterOptions(getState()); let styleOptions: T.StyleOptions = getStyleOptions(getState()); + let isDataPossessStyle = false; + let isDataPossessFilter = false; const batchDataPromises = importData.map((graphData: T.JsonImport) => { const { data: dataWithStyle } = graphData as T.TLoadFormat; if (dataWithStyle) { - const { style: importStyleOption } = graphData as T.TLoadFormat; + const { style: importStyleOption, filter: importFilterOption } = + graphData as T.TLoadFormat; if (importStyleOption) { isDataPossessStyle = true; styleOptions = importStyleOption; } + if (importFilterOption) { + isDataPossessFilter = true; + filterOptions = importFilterOption; + } + return importJson(dataWithStyle as T.GraphList, accessors, groupEdges); } @@ -192,7 +192,11 @@ export const importJsonData = const graphData: T.GraphList = flatten(graphDataArr); if (isDataPossessStyle && overwriteStyles) { - dispatch(updateStyleOption(styleOptions)); + dispatch(GraphSlices.updateStyleOption(styleOptions)); + } + + if (isDataPossessFilter) { + dispatch(GraphSlices.updateFilterOption(filterOptions)); } processResponse(dispatch, accessors, graphData); @@ -361,8 +365,8 @@ export const groupEdgesWithAggregation = edges: uniqueEdgeFields, }); - dispatch(updateGraphFlatten(modData)); - dispatch(updateGroupEdgeIds({ graphIndex, groupEdgeIds })); + dispatch(GraphSlices.updateGraphFlatten(modData)); + dispatch(GraphSlices.updateGroupEdgeIds({ graphIndex, groupEdgeIds })); }; /** @@ -393,5 +397,5 @@ export const computeEdgeSelection = }, ); - dispatch(overwriteEdgeSelection(computedEdgeSelection)); + dispatch(GraphSlices.overwriteEdgeSelection(computedEdgeSelection)); }; diff --git a/packages/motif/src/redux/graph/types.ts b/packages/motif/src/redux/graph/types.ts index 3e3dcf2f9..649f51a44 100644 --- a/packages/motif/src/redux/graph/types.ts +++ b/packages/motif/src/redux/graph/types.ts @@ -299,6 +299,7 @@ export interface TLoadFormat { data: GraphList; // not necessary to enforce overwrite styles. style?: StyleOptions; + filter?: FilterOptions; } export type NodePosParams = NodePositions & {