From 7dc9730c8b77f684bcadf488fa670217356c6647 Mon Sep 17 00:00:00 2001 From: Mason Fish Date: Tue, 1 Dec 2020 09:36:09 -0800 Subject: [PATCH] QueryLibrary model and reducer (#1239) * add QueryLibrary reducer Signed-off-by: Mason Fish * add uncle test Signed-off-by: Mason Fish * adjust pr comments Signed-off-by: Mason Fish * change queryLibrary to queries Signed-off-by: Mason Fish Co-authored-by: Mason Fish --- src/js/state/Queries/actions.ts | 38 ++++++ src/js/state/Queries/index.ts | 9 ++ src/js/state/Queries/initial.ts | 9 ++ src/js/state/Queries/reducer.ts | 85 ++++++++++++ src/js/state/Queries/selectors.ts | 6 + src/js/state/Queries/test.ts | 215 ++++++++++++++++++++++++++++++ src/js/state/Queries/types.ts | 50 +++++++ src/js/state/rootReducer.ts | 4 +- src/js/state/types.ts | 2 + 9 files changed, 417 insertions(+), 1 deletion(-) create mode 100644 src/js/state/Queries/actions.ts create mode 100644 src/js/state/Queries/index.ts create mode 100644 src/js/state/Queries/initial.ts create mode 100644 src/js/state/Queries/reducer.ts create mode 100644 src/js/state/Queries/selectors.ts create mode 100644 src/js/state/Queries/test.ts create mode 100644 src/js/state/Queries/types.ts diff --git a/src/js/state/Queries/actions.ts b/src/js/state/Queries/actions.ts new file mode 100644 index 0000000000..eb4a392e15 --- /dev/null +++ b/src/js/state/Queries/actions.ts @@ -0,0 +1,38 @@ +import { + Group, + QUERIES_ADD_ITEM, + QUERIES_EDIT_ITEM, + QUERIES_MOVE_ITEM, + QUERIES_REMOVE_ITEM, + QUERIES_SET_ALL, + Query +} from "./types" + +export default { + setAll: (rootGroup: Group): QUERIES_SET_ALL => ({ + type: "QUERIES_SET_ALL", + rootGroup + }), + addItem: (item: Query | Group, groupPath: number[]): QUERIES_ADD_ITEM => ({ + type: "QUERIES_ADD_ITEM", + item, + groupPath + }), + removeItem: (itemPath: number[]): QUERIES_REMOVE_ITEM => ({ + type: "QUERIES_REMOVE_ITEM", + itemPath + }), + editItem: (item: Query | Group, itemPath: number[]): QUERIES_EDIT_ITEM => ({ + type: "QUERIES_EDIT_ITEM", + item, + itemPath + }), + moveItem: ( + srcItemPath: number[], + destItemPath: number[] + ): QUERIES_MOVE_ITEM => ({ + type: "QUERIES_MOVE_ITEM", + srcItemPath, + destItemPath + }) +} diff --git a/src/js/state/Queries/index.ts b/src/js/state/Queries/index.ts new file mode 100644 index 0000000000..7093d5b6a3 --- /dev/null +++ b/src/js/state/Queries/index.ts @@ -0,0 +1,9 @@ +import actions from "./actions" +import reducer from "./reducer" +import selectors from "./selectors" + +export default { + ...actions, + ...selectors, + reducer +} diff --git a/src/js/state/Queries/initial.ts b/src/js/state/Queries/initial.ts new file mode 100644 index 0000000000..08bb570e2a --- /dev/null +++ b/src/js/state/Queries/initial.ts @@ -0,0 +1,9 @@ +import {QueriesState} from "./types" + +const init = (): QueriesState => ({ + id: "root", + name: "root", + items: [] +}) + +export default init diff --git a/src/js/state/Queries/reducer.ts b/src/js/state/Queries/reducer.ts new file mode 100644 index 0000000000..e9c6e81d10 --- /dev/null +++ b/src/js/state/Queries/reducer.ts @@ -0,0 +1,85 @@ +import {Group, Query, QueriesAction, QueriesState} from "./types" +import produce from "immer" +import {get, set, initial, last, isEqual} from "lodash" +import init from "./initial" + +export default produce((draft: QueriesState, action: QueriesAction) => { + switch (action.type) { + case "QUERIES_SET_ALL": + return action.rootGroup + case "QUERIES_ADD_ITEM": + addItemToGroup(draft, action.groupPath, action.item) + return + case "QUERIES_REMOVE_ITEM": + removeItemFromGroup(draft, action.itemPath) + return + case "QUERIES_EDIT_ITEM": + if (!get(draft, toItemPath(action.itemPath), null)) return + + set(draft, toItemPath(action.itemPath), action.item) + return + case "QUERIES_MOVE_ITEM": + moveItem(draft, action.srcItemPath, action.destItemPath) + return + } +}, init()) + +const toItemPath = (path: number[]): string => + path.map((pathNdx) => `items[${pathNdx}]`).join(".") + +const addItemToGroup = ( + draft: QueriesState, + groupPath: number[], + item: Query | Group, + index?: number +): void => { + const parentGroup = get(draft, toItemPath(groupPath), null) + if (!parentGroup) return + + if (typeof index === "undefined") { + parentGroup.items.push(item) + return + } + + parentGroup.items.splice(index, 0, item) +} + +const removeItemFromGroup = (draft: QueriesState, itemPath: number[]): void => { + const parentGroup = get(draft, toItemPath(initial(itemPath)), null) + if (!parentGroup) return + + parentGroup.items.splice(last(itemPath), 1) +} + +const moveItem = ( + draft: QueriesState, + srcItemPath: number[], + destItemPath: number[] +): void => { + const srcItem = get(draft, toItemPath(srcItemPath), null) + + if (!srcItem) return + if (!get(draft, toItemPath(initial(destItemPath)), null)) return + + // If the move is all in the same directory then the adjusting indices can + // cause an off by one issue since the destination index will be affected after + // removal (e.g. an item cannot be moved to the end of its current group because of this). + // For this situation we instead remove the item first, and then insert its copy + if (isEqual(initial(srcItemPath), initial(destItemPath))) { + removeItemFromGroup(draft, srcItemPath) + addItemToGroup( + draft, + initial(destItemPath), + {...srcItem}, + last(destItemPath) + ) + } else { + addItemToGroup( + draft, + initial(destItemPath), + {...srcItem}, + last(destItemPath) + ) + removeItemFromGroup(draft, srcItemPath) + } +} diff --git a/src/js/state/Queries/selectors.ts b/src/js/state/Queries/selectors.ts new file mode 100644 index 0000000000..9714ecd3e4 --- /dev/null +++ b/src/js/state/Queries/selectors.ts @@ -0,0 +1,6 @@ +import {QueriesState} from "./types" +import {State} from "../types" + +export default { + getRaw: (state: State): QueriesState => state.queries +} diff --git a/src/js/state/Queries/test.ts b/src/js/state/Queries/test.ts new file mode 100644 index 0000000000..4ea88051f9 --- /dev/null +++ b/src/js/state/Queries/test.ts @@ -0,0 +1,215 @@ +import initTestStore from "../../test/initTestStore" +import Queries from "./" +import {Group} from "./types" +import get from "lodash/get" +import {State} from "../types" + +let store +beforeEach(() => { + store = initTestStore() +}) + +const testLib = { + id: "root", + name: "root", + items: [ + { + // .items[0] + id: "testId1", + name: "testName1", + items: [ + { + // .items[0].items[0] + id: "testId2", + name: "testName2", + description: "testDescription2", + zql: "testValue2", + tags: ["testTag1", "testTag2"] + }, + { + // .items[0].items[1] + id: "testId3", + name: "testName3", + items: [ + { + // .items[0].items[1].items[0] + id: "testId4", + name: "testName4", + description: "testDescription4", + zql: "testValue4", + tags: ["testTag2"] + } + ] + }, + { + // .items[0].items[2] + id: "testId5", + name: "testName5", + description: "testDescription5", + zql: "testValue5", + tags: ["testTag1"] + } + ] + } + ] +} + +const newQuery = { + id: "newQueryId", + name: "newQueryName", + description: "newQueryDescription", + zql: "newQueryValue", + tags: [] +} + +const newGroup = { + id: "newGroupId", + name: "newGroupName", + items: [] +} + +const getGroup = (state: State, path: number[]): Group => { + return get( + Queries.getRaw(state), + path.map((pathNdx) => `items[${pathNdx}]`).join(".") + ) +} + +test("set all", () => { + store.dispatch(Queries.setAll(testLib)) + + const state = store.getState() + + expect(Queries.getRaw(state)).toEqual(testLib) +}) + +test("add query", () => { + store.dispatch(Queries.setAll(testLib)) + + expect(getGroup(store.getState(), [0]).items).toHaveLength(3) + + store.dispatch(Queries.addItem(newQuery, [0])) + + expect(getGroup(store.getState(), [0]).items).toHaveLength(4) + expect(getGroup(store.getState(), [0]).items[3]).toEqual(newQuery) +}) + +test("add query, nested", () => { + store.dispatch(Queries.setAll(testLib)) + + expect(getGroup(store.getState(), [0, 1]).items).toHaveLength(1) + + store.dispatch(Queries.addItem(newQuery, [0, 1])) + + expect(getGroup(store.getState(), [0, 1]).items).toHaveLength(2) + expect(getGroup(store.getState(), [0, 1]).items[1]).toEqual(newQuery) +}) + +test("add group, add query to new group", () => { + store.dispatch(Queries.setAll(testLib)) + + expect(getGroup(store.getState(), [0]).items).toHaveLength(3) + + store.dispatch(Queries.addItem(newGroup, [0])) + + expect(getGroup(store.getState(), [0]).items).toHaveLength(4) + expect(getGroup(store.getState(), [0]).items[3]).toEqual(newGroup) + expect(getGroup(store.getState(), [0, 3]).items).toHaveLength(0) + + store.dispatch(Queries.addItem(newQuery, [0, 3])) + + expect(getGroup(store.getState(), [0, 3]).items).toHaveLength(1) + expect(getGroup(store.getState(), [0, 3]).items[0]).toEqual(newQuery) +}) + +test("remove query, group", () => { + store.dispatch(Queries.setAll(testLib)) + + const testName1Group = getGroup(store.getState(), [0]).items + expect(testName1Group).toHaveLength(3) + + store.dispatch(Queries.removeItem([0, 0])) + + expect(getGroup(store.getState(), [0]).items).toHaveLength(2) + expect(getGroup(store.getState(), [0]).items).toEqual(testName1Group.slice(1)) + + store.dispatch(Queries.removeItem([0, 0])) + expect(getGroup(store.getState(), [0]).items).toHaveLength(1) + expect(getGroup(store.getState(), [0]).items).toEqual([testName1Group[2]]) +}) + +test("move query, same group, different group same depth", () => { + store.dispatch(Queries.setAll(testLib)) + + const testName1Group = getGroup(store.getState(), [0]).items + expect(testName1Group).toHaveLength(3) + + const testName2Query = testName1Group[0] + + // move to end + store.dispatch(Queries.moveItem([0, 0], [0, 2])) + + expect(getGroup(store.getState(), [0]).items).toHaveLength(3) + + expect(getGroup(store.getState(), [0]).items).toEqual([ + ...testName1Group.slice(1), + testName2Query + ]) + + // move back to beginning + store.dispatch(Queries.moveItem([0, 2], [0, 0])) + + expect(getGroup(store.getState(), [0]).items).toHaveLength(3) + expect(getGroup(store.getState(), [0]).items).toEqual(testName1Group) + + // move to "uncle's" group + store.dispatch(Queries.addItem(newGroup, [0])) + + expect(getGroup(store.getState(), [0, 1]).items).toHaveLength(1) + expect(getGroup(store.getState(), [0, 3]).items).toHaveLength(0) + + const testName4Query = getGroup(store.getState(), [0, 1]).items[0] + + store.dispatch(Queries.moveItem([0, 1, 0], [0, 3, 0])) + + expect(getGroup(store.getState(), [0, 1]).items).toHaveLength(0) + expect(getGroup(store.getState(), [0, 3]).items).toHaveLength(1) + expect(getGroup(store.getState(), [0, 3]).items[0]).toEqual(testName4Query) +}) + +test("move query, different group", () => { + store.dispatch(Queries.setAll(testLib)) + + const testName1Group = getGroup(store.getState(), [0]).items + const testName3Group = (testName1Group[1] as Group).items + + expect(testName1Group).toHaveLength(3) + expect(testName3Group).toHaveLength(1) + + const testName2Query = testName1Group[0] + + store.dispatch(Queries.moveItem([0, 0], [0, 1, 0])) + + const newTestName1Group = getGroup(store.getState(), [0]).items + const newTestName3Group = (newTestName1Group[0] as Group).items + + expect(newTestName1Group).toHaveLength(2) + expect(newTestName3Group).toHaveLength(2) + + expect(newTestName1Group[0].id).toEqual(testName1Group[1].id) + expect(newTestName3Group).toEqual([testName2Query, ...testName3Group]) +}) + +test("edit query", () => { + store.dispatch(Queries.setAll(testLib)) + + store.dispatch(Queries.editItem(newQuery, [0, 0])) + expect(getGroup(store.getState(), [0]).items[0]).toEqual(newQuery) +}) + +test("edit group", () => { + store.dispatch(Queries.setAll(testLib)) + + store.dispatch(Queries.editItem(newGroup, [0, 1])) + expect(getGroup(store.getState(), [0, 1])).toEqual(newGroup) +}) diff --git a/src/js/state/Queries/types.ts b/src/js/state/Queries/types.ts new file mode 100644 index 0000000000..583271cca2 --- /dev/null +++ b/src/js/state/Queries/types.ts @@ -0,0 +1,50 @@ +export type QueriesState = Group + +export interface Query { + id: string + name: string + zql: string + description: string + tags: string[] +} + +export interface Group { + id: string + name: string + items: (Group | Query)[] +} + +export type QueriesAction = + | QUERIES_SET_ALL + | QUERIES_ADD_ITEM + | QUERIES_REMOVE_ITEM + | QUERIES_EDIT_ITEM + | QUERIES_MOVE_ITEM + +export interface QUERIES_SET_ALL { + type: "QUERIES_SET_ALL" + rootGroup: Group +} + +export interface QUERIES_ADD_ITEM { + type: "QUERIES_ADD_ITEM" + item: Query | Group + groupPath: number[] +} + +export interface QUERIES_REMOVE_ITEM { + type: "QUERIES_REMOVE_ITEM" + itemPath: number[] +} + +export interface QUERIES_EDIT_ITEM { + type: "QUERIES_EDIT_ITEM" + item: Query | Group + itemPath: number[] +} + +export interface QUERIES_MOVE_ITEM { + type: "QUERIES_MOVE_ITEM" + srcItemPath: number[] + destItemPath: number[] +} diff --git a/src/js/state/rootReducer.ts b/src/js/state/rootReducer.ts index 3157a13cbc..b8d2731b4a 100644 --- a/src/js/state/rootReducer.ts +++ b/src/js/state/rootReducer.ts @@ -12,6 +12,7 @@ import Prefs from "./Prefs" import Spaces from "./Spaces" import Tabs from "./Tabs" import View from "./View" +import Queries from "./Queries" export default combineReducers({ errors: Errors.reducer, @@ -25,5 +26,6 @@ export default combineReducers({ spaces: Spaces.reducer, packets: Packets.reducer, prefs: Prefs.reducer, - connectionStatuses: ConnectionStatuses.reducer + connectionStatuses: ConnectionStatuses.reducer, + queries: Queries.reducer }) diff --git a/src/js/state/types.ts b/src/js/state/types.ts index a19663c858..106d193640 100644 --- a/src/js/state/types.ts +++ b/src/js/state/types.ts @@ -13,6 +13,7 @@ import {TabsState} from "./Tabs/types" import {ViewState} from "./View/types" import {createZealot, Zealot} from "zealot" import {ConnectionStatusesState} from "./ConnectionStatuses/types" +import {QueriesState} from "./Queries/types" export type GetState = () => State export type ThunkExtraArg = { @@ -41,4 +42,5 @@ export type State = { packets: PacketsState prefs: PrefsState connectionStatuses: ConnectionStatusesState + queries: QueriesState }