From 323c720ff223a19c7fc9560c8774d5399d60ee59 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 25 Apr 2023 13:49:32 +0200 Subject: [PATCH 01/93] Start prototyping editor v2 --- frontend/src/js/app/RightPane.tsx | 44 +++++- frontend/src/js/editor-v2/EditorV2.tsx | 211 +++++++++++++++++++++++++ frontend/src/js/user/userSettings.ts | 2 + frontend/src/localization/de.json | 4 +- 4 files changed, 252 insertions(+), 9 deletions(-) create mode 100644 frontend/src/js/editor-v2/EditorV2.tsx diff --git a/frontend/src/js/app/RightPane.tsx b/frontend/src/js/app/RightPane.tsx index 801662bf27..3f44ec36ce 100644 --- a/frontend/src/js/app/RightPane.tsx +++ b/frontend/src/js/app/RightPane.tsx @@ -1,13 +1,16 @@ import styled from "@emotion/styled"; -import { useMemo } from "react"; +import { useCallback, useMemo, useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; +import { EditorV2 } from "../editor-v2/EditorV2"; import FormsTab from "../external-forms/FormsTab"; import Pane from "../pane/Pane"; import { TabNavigationTab } from "../pane/TabNavigation"; import StandardQueryEditorTab from "../standard-query-editor/StandardQueryEditorTab"; import TimebasedQueryEditorTab from "../timebased-query-editor/TimebasedQueryEditorTab"; +import { getUserSettings, storeUserSettings } from "../user/userSettings"; import type { StateT } from "./reducers"; @@ -23,12 +26,31 @@ const SxPane = styled(Pane)` background-color: ${({ theme }) => theme.col.bgAlt}; `; +const useEditorV2 = () => { + const [showEditorV2, setShowEditorV2] = useState( + getUserSettings().showEditorV2, + ); + + const toggleEditorV2 = useCallback(() => { + setShowEditorV2(!showEditorV2); + storeUserSettings({ showEditorV2: !showEditorV2 }); + }, [showEditorV2]); + + useHotkeys("shift+alt+e", toggleEditorV2, [showEditorV2]); + + return { + showEditorV2, + }; +}; + const RightPane = () => { const { t } = useTranslation(); const activeTab = useSelector( (state) => state.panes.right.activeTab, ); + const { showEditorV2 } = useEditorV2(); + const tabs: TabNavigationTab[] = useMemo( () => [ { @@ -36,18 +58,24 @@ const RightPane = () => { label: t("rightPane.queryEditor"), tooltip: t("help.tabQueryEditor"), }, - { - key: "timebasedQueryEditor", - label: t("rightPane.timebasedQueryEditor"), - tooltip: t("help.tabTimebasedEditor"), - }, + showEditorV2 + ? { + key: "editorV2", + label: t("rightPane.editorV2"), + tooltip: t("help.tabEditorV2"), + } + : { + key: "timebasedQueryEditor", + label: t("rightPane.timebasedQueryEditor"), + tooltip: t("help.tabTimebasedEditor"), + }, { key: "externalForms", label: t("rightPane.externalForms"), tooltip: t("help.tabFormEditor"), }, ], - [t], + [t, showEditorV2], ); return ( @@ -56,7 +84,7 @@ const RightPane = () => { - + {showEditorV2 ? : } diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx new file mode 100644 index 0000000000..2ed57b3a87 --- /dev/null +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -0,0 +1,211 @@ +import styled from "@emotion/styled"; +import { useState } from "react"; + +import { useGetQuery } from "../api/api"; +import { + AndNodeT, + DateRangeT, + DateRestrictionNodeT, + NegationNodeT, + OrNodeT, + QueryConceptNodeT, + SavedQueryNodeT, +} from "../api/types"; +import { DNDType } from "../common/constants/dndTypes"; +import { getConceptById } from "../concept-trees/globalTreeStoreHelper"; +import Dropzone from "../ui-components/Dropzone"; + +const Root = styled("div")` + flex-grow: 1; + height: 100%; + padding: 8px 10px 10px 10px; + overflow: auto; +`; + +const Grid = styled("div")` + display: grid; + grid-gap: 5px; + height: 100%; + width: 100%; + place-items: center; +`; + +const SxDropzone = styled(Dropzone)` + width: 200px; + height: 100px; +`; + +interface Tree { + id: string; + name: string; + negation?: boolean; + dateRestriction?: DateRangeT; + children?: { + connection: "and" | "or" | "time"; + direction: "horizontal" | "vertical"; + items: Tree[]; + }; +} + +const useEditorState = () => { + const [tree, setTree] = useState(undefined); + + const expandNode = ( + queryNode: + | AndNodeT + | DateRestrictionNodeT + | OrNodeT + | NegationNodeT + | QueryConceptNodeT + | SavedQueryNodeT, + config: { + negation?: boolean; + dateRestriction?: DateRangeT; + } = {}, + ): Tree => { + switch (queryNode.type) { + case "AND": + return { + id: "AND", + name: "AND", + ...config, + children: { + connection: "and", + direction: "horizontal", + items: queryNode.children.map((child) => expandNode(child)), + }, + }; + case "OR": + return { + id: "OR", + name: "OR", + ...config, + children: { + connection: "or", + direction: "vertical", + items: queryNode.children.map((child) => expandNode(child)), + }, + }; + case "NEGATION": + return expandNode(queryNode.child, { ...config, negation: true }); + case "DATE_RESTRICTION": + return expandNode(queryNode.child, { + ...config, + dateRestriction: queryNode.dateRange, + }); + case "CONCEPT": + const concept = getConceptById(queryNode.ids[0]); + return { + id: queryNode.ids[0], + name: concept?.label || "Unknown", + ...config, + }; + case "SAVED_QUERY": + return { + id: queryNode.query, + name: queryNode.query, + ...config, + }; + } + }; + + const getQuery = useGetQuery(); + const expandQuery = async (id: string) => { + const query = await getQuery(id); + + if (query && query.query.root.type !== "EXTERNAL_RESOLVED") { + const tree = expandNode(query.query.root); + setTree(tree); + } + }; + + return { + expandQuery, + tree, + }; +}; + +const DROP_TYPES = [ + DNDType.PREVIOUS_QUERY, + DNDType.PREVIOUS_SECONDARY_ID_QUERY, +]; + +export function EditorV2() { + const { tree, expandQuery } = useEditorState(); + + return ( + + + {tree ? ( + + ) : ( + { + expandQuery((droppedItem as any).id); + }} + acceptedDropTypes={DROP_TYPES} + > + {() =>
Drop if you dare
} +
+ )} +
+
+ ); +} + +const Node = styled("div")<{ negated?: boolean; leaf?: boolean }>` + padding: ${({ leaf }) => (leaf ? "5px 10px" : "20px")}; + border: 1px solid + ${({ negated, theme, leaf }) => + negated ? "red" : leaf ? "black" : theme.col.grayMediumLight}; + border-radius: ${({ theme }) => theme.borderRadius}; + width: ${({ leaf }) => (leaf ? "150px" : "inherit")}; +`; + +const Connector = styled("span")` + font-weight: 700; + text-transform: uppercase; + font-size: ${({ theme }) => theme.font.xs}; + color: ${({ theme }) => theme.col.gray}; +`; + +function getGridStyles(tree: Tree) { + if (!tree.children) { + return {}; + } + + if (tree.children.direction === "horizontal") { + return { + gridAutoFlow: "column", + }; + } else { + return { + gridTemplateColumns: "1fr", + }; + } +} + +function TreeNode({ tree }: { tree: Tree }) { + const gridStyles = getGridStyles(tree); + + return ( + + + {!tree.children && tree.name} + {tree.dateRestriction ? JSON.stringify(tree.dateRestriction) : ""} + + {tree.children && ( + + {tree.children.items.map((item, i, items) => ( + <> + + {i < items.length - 1 && ( + {tree.children?.connection} + )} + + ))} + + )} + + ); +} diff --git a/frontend/src/js/user/userSettings.ts b/frontend/src/js/user/userSettings.ts index 302cd9a79c..e9976531ac 100644 --- a/frontend/src/js/user/userSettings.ts +++ b/frontend/src/js/user/userSettings.ts @@ -2,12 +2,14 @@ const localStorage: Storage = window.localStorage; interface UserSettings { + showEditorV2: boolean; arePreviousQueriesFoldersOpen: boolean; preferredDownloadEnding?: string; // Usually CSV or XLSX preferredDownloadLabel?: string; // Label of the preferred Download format (e.g. "All files") } const initialState: UserSettings = { + showEditorV2: false, arePreviousQueriesFoldersOpen: false, preferredDownloadEnding: undefined, preferredDownloadLabel: undefined, diff --git a/frontend/src/localization/de.json b/frontend/src/localization/de.json index 1b5843d915..d328800b9d 100644 --- a/frontend/src/localization/de.json +++ b/frontend/src/localization/de.json @@ -17,7 +17,8 @@ "rightPane": { "queryEditor": "Editor", "timebasedQueryEditor": "Zeit-Editor", - "externalForms": "Formular-Editor" + "externalForms": "Formular-Editor", + "editorV2": "Der neue Editor" }, "conceptTreeList": { "loading": "Lade Konzepte", @@ -442,6 +443,7 @@ "tabQueryEditor": "Hier kann eine Anfrage gestellt werden.", "tabTimebasedEditor": "Hier kann eine Zeit-Anfrage gestellt werden. Das heißt, dass mehrere Anfragen in zeitlichen Bezug gesetzt werden können.", "tabFormEditor": "Hier können Auswertungen und Analysen zu bestehenden Anfragen erstellt werden.", + "tabEditorV2": "Erweiterter Editor. Hier kann eine komplexe Anfrage gestellt werden.", "datasetSelector": "Auswahl des Datensatzes – die jeweiligen Konzepte, Anfragen und Formulare werden geladen.", "excludeTimestamps": "Auswahl führt dazu, dass die Datumswerte des Konzepts bei der Weiterverarbeitung nicht berücksichtigt werden.", "excludeFromSecondaryId": "Auswahl führt dazu, dass dieses Konzept nicht in der Analyse-Ebene berücksichtigt wird, falls eine Analyse-Ebene gewählt wurde.", From f07c97bedd7a520991c3e32387734b175d0f12c6 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 25 Apr 2023 14:35:15 +0200 Subject: [PATCH 02/93] Add dropzones --- frontend/src/js/editor-v2/EditorV2.tsx | 147 +++++++++++++++++++++---- 1 file changed, 125 insertions(+), 22 deletions(-) diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 2ed57b3a87..2567c1a35b 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -13,7 +13,7 @@ import { } from "../api/types"; import { DNDType } from "../common/constants/dndTypes"; import { getConceptById } from "../concept-trees/globalTreeStoreHelper"; -import Dropzone from "../ui-components/Dropzone"; +import Dropzone, { DropzoneProps } from "../ui-components/Dropzone"; const Root = styled("div")` flex-grow: 1; @@ -24,7 +24,7 @@ const Root = styled("div")` const Grid = styled("div")` display: grid; - grid-gap: 5px; + grid-gap: 3px; height: 100%; width: 100%; place-items: center; @@ -137,7 +137,13 @@ export function EditorV2() { {tree ? ( - + ) : ( { @@ -153,8 +159,13 @@ export function EditorV2() { ); } +const NodeContainer = styled("div")` + display: grid; + grid-gap: 5px; +`; + const Node = styled("div")<{ negated?: boolean; leaf?: boolean }>` - padding: ${({ leaf }) => (leaf ? "5px 10px" : "20px")}; + padding: ${({ leaf }) => (leaf ? "5px 10px" : "10px")}; border: 1px solid ${({ negated, theme, leaf }) => negated ? "red" : leaf ? "black" : theme.col.grayMediumLight}; @@ -185,27 +196,119 @@ function getGridStyles(tree: Tree) { } } -function TreeNode({ tree }: { tree: Tree }) { +const InvisibleDropzoneContainer = styled(Dropzone)` + width: 100%; + height: 100%; + border-width: 1px; +`; + +const InvisibleDropzone = ( + props: Omit, "acceptedDropTypes">, +) => { + return ( + + ); +}; + +function TreeNode({ + tree, + droppable, +}: { + tree: Tree; + droppable: { + h: boolean; + v: boolean; + }; +}) { const gridStyles = getGridStyles(tree); return ( - - - {!tree.children && tree.name} - {tree.dateRestriction ? JSON.stringify(tree.dateRestriction) : ""} - - {tree.children && ( - - {tree.children.items.map((item, i, items) => ( - <> - - {i < items.length - 1 && ( - {tree.children?.connection} - )} - - ))} - + + {droppable.v && ( + { + console.log(item); + }} + /> + )} + + + {droppable.h && ( + { + console.log(item); + }} + /> + )} + + + {!tree.children && tree.name} + {tree.dateRestriction ? JSON.stringify(tree.dateRestriction) : ""} + + {tree.children && ( + + { + console.log(item); + }} + /> + {tree.children.items.map((item, i, items) => ( + <> + + {i < items.length - 1 && ( + <> + {tree.children?.connection} + { + console.log(item); + }} + /> + + )} + + ))} + { + console.log(item); + }} + /> + + )} + + {droppable.h && ( + { + console.log(item); + }} + /> + )} + + {droppable.v && ( + { + console.log(item); + }} + /> )} - + ); } From 097bef0a6d35caec5e0e11b770212e1450c35f53 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 25 Apr 2023 14:53:26 +0200 Subject: [PATCH 03/93] Iterate dropzones --- frontend/src/js/editor-v2/EditorV2.tsx | 34 +++++++++++++++------- frontend/src/js/ui-components/Dropzone.tsx | 6 +++- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 2567c1a35b..e77646b1c6 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -1,4 +1,5 @@ import styled from "@emotion/styled"; +import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { useState } from "react"; import { useGetQuery } from "../api/api"; @@ -11,8 +12,10 @@ import { QueryConceptNodeT, SavedQueryNodeT, } from "../api/types"; +import IconButton from "../button/IconButton"; import { DNDType } from "../common/constants/dndTypes"; import { getConceptById } from "../concept-trees/globalTreeStoreHelper"; +import QueryNode from "../standard-query-editor/QueryNode"; import Dropzone, { DropzoneProps } from "../ui-components/Dropzone"; const Root = styled("div")` @@ -119,9 +122,14 @@ const useEditorState = () => { } }; + const onReset = () => { + setTree(undefined); + }; + return { expandQuery, tree, + onReset, }; }; @@ -131,10 +139,11 @@ const DROP_TYPES = [ ]; export function EditorV2() { - const { tree, expandQuery } = useEditorState(); + const { tree, expandQuery, onReset } = useEditorState(); return ( + {tree ? ( { return ( @@ -275,14 +285,16 @@ function TreeNode({ }} /> {i < items.length - 1 && ( - <> - {tree.children?.connection} - { - console.log(item); - }} - /> - + { + console.log(item); + }} + > + {() => {tree.children?.connection}} + )} ))} diff --git a/frontend/src/js/ui-components/Dropzone.tsx b/frontend/src/js/ui-components/Dropzone.tsx index e332607cfb..9662a49891 100644 --- a/frontend/src/js/ui-components/Dropzone.tsx +++ b/frontend/src/js/ui-components/Dropzone.tsx @@ -18,6 +18,7 @@ const Root = styled("div")<{ bare?: boolean; transparent?: boolean; canDrop?: boolean; + invisible?: boolean; }>` border: ${({ theme, isOver, canDrop, naked }) => naked @@ -29,7 +30,7 @@ const Root = styled("div")<{ : `3px dashed ${theme.col.grayMediumLight}`}; border-radius: ${({ theme }) => theme.borderRadius}; padding: ${({ bare }) => (bare ? "0" : "10px")}; - display: flex; + display: ${({ invisible }) => (invisible ? "none" : "flex")}; align-items: center; justify-content: center; background-color: ${({ theme, canDrop, naked, isOver, transparent }) => @@ -61,6 +62,7 @@ export interface DropzoneProps { naked?: boolean; bare?: boolean; transparent?: boolean; + invisible?: boolean; onDrop: (props: DroppableObject, monitor: DropTargetMonitor) => void; canDrop?: (props: DroppableObject, monitor: DropTargetMonitor) => boolean; onClick?: () => void; @@ -103,6 +105,7 @@ const Dropzone = ( canDrop, onDrop, onClick, + invisible, children, }: DropzoneProps, ref?: ForwardedRef, @@ -138,6 +141,7 @@ const Dropzone = ( } }} isOver={isOver} + invisible={invisible && !canDropResult} canDrop={canDropResult} className={className} onClick={onClick} From 9d0992b224af088f24ad200339f0b06185a7ce86 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 25 Apr 2023 16:26:49 +0200 Subject: [PATCH 04/93] Add some ui actions --- frontend/package.json | 1 + frontend/src/js/editor-v2/EditorV2.tsx | 168 ++++++++++++++++++++++--- frontend/yarn.lock | 12 ++ 3 files changed, 166 insertions(+), 15 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 6442e257c9..0e5c7a7c9d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,6 +32,7 @@ "@fortawesome/free-regular-svg-icons": "^6.3.0", "@fortawesome/free-solid-svg-icons": "^6.3.0", "@fortawesome/react-fontawesome": "^0.2.0", + "@paralleldrive/cuid2": "^2.2.0", "@react-keycloak-fork/web": "^4.0.3", "@tippyjs/react": "^4.2.6", "@types/file-saver": "^2.0.5", diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index e77646b1c6..289bcff8b9 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -1,5 +1,10 @@ import styled from "@emotion/styled"; -import { faTrash } from "@fortawesome/free-solid-svg-icons"; +import { + faLeftRight, + faTrash, + faUpDown, +} from "@fortawesome/free-solid-svg-icons"; +import { createId } from "@paralleldrive/cuid2"; import { useState } from "react"; import { useGetQuery } from "../api/api"; @@ -14,8 +19,10 @@ import { } from "../api/types"; import IconButton from "../button/IconButton"; import { DNDType } from "../common/constants/dndTypes"; -import { getConceptById } from "../concept-trees/globalTreeStoreHelper"; -import QueryNode from "../standard-query-editor/QueryNode"; +import { + getConceptById, + setTree, +} from "../concept-trees/globalTreeStoreHelper"; import Dropzone, { DropzoneProps } from "../ui-components/Dropzone"; const Root = styled("div")` @@ -23,9 +30,12 @@ const Root = styled("div")` height: 100%; padding: 8px 10px 10px 10px; overflow: auto; + display: flex; + flex-direction: column; `; const Grid = styled("div")` + flex-grow: 1; display: grid; grid-gap: 3px; height: 100%; @@ -38,11 +48,23 @@ const SxDropzone = styled(Dropzone)` height: 100px; `; +const Actions = styled("div")` + display: flex; + align-items: center; + justify-content: space-between; +`; +const Flex = styled("div")` + display: flex; + align-items: center; +`; + interface Tree { id: string; + parentId?: string; name: string; negation?: boolean; dateRestriction?: DateRangeT; + data?: any; children?: { connection: "and" | "or" | "time"; direction: "horizontal" | "vertical"; @@ -50,8 +72,24 @@ interface Tree { }; } +const findNodeById = (tree: Tree, id: string): Tree | undefined => { + if (tree.id === id) { + return tree; + } + if (tree.children) { + for (const child of tree.children.items) { + const found = findNodeById(child, id); + if (found) { + return found; + } + } + } + return undefined; +}; + const useEditorState = () => { const [tree, setTree] = useState(undefined); + const [selectedNode, setSelectedNode] = useState(undefined); const expandNode = ( queryNode: @@ -62,31 +100,38 @@ const useEditorState = () => { | QueryConceptNodeT | SavedQueryNodeT, config: { + parentId?: string; negation?: boolean; dateRestriction?: DateRangeT; } = {}, ): Tree => { switch (queryNode.type) { case "AND": + const andid = createId(); return { - id: "AND", + id: andid, name: "AND", ...config, children: { connection: "and", direction: "horizontal", - items: queryNode.children.map((child) => expandNode(child)), + items: queryNode.children.map((child) => + expandNode(child, { parentId: andid }), + ), }, }; case "OR": + const orid = createId(); return { - id: "OR", + id: orid, name: "OR", ...config, children: { connection: "or", direction: "vertical", - items: queryNode.children.map((child) => expandNode(child)), + items: queryNode.children.map((child) => + expandNode(child, { parentId: orid }), + ), }, }; case "NEGATION": @@ -99,7 +144,8 @@ const useEditorState = () => { case "CONCEPT": const concept = getConceptById(queryNode.ids[0]); return { - id: queryNode.ids[0], + id: createId(), + data: concept, name: concept?.label || "Unknown", ...config, }; @@ -107,6 +153,7 @@ const useEditorState = () => { return { id: queryNode.query, name: queryNode.query, + data: queryNode, ...config, }; } @@ -129,7 +176,10 @@ const useEditorState = () => { return { expandQuery, tree, + setTree, onReset, + selectedNode, + setSelectedNode, }; }; @@ -139,15 +189,82 @@ const DROP_TYPES = [ ]; export function EditorV2() { - const { tree, expandQuery, onReset } = useEditorState(); + const { tree, setTree, expandQuery, onReset, selectedNode, setSelectedNode } = + useEditorState(); return ( - + + + {selectedNode && ( + <> + { + setTree((tr) => { + if (selectedNode.parentId === undefined) { + return undefined; + } else { + const newTree = JSON.parse(JSON.stringify(tr)); + const parent = findNodeById( + newTree, + selectedNode.parentId, + ); + if (parent?.children) { + parent.children.items = parent.children.items.filter( + (item) => item.id !== selectedNode.id, + ); + } + return newTree; + } + }); + }} + /> + + )} + {selectedNode?.children && ( + <> + { + setTree((tr) => { + const newTree = JSON.parse(JSON.stringify(tr)); + const node = findNodeById(newTree, selectedNode.id); + if (node?.children) { + node.children.direction = "vertical"; + } + + return newTree; + }); + }} + /> + { + setTree((tr) => { + const newTree = JSON.parse(JSON.stringify(tr)); + const node = findNodeById(newTree, selectedNode.id); + if (node?.children) { + node.children.direction = "horizontal"; + } + + return newTree; + }); + }} + /> + + )} + + + {tree ? ( ` +const Node = styled("div")<{ + selected?: boolean; + negated?: boolean; + leaf?: boolean; +}>` padding: ${({ leaf }) => (leaf ? "5px 10px" : "10px")}; border: 1px solid - ${({ negated, theme, leaf }) => - negated ? "red" : leaf ? "black" : theme.col.grayMediumLight}; - border-radius: ${({ theme }) => theme.borderRadius}; + ${({ negated, theme, selected }) => + negated ? "red" : selected ? "black" : theme.col.grayMediumLight}; + border-radius: 5px; width: ${({ leaf }) => (leaf ? "150px" : "inherit")}; + background-color: ${({ selected, theme }) => + selected ? "white" : theme.col.bgAlt}; + cursor: pointer; `; const Connector = styled("span")` @@ -228,12 +352,16 @@ const InvisibleDropzone = ( function TreeNode({ tree, droppable, + selectedNode, + setSelectedNode, }: { tree: Tree; droppable: { h: boolean; v: boolean; }; + selectedNode: Tree | undefined; + setSelectedNode: (node: Tree | undefined) => void; }) { const gridStyles = getGridStyles(tree); @@ -259,7 +387,15 @@ function TreeNode({ }} /> )} - + { + e.stopPropagation(); + setSelectedNode(tree); + }} + > {!tree.children && tree.name} {tree.dateRestriction ? JSON.stringify(tree.dateRestriction) : ""} @@ -275,6 +411,8 @@ function TreeNode({ <> Date: Tue, 25 Apr 2023 16:42:28 +0200 Subject: [PATCH 05/93] Iterate flip / delete --- frontend/src/js/editor-v2/EditorV2.tsx | 87 ++++++++++++-------------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 289bcff8b9..d17137b5cc 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -1,9 +1,5 @@ import styled from "@emotion/styled"; -import { - faLeftRight, - faTrash, - faUpDown, -} from "@fortawesome/free-solid-svg-icons"; +import { faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons"; import { createId } from "@paralleldrive/cuid2"; import { useState } from "react"; @@ -19,10 +15,7 @@ import { } from "../api/types"; import IconButton from "../button/IconButton"; import { DNDType } from "../common/constants/dndTypes"; -import { - getConceptById, - setTree, -} from "../concept-trees/globalTreeStoreHelper"; +import { getConceptById } from "../concept-trees/globalTreeStoreHelper"; import Dropzone, { DropzoneProps } from "../ui-components/Dropzone"; const Root = styled("div")` @@ -107,6 +100,9 @@ const useEditorState = () => { ): Tree => { switch (queryNode.type) { case "AND": + if (queryNode.children.length === 1) { + return expandNode(queryNode.children[0], config); + } const andid = createId(); return { id: andid, @@ -121,6 +117,9 @@ const useEditorState = () => { }, }; case "OR": + if (queryNode.children.length === 1) { + return expandNode(queryNode.children[0], config); + } const orid = createId(); return { id: orid, @@ -219,45 +218,36 @@ export function EditorV2() { } }); }} - /> + > + Delete + )} {selectedNode?.children && ( - <> - { - setTree((tr) => { - const newTree = JSON.parse(JSON.stringify(tr)); - const node = findNodeById(newTree, selectedNode.id); - if (node?.children) { - node.children.direction = "vertical"; - } - - return newTree; - }); - }} - /> - { - setTree((tr) => { - const newTree = JSON.parse(JSON.stringify(tr)); - const node = findNodeById(newTree, selectedNode.id); - if (node?.children) { - node.children.direction = "horizontal"; - } + { + setTree((tr) => { + const newTree = JSON.parse(JSON.stringify(tr)); + const node = findNodeById(newTree, selectedNode.id); + if (node?.children) { + node.children.direction = + node.children.direction === "horizontal" + ? "vertical" + : "horizontal"; + } - return newTree; - }); - }} - /> - + return newTree; + }); + }} + > + Flip + )} - + + Clear + {tree ? ( @@ -299,7 +289,7 @@ const Node = styled("div")<{ border: 1px solid ${({ negated, theme, selected }) => negated ? "red" : selected ? "black" : theme.col.grayMediumLight}; - border-radius: 5px; + border-radius: ${({ theme }) => theme.borderRadius}; width: ${({ leaf }) => (leaf ? "150px" : "inherit")}; background-color: ${({ selected, theme }) => selected ? "white" : theme.col.bgAlt}; @@ -332,7 +322,6 @@ function getGridStyles(tree: Tree) { const InvisibleDropzoneContainer = styled(Dropzone)` width: 100%; height: 100%; - padding: 5px; `; const InvisibleDropzone = ( @@ -349,6 +338,11 @@ const InvisibleDropzone = ( ); }; +const Description = styled("div")` + font-size: ${({ theme }) => theme.font.xs}; + color: ${({ theme }) => theme.col.gray}; +`; + function TreeNode({ tree, droppable, @@ -396,10 +390,11 @@ function TreeNode({ setSelectedNode(tree); }} > - +
{!tree.children && tree.name} + {tree.data?.description} {tree.dateRestriction ? JSON.stringify(tree.dateRestriction) : ""} - +
{tree.children && ( Date: Tue, 25 Apr 2023 16:53:57 +0200 Subject: [PATCH 06/93] Iterate date range --- frontend/src/js/editor-v2/EditorV2.tsx | 31 +++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index d17137b5cc..f9b7a4f6d6 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -294,6 +294,8 @@ const Node = styled("div")<{ background-color: ${({ selected, theme }) => selected ? "white" : theme.col.bgAlt}; cursor: pointer; + display: flex; + flex-direction: column; `; const Connector = styled("span")` @@ -338,11 +340,29 @@ const InvisibleDropzone = ( ); }; +const Name = styled("div")` + font-size: ${({ theme }) => theme.font.sm}; +`; + const Description = styled("div")` font-size: ${({ theme }) => theme.font.xs}; color: ${({ theme }) => theme.col.gray}; `; +const DateRange = styled("div")` + font-size: ${({ theme }) => theme.font.xs}; + color: ${({ theme }) => theme.col.gray}; + font-weight: 700; +`; + +const formatDateRange = (range: DateRangeT): string => { + if (range.min === range.max) { + return range.min || ""; + } else { + return `${range.min} - ${range.max}`; + } +}; + function TreeNode({ tree, droppable, @@ -391,9 +411,13 @@ function TreeNode({ }} >
- {!tree.children && tree.name} - {tree.data?.description} - {tree.dateRestriction ? JSON.stringify(tree.dateRestriction) : ""} + {!tree.children && {tree.name}} + {tree.data?.description && ( + {tree.data?.description} + )} + {tree.dateRestriction && ( + {formatDateRange(tree.dateRestriction)} + )}
{tree.children && ( @@ -421,6 +445,7 @@ function TreeNode({ { console.log(item); From b6cbddac196e474c9b657b59c9ebd85320abbd91 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 25 Apr 2023 16:57:45 +0200 Subject: [PATCH 07/93] Unselect node on outside click --- frontend/src/js/editor-v2/EditorV2.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index f9b7a4f6d6..2570ab1165 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -192,14 +192,19 @@ export function EditorV2() { useEditorState(); return ( - + { + setSelectedNode(undefined); + }} + > {selectedNode && ( <> { + onClick={(e) => { + e.stopPropagation(); setTree((tr) => { if (selectedNode.parentId === undefined) { return undefined; @@ -226,7 +231,8 @@ export function EditorV2() { {selectedNode?.children && ( { + onClick={(e) => { + e.stopPropagation(); setTree((tr) => { const newTree = JSON.parse(JSON.stringify(tr)); const node = findNodeById(newTree, selectedNode.id); From 35f066c276ba6204efa552ab30474ae9ce5c8a5d Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 25 Apr 2023 17:03:23 +0200 Subject: [PATCH 08/93] Fix name --- frontend/src/js/editor-v2/EditorV2.tsx | 31 +++++++++++++------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 2570ab1165..4cf8f134a3 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -25,12 +25,13 @@ const Root = styled("div")` overflow: auto; display: flex; flex-direction: column; + gap: 10px; `; const Grid = styled("div")` flex-grow: 1; display: grid; - grid-gap: 3px; + gap: 3px; height: 100%; width: 100%; place-items: center; @@ -54,7 +55,6 @@ const Flex = styled("div")` interface Tree { id: string; parentId?: string; - name: string; negation?: boolean; dateRestriction?: DateRangeT; data?: any; @@ -106,7 +106,6 @@ const useEditorState = () => { const andid = createId(); return { id: andid, - name: "AND", ...config, children: { connection: "and", @@ -123,7 +122,6 @@ const useEditorState = () => { const orid = createId(); return { id: orid, - name: "OR", ...config, children: { connection: "or", @@ -145,13 +143,11 @@ const useEditorState = () => { return { id: createId(), data: concept, - name: concept?.label || "Unknown", ...config, }; case "SAVED_QUERY": return { id: queryNode.query, - name: queryNode.query, data: queryNode, ...config, }; @@ -283,7 +279,7 @@ export function EditorV2() { const NodeContainer = styled("div")` display: grid; - grid-gap: 5px; + gap: 5px; `; const Node = styled("div")<{ @@ -302,6 +298,7 @@ const Node = styled("div")<{ cursor: pointer; display: flex; flex-direction: column; + gap: 10px; `; const Connector = styled("span")` @@ -416,15 +413,17 @@ function TreeNode({ setSelectedNode(tree); }} > -
- {!tree.children && {tree.name}} - {tree.data?.description && ( - {tree.data?.description} - )} - {tree.dateRestriction && ( - {formatDateRange(tree.dateRestriction)} - )} -
+ {(!tree.children || tree.data || tree.dateRestriction) && ( +
+ {tree.data?.label && {tree.data.label[0]}} + {tree.data?.description && ( + {tree.data?.description} + )} + {tree.dateRestriction && ( + {formatDateRange(tree.dateRestriction)} + )} +
+ )} {tree.children && ( Date: Tue, 25 Apr 2023 17:17:15 +0200 Subject: [PATCH 09/93] Add negation --- frontend/src/js/editor-v2/EditorV2.tsx | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 4cf8f134a3..c5166d76aa 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -1,5 +1,5 @@ import styled from "@emotion/styled"; -import { faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { faBan, faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons"; import { createId } from "@paralleldrive/cuid2"; import { useState } from "react"; @@ -195,6 +195,26 @@ export function EditorV2() { > + {selectedNode && ( + <> + { + e.stopPropagation(); + setTree((tr) => { + const newTree = JSON.parse(JSON.stringify(tr)); + const node = findNodeById(newTree, selectedNode.id); + if (node) { + node.negation = !node.negation; + } + return newTree; + }); + }} + > + Negate + + + )} {selectedNode && ( <> Date: Tue, 2 May 2023 21:26:18 +0200 Subject: [PATCH 10/93] Modularize --- frontend/src/js/app/RightPane.tsx | 6 +- frontend/src/js/editor-v2/DateModal.tsx | 119 ++++++++++ frontend/src/js/editor-v2/EditorV2.tsx | 277 ++++++++++++++++-------- frontend/src/js/editor-v2/types.ts | 14 ++ 4 files changed, 322 insertions(+), 94 deletions(-) create mode 100644 frontend/src/js/editor-v2/DateModal.tsx create mode 100644 frontend/src/js/editor-v2/types.ts diff --git a/frontend/src/js/app/RightPane.tsx b/frontend/src/js/app/RightPane.tsx index 3f44ec36ce..9499e33b25 100644 --- a/frontend/src/js/app/RightPane.tsx +++ b/frontend/src/js/app/RightPane.tsx @@ -84,7 +84,11 @@ const RightPane = () => {
- {showEditorV2 ? : } + {showEditorV2 ? ( + + ) : ( + + )} diff --git a/frontend/src/js/editor-v2/DateModal.tsx b/frontend/src/js/editor-v2/DateModal.tsx new file mode 100644 index 0000000000..4dc1eb9164 --- /dev/null +++ b/frontend/src/js/editor-v2/DateModal.tsx @@ -0,0 +1,119 @@ +import styled from "@emotion/styled"; +import { faUndo } from "@fortawesome/free-solid-svg-icons"; +import { useState, useCallback, useMemo } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; +import { useTranslation } from "react-i18next"; + +import { DateRangeT } from "../api/types"; +import IconButton from "../button/IconButton"; +import { DateStringMinMax } from "../common/helpers/dateHelper"; +import Modal from "../modal/Modal"; +import InputDateRange from "../ui-components/InputDateRange"; + +import { Tree } from "./types"; + +export const useDateEditing = ({ + enabled, + selectedNode, +}: { + enabled: boolean; + selectedNode: Tree | undefined; +}) => { + const [showModal, setShowModal] = useState(false); + + const onClose = useCallback(() => setShowModal(false), []); + const onOpen = useCallback(() => { + if (!enabled) return; + if (!selectedNode) return; + + setShowModal(true); + }, [enabled, selectedNode]); + + useHotkeys("d", onOpen, [onOpen], { + preventDefault: true, + }); + + const headline = useMemo(() => { + if (!selectedNode) return ""; + + return ( + selectedNode.data?.label || + (selectedNode.children?.items || []).map((c) => c.data?.label).join(" ") + ); + }, [selectedNode]); + + return { + showModal, + headline, + onClose, + onOpen, + }; +}; + +const ResetAll = styled(IconButton)` + color: ${({ theme }) => theme.col.blueGrayDark}; + font-weight: 700; + margin-left: 20px; +`; + +export const DateModal = ({ + onClose, + dateRange = {}, + headline, + setDateRange, + onResetDates, +}: { + onClose: () => void; + dateRange?: DateRangeT; + headline: string; + setDateRange: (range: DateRangeT) => void; + onResetDates: () => void; +}) => { + const { t } = useTranslation(); + + useHotkeys("esc", onClose, [onClose]); + + const minDate = dateRange ? dateRange.min || null : null; + const maxDate = dateRange ? dateRange.max || null : null; + const hasActiveDate = !!(minDate || maxDate); + + const labelSuffix = useMemo(() => { + return hasActiveDate ? ( + + {t("queryNodeEditor.reset")} + + ) : null; + }, [t, hasActiveDate, onResetDates]); + + const onChange = useCallback( + (date: DateStringMinMax) => { + setDateRange({ + min: date.min || undefined, + max: date.max || undefined, + }); + }, + [setDateRange], + ); + + return ( + +
{headline}
+ +
+ ); +}; diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index c5166d76aa..342196988a 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -1,7 +1,9 @@ import styled from "@emotion/styled"; +import { faCalendar, faTrashCan } from "@fortawesome/free-regular-svg-icons"; import { faBan, faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons"; import { createId } from "@paralleldrive/cuid2"; -import { useState } from "react"; +import { useCallback, useMemo, useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; import { useGetQuery } from "../api/api"; import { @@ -18,6 +20,9 @@ import { DNDType } from "../common/constants/dndTypes"; import { getConceptById } from "../concept-trees/globalTreeStoreHelper"; import Dropzone, { DropzoneProps } from "../ui-components/Dropzone"; +import { DateModal, useDateEditing } from "./DateModal"; +import { Tree } from "./types"; + const Root = styled("div")` flex-grow: 1; height: 100%; @@ -52,19 +57,6 @@ const Flex = styled("div")` align-items: center; `; -interface Tree { - id: string; - parentId?: string; - negation?: boolean; - dateRestriction?: DateRangeT; - data?: any; - children?: { - connection: "and" | "or" | "time"; - direction: "horizontal" | "vertical"; - items: Tree[]; - }; -} - const findNodeById = (tree: Tree, id: string): Tree | undefined => { if (tree.id === id) { return tree; @@ -82,7 +74,15 @@ const findNodeById = (tree: Tree, id: string): Tree | undefined => { const useEditorState = () => { const [tree, setTree] = useState(undefined); - const [selectedNode, setSelectedNode] = useState(undefined); + const [selectedNodeId, setSelectedNodeId] = useState( + undefined, + ); + const selectedNode = useMemo(() => { + if (!tree || !selectedNodeId) { + return undefined; + } + return findNodeById(tree, selectedNodeId.id); + }, [tree, selectedNodeId]); const expandNode = ( queryNode: @@ -168,13 +168,26 @@ const useEditorState = () => { setTree(undefined); }; + const updateTreeNode = useCallback( + (id: string, update: (node: Tree) => void) => { + const newTree = JSON.parse(JSON.stringify(tree)); + const node = findNodeById(newTree, id); + if (node) { + update(node); + setTree(newTree); + } + }, + [tree], + ); + return { expandQuery, tree, setTree, + updateTreeNode, onReset, selectedNode, - setSelectedNode, + setSelectedNodeId, }; }; @@ -183,89 +196,159 @@ const DROP_TYPES = [ DNDType.PREVIOUS_SECONDARY_ID_QUERY, ]; -export function EditorV2() { - const { tree, setTree, expandQuery, onReset, selectedNode, setSelectedNode } = - useEditorState(); +const useNegationEditing = ({ + selectedNode, + updateTreeNode, + enabled, +}: { + enabled: boolean; + selectedNode: Tree | undefined; + updateTreeNode: (id: string, update: (node: Tree) => void) => void; +}) => { + const onNegateClick = useCallback(() => { + if (!selectedNode || !enabled) return; + + updateTreeNode(selectedNode.id, (node) => { + node.negation = !node.negation; + }); + }, [enabled, selectedNode, updateTreeNode]); + + useHotkeys("n", onNegateClick, [onNegateClick]); + + return { + onNegateClick, + }; +}; + +export function EditorV2({ + featureDates, + featureNegate, +}: { + featureDates: boolean; + featureNegate: boolean; +}) { + const { + tree, + setTree, + updateTreeNode, + expandQuery, + onReset, + selectedNode, + setSelectedNodeId, + } = useEditorState(); + + const onFlip = useCallback(() => { + if (!selectedNode || !selectedNode.children) return; + + updateTreeNode(selectedNode.id, (node) => { + if (!node.children) return; + + node.children.direction = + node.children.direction === "horizontal" ? "vertical" : "horizontal"; + }); + }, [selectedNode, updateTreeNode]); + + const onDelete = useCallback(() => { + if (!selectedNode) return; + + if (selectedNode.parentId === undefined) { + setTree(undefined); + } else { + updateTreeNode(selectedNode.parentId, (parent) => { + if (parent.children) { + parent.children.items = parent.children.items.filter( + (item) => item.id !== selectedNode.id, + ); + } + }); + } + }, [selectedNode, setTree, updateTreeNode]); + + useHotkeys("del", onDelete, [onDelete]); + useHotkeys("backspace", onDelete, [onDelete]); + useHotkeys("f", onFlip, [onFlip]); + + const { showModal, headline, onOpen, onClose } = useDateEditing({ + enabled: featureDates, + selectedNode, + }); + + const { onNegateClick } = useNegationEditing({ + enabled: featureNegate, + selectedNode, + updateTreeNode, + }); return ( { - setSelectedNode(undefined); + if (!selectedNode || showModal) return; + setSelectedNodeId(undefined); }} > + {showModal && selectedNode && ( + + updateTreeNode(selectedNode.id, (node) => { + node.dateRestriction = undefined; + }) + } + setDateRange={(dateRange) => { + updateTreeNode(selectedNode.id, (node) => { + node.dateRestriction = dateRange; + }); + }} + /> + )} - {selectedNode && ( - <> - { - e.stopPropagation(); - setTree((tr) => { - const newTree = JSON.parse(JSON.stringify(tr)); - const node = findNodeById(newTree, selectedNode.id); - if (node) { - node.negation = !node.negation; - } - return newTree; - }); - }} - > - Negate - - + {featureDates && selectedNode && ( + { + e.stopPropagation(); + onOpen(); + }} + > + Dates + )} - {selectedNode && ( - <> - { - e.stopPropagation(); - setTree((tr) => { - if (selectedNode.parentId === undefined) { - return undefined; - } else { - const newTree = JSON.parse(JSON.stringify(tr)); - const parent = findNodeById( - newTree, - selectedNode.parentId, - ); - if (parent?.children) { - parent.children.items = parent.children.items.filter( - (item) => item.id !== selectedNode.id, - ); - } - return newTree; - } - }); - }} - > - Delete - - + {featureNegate && selectedNode && ( + { + e.stopPropagation(); + onNegateClick(); + }} + > + Negate + )} {selectedNode?.children && ( { e.stopPropagation(); - setTree((tr) => { - const newTree = JSON.parse(JSON.stringify(tr)); - const node = findNodeById(newTree, selectedNode.id); - if (node?.children) { - node.children.direction = - node.children.direction === "horizontal" - ? "vertical" - : "horizontal"; - } - - return newTree; - }); + onFlip(); }} > Flip )} + {selectedNode && ( + { + e.stopPropagation(); + onDelete(); + }} + > + Delete + + )} Clear @@ -276,11 +359,8 @@ export function EditorV2() { ) : ( theme.font.xs}; - color: ${({ theme }) => theme.col.gray}; + font-size: ${({ theme }) => theme.font.md}; + color: black; + + border-radius: ${({ theme }) => theme.borderRadius}; + width: 40px; + height: 40px; + display: flex; + justify-content: center; + align-items: center; `; function getGridStyles(tree: Tree) { @@ -365,17 +451,16 @@ const InvisibleDropzone = ( const Name = styled("div")` font-size: ${({ theme }) => theme.font.sm}; + font-weight: 700; `; const Description = styled("div")` font-size: ${({ theme }) => theme.font.xs}; - color: ${({ theme }) => theme.col.gray}; `; const DateRange = styled("div")` font-size: ${({ theme }) => theme.font.xs}; - color: ${({ theme }) => theme.col.gray}; - font-weight: 700; + font-family: monospace; `; const formatDateRange = (range: DateRangeT): string => { @@ -435,18 +520,21 @@ function TreeNode({ > {(!tree.children || tree.data || tree.dateRestriction) && (
+ {tree.dateRestriction && ( + <> + {formatDateRange(tree.dateRestriction)} + + )} {tree.data?.label && {tree.data.label[0]}} {tree.data?.description && ( {tree.data?.description} )} - {tree.dateRestriction && ( - {formatDateRange(tree.dateRestriction)} - )}
)} {tree.children && ( { console.log(item); }} @@ -454,6 +542,7 @@ function TreeNode({ {tree.children.items.map((item, i, items) => ( <> {i < items.length - 1 && ( ))} { console.log(item); }} diff --git a/frontend/src/js/editor-v2/types.ts b/frontend/src/js/editor-v2/types.ts new file mode 100644 index 0000000000..77d8c3f313 --- /dev/null +++ b/frontend/src/js/editor-v2/types.ts @@ -0,0 +1,14 @@ +import { DateRangeT } from "../api/types"; + +export interface Tree { + id: string; + parentId?: string; + negation?: boolean; + dateRestriction?: DateRangeT; + data?: any; + children?: { + connection: "and" | "or" | "time"; + direction: "horizontal" | "vertical"; + items: Tree[]; + }; +} From 927a35c1ba5ef6ce2ef11148a07dc4d7d672578c Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 2 May 2023 21:54:37 +0200 Subject: [PATCH 11/93] Fix delete --- frontend/src/js/editor-v2/EditorV2.tsx | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 342196988a..9cf9f96133 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -255,10 +255,19 @@ export function EditorV2({ setTree(undefined); } else { updateTreeNode(selectedNode.parentId, (parent) => { - if (parent.children) { - parent.children.items = parent.children.items.filter( - (item) => item.id !== selectedNode.id, - ); + if (!parent.children) return; + + parent.children.items = parent.children.items.filter( + (item) => item.id !== selectedNode.id, + ); + + if (parent.children.items.length === 1) { + const child = parent.children.items[0]; + parent.id = child.id; + parent.children = child.children; + parent.data = child.data; + parent.dateRestriction ||= child.dateRestriction; + parent.negation ||= child.negation; } }); } @@ -533,12 +542,7 @@ function TreeNode({ )} {tree.children && ( - { - console.log(item); - }} - /> + {}} /> {tree.children.items.map((item, i, items) => ( <> Date: Tue, 2 May 2023 23:36:46 +0200 Subject: [PATCH 12/93] Support drop and rotate connection --- frontend/src/js/button/IconButton.tsx | 8 +- frontend/src/js/editor-v2/EditorV2.tsx | 166 ++++++++++++++++++++----- frontend/src/js/editor-v2/types.ts | 2 +- 3 files changed, 145 insertions(+), 31 deletions(-) diff --git a/frontend/src/js/button/IconButton.tsx b/frontend/src/js/button/IconButton.tsx index 0ee51dde73..7e333f8f37 100644 --- a/frontend/src/js/button/IconButton.tsx +++ b/frontend/src/js/button/IconButton.tsx @@ -77,6 +77,12 @@ const SxBasicButton = styled(BasicButton)<{ } `; +const Children = styled("span")` + display: flex; + align-items: center; + gap: 5px; +`; + export interface IconButtonPropsT extends BasicButtonProps { iconProps?: IconStyleProps; active?: boolean; @@ -165,7 +171,7 @@ const IconButton = forwardRef( ref={ref} > {iconElement} - {children && {children}} + {children && {children}} ); }, diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 9cf9f96133..343b68d4ec 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -1,6 +1,11 @@ import styled from "@emotion/styled"; import { faCalendar, faTrashCan } from "@fortawesome/free-regular-svg-icons"; -import { faBan, faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { + faBan, + faCircleNodes, + faRefresh, + faTrash, +} from "@fortawesome/free-solid-svg-icons"; import { createId } from "@paralleldrive/cuid2"; import { useCallback, useMemo, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; @@ -27,7 +32,6 @@ const Root = styled("div")` flex-grow: 1; height: 100%; padding: 8px 10px 10px 10px; - overflow: auto; display: flex; flex-direction: column; gap: 10px; @@ -40,6 +44,7 @@ const Grid = styled("div")` height: 100%; width: 100%; place-items: center; + overflow: auto; `; const SxDropzone = styled(Dropzone)` @@ -52,11 +57,18 @@ const Actions = styled("div")` align-items: center; justify-content: space-between; `; + const Flex = styled("div")` display: flex; align-items: center; `; +const SxIconButton = styled(IconButton)` + display: flex; + align-items: center; + gap: 5px; +`; + const findNodeById = (tree: Tree, id: string): Tree | undefined => { if (tree.id === id) { return tree; @@ -220,6 +232,12 @@ const useNegationEditing = ({ }; }; +const CONNECTORS = ["and", "or", "before"] as const; +const getNextConnector = (connector: (typeof CONNECTORS)[number]) => { + const index = CONNECTORS.indexOf(connector); + return CONNECTORS[(index + 1) % CONNECTORS.length]; +}; + export function EditorV2({ featureDates, featureNegate, @@ -273,9 +291,20 @@ export function EditorV2({ } }, [selectedNode, setTree, updateTreeNode]); + const onRotateConnector = useCallback(() => { + if (!selectedNode || !selectedNode.children) return; + + updateTreeNode(selectedNode.id, (node) => { + if (!node.children) return; + + node.children.connection = getNextConnector(node.children.connection); + }); + }, [selectedNode, updateTreeNode]); + useHotkeys("del", onDelete, [onDelete]); useHotkeys("backspace", onDelete, [onDelete]); useHotkeys("f", onFlip, [onFlip]); + useHotkeys("c", onRotateConnector, [onRotateConnector]); const { showModal, headline, onOpen, onClose } = useDateEditing({ enabled: featureDates, @@ -347,6 +376,18 @@ export function EditorV2({ Flip
)} + {selectedNode?.children && ( + { + e.stopPropagation(); + onRotateConnector(); + }} + > + Connected: + {selectedNode.children.connection} + + )} {selectedNode && ( (leaf ? "5px 10px" : "10px")}; border: 1px solid ${({ negated, theme, selected }) => - negated ? "red" : selected ? "black" : theme.col.grayMediumLight}; + negated ? "red" : selected ? theme.col.gray : theme.col.grayMediumLight}; + box-shadow: ${({ selected, theme }) => + selected ? `inset 0px 0px 0px 1px ${theme.col.gray}` : "none"}; + border-radius: ${({ theme }) => theme.borderRadius}; width: ${({ leaf }) => (leaf ? "150px" : "inherit")}; - background-color: ${({ selected, theme }) => - selected ? "white" : theme.col.bgAlt}; + background-color: ${({ leaf, theme }) => (leaf ? "white" : theme.col.bg)}; cursor: pointer; display: flex; flex-direction: column; @@ -412,12 +456,11 @@ const Node = styled("div")<{ const Connector = styled("span")` text-transform: uppercase; - font-size: ${({ theme }) => theme.font.md}; + font-size: ${({ theme }) => theme.font.sm}; color: black; border-radius: ${({ theme }) => theme.borderRadius}; - width: 40px; - height: 40px; + padding: 0px 5px; display: flex; justify-content: center; align-items: center; @@ -450,7 +493,6 @@ const InvisibleDropzone = ( return ( { function TreeNode({ tree, + updateTreeNode, droppable, selectedNode, setSelectedNode, }: { tree: Tree; + updateTreeNode: (id: string, update: (node: Tree) => void) => void; droppable: { h: boolean; v: boolean; @@ -496,16 +540,69 @@ function TreeNode({ }) { const gridStyles = getGridStyles(tree); + const onDropOutsideOfNode = ({ + pos, + direction, + item, + }: { + direction: "h" | "v"; + pos: "b" | "a"; + item: any; + }) => { + console.log("dropped outside of node", { pos, direction, item }); + // Create a new "parent" and create a new "item", make parent contain tree and item + const newParentId = createId(); + const newItemId = createId(); + updateTreeNode(tree.id, (node) => { + const newChildren: Tree[] = [ + { + id: newItemId, + negation: false, + data: item, + parentId: newParentId, + }, + { + id: tree.id, + negation: false, + data: tree.data, + children: tree.children, + parentId: newParentId, + }, + ]; + + node.id = newParentId; + node.data = undefined; + node.children = { + connection: tree.children?.connection === "and" ? "or" : "and" || "and", + direction: direction === "h" ? "horizontal" : "vertical", + items: pos === "b" ? newChildren : newChildren.reverse(), + }; + }); + }; + + const onDropAtChildrenIdx = ({ idx, item }: { idx: number; item: any }) => { + // Create a new "item" and insert it at idx of tree.children + updateTreeNode(tree.id, (node) => { + if (node.children) { + node.children.items.splice(idx, 0, { + id: createId(), + negation: false, + data: item, + parentId: node.id, + }); + } + }); + }; + return ( {droppable.v && ( { - console.log(item); - }} + onDrop={(item) => + onDropOutsideOfNode({ pos: "b", direction: "v", item }) + } /> )} - {droppable.h && ( { - console.log(item); - }} + onDrop={(item) => + onDropOutsideOfNode({ + pos: "b", + direction: "h", + item, + }) + } /> )} - {}} /> + onDropAtChildrenIdx({ idx: 0, item })} + /> {tree.children.items.map((item, i, items) => ( <> { - console.log(item); - }} + onDrop={(item) => + onDropAtChildrenIdx({ idx: i + 1, item }) + } > {() => {tree.children?.connection}} @@ -577,26 +682,29 @@ function TreeNode({ ))} { - console.log(item); - }} + onDrop={(item) => + onDropAtChildrenIdx({ + idx: tree.children!.items.length, + item, + }) + } /> )} {droppable.h && ( { - console.log(item); - }} + onDrop={(item) => + onDropOutsideOfNode({ pos: "a", direction: "h", item }) + } /> )} {droppable.v && ( { - console.log(item); - }} + onDrop={(item) => + onDropOutsideOfNode({ pos: "a", direction: "v", item }) + } /> )} diff --git a/frontend/src/js/editor-v2/types.ts b/frontend/src/js/editor-v2/types.ts index 77d8c3f313..57cb436222 100644 --- a/frontend/src/js/editor-v2/types.ts +++ b/frontend/src/js/editor-v2/types.ts @@ -7,7 +7,7 @@ export interface Tree { dateRestriction?: DateRangeT; data?: any; children?: { - connection: "and" | "or" | "time"; + connection: "and" | "or" | "before"; direction: "horizontal" | "vertical"; items: Tree[]; }; From 1b9b8cf47a44f2b1c3667268d4d376120f42cef7 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Wed, 3 May 2023 15:30:50 +0200 Subject: [PATCH 13/93] Restructure --- frontend/src/js/app/RightPane.tsx | 7 +- frontend/src/js/editor-v2/EditorV2.tsx | 254 +++++------------- .../connector-update/useConnectorRotation.ts | 39 +++ .../{ => date-restriction}/DateModal.tsx | 96 +++---- .../editor-v2/date-restriction/DateRange.tsx | 37 +++ .../date-restriction/useDateEditing.ts | 44 +++ .../src/js/editor-v2/expand/useExpandQuery.ts | 202 ++++++++++++++ .../editor-v2/negation/useNegationEditing.ts | 30 +++ frontend/src/js/editor-v2/types.ts | 11 +- frontend/src/js/editor-v2/util.ts | 16 ++ .../js/standard-query-editor/expandNode.ts | 2 +- 11 files changed, 494 insertions(+), 244 deletions(-) create mode 100644 frontend/src/js/editor-v2/connector-update/useConnectorRotation.ts rename frontend/src/js/editor-v2/{ => date-restriction}/DateModal.tsx (51%) create mode 100644 frontend/src/js/editor-v2/date-restriction/DateRange.tsx create mode 100644 frontend/src/js/editor-v2/date-restriction/useDateEditing.ts create mode 100644 frontend/src/js/editor-v2/expand/useExpandQuery.ts create mode 100644 frontend/src/js/editor-v2/negation/useNegationEditing.ts create mode 100644 frontend/src/js/editor-v2/util.ts diff --git a/frontend/src/js/app/RightPane.tsx b/frontend/src/js/app/RightPane.tsx index 9499e33b25..f3533aed26 100644 --- a/frontend/src/js/app/RightPane.tsx +++ b/frontend/src/js/app/RightPane.tsx @@ -85,7 +85,12 @@ const RightPane = () => {
{showEditorV2 ? ( - + ) : ( )} diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 343b68d4ec..3d159597f2 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -3,6 +3,7 @@ import { faCalendar, faTrashCan } from "@fortawesome/free-regular-svg-icons"; import { faBan, faCircleNodes, + faExpandArrowsAlt, faRefresh, faTrash, } from "@fortawesome/free-solid-svg-icons"; @@ -10,23 +11,23 @@ import { createId } from "@paralleldrive/cuid2"; import { useCallback, useMemo, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; -import { useGetQuery } from "../api/api"; -import { - AndNodeT, - DateRangeT, - DateRestrictionNodeT, - NegationNodeT, - OrNodeT, - QueryConceptNodeT, - SavedQueryNodeT, -} from "../api/types"; import IconButton from "../button/IconButton"; import { DNDType } from "../common/constants/dndTypes"; -import { getConceptById } from "../concept-trees/globalTreeStoreHelper"; +import { nodeIsConceptQueryNode } from "../model/node"; +import { + DragItemConceptTreeNode, + DragItemQuery, +} from "../standard-query-editor/types"; import Dropzone, { DropzoneProps } from "../ui-components/Dropzone"; -import { DateModal, useDateEditing } from "./DateModal"; +import { useConnectorEditing } from "./connector-update/useConnectorRotation"; +import { DateModal } from "./date-restriction/DateModal"; +import { DateRange } from "./date-restriction/DateRange"; +import { useDateEditing } from "./date-restriction/useDateEditing"; +import { useExpandQuery } from "./expand/useExpandQuery"; +import { useNegationEditing } from "./negation/useNegationEditing"; import { Tree } from "./types"; +import { findNodeById } from "./util"; const Root = styled("div")` flex-grow: 1; @@ -48,8 +49,8 @@ const Grid = styled("div")` `; const SxDropzone = styled(Dropzone)` - width: 200px; - height: 100px; + width: 100%; + height: 100%; `; const Actions = styled("div")` @@ -69,21 +70,6 @@ const SxIconButton = styled(IconButton)` gap: 5px; `; -const findNodeById = (tree: Tree, id: string): Tree | undefined => { - if (tree.id === id) { - return tree; - } - if (tree.children) { - for (const child of tree.children.items) { - const found = findNodeById(child, id); - if (found) { - return found; - } - } - } - return undefined; -}; - const useEditorState = () => { const [tree, setTree] = useState(undefined); const [selectedNodeId, setSelectedNodeId] = useState( @@ -96,86 +82,6 @@ const useEditorState = () => { return findNodeById(tree, selectedNodeId.id); }, [tree, selectedNodeId]); - const expandNode = ( - queryNode: - | AndNodeT - | DateRestrictionNodeT - | OrNodeT - | NegationNodeT - | QueryConceptNodeT - | SavedQueryNodeT, - config: { - parentId?: string; - negation?: boolean; - dateRestriction?: DateRangeT; - } = {}, - ): Tree => { - switch (queryNode.type) { - case "AND": - if (queryNode.children.length === 1) { - return expandNode(queryNode.children[0], config); - } - const andid = createId(); - return { - id: andid, - ...config, - children: { - connection: "and", - direction: "horizontal", - items: queryNode.children.map((child) => - expandNode(child, { parentId: andid }), - ), - }, - }; - case "OR": - if (queryNode.children.length === 1) { - return expandNode(queryNode.children[0], config); - } - const orid = createId(); - return { - id: orid, - ...config, - children: { - connection: "or", - direction: "vertical", - items: queryNode.children.map((child) => - expandNode(child, { parentId: orid }), - ), - }, - }; - case "NEGATION": - return expandNode(queryNode.child, { ...config, negation: true }); - case "DATE_RESTRICTION": - return expandNode(queryNode.child, { - ...config, - dateRestriction: queryNode.dateRange, - }); - case "CONCEPT": - const concept = getConceptById(queryNode.ids[0]); - return { - id: createId(), - data: concept, - ...config, - }; - case "SAVED_QUERY": - return { - id: queryNode.query, - data: queryNode, - ...config, - }; - } - }; - - const getQuery = useGetQuery(); - const expandQuery = async (id: string) => { - const query = await getQuery(id); - - if (query && query.query.root.type !== "EXTERNAL_RESOLVED") { - const tree = expandNode(query.query.root); - setTree(tree); - } - }; - const onReset = () => { setTree(undefined); }; @@ -193,7 +99,6 @@ const useEditorState = () => { ); return { - expandQuery, tree, setTree, updateTreeNode, @@ -204,52 +109,26 @@ const useEditorState = () => { }; const DROP_TYPES = [ + DNDType.CONCEPT_TREE_NODE, DNDType.PREVIOUS_QUERY, DNDType.PREVIOUS_SECONDARY_ID_QUERY, ]; -const useNegationEditing = ({ - selectedNode, - updateTreeNode, - enabled, -}: { - enabled: boolean; - selectedNode: Tree | undefined; - updateTreeNode: (id: string, update: (node: Tree) => void) => void; -}) => { - const onNegateClick = useCallback(() => { - if (!selectedNode || !enabled) return; - - updateTreeNode(selectedNode.id, (node) => { - node.negation = !node.negation; - }); - }, [enabled, selectedNode, updateTreeNode]); - - useHotkeys("n", onNegateClick, [onNegateClick]); - - return { - onNegateClick, - }; -}; - -const CONNECTORS = ["and", "or", "before"] as const; -const getNextConnector = (connector: (typeof CONNECTORS)[number]) => { - const index = CONNECTORS.indexOf(connector); - return CONNECTORS[(index + 1) % CONNECTORS.length]; -}; - export function EditorV2({ featureDates, featureNegate, + featureExpand, + featureConnectorRotate, }: { featureDates: boolean; featureNegate: boolean; + featureExpand: boolean; + featureConnectorRotate: boolean; }) { const { tree, setTree, updateTreeNode, - expandQuery, onReset, selectedNode, setSelectedNodeId, @@ -284,35 +163,41 @@ export function EditorV2({ parent.id = child.id; parent.children = child.children; parent.data = child.data; - parent.dateRestriction ||= child.dateRestriction; + parent.dates ||= child.dates; parent.negation ||= child.negation; } }); } }, [selectedNode, setTree, updateTreeNode]); - const onRotateConnector = useCallback(() => { - if (!selectedNode || !selectedNode.children) return; - - updateTreeNode(selectedNode.id, (node) => { - if (!node.children) return; - - node.children.connection = getNextConnector(node.children.connection); - }); - }, [selectedNode, updateTreeNode]); - useHotkeys("del", onDelete, [onDelete]); useHotkeys("backspace", onDelete, [onDelete]); useHotkeys("f", onFlip, [onFlip]); - useHotkeys("c", onRotateConnector, [onRotateConnector]); + + const { canExpand, onExpand } = useExpandQuery({ + enabled: featureExpand, + hotkey: "x", + updateTreeNode, + selectedNode, + tree, + }); const { showModal, headline, onOpen, onClose } = useDateEditing({ enabled: featureDates, + hotkey: "d", selectedNode, }); const { onNegateClick } = useNegationEditing({ enabled: featureNegate, + hotkey: "n", + selectedNode, + updateTreeNode, + }); + + const { onRotateConnector } = useConnectorEditing({ + enabled: featureConnectorRotate, + hotkey: "c", selectedNode, updateTreeNode, }); @@ -328,15 +213,24 @@ export function EditorV2({ { + updateTreeNode(selectedNode.id, (node) => { + if (!node.dates) node.dates = {}; + node.dates.excluded = excluded; + }); + }} onResetDates={() => updateTreeNode(selectedNode.id, (node) => { - node.dateRestriction = undefined; + if (!node.dates) return; + node.dates.restriction = undefined; }) } setDateRange={(dateRange) => { updateTreeNode(selectedNode.id, (node) => { - node.dateRestriction = dateRange; + if (!node.dates) node.dates = {}; + node.dates.restriction = dateRange; }); }} /> @@ -376,7 +270,7 @@ export function EditorV2({ Flip )} - {selectedNode?.children && ( + {featureConnectorRotate && selectedNode?.children && ( { @@ -388,6 +282,17 @@ export function EditorV2({ {selectedNode.children.connection} )} + {canExpand && ( + { + e.stopPropagation(); + onExpand(); + }} + > + Expand + + )} {selectedNode && ( ) : ( { - expandQuery((droppedItem as any).id); + onDrop={(item) => { + setTree({ + id: createId(), + data: item as DragItemConceptTreeNode | DragItemQuery, + }); }} acceptedDropTypes={DROP_TYPES} > @@ -494,7 +402,7 @@ const InvisibleDropzone = ( ); @@ -509,19 +417,6 @@ const Description = styled("div")` font-size: ${({ theme }) => theme.font.xs}; `; -const DateRange = styled("div")` - font-size: ${({ theme }) => theme.font.xs}; - font-family: monospace; -`; - -const formatDateRange = (range: DateRangeT): string => { - if (range.min === range.max) { - return range.min || ""; - } else { - return `${range.min} - ${range.max}`; - } -}; - function TreeNode({ tree, updateTreeNode, @@ -549,7 +444,6 @@ function TreeNode({ pos: "b" | "a"; item: any; }) => { - console.log("dropped outside of node", { pos, direction, item }); // Create a new "parent" and create a new "item", make parent contain tree and item const newParentId = createId(); const newItemId = createId(); @@ -628,15 +522,13 @@ function TreeNode({ setSelectedNode(tree); }} > - {(!tree.children || tree.data || tree.dateRestriction) && ( + {(!tree.children || tree.data || tree.dates) && (
- {tree.dateRestriction && ( - <> - {formatDateRange(tree.dateRestriction)} - + {tree.dates?.restriction && ( + )} - {tree.data?.label && {tree.data.label[0]}} - {tree.data?.description && ( + {tree.data?.label && {tree.data.label}} + {tree.data && nodeIsConceptQueryNode(tree.data) && ( {tree.data?.description} )}
diff --git a/frontend/src/js/editor-v2/connector-update/useConnectorRotation.ts b/frontend/src/js/editor-v2/connector-update/useConnectorRotation.ts new file mode 100644 index 0000000000..34b956702d --- /dev/null +++ b/frontend/src/js/editor-v2/connector-update/useConnectorRotation.ts @@ -0,0 +1,39 @@ +import { useCallback } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; + +import { Tree } from "../types"; + +const CONNECTORS = ["and", "or", "before"] as const; + +const getNextConnector = (connector: (typeof CONNECTORS)[number]) => { + const index = CONNECTORS.indexOf(connector); + return CONNECTORS[(index + 1) % CONNECTORS.length]; +}; + +export const useConnectorEditing = ({ + enabled, + hotkey, + selectedNode, + updateTreeNode, +}: { + enabled: boolean; + hotkey: string; + selectedNode: Tree | undefined; + updateTreeNode: (id: string, update: (node: Tree) => void) => void; +}) => { + const onRotateConnector = useCallback(() => { + if (!enabled || !selectedNode || !selectedNode.children) return; + + updateTreeNode(selectedNode.id, (node) => { + if (!node.children) return; + + node.children.connection = getNextConnector(node.children.connection); + }); + }, [enabled, selectedNode, updateTreeNode]); + + useHotkeys(hotkey, onRotateConnector, [onRotateConnector]); + + return { + onRotateConnector, + }; +}; diff --git a/frontend/src/js/editor-v2/DateModal.tsx b/frontend/src/js/editor-v2/date-restriction/DateModal.tsx similarity index 51% rename from frontend/src/js/editor-v2/DateModal.tsx rename to frontend/src/js/editor-v2/date-restriction/DateModal.tsx index 4dc1eb9164..22a78d4967 100644 --- a/frontend/src/js/editor-v2/DateModal.tsx +++ b/frontend/src/js/editor-v2/date-restriction/DateModal.tsx @@ -1,54 +1,21 @@ import styled from "@emotion/styled"; import { faUndo } from "@fortawesome/free-solid-svg-icons"; -import { useState, useCallback, useMemo } from "react"; +import { useCallback, useMemo } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; -import { DateRangeT } from "../api/types"; -import IconButton from "../button/IconButton"; -import { DateStringMinMax } from "../common/helpers/dateHelper"; -import Modal from "../modal/Modal"; -import InputDateRange from "../ui-components/InputDateRange"; - -import { Tree } from "./types"; - -export const useDateEditing = ({ - enabled, - selectedNode, -}: { - enabled: boolean; - selectedNode: Tree | undefined; -}) => { - const [showModal, setShowModal] = useState(false); - - const onClose = useCallback(() => setShowModal(false), []); - const onOpen = useCallback(() => { - if (!enabled) return; - if (!selectedNode) return; - - setShowModal(true); - }, [enabled, selectedNode]); - - useHotkeys("d", onOpen, [onOpen], { - preventDefault: true, - }); - - const headline = useMemo(() => { - if (!selectedNode) return ""; - - return ( - selectedNode.data?.label || - (selectedNode.children?.items || []).map((c) => c.data?.label).join(" ") - ); - }, [selectedNode]); - - return { - showModal, - headline, - onClose, - onOpen, - }; -}; +import { DateRangeT } from "../../api/types"; +import IconButton from "../../button/IconButton"; +import { DateStringMinMax } from "../../common/helpers/dateHelper"; +import Modal from "../../modal/Modal"; +import InputCheckbox from "../../ui-components/InputCheckbox"; +import InputDateRange from "../../ui-components/InputDateRange"; + +const Col = styled("div")` + display: flex; + flex-direction: column; + gap: 20px; +`; const ResetAll = styled(IconButton)` color: ${({ theme }) => theme.col.blueGrayDark}; @@ -60,13 +27,17 @@ export const DateModal = ({ onClose, dateRange = {}, headline, + excludeFromDates, + setExcludeFromDates, setDateRange, onResetDates, }: { onClose: () => void; + excludeFromDates?: boolean; + setExcludeFromDates: (exclude: boolean) => void; dateRange?: DateRangeT; - headline: string; setDateRange: (range: DateRangeT) => void; + headline: string; onResetDates: () => void; }) => { const { t } = useTranslation(); @@ -102,18 +73,25 @@ export const DateModal = ({ headline={t("queryGroupModal.explanation")} >
{headline}
- + + + + ); }; diff --git a/frontend/src/js/editor-v2/date-restriction/DateRange.tsx b/frontend/src/js/editor-v2/date-restriction/DateRange.tsx new file mode 100644 index 0000000000..3ec739dc4c --- /dev/null +++ b/frontend/src/js/editor-v2/date-restriction/DateRange.tsx @@ -0,0 +1,37 @@ +import styled from "@emotion/styled"; + +import { DateRangeT } from "../../api/types"; + +const Root = styled("div")` + font-size: ${({ theme }) => theme.font.xs}; + font-family: monospace; + display: grid; + gap: 0 5px; + grid-template-columns: auto 1fr; +`; + +const Label = styled("div")` + text-transform: uppercase; + color: ${({ theme }) => theme.col.blueGrayDark}; + font-weight: 700; + justify-self: flex-end; +`; + +export const DateRange = ({ dateRange }: { dateRange: DateRangeT }) => { + return ( + + {dateRange.min && ( + <> + + {dateRange.min} + + )} + {dateRange.max && dateRange.max !== dateRange.min && ( + <> + + {dateRange.max} + + )} + + ); +}; diff --git a/frontend/src/js/editor-v2/date-restriction/useDateEditing.ts b/frontend/src/js/editor-v2/date-restriction/useDateEditing.ts new file mode 100644 index 0000000000..ad205bef5f --- /dev/null +++ b/frontend/src/js/editor-v2/date-restriction/useDateEditing.ts @@ -0,0 +1,44 @@ +import { useState, useCallback, useMemo } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; + +import { Tree } from "../types"; + +export const useDateEditing = ({ + enabled, + hotkey, + selectedNode, +}: { + enabled: boolean; + hotkey: string; + selectedNode: Tree | undefined; +}) => { + const [showModal, setShowModal] = useState(false); + + const onClose = useCallback(() => setShowModal(false), []); + const onOpen = useCallback(() => { + if (!enabled) return; + if (!selectedNode) return; + + setShowModal(true); + }, [enabled, selectedNode]); + + useHotkeys(hotkey, onOpen, [onOpen], { + preventDefault: true, + }); + + const headline = useMemo(() => { + if (!selectedNode) return ""; + + return ( + selectedNode.data?.label || + (selectedNode.children?.items || []).map((c) => c.data?.label).join(" ") + ); + }, [selectedNode]); + + return { + showModal, + headline, + onClose, + onOpen, + }; +}; diff --git a/frontend/src/js/editor-v2/expand/useExpandQuery.ts b/frontend/src/js/editor-v2/expand/useExpandQuery.ts new file mode 100644 index 0000000000..907c514d54 --- /dev/null +++ b/frontend/src/js/editor-v2/expand/useExpandQuery.ts @@ -0,0 +1,202 @@ +import { createId } from "@paralleldrive/cuid2"; +import { useCallback, useMemo } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; +import { useSelector } from "react-redux"; + +import { useGetQuery } from "../../api/api"; +import { + AndNodeT, + DateRestrictionNodeT, + OrNodeT, + NegationNodeT, + QueryConceptNodeT, + SavedQueryNodeT, + DateRangeT, +} from "../../api/types"; +import { StateT } from "../../app/reducers"; +import { DNDType } from "../../common/constants/dndTypes"; +import { getConceptsByIdsWithTablesAndSelects } from "../../concept-trees/globalTreeStoreHelper"; +import { TreesT } from "../../concept-trees/reducer"; +import { mergeFromSavedConceptIntoNode } from "../../standard-query-editor/expandNode"; +import { + DragItemConceptTreeNode, + DragItemQuery, +} from "../../standard-query-editor/types"; +import { Tree } from "../types"; +import { findNodeById } from "../util"; + +export const useExpandQuery = ({ + selectedNode, + hotkey, + enabled, + tree, + updateTreeNode, +}: { + enabled: boolean; + hotkey: string; + selectedNode?: Tree; + tree?: Tree; + updateTreeNode: (id: string, update: (node: Tree) => void) => void; +}) => { + const rootConcepts = useSelector( + (state) => state.conceptTrees.trees, + ); + const expandNode = useCallback( + ( + queryNode: + | AndNodeT + | DateRestrictionNodeT + | OrNodeT + | NegationNodeT + | QueryConceptNodeT + | SavedQueryNodeT, + config: { + parentId?: string; + negation?: boolean; + dateRestriction?: DateRangeT; + } = {}, + ): Tree => { + switch (queryNode.type) { + case "AND": + if (queryNode.children.length === 1) { + return expandNode(queryNode.children[0], config); + } + const andid = createId(); + return { + id: andid, + ...config, + children: { + connection: "and", + direction: "horizontal", + items: queryNode.children.map((child) => + expandNode(child, { parentId: andid }), + ), + }, + }; + case "OR": + if (queryNode.children.length === 1) { + return expandNode(queryNode.children[0], config); + } + const orid = createId(); + return { + id: orid, + ...config, + children: { + connection: "or", + direction: "vertical", + items: queryNode.children.map((child) => + expandNode(child, { parentId: orid }), + ), + }, + }; + case "NEGATION": + return expandNode(queryNode.child, { ...config, negation: true }); + case "DATE_RESTRICTION": + return expandNode(queryNode.child, { + ...config, + dateRestriction: queryNode.dateRange, + }); + case "CONCEPT": + const lookupResult = getConceptsByIdsWithTablesAndSelects( + rootConcepts, + queryNode.ids, + { useDefaults: false }, + ); + if (!lookupResult) { + throw new Error("Concept not found"); + } + const { tables, selects } = mergeFromSavedConceptIntoNode(queryNode, { + tables: lookupResult.tables, + selects: lookupResult.selects || [], + }); + const label = queryNode.label || lookupResult.concepts[0].label; + const description = lookupResult.concepts[0].description; + + const dataNode: DragItemConceptTreeNode = { + ...queryNode, + dragContext: { width: 0, height: 0 }, + additionalInfos: lookupResult.concepts[0].additionalInfos, + matchingEntities: lookupResult.concepts[0].matchingEntities, + matchingEntries: lookupResult.concepts[0].matchingEntries, + type: DNDType.CONCEPT_TREE_NODE, + label, + description, + tables, + selects, + tree: lookupResult.root, + }; + + return { + id: createId(), + data: dataNode, + dates: config.dateRestriction + ? { + ...config.dateRestriction, + ...(queryNode.excludeFromTimeAggregation + ? { excluded: true } + : {}), + } + : undefined, + ...config, + }; + case "SAVED_QUERY": + const dataQuery: DragItemQuery = { + ...queryNode, + query: undefined, + dragContext: { width: 0, height: 0 }, + label: "", // TODO: DOUBLE CHECK + tags: [], + type: DNDType.PREVIOUS_QUERY, + id: queryNode.query, + }; + return { + id: queryNode.query, + data: dataQuery, + ...config, + }; + } + }, + [rootConcepts], + ); + + const getQuery = useGetQuery(); + const expandQuery = useCallback( + async (id: string) => { + if (!tree) return; + const queryId = (findNodeById(tree, id)?.data as DragItemQuery).id; + const query = await getQuery(queryId); + updateTreeNode(id, (node) => { + if (!query.query || query.query.root.type === "EXTERNAL_RESOLVED") + return; + + const expanded = expandNode(query.query.root); + + Object.assign(node, expanded); + }); + }, + [getQuery, expandNode, updateTreeNode, tree], + ); + + const canExpand = useMemo(() => { + return ( + enabled && + selectedNode && + !selectedNode.children && + selectedNode.data?.type !== DNDType.CONCEPT_TREE_NODE && + selectedNode.data?.id + ); + }, [enabled, selectedNode]); + + const onExpand = useCallback(() => { + if (!canExpand) return; + + expandQuery(selectedNode!.id); + }, [selectedNode, expandQuery, canExpand]); + + useHotkeys(hotkey, onExpand, [onExpand]); + + return { + canExpand, + onExpand, + }; +}; diff --git a/frontend/src/js/editor-v2/negation/useNegationEditing.ts b/frontend/src/js/editor-v2/negation/useNegationEditing.ts new file mode 100644 index 0000000000..040673e971 --- /dev/null +++ b/frontend/src/js/editor-v2/negation/useNegationEditing.ts @@ -0,0 +1,30 @@ +import { useCallback } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; + +import { Tree } from "../types"; + +export const useNegationEditing = ({ + enabled, + hotkey, + selectedNode, + updateTreeNode, +}: { + enabled: boolean; + hotkey: string; + selectedNode: Tree | undefined; + updateTreeNode: (id: string, update: (node: Tree) => void) => void; +}) => { + const onNegateClick = useCallback(() => { + if (!selectedNode || !enabled) return; + + updateTreeNode(selectedNode.id, (node) => { + node.negation = !node.negation; + }); + }, [enabled, selectedNode, updateTreeNode]); + + useHotkeys(hotkey, onNegateClick, [onNegateClick]); + + return { + onNegateClick, + }; +}; diff --git a/frontend/src/js/editor-v2/types.ts b/frontend/src/js/editor-v2/types.ts index 57cb436222..671b3258a6 100644 --- a/frontend/src/js/editor-v2/types.ts +++ b/frontend/src/js/editor-v2/types.ts @@ -1,11 +1,18 @@ import { DateRangeT } from "../api/types"; +import { + DragItemConceptTreeNode, + DragItemQuery, +} from "../standard-query-editor/types"; export interface Tree { id: string; parentId?: string; negation?: boolean; - dateRestriction?: DateRangeT; - data?: any; + dates?: { + restriction?: DateRangeT; + excluded?: boolean; + }; + data?: DragItemQuery | DragItemConceptTreeNode; children?: { connection: "and" | "or" | "before"; direction: "horizontal" | "vertical"; diff --git a/frontend/src/js/editor-v2/util.ts b/frontend/src/js/editor-v2/util.ts new file mode 100644 index 0000000000..f850a4ca49 --- /dev/null +++ b/frontend/src/js/editor-v2/util.ts @@ -0,0 +1,16 @@ +import { Tree } from "./types"; + +export const findNodeById = (tree: Tree, id: string): Tree | undefined => { + if (tree.id === id) { + return tree; + } + if (tree.children) { + for (const child of tree.children.items) { + const found = findNodeById(child, id); + if (found) { + return found; + } + } + } + return undefined; +}; diff --git a/frontend/src/js/standard-query-editor/expandNode.ts b/frontend/src/js/standard-query-editor/expandNode.ts index eff46eff83..acae698ecf 100644 --- a/frontend/src/js/standard-query-editor/expandNode.ts +++ b/frontend/src/js/standard-query-editor/expandNode.ts @@ -250,7 +250,7 @@ const mergeTables = ( // Look for tables in the already savedConcept. If they were not included in the // respective query concept, exclude them. // Also, apply all necessary filters -const mergeFromSavedConceptIntoNode = ( +export const mergeFromSavedConceptIntoNode = ( node: QueryConceptNodeT, { tables, From 2c4e9fd2f4022b15b624b3e0ddd57f061591b300 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 8 May 2023 14:37:32 +0200 Subject: [PATCH 14/93] Extract TreeNode and iterate a little further --- frontend/src/js/editor-v2/EditorLayout.ts | 23 ++ frontend/src/js/editor-v2/EditorV2.tsx | 293 +---------------- frontend/src/js/editor-v2/TreeNode.tsx | 303 ++++++++++++++++++ frontend/src/js/editor-v2/config.ts | 7 + .../editor-v2/date-restriction/DateRange.tsx | 2 +- .../src/js/editor-v2/expand/useExpandQuery.ts | 1 + 6 files changed, 340 insertions(+), 289 deletions(-) create mode 100644 frontend/src/js/editor-v2/EditorLayout.ts create mode 100644 frontend/src/js/editor-v2/TreeNode.tsx create mode 100644 frontend/src/js/editor-v2/config.ts diff --git a/frontend/src/js/editor-v2/EditorLayout.ts b/frontend/src/js/editor-v2/EditorLayout.ts new file mode 100644 index 0000000000..c6f993e92d --- /dev/null +++ b/frontend/src/js/editor-v2/EditorLayout.ts @@ -0,0 +1,23 @@ +import styled from "@emotion/styled"; + +export const Grid = styled("div")` + flex-grow: 1; + display: grid; + gap: 3px; + height: 100%; + width: 100%; + place-items: center; + overflow: auto; +`; + +export const Connector = styled("span")` + text-transform: uppercase; + font-size: ${({ theme }) => theme.font.sm}; + color: black; + + border-radius: ${({ theme }) => theme.borderRadius}; + padding: 0px 5px; + display: flex; + justify-content: center; + align-items: center; +`; diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 3d159597f2..2b9fefc4ad 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -12,17 +12,17 @@ import { useCallback, useMemo, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import IconButton from "../button/IconButton"; -import { DNDType } from "../common/constants/dndTypes"; -import { nodeIsConceptQueryNode } from "../model/node"; import { DragItemConceptTreeNode, DragItemQuery, } from "../standard-query-editor/types"; -import Dropzone, { DropzoneProps } from "../ui-components/Dropzone"; +import Dropzone from "../ui-components/Dropzone"; +import { Connector, Grid } from "./EditorLayout"; +import { TreeNode } from "./TreeNode"; +import { EDITOR_DROP_TYPES } from "./config"; import { useConnectorEditing } from "./connector-update/useConnectorRotation"; import { DateModal } from "./date-restriction/DateModal"; -import { DateRange } from "./date-restriction/DateRange"; import { useDateEditing } from "./date-restriction/useDateEditing"; import { useExpandQuery } from "./expand/useExpandQuery"; import { useNegationEditing } from "./negation/useNegationEditing"; @@ -38,16 +38,6 @@ const Root = styled("div")` gap: 10px; `; -const Grid = styled("div")` - flex-grow: 1; - display: grid; - gap: 3px; - height: 100%; - width: 100%; - place-items: center; - overflow: auto; -`; - const SxDropzone = styled(Dropzone)` width: 100%; height: 100%; @@ -108,12 +98,6 @@ const useEditorState = () => { }; }; -const DROP_TYPES = [ - DNDType.CONCEPT_TREE_NODE, - DNDType.PREVIOUS_QUERY, - DNDType.PREVIOUS_SECONDARY_ID_QUERY, -]; - export function EditorV2({ featureDates, featureNegate, @@ -326,7 +310,7 @@ export function EditorV2({ data: item as DragItemConceptTreeNode | DragItemQuery, }); }} - acceptedDropTypes={DROP_TYPES} + acceptedDropTypes={EDITOR_DROP_TYPES} > {() =>
Drop if you dare
}
@@ -335,270 +319,3 @@ export function EditorV2({ ); } - -const NodeContainer = styled("div")` - display: grid; - gap: 5px; -`; - -const Node = styled("div")<{ - selected?: boolean; - negated?: boolean; - leaf?: boolean; -}>` - padding: ${({ leaf }) => (leaf ? "5px 10px" : "10px")}; - border: 1px solid - ${({ negated, theme, selected }) => - negated ? "red" : selected ? theme.col.gray : theme.col.grayMediumLight}; - box-shadow: ${({ selected, theme }) => - selected ? `inset 0px 0px 0px 1px ${theme.col.gray}` : "none"}; - - border-radius: ${({ theme }) => theme.borderRadius}; - width: ${({ leaf }) => (leaf ? "150px" : "inherit")}; - background-color: ${({ leaf, theme }) => (leaf ? "white" : theme.col.bg)}; - cursor: pointer; - display: flex; - flex-direction: column; - gap: 10px; -`; - -const Connector = styled("span")` - text-transform: uppercase; - font-size: ${({ theme }) => theme.font.sm}; - color: black; - - border-radius: ${({ theme }) => theme.borderRadius}; - padding: 0px 5px; - display: flex; - justify-content: center; - align-items: center; -`; - -function getGridStyles(tree: Tree) { - if (!tree.children) { - return {}; - } - - if (tree.children.direction === "horizontal") { - return { - gridAutoFlow: "column", - }; - } else { - return { - gridTemplateColumns: "1fr", - }; - } -} - -const InvisibleDropzoneContainer = styled(Dropzone)` - width: 100%; - height: 100%; -`; - -const InvisibleDropzone = ( - props: Omit, "acceptedDropTypes">, -) => { - return ( - - ); -}; - -const Name = styled("div")` - font-size: ${({ theme }) => theme.font.sm}; - font-weight: 700; -`; - -const Description = styled("div")` - font-size: ${({ theme }) => theme.font.xs}; -`; - -function TreeNode({ - tree, - updateTreeNode, - droppable, - selectedNode, - setSelectedNode, -}: { - tree: Tree; - updateTreeNode: (id: string, update: (node: Tree) => void) => void; - droppable: { - h: boolean; - v: boolean; - }; - selectedNode: Tree | undefined; - setSelectedNode: (node: Tree | undefined) => void; -}) { - const gridStyles = getGridStyles(tree); - - const onDropOutsideOfNode = ({ - pos, - direction, - item, - }: { - direction: "h" | "v"; - pos: "b" | "a"; - item: any; - }) => { - // Create a new "parent" and create a new "item", make parent contain tree and item - const newParentId = createId(); - const newItemId = createId(); - updateTreeNode(tree.id, (node) => { - const newChildren: Tree[] = [ - { - id: newItemId, - negation: false, - data: item, - parentId: newParentId, - }, - { - id: tree.id, - negation: false, - data: tree.data, - children: tree.children, - parentId: newParentId, - }, - ]; - - node.id = newParentId; - node.data = undefined; - node.children = { - connection: tree.children?.connection === "and" ? "or" : "and" || "and", - direction: direction === "h" ? "horizontal" : "vertical", - items: pos === "b" ? newChildren : newChildren.reverse(), - }; - }); - }; - - const onDropAtChildrenIdx = ({ idx, item }: { idx: number; item: any }) => { - // Create a new "item" and insert it at idx of tree.children - updateTreeNode(tree.id, (node) => { - if (node.children) { - node.children.items.splice(idx, 0, { - id: createId(), - negation: false, - data: item, - parentId: node.id, - }); - } - }); - }; - - return ( - - {droppable.v && ( - - onDropOutsideOfNode({ pos: "b", direction: "v", item }) - } - /> - )} - - {droppable.h && ( - - onDropOutsideOfNode({ - pos: "b", - direction: "h", - item, - }) - } - /> - )} - { - e.stopPropagation(); - setSelectedNode(tree); - }} - > - {(!tree.children || tree.data || tree.dates) && ( -
- {tree.dates?.restriction && ( - - )} - {tree.data?.label && {tree.data.label}} - {tree.data && nodeIsConceptQueryNode(tree.data) && ( - {tree.data?.description} - )} -
- )} - {tree.children && ( - - onDropAtChildrenIdx({ idx: 0, item })} - /> - {tree.children.items.map((item, i, items) => ( - <> - - {i < items.length - 1 && ( - - onDropAtChildrenIdx({ idx: i + 1, item }) - } - > - {() => {tree.children?.connection}} - - )} - - ))} - - onDropAtChildrenIdx({ - idx: tree.children!.items.length, - item, - }) - } - /> - - )} -
- {droppable.h && ( - - onDropOutsideOfNode({ pos: "a", direction: "h", item }) - } - /> - )} -
- {droppable.v && ( - - onDropOutsideOfNode({ pos: "a", direction: "v", item }) - } - /> - )} -
- ); -} diff --git a/frontend/src/js/editor-v2/TreeNode.tsx b/frontend/src/js/editor-v2/TreeNode.tsx new file mode 100644 index 0000000000..6d93d1d813 --- /dev/null +++ b/frontend/src/js/editor-v2/TreeNode.tsx @@ -0,0 +1,303 @@ +import styled from "@emotion/styled"; +import { createId } from "@paralleldrive/cuid2"; +import { useTranslation } from "react-i18next"; + +import { DNDType } from "../common/constants/dndTypes"; +import { nodeIsConceptQueryNode } from "../model/node"; +import { getRootNodeLabel } from "../standard-query-editor/helper"; +import Dropzone, { DropzoneProps } from "../ui-components/Dropzone"; + +import { Connector, Grid } from "./EditorLayout"; +import { EDITOR_DROP_TYPES } from "./config"; +import { DateRange } from "./date-restriction/DateRange"; +import { Tree } from "./types"; + +const NodeContainer = styled("div")` + display: grid; + gap: 5px; +`; + +const Node = styled("div")<{ + selected?: boolean; + negated?: boolean; + leaf?: boolean; +}>` + padding: ${({ leaf }) => (leaf ? "8px 10px" : "12px")}; + border: 1px solid + ${({ negated, theme, selected }) => + negated ? "red" : selected ? theme.col.gray : theme.col.grayMediumLight}; + box-shadow: ${({ selected, theme }) => + selected ? `inset 0px 0px 0px 1px ${theme.col.gray}` : "none"}; + + border-radius: ${({ theme }) => theme.borderRadius}; + width: ${({ leaf }) => (leaf ? "150px" : "inherit")}; + background-color: ${({ leaf, theme }) => (leaf ? "white" : theme.col.bg)}; + cursor: pointer; + display: flex; + flex-direction: column; + gap: 10px; +`; + +function getGridStyles(tree: Tree) { + if (!tree.children) { + return {}; + } + + if (tree.children.direction === "horizontal") { + return { + gridAutoFlow: "column", + }; + } else { + return { + gridTemplateColumns: "1fr", + }; + } +} + +const InvisibleDropzoneContainer = styled(Dropzone)` + width: 100%; + height: 100%; +`; + +const InvisibleDropzone = ( + props: Omit, "acceptedDropTypes">, +) => { + return ( + + ); +}; + +const Name = styled("div")` + font-size: ${({ theme }) => theme.font.sm}; + font-weight: 400; +`; + +const Description = styled("div")` + font-size: ${({ theme }) => theme.font.xs}; +`; + +const PreviousQueryLabel = styled("p")` + margin: 0 0 4px; + line-height: 1.2; + font-size: ${({ theme }) => theme.font.xs}; + text-transform: uppercase; + font-weight: 700; + color: ${({ theme }) => theme.col.blueGrayDark}; +`; + +const RootNode = styled("p")` + margin: 0 0 4px; + line-height: 1; + text-transform: uppercase; + font-weight: 700; + font-size: ${({ theme }) => theme.font.xs}; + color: ${({ theme }) => theme.col.blueGrayDark}; + word-break: break-word; +`; + +const Dates = styled("div")` + text-align: right; +`; + +export function TreeNode({ + tree, + updateTreeNode, + droppable, + selectedNode, + setSelectedNode, +}: { + tree: Tree; + updateTreeNode: (id: string, update: (node: Tree) => void) => void; + droppable: { + h: boolean; + v: boolean; + }; + selectedNode: Tree | undefined; + setSelectedNode: (node: Tree | undefined) => void; +}) { + const gridStyles = getGridStyles(tree); + + const { t } = useTranslation(); + + const rootNodeLabel = tree.data ? getRootNodeLabel(tree.data) : null; + + const onDropOutsideOfNode = ({ + pos, + direction, + item, + }: { + direction: "h" | "v"; + pos: "b" | "a"; + item: any; + }) => { + // Create a new "parent" and create a new "item", make parent contain tree and item + const newParentId = createId(); + const newItemId = createId(); + updateTreeNode(tree.id, (node) => { + const newChildren: Tree[] = [ + { + id: newItemId, + negation: false, + data: item, + parentId: newParentId, + }, + { + id: tree.id, + negation: false, + data: tree.data, + children: tree.children, + parentId: newParentId, + }, + ]; + + node.id = newParentId; + node.data = undefined; + node.children = { + connection: tree.children?.connection === "and" ? "or" : "and" || "and", + direction: direction === "h" ? "horizontal" : "vertical", + items: pos === "b" ? newChildren : newChildren.reverse(), + }; + }); + }; + + const onDropAtChildrenIdx = ({ idx, item }: { idx: number; item: any }) => { + // Create a new "item" and insert it at idx of tree.children + updateTreeNode(tree.id, (node) => { + if (node.children) { + node.children.items.splice(idx, 0, { + id: createId(), + negation: false, + data: item, + parentId: node.id, + }); + } + }); + }; + + return ( + + {droppable.v && ( + + onDropOutsideOfNode({ pos: "b", direction: "v", item }) + } + /> + )} + + {droppable.h && ( + + onDropOutsideOfNode({ + pos: "b", + direction: "h", + item, + }) + } + /> + )} + { + e.stopPropagation(); + setSelectedNode(tree); + }} + > + {tree.dates?.restriction && ( + + + + )} + {(!tree.children || tree.data) && ( +
+ {tree.data?.type !== DNDType.CONCEPT_TREE_NODE && ( + + {t("queryEditor.previousQuery")} + + )} + {rootNodeLabel && {rootNodeLabel}} + {tree.data?.label && {tree.data.label}} + {tree.data && nodeIsConceptQueryNode(tree.data) && ( + {tree.data?.description} + )} +
+ )} + {tree.children && ( + + onDropAtChildrenIdx({ idx: 0, item })} + /> + {tree.children.items.map((item, i, items) => ( + <> + + {i < items.length - 1 && ( + + onDropAtChildrenIdx({ idx: i + 1, item }) + } + > + {() => {tree.children?.connection}} + + )} + + ))} + + onDropAtChildrenIdx({ + idx: tree.children!.items.length, + item, + }) + } + /> + + )} +
+ {droppable.h && ( + + onDropOutsideOfNode({ pos: "a", direction: "h", item }) + } + /> + )} +
+ {droppable.v && ( + + onDropOutsideOfNode({ pos: "a", direction: "v", item }) + } + /> + )} +
+ ); +} diff --git a/frontend/src/js/editor-v2/config.ts b/frontend/src/js/editor-v2/config.ts new file mode 100644 index 0000000000..5dd3382b72 --- /dev/null +++ b/frontend/src/js/editor-v2/config.ts @@ -0,0 +1,7 @@ +import { DNDType } from "../common/constants/dndTypes"; + +export const EDITOR_DROP_TYPES = [ + DNDType.CONCEPT_TREE_NODE, + DNDType.PREVIOUS_QUERY, + DNDType.PREVIOUS_SECONDARY_ID_QUERY, +]; diff --git a/frontend/src/js/editor-v2/date-restriction/DateRange.tsx b/frontend/src/js/editor-v2/date-restriction/DateRange.tsx index 3ec739dc4c..2371c75f49 100644 --- a/frontend/src/js/editor-v2/date-restriction/DateRange.tsx +++ b/frontend/src/js/editor-v2/date-restriction/DateRange.tsx @@ -5,7 +5,7 @@ import { DateRangeT } from "../../api/types"; const Root = styled("div")` font-size: ${({ theme }) => theme.font.xs}; font-family: monospace; - display: grid; + display: inline-grid; gap: 0 5px; grid-template-columns: auto 1fr; `; diff --git a/frontend/src/js/editor-v2/expand/useExpandQuery.ts b/frontend/src/js/editor-v2/expand/useExpandQuery.ts index 907c514d54..bda5bf2bff 100644 --- a/frontend/src/js/editor-v2/expand/useExpandQuery.ts +++ b/frontend/src/js/editor-v2/expand/useExpandQuery.ts @@ -140,6 +140,7 @@ export const useExpandQuery = ({ ...config, }; case "SAVED_QUERY": + console.log(queryNode); const dataQuery: DragItemQuery = { ...queryNode, query: undefined, From 243eb1aa60263c23394be46a5dc49c36e2055e22 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 8 May 2023 14:57:40 +0200 Subject: [PATCH 15/93] Select newly created nodes --- frontend/src/js/editor-v2/EditorV2.tsx | 7 ++++--- frontend/src/js/editor-v2/TreeNode.tsx | 20 ++++++++++--------- .../src/js/editor-v2/expand/useExpandQuery.ts | 6 +++++- frontend/src/localization/de.json | 2 +- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 2b9fefc4ad..bc29dae6e1 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -62,14 +62,14 @@ const SxIconButton = styled(IconButton)` const useEditorState = () => { const [tree, setTree] = useState(undefined); - const [selectedNodeId, setSelectedNodeId] = useState( + const [selectedNodeId, setSelectedNodeId] = useState( undefined, ); const selectedNode = useMemo(() => { if (!tree || !selectedNodeId) { return undefined; } - return findNodeById(tree, selectedNodeId.id); + return findNodeById(tree, selectedNodeId); }, [tree, selectedNodeId]); const onReset = () => { @@ -163,6 +163,7 @@ export function EditorV2({ hotkey: "x", updateTreeNode, selectedNode, + setSelectedNodeId, tree, }); @@ -299,7 +300,7 @@ export function EditorV2({ tree={tree} updateTreeNode={updateTreeNode} selectedNode={selectedNode} - setSelectedNode={setSelectedNodeId} + setSelectedNodeId={setSelectedNodeId} droppable={{ h: true, v: true }} /> ) : ( diff --git a/frontend/src/js/editor-v2/TreeNode.tsx b/frontend/src/js/editor-v2/TreeNode.tsx index 6d93d1d813..947bb0d414 100644 --- a/frontend/src/js/editor-v2/TreeNode.tsx +++ b/frontend/src/js/editor-v2/TreeNode.tsx @@ -109,7 +109,7 @@ export function TreeNode({ updateTreeNode, droppable, selectedNode, - setSelectedNode, + setSelectedNodeId, }: { tree: Tree; updateTreeNode: (id: string, update: (node: Tree) => void) => void; @@ -118,7 +118,7 @@ export function TreeNode({ v: boolean; }; selectedNode: Tree | undefined; - setSelectedNode: (node: Tree | undefined) => void; + setSelectedNodeId: (id: Tree["id"] | undefined) => void; }) { const gridStyles = getGridStyles(tree); @@ -138,6 +138,7 @@ export function TreeNode({ // Create a new "parent" and create a new "item", make parent contain tree and item const newParentId = createId(); const newItemId = createId(); + updateTreeNode(tree.id, (node) => { const newChildren: Tree[] = [ { @@ -147,36 +148,37 @@ export function TreeNode({ parentId: newParentId, }, { - id: tree.id, - negation: false, - data: tree.data, - children: tree.children, + ...tree, parentId: newParentId, }, ]; node.id = newParentId; node.data = undefined; + node.dates = undefined; node.children = { connection: tree.children?.connection === "and" ? "or" : "and" || "and", direction: direction === "h" ? "horizontal" : "vertical", items: pos === "b" ? newChildren : newChildren.reverse(), }; }); + setSelectedNodeId(newItemId); }; const onDropAtChildrenIdx = ({ idx, item }: { idx: number; item: any }) => { + const newItemId = createId(); // Create a new "item" and insert it at idx of tree.children updateTreeNode(tree.id, (node) => { if (node.children) { node.children.items.splice(idx, 0, { - id: createId(), + id: newItemId, negation: false, data: item, parentId: node.id, }); } }); + setSelectedNodeId(newItemId); }; return ( @@ -210,7 +212,7 @@ export function TreeNode({ selected={selectedNode?.id === tree.id} onClick={(e) => { e.stopPropagation(); - setSelectedNode(tree); + setSelectedNodeId(tree.id); }} > {tree.dates?.restriction && ( @@ -245,7 +247,7 @@ export function TreeNode({ tree={item} updateTreeNode={updateTreeNode} selectedNode={selectedNode} - setSelectedNode={setSelectedNode} + setSelectedNodeId={setSelectedNodeId} droppable={{ h: !item.children && diff --git a/frontend/src/js/editor-v2/expand/useExpandQuery.ts b/frontend/src/js/editor-v2/expand/useExpandQuery.ts index bda5bf2bff..f7631fe0c6 100644 --- a/frontend/src/js/editor-v2/expand/useExpandQuery.ts +++ b/frontend/src/js/editor-v2/expand/useExpandQuery.ts @@ -30,11 +30,13 @@ export const useExpandQuery = ({ hotkey, enabled, tree, + setSelectedNodeId, updateTreeNode, }: { enabled: boolean; hotkey: string; selectedNode?: Tree; + setSelectedNodeId: (id: Tree["id"] | undefined) => void; tree?: Tree; updateTreeNode: (id: string, update: (node: Tree) => void) => void; }) => { @@ -166,16 +168,18 @@ export const useExpandQuery = ({ if (!tree) return; const queryId = (findNodeById(tree, id)?.data as DragItemQuery).id; const query = await getQuery(queryId); + updateTreeNode(id, (node) => { if (!query.query || query.query.root.type === "EXTERNAL_RESOLVED") return; const expanded = expandNode(query.query.root); + setSelectedNodeId(expanded.id); Object.assign(node, expanded); }); }, - [getQuery, expandNode, updateTreeNode, tree], + [getQuery, expandNode, updateTreeNode, tree, setSelectedNodeId], ); const canExpand = useMemo(() => { diff --git a/frontend/src/localization/de.json b/frontend/src/localization/de.json index d328800b9d..79ffcff974 100644 --- a/frontend/src/localization/de.json +++ b/frontend/src/localization/de.json @@ -18,7 +18,7 @@ "queryEditor": "Editor", "timebasedQueryEditor": "Zeit-Editor", "externalForms": "Formular-Editor", - "editorV2": "Der neue Editor" + "editorV2": "Wissenschaftlicher Editor" }, "conceptTreeList": { "loading": "Lade Konzepte", From b705a8bbf879a10d993def309745a1bdf29c7464 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 9 May 2023 11:55:51 +0200 Subject: [PATCH 16/93] Translate --- frontend/src/js/editor-v2/EditorV2.tsx | 26 ++++++++++++------- frontend/src/js/editor-v2/TreeNode.tsx | 23 +++++++++++++--- .../editor-v2/date-restriction/DateModal.tsx | 2 ++ .../src/js/editor-v2/expand/useExpandQuery.ts | 3 +-- frontend/src/js/editor-v2/types.ts | 7 +++-- frontend/src/js/editor-v2/util.ts | 23 +++++++++++++++- frontend/src/localization/de.json | 14 ++++++++++ frontend/src/localization/en.json | 14 ++++++++++ 8 files changed, 94 insertions(+), 18 deletions(-) diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index bc29dae6e1..3f93c20d80 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -10,6 +10,7 @@ import { import { createId } from "@paralleldrive/cuid2"; import { useCallback, useMemo, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; +import { useTranslation } from "react-i18next"; import IconButton from "../button/IconButton"; import { @@ -27,7 +28,7 @@ import { useDateEditing } from "./date-restriction/useDateEditing"; import { useExpandQuery } from "./expand/useExpandQuery"; import { useNegationEditing } from "./negation/useNegationEditing"; import { Tree } from "./types"; -import { findNodeById } from "./util"; +import { findNodeById, useTranslatedConnection } from "./util"; const Root = styled("div")` flex-grow: 1; @@ -109,6 +110,7 @@ export function EditorV2({ featureExpand: boolean; featureConnectorRotate: boolean; }) { + const { t } = useTranslation(); const { tree, setTree, @@ -187,6 +189,10 @@ export function EditorV2({ updateTreeNode, }); + const connection = useTranslatedConnection( + selectedNode?.children?.connection, + ); + return ( { @@ -230,7 +236,7 @@ export function EditorV2({ onOpen(); }} > - Dates + {t("editorV2.dates")}
)} {featureNegate && selectedNode && ( @@ -241,7 +247,7 @@ export function EditorV2({ onNegateClick(); }} > - Negate + {t("editorV2.negate")} )} {selectedNode?.children && ( @@ -252,7 +258,7 @@ export function EditorV2({ onFlip(); }} > - Flip + {t("editorV2.flip")} )} {featureConnectorRotate && selectedNode?.children && ( @@ -263,8 +269,8 @@ export function EditorV2({ onRotateConnector(); }} > - Connected: - {selectedNode.children.connection} + {t("editorV2.connector")} + {connection} )} {canExpand && ( @@ -275,7 +281,7 @@ export function EditorV2({ onExpand(); }} > - Expand + {t("editorV2.expand")} )} {selectedNode && ( @@ -286,12 +292,12 @@ export function EditorV2({ onDelete(); }} > - Delete + {t("editorV2.delete")} )} - Clear + {t("editorV2.clear")} @@ -313,7 +319,7 @@ export function EditorV2({ }} acceptedDropTypes={EDITOR_DROP_TYPES} > - {() =>
Drop if you dare
} + {() =>
{t("editorV2.initialDropText")}
} )}
diff --git a/frontend/src/js/editor-v2/TreeNode.tsx b/frontend/src/js/editor-v2/TreeNode.tsx index 947bb0d414..6838f1dc7a 100644 --- a/frontend/src/js/editor-v2/TreeNode.tsx +++ b/frontend/src/js/editor-v2/TreeNode.tsx @@ -1,5 +1,6 @@ import styled from "@emotion/styled"; import { createId } from "@paralleldrive/cuid2"; +import { memo, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { DNDType } from "../common/constants/dndTypes"; @@ -10,7 +11,8 @@ import Dropzone, { DropzoneProps } from "../ui-components/Dropzone"; import { Connector, Grid } from "./EditorLayout"; import { EDITOR_DROP_TYPES } from "./config"; import { DateRange } from "./date-restriction/DateRange"; -import { Tree } from "./types"; +import { ConnectionKind, Tree } from "./types"; +import { useTranslatedConnection } from "./util"; const NodeContainer = styled("div")` display: grid; @@ -104,14 +106,22 @@ const Dates = styled("div")` text-align: right; `; +const Connection = memo(({ connection }: { connection?: ConnectionKind }) => { + const message = useTranslatedConnection(connection); + + return {message}; +}); + export function TreeNode({ tree, + treeParent, updateTreeNode, droppable, selectedNode, setSelectedNodeId, }: { tree: Tree; + treeParent?: Tree; updateTreeNode: (id: string, update: (node: Tree) => void) => void; droppable: { h: boolean; @@ -156,8 +166,12 @@ export function TreeNode({ node.id = newParentId; node.data = undefined; node.dates = undefined; + + const connection = + treeParent?.children?.connection || tree.children?.connection; + node.children = { - connection: tree.children?.connection === "and" ? "or" : "and" || "and", + connection: connection === "and" ? "or" : "and" || "and", direction: direction === "h" ? "horizontal" : "vertical", items: pos === "b" ? newChildren : newChildren.reverse(), }; @@ -245,6 +259,7 @@ export function TreeNode({ - {() => {tree.children?.connection}} + {() => ( + + )} )} diff --git a/frontend/src/js/editor-v2/date-restriction/DateModal.tsx b/frontend/src/js/editor-v2/date-restriction/DateModal.tsx index 22a78d4967..8385011f36 100644 --- a/frontend/src/js/editor-v2/date-restriction/DateModal.tsx +++ b/frontend/src/js/editor-v2/date-restriction/DateModal.tsx @@ -58,6 +58,8 @@ export const DateModal = ({ const onChange = useCallback( (date: DateStringMinMax) => { + if (!date.min && !date.max) return; + setDateRange({ min: date.min || undefined, max: date.max || undefined, diff --git a/frontend/src/js/editor-v2/expand/useExpandQuery.ts b/frontend/src/js/editor-v2/expand/useExpandQuery.ts index f7631fe0c6..d7ffe635e6 100644 --- a/frontend/src/js/editor-v2/expand/useExpandQuery.ts +++ b/frontend/src/js/editor-v2/expand/useExpandQuery.ts @@ -142,12 +142,11 @@ export const useExpandQuery = ({ ...config, }; case "SAVED_QUERY": - console.log(queryNode); const dataQuery: DragItemQuery = { ...queryNode, query: undefined, dragContext: { width: 0, height: 0 }, - label: "", // TODO: DOUBLE CHECK + label: "", // TODO: Clarify why there is no label at this point. tags: [], type: DNDType.PREVIOUS_QUERY, id: queryNode.query, diff --git a/frontend/src/js/editor-v2/types.ts b/frontend/src/js/editor-v2/types.ts index 671b3258a6..8237edc155 100644 --- a/frontend/src/js/editor-v2/types.ts +++ b/frontend/src/js/editor-v2/types.ts @@ -4,6 +4,9 @@ import { DragItemQuery, } from "../standard-query-editor/types"; +export type ConnectionKind = "and" | "or" | "before"; +export type DirectionKind = "horizontal" | "vertical"; + export interface Tree { id: string; parentId?: string; @@ -14,8 +17,8 @@ export interface Tree { }; data?: DragItemQuery | DragItemConceptTreeNode; children?: { - connection: "and" | "or" | "before"; - direction: "horizontal" | "vertical"; + connection: ConnectionKind; + direction: DirectionKind; items: Tree[]; }; } diff --git a/frontend/src/js/editor-v2/util.ts b/frontend/src/js/editor-v2/util.ts index f850a4ca49..e014f5327e 100644 --- a/frontend/src/js/editor-v2/util.ts +++ b/frontend/src/js/editor-v2/util.ts @@ -1,4 +1,7 @@ -import { Tree } from "./types"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; + +import { ConnectionKind, Tree } from "./types"; export const findNodeById = (tree: Tree, id: string): Tree | undefined => { if (tree.id === id) { @@ -14,3 +17,21 @@ export const findNodeById = (tree: Tree, id: string): Tree | undefined => { } return undefined; }; + +export const useTranslatedConnection = ( + connection: ConnectionKind | undefined, +) => { + const { t } = useTranslation(); + + return useMemo(() => { + if (connection === "and") { + return t("editorV2.and"); + } else if (connection === "or") { + return t("editorV2.or"); + } else if (connection === "before") { + return t("editorV2.before"); + } else { + return ""; + } + }, [t, connection]); +}; diff --git a/frontend/src/localization/de.json b/frontend/src/localization/de.json index 79ffcff974..83d55cd51c 100644 --- a/frontend/src/localization/de.json +++ b/frontend/src/localization/de.json @@ -514,5 +514,19 @@ "paste": "Aus Zwischenablage einfügen", "submit": "Übernehmen", "pasted": "Importiert" + }, + "editorV2": { + "before": "VOR", + "and": "UND", + "or": "ODER", + "clear": "Leeren", + "flip": "Drehen", + "dates": "Datum", + "negate": "Nicht", + "connector": "Verknüpfung", + "delete": "Löschen", + "expand": "Expandieren", + "edit": "Details", + "initialDropText": "Ziehe ein Konzept oder eine Anfrage hier hinein." } } diff --git a/frontend/src/localization/en.json b/frontend/src/localization/en.json index 52d4c1f0ef..fa8c4deac6 100644 --- a/frontend/src/localization/en.json +++ b/frontend/src/localization/en.json @@ -513,5 +513,19 @@ "paste": "Paste from clipboard", "submit": "Submit", "pasted": "Imported" + }, + "editorV2": { + "before": "BEFORe", + "and": "AND", + "or": "OR", + "clear": "Clear", + "flip": "Flip", + "dates": "Dates", + "negate": "Negate", + "connector": "Connector", + "delete": "Delete", + "expand": "Expand", + "edit": "Details", + "initialDropText": "Drop a concept or query here." } } From fd38fa801a26253dc5a138f0d1e158dd177aca7d Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 15 May 2023 13:36:41 +0200 Subject: [PATCH 17/93] Add query runner and start trying to submit query --- frontend/src/js/api/api.ts | 6 +- frontend/src/js/api/apiHelper.ts | 36 ++- frontend/src/js/app/reducers.ts | 2 + frontend/src/js/editor-v2/EditorV2.tsx | 251 +++++++++--------- .../src/js/editor-v2/EditorV2QueryRunner.tsx | 43 +++ frontend/src/js/editor-v2/types.ts | 4 + .../js/external-forms/FormsQueryRunner.tsx | 5 +- frontend/src/js/query-runner/QueryRunner.tsx | 10 +- frontend/src/js/query-runner/actions.ts | 13 +- .../StandardQueryRunner.tsx | 6 +- .../TimebasedQueryRunner.tsx | 2 +- 11 files changed, 241 insertions(+), 137 deletions(-) create mode 100644 frontend/src/js/editor-v2/EditorV2QueryRunner.tsx diff --git a/frontend/src/js/api/api.ts b/frontend/src/js/api/api.ts index b651aedcac..5b0a7f5836 100644 --- a/frontend/src/js/api/api.ts +++ b/frontend/src/js/api/api.ts @@ -1,5 +1,6 @@ import { useCallback } from "react"; +import { EditorV2Query } from "../editor-v2/types"; import { EntityId } from "../entity-history/reducer"; import { apiUrl } from "../environment"; import type { FormConfigT } from "../previous-queries/list/reducer"; @@ -95,7 +96,10 @@ export const usePostQueries = () => { return useCallback( ( datasetId: DatasetT["id"], - query: StandardQueryStateT | ValidatedTimebasedQueryStateT, + query: + | StandardQueryStateT + | ValidatedTimebasedQueryStateT + | EditorV2Query, options: { queryType: string; selectedSecondaryId?: string | null }, ) => api({ diff --git a/frontend/src/js/api/apiHelper.ts b/frontend/src/js/api/apiHelper.ts index 852e4185b3..0b18c8a12e 100644 --- a/frontend/src/js/api/apiHelper.ts +++ b/frontend/src/js/api/apiHelper.ts @@ -6,6 +6,7 @@ // Some keys are added (e.g. the query type attribute) import { isEmpty } from "../common/helpers/commonHelper"; import { exists } from "../common/helpers/exists"; +import { EditorV2Query, Tree } from "../editor-v2/types"; import { nodeIsConceptQueryNode } from "../model/node"; import { isLabelPristine } from "../standard-query-editor/helper"; import type { StandardQueryStateT } from "../standard-query-editor/queryReducer"; @@ -204,11 +205,42 @@ const transformTimebasedQueryToApi = (query: ValidatedTimebasedQueryStateT) => ), ); +const transformTreeToApi = (tree: Tree) => { + let dateRestriction; + if (tree.dates?.restriction) { + dateRestriction = createDateRestriction(tree.dates.restriction, tree); + } + + let negation; + if (tree.negation) { + negation = createNegation(tree); + } + + let combined; + if (dateRestriction && negation) { + combined = dateRestriction; + combined.child = negation; + } else if (dateRestriction) { + combined = dateRestriction; + } else if (negation) { + combined = negation; + } else { + combined = tree; + + tree.dates; +}; + +const transformEditorV2QueryToApi = (query: EditorV2Query) => { + if (!query.tree) return null; + + return transformTreeToApi(query.tree); +}; + // The query state already contains the query. // But small additions are made (properties allowlisted), empty things filtered out // to make it compatible with the backend API export const transformQueryToApi = ( - query: StandardQueryStateT | ValidatedTimebasedQueryStateT, + query: StandardQueryStateT | ValidatedTimebasedQueryStateT | EditorV2Query, options: { queryType: string; selectedSecondaryId?: string | null }, ) => { switch (options.queryType) { @@ -221,6 +253,8 @@ export const transformQueryToApi = ( query as StandardQueryStateT, options.selectedSecondaryId, ); + case "editorV2": + return transformEditorV2QueryToApi(query as EditorV2Query); default: return null; } diff --git a/frontend/src/js/app/reducers.ts b/frontend/src/js/app/reducers.ts index daf3f1c56e..af19ae2b33 100644 --- a/frontend/src/js/app/reducers.ts +++ b/frontend/src/js/app/reducers.ts @@ -64,6 +64,7 @@ export type StateT = { previousQueriesFolderFilter: PreviousQueriesFolderFilterStateT; preview: PreviewStateT; snackMessage: SnackMessageStateT; + editorV2QueryRunner: QueryRunnerStateT; queryEditor: { query: StandardQueryStateT; selectedSecondaryId: SelectedSecondaryIdStateT; @@ -101,6 +102,7 @@ const buildAppReducer = () => { preview, user, entityHistory, + editorV2QueryRunner: createQueryRunnerReducer("editorV2"), queryEditor: combineReducers({ query: queryReducer, selectedSecondaryId: selectedSecondaryIdsReducer, diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 3f93c20d80..ea4c3fa014 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -20,6 +20,7 @@ import { import Dropzone from "../ui-components/Dropzone"; import { Connector, Grid } from "./EditorLayout"; +import { EditorV2QueryRunner } from "./EditorV2QueryRunner"; import { TreeNode } from "./TreeNode"; import { EDITOR_DROP_TYPES } from "./config"; import { useConnectorEditing } from "./connector-update/useConnectorRotation"; @@ -31,6 +32,13 @@ import { Tree } from "./types"; import { findNodeById, useTranslatedConnection } from "./util"; const Root = styled("div")` + flex-grow: 1; + height: 100%; + display: flex; + flex-direction: column; +`; + +const Main = styled("div")` flex-grow: 1; height: 100%; padding: 8px 10px 10px 10px; @@ -200,129 +208,132 @@ export function EditorV2({ setSelectedNodeId(undefined); }} > - {showModal && selectedNode && ( - { - updateTreeNode(selectedNode.id, (node) => { - if (!node.dates) node.dates = {}; - node.dates.excluded = excluded; - }); - }} - onResetDates={() => - updateTreeNode(selectedNode.id, (node) => { - if (!node.dates) return; - node.dates.restriction = undefined; - }) - } - setDateRange={(dateRange) => { - updateTreeNode(selectedNode.id, (node) => { - if (!node.dates) node.dates = {}; - node.dates.restriction = dateRange; - }); - }} - /> - )} - - - {featureDates && selectedNode && ( - { - e.stopPropagation(); - onOpen(); - }} - > - {t("editorV2.dates")} - - )} - {featureNegate && selectedNode && ( - { - e.stopPropagation(); - onNegateClick(); - }} - > - {t("editorV2.negate")} - - )} - {selectedNode?.children && ( - { - e.stopPropagation(); - onFlip(); - }} - > - {t("editorV2.flip")} - - )} - {featureConnectorRotate && selectedNode?.children && ( - { - e.stopPropagation(); - onRotateConnector(); - }} - > - {t("editorV2.connector")} - {connection} - - )} - {canExpand && ( - { - e.stopPropagation(); - onExpand(); - }} - > - {t("editorV2.expand")} - - )} - {selectedNode && ( - { - e.stopPropagation(); - onDelete(); - }} - > - {t("editorV2.delete")} - - )} - - - {t("editorV2.clear")} - - - - {tree ? ( - - ) : ( - { - setTree({ - id: createId(), - data: item as DragItemConceptTreeNode | DragItemQuery, +
+ {showModal && selectedNode && ( + { + updateTreeNode(selectedNode.id, (node) => { + if (!node.dates) node.dates = {}; + node.dates.excluded = excluded; + }); + }} + onResetDates={() => + updateTreeNode(selectedNode.id, (node) => { + if (!node.dates) return; + node.dates.restriction = undefined; + }) + } + setDateRange={(dateRange) => { + updateTreeNode(selectedNode.id, (node) => { + if (!node.dates) node.dates = {}; + node.dates.restriction = dateRange; }); }} - acceptedDropTypes={EDITOR_DROP_TYPES} - > - {() =>
{t("editorV2.initialDropText")}
} - + /> )} - + + + {featureDates && selectedNode && ( + { + e.stopPropagation(); + onOpen(); + }} + > + {t("editorV2.dates")} + + )} + {featureNegate && selectedNode && ( + { + e.stopPropagation(); + onNegateClick(); + }} + > + {t("editorV2.negate")} + + )} + {selectedNode?.children && ( + { + e.stopPropagation(); + onFlip(); + }} + > + {t("editorV2.flip")} + + )} + {featureConnectorRotate && selectedNode?.children && ( + { + e.stopPropagation(); + onRotateConnector(); + }} + > + {t("editorV2.connector")} + {connection} + + )} + {canExpand && ( + { + e.stopPropagation(); + onExpand(); + }} + > + {t("editorV2.expand")} + + )} + {selectedNode && ( + { + e.stopPropagation(); + onDelete(); + }} + > + {t("editorV2.delete")} + + )} + + + {t("editorV2.clear")} + + + + {tree ? ( + + ) : ( + { + setTree({ + id: createId(), + data: item as DragItemConceptTreeNode | DragItemQuery, + }); + }} + acceptedDropTypes={EDITOR_DROP_TYPES} + > + {() =>
{t("editorV2.initialDropText")}
} +
+ )} +
+
+ ); } diff --git a/frontend/src/js/editor-v2/EditorV2QueryRunner.tsx b/frontend/src/js/editor-v2/EditorV2QueryRunner.tsx new file mode 100644 index 0000000000..432e007845 --- /dev/null +++ b/frontend/src/js/editor-v2/EditorV2QueryRunner.tsx @@ -0,0 +1,43 @@ +import { useSelector } from "react-redux"; + +import { StateT } from "../app/reducers"; +import { useDatasetId } from "../dataset/selectors"; +import QueryRunner from "../query-runner/QueryRunner"; +import { useStartQuery, useStopQuery } from "../query-runner/actions"; +import { QueryRunnerStateT } from "../query-runner/reducer"; + +import { EditorV2Query } from "./types"; + +export const EditorV2QueryRunner = ({ query }: { query: EditorV2Query }) => { + const datasetId = useDatasetId(); + const queryRunner = useSelector( + (state) => state.editorV2QueryRunner, + ); + const startStandardQuery = useStartQuery("editorV2"); + const stopStandardQuery = useStopQuery("editorV2"); + + const startQuery = () => { + if (datasetId) { + startStandardQuery(datasetId, query); + } + }; + + const queryId = queryRunner.runningQuery; + const stopQuery = () => { + if (queryId) { + stopStandardQuery(queryId); + } + }; + + const disabled = !query.tree; + + return ( + + ); +}; diff --git a/frontend/src/js/editor-v2/types.ts b/frontend/src/js/editor-v2/types.ts index 8237edc155..d4f7a82a11 100644 --- a/frontend/src/js/editor-v2/types.ts +++ b/frontend/src/js/editor-v2/types.ts @@ -22,3 +22,7 @@ export interface Tree { items: Tree[]; }; } + +export interface EditorV2Query { + tree?: Tree; +} diff --git a/frontend/src/js/external-forms/FormsQueryRunner.tsx b/frontend/src/js/external-forms/FormsQueryRunner.tsx index b79d022a09..0bbcff5a14 100644 --- a/frontend/src/js/external-forms/FormsQueryRunner.tsx +++ b/frontend/src/js/external-forms/FormsQueryRunner.tsx @@ -1,4 +1,3 @@ -import { FC } from "react"; import { useFormContext, useFormState } from "react-hook-form"; import { useSelector } from "react-redux"; @@ -37,7 +36,7 @@ const isButtonEnabled = ({ ); }; -const FormQueryRunner: FC = () => { +const FormQueryRunner = () => { const datasetId = useDatasetId(); const queryRunner = useSelector( selectQueryRunner, @@ -83,7 +82,7 @@ const FormQueryRunner: FC = () => { return ( void; stopQuery: () => void; @@ -52,7 +52,7 @@ const QueryRunner: FC = ({ stopQuery, buttonTooltip, isQueryRunning, - isButtonEnabled, + disabled, }) => { const btnAction = isQueryRunning ? stopQuery : startQuery; const isStartStopLoading = @@ -64,9 +64,9 @@ const QueryRunner: FC = ({ useHotkeys( "shift+enter", () => { - if (isButtonEnabled) btnAction(); + if (!disabled) btnAction(); }, - [isButtonEnabled, btnAction], + [disabled, btnAction], ); return ( @@ -77,7 +77,7 @@ const QueryRunner: FC = ({ onClick={btnAction} isStartStopLoading={isStartStopLoading} isQueryRunning={isQueryRunning} - disabled={!isButtonEnabled} + disabled={disabled} /> diff --git a/frontend/src/js/query-runner/actions.ts b/frontend/src/js/query-runner/actions.ts index ac51774bb9..1c713af6a1 100644 --- a/frontend/src/js/query-runner/actions.ts +++ b/frontend/src/js/query-runner/actions.ts @@ -23,6 +23,7 @@ import { errorPayload, successPayload, } from "../common/actions/genericActions"; +import { EditorV2Query } from "../editor-v2/types"; import { getExternalSupportedErrorMessage } from "../environment"; import { useLoadFormConfigs, @@ -58,7 +59,11 @@ export type QueryRunnerActions = ActionType< by sending a DELETE request for that query ID */ -export type QueryTypeT = "standard" | "timebased" | "externalForms"; +export type QueryTypeT = + | "standard" + | "editorV2" + | "timebased" + | "externalForms"; export const startQuery = createAsyncAction( "query-runners/START_QUERY_START", @@ -80,6 +85,7 @@ export const useStartQuery = (queryType: QueryTypeT) => { datasetId: DatasetT["id"], query: | StandardQueryStateT + | EditorV2Query | ValidatedTimebasedQueryStateT | FormQueryPostPayload, { @@ -96,7 +102,10 @@ export const useStartQuery = (queryType: QueryTypeT) => { : () => postQueries( datasetId, - query as StandardQueryStateT | ValidatedTimebasedQueryStateT, + query as + | StandardQueryStateT + | EditorV2Query + | ValidatedTimebasedQueryStateT, { queryType, selectedSecondaryId, diff --git a/frontend/src/js/standard-query-editor/StandardQueryRunner.tsx b/frontend/src/js/standard-query-editor/StandardQueryRunner.tsx index 56b94ce5a1..8ebc3a42bb 100644 --- a/frontend/src/js/standard-query-editor/StandardQueryRunner.tsx +++ b/frontend/src/js/standard-query-editor/StandardQueryRunner.tsx @@ -49,7 +49,7 @@ const StandardQueryRunner = () => { const isDatasetValid = validateDataset(datasetId); const hasQueryValidDates = validateQueryDates(query); const isQueryValid = validateQueryLength(query) && hasQueryValidDates; - const isQueryNotStartedOrStopped = validateQueryStartStop(queryRunner); + const queryStartStopReady = validateQueryStartStop(queryRunner); const buttonTooltip = useButtonTooltip(hasQueryValidDates); @@ -73,9 +73,7 @@ const StandardQueryRunner = () => { { return ( Date: Tue, 16 May 2023 17:55:52 +0200 Subject: [PATCH 18/93] Add keyboard shortcut tooltips, make send query possible --- frontend/src/js/api/apiHelper.ts | 72 +++++++-- frontend/src/js/button/IconButton.tsx | 6 +- .../src/js/common/components/KeyboardKey.tsx | 16 ++ frontend/src/js/editor-v2/EditorV2.tsx | 153 ++++++++++-------- .../js/editor-v2/KeyboardShortcutTooltip.tsx | 50 ++++++ frontend/src/js/editor-v2/TreeNode.tsx | 7 +- frontend/src/js/editor-v2/config.ts | 10 ++ frontend/src/localization/de.json | 3 +- frontend/src/localization/en.json | 3 +- 9 files changed, 236 insertions(+), 84 deletions(-) create mode 100644 frontend/src/js/common/components/KeyboardKey.tsx create mode 100644 frontend/src/js/editor-v2/KeyboardShortcutTooltip.tsx diff --git a/frontend/src/js/api/apiHelper.ts b/frontend/src/js/api/apiHelper.ts index 0b18c8a12e..7db40ec3ab 100644 --- a/frontend/src/js/api/apiHelper.ts +++ b/frontend/src/js/api/apiHelper.ts @@ -115,6 +115,11 @@ const createConceptQuery = (root: T) => ({ root, }); +const createOr = (children: T) => ({ + type: "OR" as const, + children, +}); + const createAnd = (children: T) => ({ type: "AND" as const, children, @@ -205,35 +210,76 @@ const transformTimebasedQueryToApi = (query: ValidatedTimebasedQueryStateT) => ), ); -const transformTreeToApi = (tree: Tree) => { +const transformTreeToApi = (tree: Tree): unknown => { let dateRestriction; if (tree.dates?.restriction) { - dateRestriction = createDateRestriction(tree.dates.restriction, tree); + dateRestriction = createDateRestriction(tree.dates.restriction, null); } let negation; if (tree.negation) { - negation = createNegation(tree); + negation = createNegation(null); + } + + let node; + if (!tree.children) { + if (!tree.data) { + throw new Error( + "Tree has no children and no data, this shouldn't happen.", + ); + } + + node = nodeIsConceptQueryNode(tree.data) + ? createQueryConcept(tree.data) + : createSavedQuery(tree.data.id); + } else { + switch (tree.children.connection) { + case "and": + node = createAnd(tree.children.items.map(transformTreeToApi)); + break; + case "or": + node = createOr(tree.children.items.map(transformTreeToApi)); + break; + case "before": + node = { + type: "BEFORE", + // TODO: + // ...days, + preceding: { + sampler: "EARLIEST", + child: transformTreeToApi(tree.children.items[0]), + }, + index: { + sampler: "EARLIEST", + child: transformTreeToApi(tree.children.items[1]), + }, + }; + break; + } } - let combined; if (dateRestriction && negation) { - combined = dateRestriction; - combined.child = negation; + return { + ...dateRestriction, + child: { + ...negation, + child: node, + }, + }; } else if (dateRestriction) { - combined = dateRestriction; - } else if (negation) { - combined = negation; + return { + ...dateRestriction, + child: node, + }; } else { - combined = tree; - - tree.dates; + return node; + } }; const transformEditorV2QueryToApi = (query: EditorV2Query) => { if (!query.tree) return null; - return transformTreeToApi(query.tree); + return createConceptQuery(transformTreeToApi(query.tree)); }; // The query state already contains the query. diff --git a/frontend/src/js/button/IconButton.tsx b/frontend/src/js/button/IconButton.tsx index 7e333f8f37..2de24b10fb 100644 --- a/frontend/src/js/button/IconButton.tsx +++ b/frontend/src/js/button/IconButton.tsx @@ -45,12 +45,12 @@ const SxBasicButton = styled(BasicButton)<{ }>` background-color: transparent; color: ${({ theme, active, secondary, red }) => - active + red + ? theme.col.red + : active ? theme.col.blueGrayDark : secondary ? theme.col.orange - : red - ? theme.col.red : theme.col.black}; opacity: ${({ frame }) => (frame ? 1 : 0.75)}; transition: opacity ${({ theme }) => theme.transitionTime}, diff --git a/frontend/src/js/common/components/KeyboardKey.tsx b/frontend/src/js/common/components/KeyboardKey.tsx new file mode 100644 index 0000000000..ba19a4788a --- /dev/null +++ b/frontend/src/js/common/components/KeyboardKey.tsx @@ -0,0 +1,16 @@ +import styled from "@emotion/styled"; +import { ReactNode } from "react"; + +const KeyShape = styled("kbd")` + padding: 2px 4px; + border: 1px solid ${({ theme }) => theme.col.grayLight}; + box-shadow: 0 0 3px 0 ${({ theme }) => theme.col.grayLight}; + font-size: ${({ theme }) => theme.font.xs}; + line-height: 1; + border-radius: ${({ theme }) => theme.borderRadius}; + text-transform: uppercase; +`; + +export const KeyboardKey = ({ children }: { children: ReactNode }) => ( + {children} +); diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index ea4c3fa014..bcfcbeee24 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -21,8 +21,9 @@ import Dropzone from "../ui-components/Dropzone"; import { Connector, Grid } from "./EditorLayout"; import { EditorV2QueryRunner } from "./EditorV2QueryRunner"; +import { KeyboardShortcutTooltip } from "./KeyboardShortcutTooltip"; import { TreeNode } from "./TreeNode"; -import { EDITOR_DROP_TYPES } from "./config"; +import { EDITOR_DROP_TYPES, HOTKEYS } from "./config"; import { useConnectorEditing } from "./connector-update/useConnectorRotation"; import { DateModal } from "./date-restriction/DateModal"; import { useDateEditing } from "./date-restriction/useDateEditing"; @@ -81,9 +82,9 @@ const useEditorState = () => { return findNodeById(tree, selectedNodeId); }, [tree, selectedNodeId]); - const onReset = () => { + const onReset = useCallback(() => { setTree(undefined); - }; + }, []); const updateTreeNode = useCallback( (id: string, update: (node: Tree) => void) => { @@ -164,9 +165,10 @@ export function EditorV2({ } }, [selectedNode, setTree, updateTreeNode]); - useHotkeys("del", onDelete, [onDelete]); - useHotkeys("backspace", onDelete, [onDelete]); - useHotkeys("f", onFlip, [onFlip]); + useHotkeys(HOTKEYS.delete[0].keyname, onDelete, [onDelete]); + useHotkeys(HOTKEYS.delete[1].keyname, onDelete, [onDelete]); + useHotkeys(HOTKEYS.flip.keyname, onFlip, [onFlip]); + useHotkeys(HOTKEYS.reset.keyname, onReset, [onReset]); const { canExpand, onExpand } = useExpandQuery({ enabled: featureExpand, @@ -238,76 +240,101 @@ export function EditorV2({ {featureDates && selectedNode && ( - { - e.stopPropagation(); - onOpen(); - }} - > - {t("editorV2.dates")} - + + { + e.stopPropagation(); + onOpen(); + }} + > + {t("editorV2.dates")} + + )} {featureNegate && selectedNode && ( - { - e.stopPropagation(); - onNegateClick(); - }} - > - {t("editorV2.negate")} - + + { + e.stopPropagation(); + onNegateClick(); + }} + > + {t("editorV2.negate")} + + )} {selectedNode?.children && ( - { - e.stopPropagation(); - onFlip(); - }} - > - {t("editorV2.flip")} - + + { + e.stopPropagation(); + onFlip(); + }} + > + {t("editorV2.flip")} + + )} {featureConnectorRotate && selectedNode?.children && ( - { - e.stopPropagation(); - onRotateConnector(); - }} + - {t("editorV2.connector")} - {connection} - + { + e.stopPropagation(); + onRotateConnector(); + }} + > + {t("editorV2.connector")} + {connection} + + )} {canExpand && ( - { - e.stopPropagation(); - onExpand(); - }} - > - {t("editorV2.expand")} - + + { + e.stopPropagation(); + onExpand(); + }} + > + {t("editorV2.expand")} + + )} {selectedNode && ( - { - e.stopPropagation(); - onDelete(); - }} - > - {t("editorV2.delete")} - + + { + e.stopPropagation(); + onDelete(); + }} + > + {t("editorV2.delete")} + + )} - - {t("editorV2.clear")} - + + + {t("editorV2.clear")} + + {tree ? ( diff --git a/frontend/src/js/editor-v2/KeyboardShortcutTooltip.tsx b/frontend/src/js/editor-v2/KeyboardShortcutTooltip.tsx new file mode 100644 index 0000000000..8d7124de45 --- /dev/null +++ b/frontend/src/js/editor-v2/KeyboardShortcutTooltip.tsx @@ -0,0 +1,50 @@ +import styled from "@emotion/styled"; +import { ReactElement } from "react"; +import { useTranslation } from "react-i18next"; + +import { KeyboardKey } from "../common/components/KeyboardKey"; +import WithTooltip from "../tooltip/WithTooltip"; + +const KeyTooltip = styled("div")` + padding: 8px 15px; + display: flex; + align-items: center; + gap: 5px; +`; + +const Keys = styled("div")` + display: flex; + align-items: center; + gap: 2px; +`; + +export const KeyboardShortcutTooltip = ({ + keyname, + children, +}: { + keyname: string; + children: ReactElement; +}) => { + const { t } = useTranslation(); + const keynames = keyname.split("+"); + + return ( + + {t("common.shortcut")}:{" "} + + {keynames.map((keyPart, i) => ( + <> + {keyPart} + {i < keynames.length - 1 && "+"} + + ))} + + + } + > + {children} + + ); +}; diff --git a/frontend/src/js/editor-v2/TreeNode.tsx b/frontend/src/js/editor-v2/TreeNode.tsx index 6838f1dc7a..dda372fc5b 100644 --- a/frontend/src/js/editor-v2/TreeNode.tsx +++ b/frontend/src/js/editor-v2/TreeNode.tsx @@ -1,6 +1,6 @@ import styled from "@emotion/styled"; import { createId } from "@paralleldrive/cuid2"; -import { memo, useMemo } from "react"; +import { memo } from "react"; import { useTranslation } from "react-i18next"; import { DNDType } from "../common/constants/dndTypes"; @@ -24,7 +24,7 @@ const Node = styled("div")<{ negated?: boolean; leaf?: boolean; }>` - padding: ${({ leaf }) => (leaf ? "8px 10px" : "12px")}; + padding: ${({ leaf }) => (leaf ? "8px 10px" : "20px")}; border: 1px solid ${({ negated, theme, selected }) => negated ? "red" : selected ? theme.col.gray : theme.col.grayMediumLight}; @@ -32,7 +32,7 @@ const Node = styled("div")<{ selected ? `inset 0px 0px 0px 1px ${theme.col.gray}` : "none"}; border-radius: ${({ theme }) => theme.borderRadius}; - width: ${({ leaf }) => (leaf ? "150px" : "inherit")}; + width: ${({ leaf }) => (leaf ? "180px" : "inherit")}; background-color: ${({ leaf, theme }) => (leaf ? "white" : theme.col.bg)}; cursor: pointer; display: flex; @@ -166,6 +166,7 @@ export function TreeNode({ node.id = newParentId; node.data = undefined; node.dates = undefined; + node.negation = false; const connection = treeParent?.children?.connection || tree.children?.connection; diff --git a/frontend/src/js/editor-v2/config.ts b/frontend/src/js/editor-v2/config.ts index 5dd3382b72..7fd22a71a3 100644 --- a/frontend/src/js/editor-v2/config.ts +++ b/frontend/src/js/editor-v2/config.ts @@ -5,3 +5,13 @@ export const EDITOR_DROP_TYPES = [ DNDType.PREVIOUS_QUERY, DNDType.PREVIOUS_SECONDARY_ID_QUERY, ]; + +export const HOTKEYS = { + expand: { keyname: "x" }, + negate: { keyname: "n" }, + editDates: { keyname: "d" }, + delete: [{ keyname: "backspace" }, { keyname: "del" }], + flip: { keyname: "f" }, + rotateConnector: { keyname: "c" }, + reset: { keyname: "shift+backspace" }, +}; diff --git a/frontend/src/localization/de.json b/frontend/src/localization/de.json index 83d55cd51c..669d114f28 100644 --- a/frontend/src/localization/de.json +++ b/frontend/src/localization/de.json @@ -170,7 +170,8 @@ "dateInvalid": "Ungültiges Datum", "missingLabel": "Unbenannt", "import": "Importieren", - "openFileDialog": "Datei auswählen" + "openFileDialog": "Datei auswählen", + "shortcut": "Kürzel" }, "tooltip": { "headline": "Info", diff --git a/frontend/src/localization/en.json b/frontend/src/localization/en.json index fa8c4deac6..4c79979470 100644 --- a/frontend/src/localization/en.json +++ b/frontend/src/localization/en.json @@ -170,7 +170,8 @@ "dateInvalid": "Invalid date", "missingLabel": "Unknown", "import": "Import", - "openFileDialog": "Select file" + "openFileDialog": "Select file", + "shortcut": "Key" }, "tooltip": { "headline": "Info", From b1369a0f565554b264b9a422c88deb35678193ef Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Wed, 17 May 2023 15:28:19 +0200 Subject: [PATCH 19/93] split JobManagerStatus by datasets for better display --- .../bakdata/conquery/commands/ShardNode.java | 26 ++-- .../conquery/models/jobs/ImportJob.java | 2 +- .../conquery/models/jobs/JobManager.java | 14 +- .../models/jobs/JobManagerStatus.java | 32 +++-- .../specific/UpdateJobManagerStatus.java | 26 ++-- .../models/worker/ShardNodeInformation.java | 47 +++++-- .../resources/admin/rest/AdminProcessor.java | 128 ++++++++---------- .../resources/admin/rest/AdminResource.java | 4 +- .../conquery/resources/admin/ui/jobs.html.ftl | 61 +++++---- 9 files changed, 178 insertions(+), 162 deletions(-) diff --git a/backend/src/main/java/com/bakdata/conquery/commands/ShardNode.java b/backend/src/main/java/com/bakdata/conquery/commands/ShardNode.java index cde4efe2c8..a2bb9e62a2 100644 --- a/backend/src/main/java/com/bakdata/conquery/commands/ShardNode.java +++ b/backend/src/main/java/com/bakdata/conquery/commands/ShardNode.java @@ -314,23 +314,27 @@ private void reportJobManagerStatus() { // Collect the ShardNode and all its workers jobs into a single queue - final JobManagerStatus jobManagerStatus = jobManager.reportStatus(); for (Worker worker : workers.getWorkers().values()) { - jobManagerStatus.getJobs().addAll(worker.getJobManager().reportStatus().getJobs()); - } - + final JobManagerStatus jobManagerStatus = new JobManagerStatus( + null, worker.getInfo().getDataset(), + worker.getJobManager().getJobStatus() + ); - try { - context.trySend(new UpdateJobManagerStatus(jobManagerStatus)); - } - catch (Exception e) { - log.warn("Failed to report job manager status", e); + try { + context.trySend(new UpdateJobManagerStatus(jobManagerStatus)); + } + catch (Exception e) { + log.warn("Failed to report job manager status", e); - if (config.isFailOnError()) { - System.exit(1); + if (config.isFailOnError()) { + System.exit(1); + } } } + + + } public boolean isBusy() { diff --git a/backend/src/main/java/com/bakdata/conquery/models/jobs/ImportJob.java b/backend/src/main/java/com/bakdata/conquery/models/jobs/ImportJob.java index d02838e864..3e9aa63cbe 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/jobs/ImportJob.java +++ b/backend/src/main/java/com/bakdata/conquery/models/jobs/ImportJob.java @@ -350,7 +350,7 @@ private Map> sendBuckets(Map starts, M private void awaitFreeJobQueue(WorkerInformation responsibleWorker) { try { - responsibleWorker.getConnectedShardNode().waitForFreeJobqueue(); + responsibleWorker.getConnectedShardNode().waitForFreeJobQueue(); } catch (InterruptedException e) { log.error("Interrupted while waiting for worker[{}] to have free space in queue", responsibleWorker, e); diff --git a/backend/src/main/java/com/bakdata/conquery/models/jobs/JobManager.java b/backend/src/main/java/com/bakdata/conquery/models/jobs/JobManager.java index 1783db1ae2..b9be29b718 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/jobs/JobManager.java +++ b/backend/src/main/java/com/bakdata/conquery/models/jobs/JobManager.java @@ -38,14 +38,12 @@ public void addFastJob(Job job) { fastExecutor.add(job); } - public JobManagerStatus reportStatus() { - - return new JobManagerStatus( - getSlowJobs() - .stream() - .map(job -> new JobStatus(job.getJobId(), job.getProgressReporter().getProgress(), job.getLabel(), job.isCancelled())) - .collect(Collectors.toList()) - ); + public List getJobStatus() { + return getSlowJobs().stream() + .map(job -> new JobStatus(job.getJobId(), job.getProgressReporter().getProgress(), job.getLabel(), job.isCancelled())) + .sorted() + .collect(Collectors.toList()); + } public List getSlowJobs() { diff --git a/backend/src/main/java/com/bakdata/conquery/models/jobs/JobManagerStatus.java b/backend/src/main/java/com/bakdata/conquery/models/jobs/JobManagerStatus.java index b2524f9b01..4c6329b4e4 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/jobs/JobManagerStatus.java +++ b/backend/src/main/java/com/bakdata/conquery/models/jobs/JobManagerStatus.java @@ -3,29 +3,36 @@ import java.time.Duration; import java.time.LocalDateTime; import java.util.Collection; -import java.util.SortedSet; -import java.util.TreeSet; +import javax.annotation.Nullable; import javax.validation.constraints.NotNull; +import com.bakdata.conquery.models.identifiable.ids.specific.DatasetId; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; -import lombok.NonNull; +import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; +import lombok.With; +import org.apache.commons.lang3.time.DurationFormatUtils; @Data @RequiredArgsConstructor(onConstructor_ = @JsonCreator) public class JobManagerStatus { - @NonNull + @With + @Nullable + private final String origin; + @Nullable + private final DatasetId dataset; @NotNull - private final LocalDateTime timestamp = LocalDateTime.now(); + @EqualsAndHashCode.Exclude + private final LocalDateTime timestamp; @NotNull - private final SortedSet jobs = new TreeSet<>(); - - public JobManagerStatus(Collection jobs) { - this.jobs.addAll(jobs); + @EqualsAndHashCode.Exclude + private final Collection jobs; + public JobManagerStatus(String origin, DatasetId dataset, Collection statuses) { + this(origin, dataset, LocalDateTime.now(), statuses); } public int size() { @@ -35,11 +42,8 @@ public int size() { // Used in AdminUIResource/jobs @JsonIgnore public String getAgeString() { - Duration duration = Duration.between(timestamp, LocalDateTime.now()); + final Duration duration = Duration.between(timestamp, LocalDateTime.now()); - if (duration.toSeconds() > 0) { - return Long.toString(duration.toSeconds()) + " s"; - } - return Long.toString(duration.toMillis()) + " ms"; + return DurationFormatUtils.formatDurationWords(duration.toMillis(), true, true); } } diff --git a/backend/src/main/java/com/bakdata/conquery/models/messages/network/specific/UpdateJobManagerStatus.java b/backend/src/main/java/com/bakdata/conquery/models/messages/network/specific/UpdateJobManagerStatus.java index 6668b6d446..86f50b302c 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/messages/network/specific/UpdateJobManagerStatus.java +++ b/backend/src/main/java/com/bakdata/conquery/models/messages/network/specific/UpdateJobManagerStatus.java @@ -8,31 +8,27 @@ import com.bakdata.conquery.models.messages.network.NetworkMessage; import com.bakdata.conquery.models.messages.network.NetworkMessageContext.ManagerNodeNetworkContext; import com.bakdata.conquery.models.worker.ShardNodeInformation; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; +import lombok.Data; import lombok.extern.slf4j.Slf4j; -@CPSType(id="UPDATE_JOB_MANAGER_STATUS", base=NetworkMessage.class) -@NoArgsConstructor @AllArgsConstructor @Getter @Setter @ToString(of = "status") +@CPSType(id = "UPDATE_JOB_MANAGER_STATUS", base = NetworkMessage.class) @Slf4j +@Data public class UpdateJobManagerStatus extends MessageToManagerNode { @NotNull - private JobManagerStatus status; + private final JobManagerStatus status; @Override public void react(ManagerNodeNetworkContext context) throws Exception { - ShardNodeInformation node = context.getNamespaces() - .getShardNodes() - .get(context.getRemoteAddress()); + final ShardNodeInformation node = context.getNamespaces() + .getShardNodes() + .get(context.getRemoteAddress()); if (node == null) { - log.error("Could not find ShardNode {}, I only know of {}", context.getRemoteAddress(), context.getNamespaces().getShardNodes().keySet()); - } - else { - node.setJobManagerStatus(status); + log.error("Could not find ShardNode `{}`, I only know of {}", context.getRemoteAddress(), context.getNamespaces().getShardNodes().keySet()); + return; } + // The shards don't know their own name so we attach it here + node.addJobManagerStatus(status.withOrigin(context.getRemoteAddress().toString())); } } diff --git a/backend/src/main/java/com/bakdata/conquery/models/worker/ShardNodeInformation.java b/backend/src/main/java/com/bakdata/conquery/models/worker/ShardNodeInformation.java index ff1b27d619..e369c9819b 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/worker/ShardNodeInformation.java +++ b/backend/src/main/java/com/bakdata/conquery/models/worker/ShardNodeInformation.java @@ -2,6 +2,8 @@ import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; +import java.util.HashSet; +import java.util.Set; import com.bakdata.conquery.io.mina.MessageSender; import com.bakdata.conquery.io.mina.NetworkSession; @@ -20,7 +22,11 @@ public class ShardNodeInformation extends MessageSender.Simple jobManagerStatus = new HashSet<>(); - /** - * Used to await/notify for full job-queues. - */ - @JsonIgnore - private final transient Object jobManagerSync = new Object(); + private LocalDateTime lastStatusTime = LocalDateTime.now(); public ShardNodeInformation(NetworkSession session, int backpressure) { super(session); @@ -55,7 +57,11 @@ private String getLatenessMetricName() { * Calculate the time in Milliseconds since we last received a {@link JobManagerStatus} from the corresponding shard. */ private long getMillisSinceLastStatus() { - return getJobManagerStatus().getTimestamp().until(LocalDateTime.now(), ChronoUnit.MILLIS); + if(getJobManagerStatus().isEmpty()){ + return -1; + } + + return lastStatusTime.until(LocalDateTime.now(), ChronoUnit.MILLIS); } @Override @@ -65,17 +71,32 @@ public void awaitClose() { SharedMetricRegistries.getDefault().remove(getLatenessMetricName()); } - public void setJobManagerStatus(JobManagerStatus status) { - jobManagerStatus = status; - if (status.size() < backpressure) { + public long calculatePressure() { + return jobManagerStatus.stream().mapToLong(status -> status.getJobs().size()).sum(); + } + + public void addJobManagerStatus(JobManagerStatus incoming) { + lastStatusTime = LocalDateTime.now(); + + synchronized (jobManagerStatus) { + // replace with new status + jobManagerStatus.remove(incoming); + jobManagerStatus.add(incoming); + } + + if (calculatePressure() < backpressure) { synchronized (jobManagerSync) { jobManagerSync.notifyAll(); } } } - public void waitForFreeJobqueue() throws InterruptedException { - if (jobManagerStatus.size() >= backpressure) { + public void waitForFreeJobQueue() throws InterruptedException { + if (jobManagerStatus.isEmpty()) { + return; + } + + if (calculatePressure() >= backpressure) { log.trace("Have to wait for free JobQueue (size = {})", jobManagerStatus.size()); synchronized (jobManagerSync) { jobManagerSync.wait(); diff --git a/backend/src/main/java/com/bakdata/conquery/resources/admin/rest/AdminProcessor.java b/backend/src/main/java/com/bakdata/conquery/resources/admin/rest/AdminProcessor.java index 2aa0277602..76261723dd 100644 --- a/backend/src/main/java/com/bakdata/conquery/resources/admin/rest/AdminProcessor.java +++ b/backend/src/main/java/com/bakdata/conquery/resources/admin/rest/AdminProcessor.java @@ -4,7 +4,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Objects; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.ScheduledExecutorService; @@ -28,10 +27,10 @@ import com.bakdata.conquery.models.jobs.JobManager; import com.bakdata.conquery.models.jobs.JobManagerStatus; import com.bakdata.conquery.models.worker.DatasetRegistry; +import com.bakdata.conquery.models.worker.Namespace; import com.bakdata.conquery.models.worker.ShardNodeInformation; import com.bakdata.conquery.util.ConqueryEscape; import com.fasterxml.jackson.databind.ObjectWriter; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.Multimap; import com.univocity.parsers.csv.CsvWriter; import groovy.lang.GroovyShell; @@ -58,14 +57,6 @@ public class AdminProcessor { private final Validator validator; private final ObjectWriter jsonWriter = Jackson.MAPPER.writer(); - - - public synchronized void addRole(Role role) throws JSONException { - ValidatorHelper.failOnError(log, validator.validate(role)); - log.trace("New role:\tLabel: {}\tName: {}\tId: {} ", role.getLabel(), role.getName(), role.getId()); - storage.addRole(role); - } - public void addRoles(List roles) { for (Role role : roles) { @@ -78,6 +69,12 @@ public void addRoles(List roles) { } } + public synchronized void addRole(Role role) throws JSONException { + ValidatorHelper.failOnError(log, validator.validate(role)); + log.trace("New role:\tLabel: {}\tName: {}\tId: {} ", role.getLabel(), role.getName(), role.getId()); + storage.addRole(role); + } + /** * Deletes the mandator, that is identified by the id. Its references are * removed from the users, the groups, and from the storage. @@ -106,7 +103,7 @@ public SortedSet getAllRoles() { /** * Handles creation of permissions. * - * @param owner to which the permission is assigned + * @param owner to which the permission is assigned * @param permission The permission to create. * @throws JSONException is thrown upon processing JSONs. */ @@ -117,8 +114,7 @@ public void createPermission(PermissionOwner owner, ConqueryPermission permis /** * Handles deletion of permissions. * - * - * @param owner the owner of the permission + * @param owner the owner of the permission * @param permission The permission to delete. */ public void deletePermission(PermissionOwner owner, ConqueryPermission permission) { @@ -138,11 +134,6 @@ public synchronized void deleteUser(User user) { log.trace("Removed user {} from the storage.", user.getId()); } - public void addUser(User user) { - storage.addUser(user); - log.trace("New user:\tLabel: {}\tName: {}\tId: {} ", user.getLabel(), user.getName(), user.getId()); - } - public void addUsers(List users) { for (User user : users) { @@ -155,15 +146,13 @@ public void addUsers(List users) { } } - public TreeSet getAllGroups() { - return new TreeSet<>(storage.getAllGroups()); + public void addUser(User user) { + storage.addUser(user); + log.trace("New user:\tLabel: {}\tName: {}\tId: {} ", user.getLabel(), user.getName(), user.getId()); } - public synchronized void addGroup(Group group) throws JSONException { - ValidatorHelper.failOnError(log, validator.validate(group)); - storage.addGroup(group); - log.trace("New group:\tLabel: {}\tName: {}\tId: {} ", group.getLabel(), group.getName(), group.getId()); - + public TreeSet getAllGroups() { + return new TreeSet<>(storage.getAllGroups()); } public void addGroups(List groups) { @@ -178,6 +167,13 @@ public void addGroups(List groups) { } } + public synchronized void addGroup(Group group) throws JSONException { + ValidatorHelper.failOnError(log, validator.validate(group)); + storage.addGroup(group); + log.trace("New group:\tLabel: {}\tName: {}\tId: {} ", group.getLabel(), group.getName(), group.getId()); + + } + public void addUserToGroup(Group group, User user) { group.addMember(user); log.trace("Added user {} to group {}", user, group); @@ -193,12 +189,12 @@ public void deleteGroup(Group group) { log.trace("Removed group {}", group); } - public void deleteRoleFrom(RoleOwner owner, Role role) { + public void deleteRoleFrom(RoleOwner owner, Role role) { owner.removeRole(role); log.trace("Removed role {} from {}", role, owner); } - public void addRoleTo(RoleOwner owner, Role role) { + public void addRoleTo(RoleOwner owner, Role role) { owner.addRole(role); log.trace("Added role {} to {}", role, owner); } @@ -210,23 +206,15 @@ public String getPermissionOverviewAsCSV() { return getPermissionOverviewAsCSV(storage.getAllUsers()); } - - /** - * Renders the permission overview for all users in a certain {@link Group} in form of a CSV. - */ - public String getPermissionOverviewAsCSV(Group group) { - return getPermissionOverviewAsCSV(group.getMembers().stream().map(storage::getUser).collect(Collectors.toList())); - } - /** * Renders the permission overview for certain {@link User} in form of a CSV. */ public String getPermissionOverviewAsCSV(Collection users) { - StringWriter sWriter = new StringWriter(); - CsvWriter writer = config.getCsv().createWriter(sWriter); - List scope = config - .getAuthorizationRealms() - .getOverviewScope(); + final StringWriter sWriter = new StringWriter(); + final CsvWriter writer = config.getCsv().createWriter(sWriter); + final List scope = config + .getAuthorizationRealms() + .getOverviewScope(); // Header writeAuthOverviewHeader(writer, scope); // Body @@ -240,7 +228,7 @@ public String getPermissionOverviewAsCSV(Collection users) { * Writes the header of the CSV auth overview to the specified writer. */ private static void writeAuthOverviewHeader(CsvWriter writer, List scope) { - List headers = new ArrayList<>(); + final List headers = new ArrayList<>(); headers.add("User"); headers.addAll(scope); writer.writeHeaders(headers); @@ -254,7 +242,7 @@ private static void writeAuthOverviewUser(CsvWriter writer, List scope, writer.addValue(String.format("%s %s", user.getLabel(), ConqueryEscape.unescape(user.getName()))); // Print the permission per domain in the remaining columns - Multimap permissions = AuthorizationHelper.getEffectiveUserPermissions(user, scope, storage); + final Multimap permissions = AuthorizationHelper.getEffectiveUserPermissions(user, scope, storage); for (String domain : scope) { writer.addValue(permissions.get(domain).stream() .map(Object::toString) @@ -262,41 +250,45 @@ private static void writeAuthOverviewUser(CsvWriter writer, List scope, } writer.writeValuesToRow(); } - public ImmutableMap getJobs() { - return ImmutableMap.builder() - .put("ManagerNode", getJobManager().reportStatus()) - // Namespace JobManagers on ManagerNode - .putAll( - getDatasetRegistry().getDatasets().stream() - .collect(Collectors.toMap( - ns -> String.format("ManagerNode::%s", ns.getDataset().getId()), - ns -> ns.getJobManager().reportStatus() - ))) - // Remote Worker JobManagers - .putAll( - getDatasetRegistry() - .getShardNodes() - .values() - .stream() - .collect(Collectors.toMap( - si -> Objects.toString(si.getRemoteAddress()), - ShardNodeInformation::getJobManagerStatus - )) - ) - .build(); + + /** + * Renders the permission overview for all users in a certain {@link Group} in form of a CSV. + */ + public String getPermissionOverviewAsCSV(Group group) { + return getPermissionOverviewAsCSV(group.getMembers().stream().map(storage::getUser).collect(Collectors.toList())); } public boolean isBusy() { //Note that this does not and cannot check for fast jobs! - return getJobs().values().stream() + return getJobs().stream() .map(JobManagerStatus::getJobs) .anyMatch(Predicate.not(Collection::isEmpty)); } + public Collection getJobs() { + final List out = new ArrayList<>(); + + out.add(new JobManagerStatus("Manager", null, getJobManager().getJobStatus())); + + for (Namespace namespace : getDatasetRegistry().getDatasets()) { + out.add(new JobManagerStatus( + "Manager", namespace.getDataset().getId(), + namespace.getJobManager().getJobStatus() + )); + } + + for (ShardNodeInformation si : getDatasetRegistry().getShardNodes().values()) { + out.addAll(si.getJobManagerStatus()); + } + + return out; + } public Object executeScript(String script) { - CompilerConfiguration config = new CompilerConfiguration(); - GroovyShell groovy = new GroovyShell(config); + + final CompilerConfiguration config = new CompilerConfiguration(); + final GroovyShell groovy = new GroovyShell(config); + groovy.setProperty("datasetRegistry", getDatasetRegistry()); groovy.setProperty("jobManager", getJobManager()); groovy.setProperty("config", getConfig()); @@ -305,7 +297,7 @@ public Object executeScript(String script) { try { return groovy.evaluate(script); } - catch(Exception e) { + catch (Exception e) { return ExceptionUtils.getStackTrace(e); } } diff --git a/backend/src/main/java/com/bakdata/conquery/resources/admin/rest/AdminResource.java b/backend/src/main/java/com/bakdata/conquery/resources/admin/rest/AdminResource.java index dec6699bb0..479e0e8751 100644 --- a/backend/src/main/java/com/bakdata/conquery/resources/admin/rest/AdminResource.java +++ b/backend/src/main/java/com/bakdata/conquery/resources/admin/rest/AdminResource.java @@ -3,6 +3,7 @@ import static com.bakdata.conquery.resources.ResourceConstants.JOB_ID; import java.time.LocalDate; +import java.util.Collection; import java.util.Objects; import java.util.Optional; import java.util.OptionalLong; @@ -35,7 +36,6 @@ import com.bakdata.conquery.models.worker.DatasetRegistry; import com.bakdata.conquery.models.worker.ShardNodeInformation; import com.bakdata.conquery.resources.admin.ui.AdminUIResource; -import com.google.common.collect.ImmutableMap; import io.dropwizard.auth.Auth; import lombok.RequiredArgsConstructor; @@ -89,7 +89,7 @@ public Response cancelJob(@PathParam(JOB_ID) UUID jobId) { @GET @Path("/jobs/") - public ImmutableMap getJobs() { + public Collection getJobs() { return processor.getJobs(); } diff --git a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl index 26e0847442..b6992afffd 100644 --- a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl +++ b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl @@ -1,12 +1,41 @@ <#import "templates/template.html.ftl" as layout> <@layout.layout> - <#list c as node, status> +
+
+ +
+ +
+
+ + + <#list c as status>
- ${node} + ${status.origin} ${status.dataset?} updated ${status.ageString} ago ${status.jobs?size} @@ -40,32 +69,4 @@
- -
-
- -
- -
-
\ No newline at end of file From d1c38e1309412de5b2d45792d5e0784c336d3849 Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Wed, 17 May 2023 16:38:51 +0200 Subject: [PATCH 20/93] adds missing JsonCreator to UpdateJobManagerStatus --- .../conquery/models/jobs/JobManagerStatus.java | 11 ++++++----- .../network/specific/UpdateJobManagerStatus.java | 7 ++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/backend/src/main/java/com/bakdata/conquery/models/jobs/JobManagerStatus.java b/backend/src/main/java/com/bakdata/conquery/models/jobs/JobManagerStatus.java index 4c6329b4e4..ec2397604c 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/jobs/JobManagerStatus.java +++ b/backend/src/main/java/com/bakdata/conquery/models/jobs/JobManagerStatus.java @@ -2,7 +2,7 @@ import java.time.Duration; import java.time.LocalDateTime; -import java.util.Collection; +import java.util.List; import javax.annotation.Nullable; import javax.validation.constraints.NotNull; @@ -10,15 +10,15 @@ import com.bakdata.conquery.models.identifiable.ids.specific.DatasetId; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.RequiredArgsConstructor; import lombok.With; import org.apache.commons.lang3.time.DurationFormatUtils; @Data -@RequiredArgsConstructor(onConstructor_ = @JsonCreator) +@AllArgsConstructor(onConstructor_ = @JsonCreator) public class JobManagerStatus { @With @Nullable @@ -30,8 +30,9 @@ public class JobManagerStatus { private final LocalDateTime timestamp; @NotNull @EqualsAndHashCode.Exclude - private final Collection jobs; - public JobManagerStatus(String origin, DatasetId dataset, Collection statuses) { + private final List jobs; + + public JobManagerStatus(String origin, DatasetId dataset, List statuses) { this(origin, dataset, LocalDateTime.now(), statuses); } diff --git a/backend/src/main/java/com/bakdata/conquery/models/messages/network/specific/UpdateJobManagerStatus.java b/backend/src/main/java/com/bakdata/conquery/models/messages/network/specific/UpdateJobManagerStatus.java index 86f50b302c..3f2c339ecf 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/messages/network/specific/UpdateJobManagerStatus.java +++ b/backend/src/main/java/com/bakdata/conquery/models/messages/network/specific/UpdateJobManagerStatus.java @@ -8,21 +8,22 @@ import com.bakdata.conquery.models.messages.network.NetworkMessage; import com.bakdata.conquery.models.messages.network.NetworkMessageContext.ManagerNodeNetworkContext; import com.bakdata.conquery.models.worker.ShardNodeInformation; +import com.fasterxml.jackson.annotation.JsonCreator; import lombok.Data; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @CPSType(id = "UPDATE_JOB_MANAGER_STATUS", base = NetworkMessage.class) @Slf4j @Data +@RequiredArgsConstructor(onConstructor_ = {@JsonCreator}) public class UpdateJobManagerStatus extends MessageToManagerNode { @NotNull private final JobManagerStatus status; @Override public void react(ManagerNodeNetworkContext context) throws Exception { - final ShardNodeInformation node = context.getNamespaces() - .getShardNodes() - .get(context.getRemoteAddress()); + final ShardNodeInformation node = context.getNamespaces().getShardNodes().get(context.getRemoteAddress()); if (node == null) { log.error("Could not find ShardNode `{}`, I only know of {}", context.getRemoteAddress(), context.getNamespaces().getShardNodes().keySet()); From 490697b9cbb9923ae4cdecddee5c4bed66915e1a Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Wed, 17 May 2023 16:50:20 +0200 Subject: [PATCH 21/93] removes faulty `?` in ftl --- .../com/bakdata/conquery/resources/admin/ui/jobs.html.ftl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl index b6992afffd..cc00ba8a93 100644 --- a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl +++ b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl @@ -35,7 +35,7 @@
- ${status.origin} ${status.dataset?} + ${status.origin} ${status.dataset} updated ${status.ageString} ago ${status.jobs?size} From 89ebd1fe25ae5efe1ce6a6ecb280d813389aa42a Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Wed, 17 May 2023 17:02:58 +0200 Subject: [PATCH 22/93] fixes null handling of dataset --- .../com/bakdata/conquery/resources/admin/ui/jobs.html.ftl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl index cc00ba8a93..a08129260e 100644 --- a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl +++ b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl @@ -35,7 +35,7 @@
- ${status.origin} ${status.dataset} + ${status.origin} ${(status.dataset)!} updated ${status.ageString} ago ${status.jobs?size} From 615536e8de72c5fe0ca7151fa513caee2ef2eb14 Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Wed, 17 May 2023 17:19:20 +0200 Subject: [PATCH 23/93] fix mapping of progress --- .../com/bakdata/conquery/resources/admin/ui/jobs.html.ftl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl index a08129260e..a29f0563d0 100644 --- a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl +++ b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl @@ -50,7 +50,7 @@
-
+
From 4b1ed62645127b085ffd0911e78e6e9e67966cad Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 22 May 2023 17:52:09 +0200 Subject: [PATCH 24/93] Add query node editing --- frontend/src/js/api/apiHelper.ts | 9 +- frontend/src/js/app/RightPane.tsx | 1 + frontend/src/js/editor-v2/EditorV2.tsx | 82 +++++++- .../js/editor-v2/KeyboardShortcutTooltip.tsx | 6 +- frontend/src/js/editor-v2/TreeNode.tsx | 5 +- frontend/src/js/editor-v2/config.ts | 1 + .../EditorV2QueryNodeEditor.tsx | 182 ++++++++++++++++++ .../query-node-edit/useQueryNodeEditing.ts | 34 ++++ .../js/query-node-editor/ContentColumn.tsx | 14 +- frontend/src/localization/de.json | 2 +- frontend/src/localization/en.json | 2 +- 11 files changed, 314 insertions(+), 24 deletions(-) create mode 100644 frontend/src/js/editor-v2/query-node-edit/EditorV2QueryNodeEditor.tsx create mode 100644 frontend/src/js/editor-v2/query-node-edit/useQueryNodeEditing.ts diff --git a/frontend/src/js/api/apiHelper.ts b/frontend/src/js/api/apiHelper.ts index 7db40ec3ab..815a3020f0 100644 --- a/frontend/src/js/api/apiHelper.ts +++ b/frontend/src/js/api/apiHelper.ts @@ -229,9 +229,7 @@ const transformTreeToApi = (tree: Tree): unknown => { ); } - node = nodeIsConceptQueryNode(tree.data) - ? createQueryConcept(tree.data) - : createSavedQuery(tree.data.id); + node = createQueryConcept(tree.data); } else { switch (tree.children.connection) { case "and": @@ -271,6 +269,11 @@ const transformTreeToApi = (tree: Tree): unknown => { ...dateRestriction, child: node, }; + } else if (negation) { + return { + ...negation, + child: node, + }; } else { return node; } diff --git a/frontend/src/js/app/RightPane.tsx b/frontend/src/js/app/RightPane.tsx index f3533aed26..3013cc98a9 100644 --- a/frontend/src/js/app/RightPane.tsx +++ b/frontend/src/js/app/RightPane.tsx @@ -90,6 +90,7 @@ const RightPane = () => { featureNegate featureExpand featureConnectorRotate + featureQueryNodeEdit /> ) : ( diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index bcfcbeee24..8c925f0799 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -3,6 +3,7 @@ import { faCalendar, faTrashCan } from "@fortawesome/free-regular-svg-icons"; import { faBan, faCircleNodes, + faEdit, faExpandArrowsAlt, faRefresh, faTrash, @@ -13,6 +14,7 @@ import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; import IconButton from "../button/IconButton"; +import { nodeIsConceptQueryNode } from "../model/node"; import { DragItemConceptTreeNode, DragItemQuery, @@ -29,6 +31,8 @@ import { DateModal } from "./date-restriction/DateModal"; import { useDateEditing } from "./date-restriction/useDateEditing"; import { useExpandQuery } from "./expand/useExpandQuery"; import { useNegationEditing } from "./negation/useNegationEditing"; +import { EditorV2QueryNodeEditor } from "./query-node-edit/EditorV2QueryNodeEditor"; +import { useQueryNodeEditing } from "./query-node-edit/useQueryNodeEditing"; import { Tree } from "./types"; import { findNodeById, useTranslatedConnection } from "./util"; @@ -113,11 +117,13 @@ export function EditorV2({ featureNegate, featureExpand, featureConnectorRotate, + featureQueryNodeEdit, }: { featureDates: boolean; featureNegate: boolean; featureExpand: boolean; featureConnectorRotate: boolean; + featureQueryNodeEdit: boolean; }) { const { t } = useTranslation(); const { @@ -187,30 +193,54 @@ export function EditorV2({ const { onNegateClick } = useNegationEditing({ enabled: featureNegate, - hotkey: "n", + hotkey: HOTKEYS.negate.keyname, selectedNode, updateTreeNode, }); const { onRotateConnector } = useConnectorEditing({ enabled: featureConnectorRotate, - hotkey: "c", + hotkey: HOTKEYS.rotateConnector.keyname, selectedNode, updateTreeNode, }); + const { + showModal: showQueryNodeEditor, + onOpen: onOpenQueryNodeEditor, + onClose: onCloseQueryNodeEditor, + } = useQueryNodeEditing({ + enabled: featureQueryNodeEdit, + hotkey: HOTKEYS.editQueryNode.keyname, + selectedNode, + }); + const connection = useTranslatedConnection( selectedNode?.children?.connection, ); + const onChangeData = useCallback( + (data: DragItemConceptTreeNode) => { + if (!selectedNode) return; + updateTreeNode(selectedNode.id, (node) => { + node.data = data; + }); + }, + [selectedNode, updateTreeNode], + ); + return ( - { - if (!selectedNode || showModal) return; - setSelectedNodeId(undefined); - }} - > +
+ {showQueryNodeEditor && + selectedNode?.data && + nodeIsConceptQueryNode(selectedNode.data) && ( + + )} {showModal && selectedNode && ( + {featureQueryNodeEdit && + selectedNode?.data && + nodeIsConceptQueryNode(selectedNode.data) && ( + + { + e.stopPropagation(); + onOpenQueryNodeEditor(); + }} + > + {t("editorV2.edit")} + + + )} {featureDates && selectedNode && ( - + { + if (!selectedNode || showModal) return; + setSelectedNodeId(undefined); + }} + > {tree ? ( { + e.stopPropagation(); + if (!selectedNode) return; + if ( + selectedNode?.data && + nodeIsConceptQueryNode(selectedNode.data) + ) { + onOpenQueryNodeEditor(); + } + }} tree={tree} updateTreeNode={updateTreeNode} selectedNode={selectedNode} diff --git a/frontend/src/js/editor-v2/KeyboardShortcutTooltip.tsx b/frontend/src/js/editor-v2/KeyboardShortcutTooltip.tsx index 8d7124de45..059894767f 100644 --- a/frontend/src/js/editor-v2/KeyboardShortcutTooltip.tsx +++ b/frontend/src/js/editor-v2/KeyboardShortcutTooltip.tsx @@ -1,5 +1,5 @@ import styled from "@emotion/styled"; -import { ReactElement } from "react"; +import { Fragment, ReactElement } from "react"; import { useTranslation } from "react-i18next"; import { KeyboardKey } from "../common/components/KeyboardKey"; @@ -35,10 +35,10 @@ export const KeyboardShortcutTooltip = ({ {t("common.shortcut")}:{" "} {keynames.map((keyPart, i) => ( - <> + {keyPart} {i < keynames.length - 1 && "+"} - + ))} diff --git a/frontend/src/js/editor-v2/TreeNode.tsx b/frontend/src/js/editor-v2/TreeNode.tsx index dda372fc5b..9cebacc2f4 100644 --- a/frontend/src/js/editor-v2/TreeNode.tsx +++ b/frontend/src/js/editor-v2/TreeNode.tsx @@ -1,6 +1,6 @@ import styled from "@emotion/styled"; import { createId } from "@paralleldrive/cuid2"; -import { memo } from "react"; +import { DOMAttributes, memo } from "react"; import { useTranslation } from "react-i18next"; import { DNDType } from "../common/constants/dndTypes"; @@ -119,6 +119,7 @@ export function TreeNode({ droppable, selectedNode, setSelectedNodeId, + onDoubleClick, }: { tree: Tree; treeParent?: Tree; @@ -129,6 +130,7 @@ export function TreeNode({ }; selectedNode: Tree | undefined; setSelectedNodeId: (id: Tree["id"] | undefined) => void; + onDoubleClick?: DOMAttributes["onDoubleClick"]; }) { const gridStyles = getGridStyles(tree); @@ -225,6 +227,7 @@ export function TreeNode({ negated={tree.negation} leaf={!tree.children} selected={selectedNode?.id === tree.id} + onDoubleClick={onDoubleClick} onClick={(e) => { e.stopPropagation(); setSelectedNodeId(tree.id); diff --git a/frontend/src/js/editor-v2/config.ts b/frontend/src/js/editor-v2/config.ts index 7fd22a71a3..8938a334f8 100644 --- a/frontend/src/js/editor-v2/config.ts +++ b/frontend/src/js/editor-v2/config.ts @@ -14,4 +14,5 @@ export const HOTKEYS = { flip: { keyname: "f" }, rotateConnector: { keyname: "c" }, reset: { keyname: "shift+backspace" }, + editQueryNode: { keyname: "Enter" }, }; diff --git a/frontend/src/js/editor-v2/query-node-edit/EditorV2QueryNodeEditor.tsx b/frontend/src/js/editor-v2/query-node-edit/EditorV2QueryNodeEditor.tsx new file mode 100644 index 0000000000..8793879912 --- /dev/null +++ b/frontend/src/js/editor-v2/query-node-edit/EditorV2QueryNodeEditor.tsx @@ -0,0 +1,182 @@ +import { useCallback } from "react"; + +import { SelectOptionT } from "../../api/types"; +import { NodeResetConfig } from "../../model/node"; +import { resetSelects } from "../../model/select"; +import { + resetTables, + tableIsEditable, + tableWithDefaults, +} from "../../model/table"; +import QueryNodeEditor from "../../query-node-editor/QueryNodeEditor"; +import { DragItemConceptTreeNode } from "../../standard-query-editor/types"; +import { ModeT } from "../../ui-components/InputRange"; + +export const EditorV2QueryNodeEditor = ({ + node, + onClose, + onChange, +}: { + node: DragItemConceptTreeNode; + onClose: () => void; + onChange: (node: DragItemConceptTreeNode) => void; +}) => { + const showTables = + node.tables.length > 1 && + node.tables.some((table) => tableIsEditable(table)); + + const onUpdateLabel = useCallback( + (label: string) => onChange({ ...node, label }), + [onChange, node], + ); + + const onToggleTable = useCallback( + (tableIdx: number, isExcluded: boolean) => { + const tables = [...node.tables]; + tables[tableIdx] = { ...tables[tableIdx], exclude: isExcluded }; + onChange({ ...node, tables }); + }, + [node, onChange], + ); + + const onLoadFilterSuggestions = useCallback( + ( + params: any, + tableIdx: number, + filterIdx: number, + config?: { returnOnly?: boolean }, + ) => { + return Promise.resolve(null); + }, + [], + ); + + const onDropConcept = useCallback( + (concept: DragItemConceptTreeNode) => { + const ids = [...node.ids, concept.ids[0]]; + onChange({ ...node, ids }); + }, + [node, onChange], + ); + + const onRemoveConcept = useCallback( + (conceptId: string) => { + const ids = node.ids.filter((id) => id !== conceptId); + onChange({ ...node, ids }); + }, + [node, onChange], + ); + + const onSelectSelects = useCallback( + (value: SelectOptionT[]) => { + onChange({ + ...node, + selects: node.selects.map((select) => ({ + ...select, + selected: !!value.find((s) => s.value === select.id), + })), + }); + }, + [node, onChange], + ); + + const onSelectTableSelects = useCallback( + (tableIdx: number, value: SelectOptionT[]) => { + const tables = [...node.tables]; + tables[tableIdx] = { + ...tables[tableIdx], + selects: tables[tableIdx].selects.map((select) => ({ + ...select, + selected: !!value.find((s) => s.value === select.id), + })), + }; + onChange({ ...node, tables }); + }, + [node, onChange], + ); + + const onSetFilterValue = useCallback( + (tableIdx: number, filterIdx: number, value: any) => { + const tables = [...node.tables]; + tables[tableIdx] = { + ...tables[tableIdx], + filters: tables[tableIdx].filters.map((filter, idx) => + idx === filterIdx ? { ...filter, value } : filter, + ), + }; + onChange({ ...node, tables }); + }, + [node, onChange], + ); + + const onSwitchFilterMode = useCallback( + (tableIdx: number, filterIdx: number, mode: ModeT) => { + const tables = [...node.tables]; + tables[tableIdx] = { + ...tables[tableIdx], + filters: tables[tableIdx].filters.map((filter, idx) => + idx === filterIdx ? { ...filter, mode } : filter, + ), + }; + onChange({ ...node, tables }); + }, + [node, onChange], + ); + + const onResetTable = useCallback( + (tableIdx: number, config: NodeResetConfig) => { + const table = node.tables[tableIdx]; + const resetTable = tableWithDefaults(config)(table); + + const tables = [...node.tables]; + tables[tableIdx] = resetTable; + onChange({ ...node, tables }); + }, + [node, onChange], + ); + + const onResetAllSettings = useCallback( + (config: NodeResetConfig) => { + const tables = resetTables(node.tables, config); + const selects = resetSelects(node.selects, config); + onChange({ ...node, tables, selects }); + }, + [node, onChange], + ); + + const onSetDateColumn = useCallback( + (tableIdx: number, value: string) => { + const tables = [...node.tables]; + tables[tableIdx] = { + ...tables[tableIdx], + dateColumn: { + ...tables[tableIdx].dateColumn!, + value, + }, + }; + onChange({ ...node, tables }); + }, + [node, onChange], + ); + + return ( + + ); +}; diff --git a/frontend/src/js/editor-v2/query-node-edit/useQueryNodeEditing.ts b/frontend/src/js/editor-v2/query-node-edit/useQueryNodeEditing.ts new file mode 100644 index 0000000000..a481c05c69 --- /dev/null +++ b/frontend/src/js/editor-v2/query-node-edit/useQueryNodeEditing.ts @@ -0,0 +1,34 @@ +import { useCallback, useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; + +import { Tree } from "../types"; + +export const useQueryNodeEditing = ({ + enabled, + hotkey, + selectedNode, +}: { + enabled: boolean; + hotkey: string; + selectedNode: Tree | undefined; +}) => { + const [showModal, setShowModal] = useState(false); + + const onClose = useCallback(() => setShowModal(false), []); + const onOpen = useCallback(() => { + if (!enabled) return; + if (!selectedNode) return; + + setShowModal(true); + }, [enabled, selectedNode]); + + useHotkeys(hotkey, onOpen, [onOpen], { + preventDefault: true, + }); + + return { + showModal, + onClose, + onOpen, + }; +}; diff --git a/frontend/src/js/query-node-editor/ContentColumn.tsx b/frontend/src/js/query-node-editor/ContentColumn.tsx index 58580cc673..667d813f5b 100644 --- a/frontend/src/js/query-node-editor/ContentColumn.tsx +++ b/frontend/src/js/query-node-editor/ContentColumn.tsx @@ -105,12 +105,14 @@ const ContentColumn: FC = ({ {t("queryNodeEditor.properties")} - + {(onToggleSecondaryIdExclude || onToggleTimestamps) && ( + + )} {nodeIsConceptQueryNode(node) && node.selects && ( Date: Tue, 23 May 2023 17:22:25 +0200 Subject: [PATCH 25/93] Fix dates and more --- frontend/src/js/editor-v2/TreeNode.tsx | 204 +++++++++++------- .../editor-v2/date-restriction/DateModal.tsx | 31 ++- .../editor-v2/date-restriction/DateRange.tsx | 30 ++- .../date-restriction/useDateEditing.ts | 2 +- frontend/src/js/icon/FaIcon.tsx | 5 +- .../src/js/ui-components/InputDateRange.tsx | 4 +- frontend/src/localization/de.json | 3 +- frontend/src/localization/en.json | 3 +- 8 files changed, 182 insertions(+), 100 deletions(-) diff --git a/frontend/src/js/editor-v2/TreeNode.tsx b/frontend/src/js/editor-v2/TreeNode.tsx index 9cebacc2f4..1c84aa310a 100644 --- a/frontend/src/js/editor-v2/TreeNode.tsx +++ b/frontend/src/js/editor-v2/TreeNode.tsx @@ -1,9 +1,11 @@ import styled from "@emotion/styled"; +import { faCalendarMinus } from "@fortawesome/free-regular-svg-icons"; import { createId } from "@paralleldrive/cuid2"; import { DOMAttributes, memo } from "react"; import { useTranslation } from "react-i18next"; import { DNDType } from "../common/constants/dndTypes"; +import { Icon } from "../icon/FaIcon"; import { nodeIsConceptQueryNode } from "../model/node"; import { getRootNodeLabel } from "../standard-query-editor/helper"; import Dropzone, { DropzoneProps } from "../ui-components/Dropzone"; @@ -23,13 +25,15 @@ const Node = styled("div")<{ selected?: boolean; negated?: boolean; leaf?: boolean; + isDragging?: boolean; }>` - padding: ${({ leaf }) => (leaf ? "8px 10px" : "20px")}; - border: 1px solid + padding: ${({ leaf, isDragging }) => + leaf ? "8px 10px" : isDragging ? "5px" : "24px"}; + border: 2px solid ${({ negated, theme, selected }) => negated ? "red" : selected ? theme.col.gray : theme.col.grayMediumLight}; box-shadow: ${({ selected, theme }) => - selected ? `inset 0px 0px 0px 1px ${theme.col.gray}` : "none"}; + selected ? `inset 0px 0px 0px 2px ${theme.col.gray}` : "none"}; border-radius: ${({ theme }) => theme.borderRadius}; width: ${({ leaf }) => (leaf ? "180px" : "inherit")}; @@ -56,9 +60,10 @@ function getGridStyles(tree: Tree) { } } -const InvisibleDropzoneContainer = styled(Dropzone)` +const InvisibleDropzoneContainer = styled(Dropzone)<{ bare?: boolean }>` width: 100%; height: 100%; + padding: ${({ bare }) => (bare ? "6px" : "20px")}; `; const InvisibleDropzone = ( @@ -77,10 +82,12 @@ const InvisibleDropzone = ( const Name = styled("div")` font-size: ${({ theme }) => theme.font.sm}; font-weight: 400; + color: ${({ theme }) => theme.col.black}; `; const Description = styled("div")` font-size: ${({ theme }) => theme.font.xs}; + color: ${({ theme }) => theme.col.black}; `; const PreviousQueryLabel = styled("p")` @@ -104,6 +111,9 @@ const RootNode = styled("p")` const Dates = styled("div")` text-align: right; + font-size: ${({ theme }) => theme.font.xs}; + text-transform: uppercase; + font-weight: 400; `; const Connection = memo(({ connection }: { connection?: ConnectionKind }) => { @@ -223,89 +233,119 @@ export function TreeNode({ } /> )} - { - e.stopPropagation(); - setSelectedNodeId(tree.id); - }} + {}} > - {tree.dates?.restriction && ( - - - - )} - {(!tree.children || tree.data) && ( -
- {tree.data?.type !== DNDType.CONCEPT_TREE_NODE && ( - - {t("queryEditor.previousQuery")} - + {({ canDrop }) => ( + { + e.stopPropagation(); + setSelectedNodeId(tree.id); + }} + > + {tree.dates?.restriction && ( + + + )} - {rootNodeLabel && {rootNodeLabel}} - {tree.data?.label && {tree.data.label}} - {tree.data && nodeIsConceptQueryNode(tree.data) && ( - {tree.data?.description} + {tree.dates?.excluded && ( + + + {t("editorV2.datesExcluded")} + )} -
- )} - {tree.children && ( - - onDropAtChildrenIdx({ idx: 0, item })} - /> - {tree.children.items.map((item, i, items) => ( - <> - - {i < items.length - 1 && ( - - onDropAtChildrenIdx({ idx: i + 1, item }) - } - > - {() => ( - - )} - + {(!tree.children || tree.data) && ( +
+ {tree.data?.type !== DNDType.CONCEPT_TREE_NODE && ( + + {t("queryEditor.previousQuery")} + + )} + {rootNodeLabel && {rootNodeLabel}} + {tree.data?.label && {tree.data.label}} + {tree.data && nodeIsConceptQueryNode(tree.data) && ( + {tree.data?.description} )} - - ))} - - onDropAtChildrenIdx({ - idx: tree.children!.items.length, - item, - }) - } - /> - +
+ )} + {tree.children && ( + + onDropAtChildrenIdx({ idx: 0, item })} + > + {() => ( + + )} + + {tree.children.items.map((item, i, items) => ( + <> + + {i < items.length - 1 && ( + + onDropAtChildrenIdx({ idx: i + 1, item }) + } + > + {() => ( + + )} + + )} + + ))} + + onDropAtChildrenIdx({ + idx: tree.children!.items.length, + item, + }) + } + > + {() => ( + + )} + + + )} +
)} - + {droppable.h && ( diff --git a/frontend/src/js/editor-v2/date-restriction/DateModal.tsx b/frontend/src/js/editor-v2/date-restriction/DateModal.tsx index 8385011f36..27da9d25a9 100644 --- a/frontend/src/js/editor-v2/date-restriction/DateModal.tsx +++ b/frontend/src/js/editor-v2/date-restriction/DateModal.tsx @@ -1,4 +1,5 @@ import styled from "@emotion/styled"; +import { faCalendarMinus } from "@fortawesome/free-regular-svg-icons"; import { faUndo } from "@fortawesome/free-solid-svg-icons"; import { useCallback, useMemo } from "react"; import { useHotkeys } from "react-hotkeys-hook"; @@ -7,6 +8,7 @@ import { useTranslation } from "react-i18next"; import { DateRangeT } from "../../api/types"; import IconButton from "../../button/IconButton"; import { DateStringMinMax } from "../../common/helpers/dateHelper"; +import { Icon } from "../../icon/FaIcon"; import Modal from "../../modal/Modal"; import InputCheckbox from "../../ui-components/InputCheckbox"; import InputDateRange from "../../ui-components/InputDateRange"; @@ -14,7 +16,16 @@ import InputDateRange from "../../ui-components/InputDateRange"; const Col = styled("div")` display: flex; flex-direction: column; - gap: 20px; + gap: 32px; +`; + +const SectionHeadline = styled("p")` + display: flex; + align-items: center; + gap: 10px; + margin: 0 0 10px; + font-size: ${({ theme }) => theme.font.md}; + font-weight: 400; `; const ResetAll = styled(IconButton)` @@ -74,8 +85,8 @@ export const DateModal = ({ doneButton headline={t("queryGroupModal.explanation")} > -
{headline}
+
{headline}
- +
+ + + {t("queryNodeEditor.excludeTimestamps")} + + +
); diff --git a/frontend/src/js/editor-v2/date-restriction/DateRange.tsx b/frontend/src/js/editor-v2/date-restriction/DateRange.tsx index 2371c75f49..f420539f06 100644 --- a/frontend/src/js/editor-v2/date-restriction/DateRange.tsx +++ b/frontend/src/js/editor-v2/date-restriction/DateRange.tsx @@ -1,6 +1,8 @@ import styled from "@emotion/styled"; +import { useTranslation } from "react-i18next"; import { DateRangeT } from "../../api/types"; +import { formatDate } from "../../common/helpers/dateHelper"; const Root = styled("div")` font-size: ${({ theme }) => theme.font.xs}; @@ -17,19 +19,35 @@ const Label = styled("div")` justify-self: flex-end; `; +const getFormattedDate = (date: string | undefined, dateFormat: string) => { + if (!date) return null; + + const d = new Date(date); + + if (isNaN(d.getTime())) return null; + + return formatDate(d, dateFormat); +}; + export const DateRange = ({ dateRange }: { dateRange: DateRangeT }) => { + const { t } = useTranslation(); + const dateFormat = t("inputDateRange.dateFormat"); + + const dateMin = getFormattedDate(dateRange.min, dateFormat); + const dateMax = getFormattedDate(dateRange.max, dateFormat); + return ( - {dateRange.min && ( + {dateMin && ( <> - - {dateRange.min} + + {dateMin} )} - {dateRange.max && dateRange.max !== dateRange.min && ( + {dateMax && dateMax !== dateMin && ( <> - - {dateRange.max} + + {dateMax} )} diff --git a/frontend/src/js/editor-v2/date-restriction/useDateEditing.ts b/frontend/src/js/editor-v2/date-restriction/useDateEditing.ts index ad205bef5f..5ce236992e 100644 --- a/frontend/src/js/editor-v2/date-restriction/useDateEditing.ts +++ b/frontend/src/js/editor-v2/date-restriction/useDateEditing.ts @@ -31,7 +31,7 @@ export const useDateEditing = ({ return ( selectedNode.data?.label || - (selectedNode.children?.items || []).map((c) => c.data?.label).join(" ") + (selectedNode.children?.items || []).map((c) => c.data?.label).join(" - ") ); }, [selectedNode]); diff --git a/frontend/src/js/icon/FaIcon.tsx b/frontend/src/js/icon/FaIcon.tsx index d970f310d7..36497900a4 100644 --- a/frontend/src/js/icon/FaIcon.tsx +++ b/frontend/src/js/icon/FaIcon.tsx @@ -10,6 +10,7 @@ export interface IconStyleProps { center?: boolean; right?: boolean; white?: boolean; + red?: boolean; light?: boolean; gray?: boolean; main?: boolean; @@ -48,9 +49,11 @@ export const Icon = styled(FontAwesomeIcon, { text-align: ${({ center }) => (center ? "center" : "left")}; font-size: ${({ theme, large, tiny }) => large ? theme.font.md : tiny ? theme.font.tiny : theme.font.sm}; - color: ${({ theme, white, gray, light, main, active, disabled }) => + color: ${({ theme, white, gray, red, light, main, active, disabled }) => disabled ? theme.col.grayMediumLight + : red + ? theme.col.red : gray ? theme.col.gray : active diff --git a/frontend/src/js/ui-components/InputDateRange.tsx b/frontend/src/js/ui-components/InputDateRange.tsx index e14e0458b8..82cc216f7b 100644 --- a/frontend/src/js/ui-components/InputDateRange.tsx +++ b/frontend/src/js/ui-components/InputDateRange.tsx @@ -1,5 +1,6 @@ import { css } from "@emotion/react"; import styled from "@emotion/styled"; +import { faCalendar } from "@fortawesome/free-regular-svg-icons"; import { FC, ReactNode, createRef, useMemo } from "react"; import { useTranslation } from "react-i18next"; @@ -12,6 +13,7 @@ import { DateStringMinMax, } from "../common/helpers/dateHelper"; import { exists } from "../common/helpers/exists"; +import { Icon } from "../icon/FaIcon"; import InfoTooltip from "../tooltip/InfoTooltip"; import InputDate from "./InputDate"; @@ -33,7 +35,6 @@ const StyledLabel = styled(Label)<{ large?: boolean }>` large && css` font-size: ${theme.font.md}; - margin: 20px 0 10px; `} `; @@ -175,6 +176,7 @@ const InputDateRange: FC = ({ return ( + {exists(indexPrefix) && # {indexPrefix}} {optional && } {label} diff --git a/frontend/src/localization/de.json b/frontend/src/localization/de.json index 4bf397fb7e..71dcdad502 100644 --- a/frontend/src/localization/de.json +++ b/frontend/src/localization/de.json @@ -528,6 +528,7 @@ "delete": "Löschen", "expand": "Expandieren", "edit": "Details", - "initialDropText": "Ziehe ein Konzept oder eine Anfrage hier hinein." + "initialDropText": "Ziehe ein Konzept oder eine Anfrage hier hinein.", + "datesExcluded": "Keine Datumswerte" } } diff --git a/frontend/src/localization/en.json b/frontend/src/localization/en.json index f4eb721b5b..99ce82dd82 100644 --- a/frontend/src/localization/en.json +++ b/frontend/src/localization/en.json @@ -527,6 +527,7 @@ "delete": "Delete", "expand": "Expand", "edit": "Details", - "initialDropText": "Drop a concept or query here." + "initialDropText": "Drop a concept or query here.", + "datesExcluded": "No dates" } } From c2be3f3dc8a68b2e3ebc0499d5c600bb067c546c Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 30 May 2023 10:58:29 +0200 Subject: [PATCH 26/93] Fix active state --- frontend/src/js/editor-v2/TreeNode.tsx | 225 +++++++++--------- frontend/src/js/model/node.ts | 26 ++ .../js/standard-query-editor/QueryNode.tsx | 15 +- 3 files changed, 149 insertions(+), 117 deletions(-) diff --git a/frontend/src/js/editor-v2/TreeNode.tsx b/frontend/src/js/editor-v2/TreeNode.tsx index 1c84aa310a..810eeb7aa4 100644 --- a/frontend/src/js/editor-v2/TreeNode.tsx +++ b/frontend/src/js/editor-v2/TreeNode.tsx @@ -6,8 +6,9 @@ import { useTranslation } from "react-i18next"; import { DNDType } from "../common/constants/dndTypes"; import { Icon } from "../icon/FaIcon"; -import { nodeIsConceptQueryNode } from "../model/node"; +import { nodeIsConceptQueryNode, useActiveState } from "../model/node"; import { getRootNodeLabel } from "../standard-query-editor/helper"; +import WithTooltip from "../tooltip/WithTooltip"; import Dropzone, { DropzoneProps } from "../ui-components/Dropzone"; import { Connector, Grid } from "./EditorLayout"; @@ -23,6 +24,7 @@ const NodeContainer = styled("div")` const Node = styled("div")<{ selected?: boolean; + active?: boolean; negated?: boolean; leaf?: boolean; isDragging?: boolean; @@ -30,10 +32,16 @@ const Node = styled("div")<{ padding: ${({ leaf, isDragging }) => leaf ? "8px 10px" : isDragging ? "5px" : "24px"}; border: 2px solid - ${({ negated, theme, selected }) => - negated ? "red" : selected ? theme.col.gray : theme.col.grayMediumLight}; + ${({ negated, theme, selected, active }) => + negated + ? theme.col.red + : active + ? theme.col.blueGrayDark + : selected + ? theme.col.gray + : theme.col.grayMediumLight}; box-shadow: ${({ selected, theme }) => - selected ? `inset 0px 0px 0px 2px ${theme.col.gray}` : "none"}; + selected ? `inset 0px 0px 0px 4px ${theme.col.blueGrayVeryLight}` : "none"}; border-radius: ${({ theme }) => theme.borderRadius}; width: ${({ leaf }) => (leaf ? "180px" : "inherit")}; @@ -148,6 +156,8 @@ export function TreeNode({ const rootNodeLabel = tree.data ? getRootNodeLabel(tree.data) : null; + const { active, tooltipText } = useActiveState(tree.data); + const onDropOutsideOfNode = ({ pos, direction, @@ -240,110 +250,113 @@ export function TreeNode({ onDrop={() => {}} > {({ canDrop }) => ( - { - e.stopPropagation(); - setSelectedNodeId(tree.id); - }} - > - {tree.dates?.restriction && ( - - - - )} - {tree.dates?.excluded && ( - - - {t("editorV2.datesExcluded")} - - )} - {(!tree.children || tree.data) && ( -
- {tree.data?.type !== DNDType.CONCEPT_TREE_NODE && ( - - {t("queryEditor.previousQuery")} - - )} - {rootNodeLabel && {rootNodeLabel}} - {tree.data?.label && {tree.data.label}} - {tree.data && nodeIsConceptQueryNode(tree.data) && ( - {tree.data?.description} - )} -
- )} - {tree.children && ( - - onDropAtChildrenIdx({ idx: 0, item })} - > - {() => ( - + + { + e.stopPropagation(); + setSelectedNodeId(tree.id); + }} + > + {tree.dates?.restriction && ( + + + + )} + {tree.dates?.excluded && ( + + + {t("editorV2.datesExcluded")} + + )} + {(!tree.children || tree.data) && ( +
+ {tree.data?.type !== DNDType.CONCEPT_TREE_NODE && ( + + {t("queryEditor.previousQuery")} + )} - - {tree.children.items.map((item, i, items) => ( - <> - - {i < items.length - 1 && ( - - onDropAtChildrenIdx({ idx: i + 1, item }) - } - > - {() => ( - - )} - - )} - - ))} - - onDropAtChildrenIdx({ - idx: tree.children!.items.length, - item, - }) - } - > - {() => ( - + {rootNodeLabel && {rootNodeLabel}} + {tree.data?.label && {tree.data.label}} + {tree.data && nodeIsConceptQueryNode(tree.data) && ( + {tree.data?.description} )} - - - )} - +
+ )} + {tree.children && ( + + onDropAtChildrenIdx({ idx: 0, item })} + > + {() => ( + + )} + + {tree.children.items.map((item, i, items) => ( + <> + + {i < items.length - 1 && ( + + onDropAtChildrenIdx({ idx: i + 1, item }) + } + > + {() => ( + + )} + + )} + + ))} + + onDropAtChildrenIdx({ + idx: tree.children!.items.length, + item, + }) + } + > + {() => ( + + )} + + + )} +
+
)} {droppable.h && ( diff --git a/frontend/src/js/model/node.ts b/frontend/src/js/model/node.ts index 92f2dc8012..4cf533d58e 100644 --- a/frontend/src/js/model/node.ts +++ b/frontend/src/js/model/node.ts @@ -7,6 +7,7 @@ import { faFolderOpen, faMinus, } from "@fortawesome/free-solid-svg-icons"; +import { useTranslation } from "react-i18next"; import { ConceptElementT, ConceptT } from "../api/types"; import { DNDType } from "../common/constants/dndTypes"; @@ -149,3 +150,28 @@ export const canNodeBeDropped = ( const itemHasConceptRoot = item.tree === node.tree; return itemHasConceptRoot && !itemAlreadyInNode; }; + +export const useActiveState = (node?: StandardQueryNodeT) => { + const { t } = useTranslation(); + + if (!node) { + return { + active: false, + tooltipText: undefined, + }; + } + + const hasNonDefaultSettings = !node.error && nodeHasNonDefaultSettings(node); + const hasFilterValues = nodeHasFilterValues(node); + + const tooltipText = hasNonDefaultSettings + ? t("queryEditor.hasNonDefaultSettings") + : hasFilterValues + ? t("queryEditor.hasDefaultSettings") + : undefined; + + return { + active: hasNonDefaultSettings || hasFilterValues, + tooltipText, + }; +}; diff --git a/frontend/src/js/standard-query-editor/QueryNode.tsx b/frontend/src/js/standard-query-editor/QueryNode.tsx index 73951a4ba5..353405fdd2 100644 --- a/frontend/src/js/standard-query-editor/QueryNode.tsx +++ b/frontend/src/js/standard-query-editor/QueryNode.tsx @@ -13,6 +13,7 @@ import { nodeHasFilterValues, nodeIsConceptQueryNode, canNodeBeDropped, + useActiveState, } from "../model/node"; import { isQueryExpandable } from "../model/query"; import { HoverNavigatable } from "../small-tab-navigation/HoverNavigatable"; @@ -93,21 +94,19 @@ const QueryNode = ({ onToggleTimestamps, onToggleSecondaryIdExclude, }: PropsT) => { - const { t } = useTranslation(); const rootNodeLabel = getRootNodeLabel(node); const ref = useRef(null); const activeSecondaryId = useSelector( (state) => state.queryEditor.selectedSecondaryId, ); - - const hasNonDefaultSettings = !node.error && nodeHasNonDefaultSettings(node); - const hasFilterValues = nodeHasFilterValues(node); const hasActiveSecondaryId = nodeHasActiveSecondaryId( node, activeSecondaryId, ); + const { active, tooltipText } = useActiveState(node); + const item: StandardQueryNodeT = { // Return the data describing the dragged item // NOT using `...node` since that would also spread `children` in. @@ -161,12 +160,6 @@ const QueryNode = ({ } as StandardQueryNodeT), }); - const tooltipText = hasNonDefaultSettings - ? t("queryEditor.hasNonDefaultSettings") - : hasFilterValues - ? t("queryEditor.hasDefaultSettings") - : undefined; - const expandClick = useCallback(() => { if (nodeIsConceptQueryNode(node) || !node.query) return; @@ -195,7 +188,7 @@ const QueryNode = ({ ref.current = instance; drag(instance); }} - active={hasNonDefaultSettings || hasFilterValues} + active={active} onClick={node.error ? undefined : () => onEditClick(andIdx, orIdx)} > Date: Tue, 30 May 2023 12:53:09 +0200 Subject: [PATCH 27/93] Add filter suggestions loading --- .../EditorV2QueryNodeEditor.tsx | 65 ++++++++++++++----- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/frontend/src/js/editor-v2/query-node-edit/EditorV2QueryNodeEditor.tsx b/frontend/src/js/editor-v2/query-node-edit/EditorV2QueryNodeEditor.tsx index 8793879912..7faecf403f 100644 --- a/frontend/src/js/editor-v2/query-node-edit/EditorV2QueryNodeEditor.tsx +++ b/frontend/src/js/editor-v2/query-node-edit/EditorV2QueryNodeEditor.tsx @@ -1,6 +1,12 @@ import { useCallback } from "react"; +import { + PostPrefixForSuggestionsParams, + usePostPrefixForSuggestions, +} from "../../api/api"; import { SelectOptionT } from "../../api/types"; +import { exists } from "../../common/helpers/exists"; +import { mergeFilterOptions } from "../../model/filter"; import { NodeResetConfig } from "../../model/node"; import { resetSelects } from "../../model/select"; import { @@ -9,6 +15,7 @@ import { tableWithDefaults, } from "../../model/table"; import QueryNodeEditor from "../../query-node-editor/QueryNodeEditor"; +import { filterSuggestionToSelectOption } from "../../query-node-editor/suggestionsHelper"; import { DragItemConceptTreeNode } from "../../standard-query-editor/types"; import { ModeT } from "../../ui-components/InputRange"; @@ -39,18 +46,6 @@ export const EditorV2QueryNodeEditor = ({ [node, onChange], ); - const onLoadFilterSuggestions = useCallback( - ( - params: any, - tableIdx: number, - filterIdx: number, - config?: { returnOnly?: boolean }, - ) => { - return Promise.resolve(null); - }, - [], - ); - const onDropConcept = useCallback( (concept: DragItemConceptTreeNode) => { const ids = [...node.ids, concept.ids[0]]; @@ -95,13 +90,13 @@ export const EditorV2QueryNodeEditor = ({ [node, onChange], ); - const onSetFilterValue = useCallback( - (tableIdx: number, filterIdx: number, value: any) => { + const setFilterProperties = useCallback( + (tableIdx: number, filterIdx: number, properties: any) => { const tables = [...node.tables]; tables[tableIdx] = { ...tables[tableIdx], filters: tables[tableIdx].filters.map((filter, idx) => - idx === filterIdx ? { ...filter, value } : filter, + idx === filterIdx ? { ...filter, ...properties } : filter, ), }; onChange({ ...node, tables }); @@ -109,6 +104,13 @@ export const EditorV2QueryNodeEditor = ({ [node, onChange], ); + const onSetFilterValue = useCallback( + (tableIdx: number, filterIdx: number, value: any) => { + setFilterProperties(tableIdx, filterIdx, { value }); + }, + [setFilterProperties], + ); + const onSwitchFilterMode = useCallback( (tableIdx: number, filterIdx: number, mode: ModeT) => { const tables = [...node.tables]; @@ -123,6 +125,39 @@ export const EditorV2QueryNodeEditor = ({ [node, onChange], ); + const postPrefixForSuggestions = usePostPrefixForSuggestions(); + const onLoadFilterSuggestions = useCallback( + async ( + params: PostPrefixForSuggestionsParams, + tableIdx: number, + filterIdx: number, + config?: { returnOnly?: boolean }, + ) => { + const suggestions = await postPrefixForSuggestions(params); + + if (!config?.returnOnly) { + const newOptions: SelectOptionT[] = suggestions.values.map( + filterSuggestionToSelectOption, + ); + + const filter = node.tables[tableIdx].filters[filterIdx]; + const options = + params.page === 0 + ? newOptions + : mergeFilterOptions(filter, newOptions); + + if (exists(options)) { + const props = { options, total: suggestions.total }; + + setFilterProperties(tableIdx, filterIdx, props); + } + } + + return suggestions; + }, + [postPrefixForSuggestions, node, setFilterProperties], + ); + const onResetTable = useCallback( (tableIdx: number, config: NodeResetConfig) => { const table = node.tables[tableIdx]; From 81430fd6fda04691f6d17b628eff5a4b44110f46 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 30 May 2023 15:23:14 +0200 Subject: [PATCH 28/93] Feature content infos --- frontend/src/js/app/RightPane.tsx | 1 + frontend/src/js/editor-v2/EditorV2.tsx | 222 +++++++++++++------------ frontend/src/js/editor-v2/TreeNode.tsx | 162 ++++++++++++++++-- frontend/src/localization/de.json | 4 +- frontend/src/localization/en.json | 4 +- 5 files changed, 272 insertions(+), 121 deletions(-) diff --git a/frontend/src/js/app/RightPane.tsx b/frontend/src/js/app/RightPane.tsx index 3013cc98a9..0764ff6aad 100644 --- a/frontend/src/js/app/RightPane.tsx +++ b/frontend/src/js/app/RightPane.tsx @@ -91,6 +91,7 @@ const RightPane = () => { featureExpand featureConnectorRotate featureQueryNodeEdit + featureContentInfos /> ) : ( diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 8c925f0799..4584a79180 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -15,6 +15,7 @@ import { useTranslation } from "react-i18next"; import IconButton from "../button/IconButton"; import { nodeIsConceptQueryNode } from "../model/node"; +import { EmptyQueryEditorDropzone } from "../standard-query-editor/EmptyQueryEditorDropzone"; import { DragItemConceptTreeNode, DragItemQuery, @@ -118,12 +119,14 @@ export function EditorV2({ featureExpand, featureConnectorRotate, featureQueryNodeEdit, + featureContentInfos, }: { featureDates: boolean; featureNegate: boolean; featureExpand: boolean; featureConnectorRotate: boolean; featureQueryNodeEdit: boolean; + featureContentInfos: boolean; }) { const { t } = useTranslation(); const { @@ -267,124 +270,126 @@ export function EditorV2({ }} /> )} - - - {featureQueryNodeEdit && - selectedNode?.data && - nodeIsConceptQueryNode(selectedNode.data) && ( + {tree && ( + + + {featureQueryNodeEdit && + selectedNode?.data && + nodeIsConceptQueryNode(selectedNode.data) && ( + + { + e.stopPropagation(); + onOpenQueryNodeEditor(); + }} + > + {t("editorV2.edit")} + + + )} + {featureDates && selectedNode && ( + + { + e.stopPropagation(); + onOpen(); + }} + > + {t("editorV2.dates")} + + + )} + {featureNegate && selectedNode && ( + + { + e.stopPropagation(); + onNegateClick(); + }} + > + {t("editorV2.negate")} + + + )} + {selectedNode?.children && ( + + { + e.stopPropagation(); + onFlip(); + }} + > + {t("editorV2.flip")} + + + )} + {featureConnectorRotate && selectedNode?.children && ( + { + e.stopPropagation(); + onRotateConnector(); + }} + > + {t("editorV2.connector")} + {connection} + + + )} + {canExpand && ( + { e.stopPropagation(); - onOpenQueryNodeEditor(); + onExpand(); }} > - {t("editorV2.edit")} + {t("editorV2.expand")} )} - {featureDates && selectedNode && ( - - { - e.stopPropagation(); - onOpen(); - }} - > - {t("editorV2.dates")} - - - )} - {featureNegate && selectedNode && ( - - { - e.stopPropagation(); - onNegateClick(); - }} - > - {t("editorV2.negate")} - - - )} - {selectedNode?.children && ( - - { - e.stopPropagation(); - onFlip(); - }} - > - {t("editorV2.flip")} - - - )} - {featureConnectorRotate && selectedNode?.children && ( - - { - e.stopPropagation(); - onRotateConnector(); - }} - > - {t("editorV2.connector")} - {connection} - - - )} - {canExpand && ( - - { - e.stopPropagation(); - onExpand(); - }} - > - {t("editorV2.expand")} - - - )} - {selectedNode && ( - - { - e.stopPropagation(); - onDelete(); - }} - > - {t("editorV2.delete")} - - - )} - - - - {t("editorV2.clear")} - - - + {selectedNode && ( + + { + e.stopPropagation(); + onDelete(); + }} + > + {t("editorV2.delete")} + + + )} + + + + {t("editorV2.clear")} + + + + )} { if (!selectedNode || showModal) return; @@ -408,6 +413,7 @@ export function EditorV2({ selectedNode={selectedNode} setSelectedNodeId={setSelectedNodeId} droppable={{ h: true, v: true }} + featureContentInfos={featureContentInfos} /> ) : ( - {() =>
{t("editorV2.initialDropText")}
} + {() => }
)}
diff --git a/frontend/src/js/editor-v2/TreeNode.tsx b/frontend/src/js/editor-v2/TreeNode.tsx index 810eeb7aa4..c58c23b3fc 100644 --- a/frontend/src/js/editor-v2/TreeNode.tsx +++ b/frontend/src/js/editor-v2/TreeNode.tsx @@ -5,9 +5,11 @@ import { DOMAttributes, memo } from "react"; import { useTranslation } from "react-i18next"; import { DNDType } from "../common/constants/dndTypes"; +import { exists } from "../common/helpers/exists"; import { Icon } from "../icon/FaIcon"; import { nodeIsConceptQueryNode, useActiveState } from "../model/node"; import { getRootNodeLabel } from "../standard-query-editor/helper"; +import { DragItemConceptTreeNode } from "../standard-query-editor/types"; import WithTooltip from "../tooltip/WithTooltip"; import Dropzone, { DropzoneProps } from "../ui-components/Dropzone"; @@ -44,7 +46,7 @@ const Node = styled("div")<{ selected ? `inset 0px 0px 0px 4px ${theme.col.blueGrayVeryLight}` : "none"}; border-radius: ${({ theme }) => theme.borderRadius}; - width: ${({ leaf }) => (leaf ? "180px" : "inherit")}; + width: ${({ leaf }) => (leaf ? "230px" : "inherit")}; background-color: ${({ leaf, theme }) => (leaf ? "white" : theme.col.bg)}; cursor: pointer; display: flex; @@ -96,10 +98,14 @@ const Name = styled("div")` const Description = styled("div")` font-size: ${({ theme }) => theme.font.xs}; color: ${({ theme }) => theme.col.black}; + display: flex; + align-items: center; + gap: 0px 5px; + flex-wrap: wrap; `; const PreviousQueryLabel = styled("p")` - margin: 0 0 4px; + margin: 0; line-height: 1.2; font-size: ${({ theme }) => theme.font.xs}; text-transform: uppercase; @@ -107,8 +113,14 @@ const PreviousQueryLabel = styled("p")` color: ${({ theme }) => theme.col.blueGrayDark}; `; +const ContentContainer = styled("div")` + display: flex; + flex-direction: column; + gap: 4px; +`; + const RootNode = styled("p")` - margin: 0 0 4px; + margin: 0; line-height: 1; text-transform: uppercase; font-weight: 700; @@ -124,11 +136,9 @@ const Dates = styled("div")` font-weight: 400; `; -const Connection = memo(({ connection }: { connection?: ConnectionKind }) => { - const message = useTranslatedConnection(connection); - - return {message}; -}); +const Bold = styled("span")` + font-weight: 400; +`; export function TreeNode({ tree, @@ -138,6 +148,7 @@ export function TreeNode({ selectedNode, setSelectedNodeId, onDoubleClick, + featureContentInfos, }: { tree: Tree; treeParent?: Tree; @@ -149,6 +160,7 @@ export function TreeNode({ selectedNode: Tree | undefined; setSelectedNodeId: (id: Tree["id"] | undefined) => void; onDoubleClick?: DOMAttributes["onDoubleClick"]; + featureContentInfos?: boolean; }) { const gridStyles = getGridStyles(tree); @@ -275,7 +287,7 @@ export function TreeNode({ )} {(!tree.children || tree.data) && ( -
+ {tree.data?.type !== DNDType.CONCEPT_TREE_NODE && ( {t("queryEditor.previousQuery")} @@ -284,9 +296,12 @@ export function TreeNode({ {rootNodeLabel && {rootNodeLabel}} {tree.data?.label && {tree.data.label}} {tree.data && nodeIsConceptQueryNode(tree.data) && ( - {tree.data?.description} + )} -
+ )} {tree.children && ( @@ -304,6 +319,7 @@ export function TreeNode({ <> ); } + +const Value = ({ + value, + isElement, +}: { + value: unknown; + isElement?: boolean; +}) => { + if (typeof value === "string" || typeof value === "number") { + return ( + + {value} + {isElement && ","} + + ); + } else if (typeof value === "boolean") { + return {value ? "" : "false"}; + } else if (value instanceof Array) { + return ( + <> + {value.slice(0, 10).map((v, idx) => ( + <> + + + ))} + {value.length > 10 && {`... +${value.length - 10}`}} + + ); + } else if ( + value instanceof Object && + "label" in value && + typeof value.label === "string" + ) { + return ( + + {value.label} + {isElement && ","} + + ); + } else if (value instanceof Object && "min" in value && "max" in value) { + return ( + + {JSON.stringify(value.min)}-{JSON.stringify(value.max)} + + ); + } else { + return {JSON.stringify(value)}; + } +}; + +const Connection = memo(({ connection }: { connection?: ConnectionKind }) => { + const message = useTranslatedConnection(connection); + + return {message}; +}); + +const SectionHeading = styled("h4")` + font-weight: 400; + color: ${(props) => props.theme.col.blueGrayDark}; + margin: 0; + text-transform: uppercase; + font-size: ${({ theme }) => theme.font.xs}; +`; + +const Appendix = styled("div")` + display: flex; + flex-direction: column; + gap: 6px; + margin-top: 8px; +`; + +const TreeNodeConcept = ({ + node, + featureContentInfos, +}: { + node: DragItemConceptTreeNode; + featureContentInfos?: boolean; +}) => { + const { t } = useTranslation(); + const selectedSelects = [ + ...node.selects, + ...node.tables.flatMap((t) => t.selects), + ].filter((s) => s.selected); + + const filtersWithValues = node.tables.flatMap((t) => + t.filters.filter( + (f) => + exists(f.value) && (!(f.value instanceof Array) || f.value.length > 0), + ), + ); + + const showAppendix = + featureContentInfos && + (selectedSelects.length > 0 || filtersWithValues.length > 0); + + return ( + <> + {node.description && {node.description}} + {showAppendix && ( + + {selectedSelects.length > 0 && ( +
+ {t("editorV2.outputSection")} + + + +
+ )} + {filtersWithValues.length > 0 && ( +
+ {t("editorV2.filtersSection")} + {filtersWithValues.map((f) => ( + + {f.label}: + + + ))} +
+ )} +
+ )} + + ); +}; diff --git a/frontend/src/localization/de.json b/frontend/src/localization/de.json index 71dcdad502..d34decb337 100644 --- a/frontend/src/localization/de.json +++ b/frontend/src/localization/de.json @@ -529,6 +529,8 @@ "expand": "Expandieren", "edit": "Details", "initialDropText": "Ziehe ein Konzept oder eine Anfrage hier hinein.", - "datesExcluded": "Keine Datumswerte" + "datesExcluded": "Keine Datumswerte", + "outputSection": "Ausgabewerte", + "filtersSection": "Filter" } } diff --git a/frontend/src/localization/en.json b/frontend/src/localization/en.json index 99ce82dd82..88b7453bcd 100644 --- a/frontend/src/localization/en.json +++ b/frontend/src/localization/en.json @@ -528,6 +528,8 @@ "expand": "Expand", "edit": "Details", "initialDropText": "Drop a concept or query here.", - "datesExcluded": "No dates" + "datesExcluded": "No dates", + "outputSection": "Output values", + "filtersSection": "Filters" } } From 2030c515fe1c180b6f5927a56c016d292bbd3a49 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 30 May 2023 15:46:03 +0200 Subject: [PATCH 29/93] Improve theme --- frontend/src/app-theme.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/app-theme.ts b/frontend/src/app-theme.ts index 7804351455..d7f0b9dd2c 100644 --- a/frontend/src/app-theme.ts +++ b/frontend/src/app-theme.ts @@ -7,16 +7,11 @@ import spinner from "./images/spinner.png"; export const theme: Theme = { col: { bg: "#fafafa", - bgAlt: "#f4f6f5", black: "#222", gray: "#888", grayMediumLight: "#aaa", grayLight: "#dadada", grayVeryLight: "#eee", - blueGrayDark: "#0C6427", - blueGray: "#72757C", - blueGrayLight: "#52A55C", - blueGrayVeryLight: "#A4E6AC", red: "#b22125", green: "#36971C", orange: "#E9711C", @@ -32,6 +27,11 @@ export const theme: Theme = { "#777", "#fff", ], + bgAlt: "#f4f6f5", + blueGrayDark: "#1f5f30", + blueGray: "#98b099", + blueGrayLight: "#ccd6d0", + blueGrayVeryLight: "#dadedb", }, img: { logo: logo, From b142e58940dcccd733cb923e7d2c3e374d563eca Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 30 May 2023 16:12:05 +0200 Subject: [PATCH 30/93] Fix interaction details --- frontend/src/js/editor-v2/EditorV2.tsx | 4 +++- frontend/src/js/editor-v2/TreeNode.tsx | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 4584a79180..721edc621d 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -418,10 +418,12 @@ export function EditorV2({ ) : ( { + const id = createId(); setTree({ - id: createId(), + id, data: item as DragItemConceptTreeNode | DragItemQuery, }); + setSelectedNodeId(id); }} acceptedDropTypes={EDITOR_DROP_TYPES} > diff --git a/frontend/src/js/editor-v2/TreeNode.tsx b/frontend/src/js/editor-v2/TreeNode.tsx index c58c23b3fc..b00e9e4d76 100644 --- a/frontend/src/js/editor-v2/TreeNode.tsx +++ b/frontend/src/js/editor-v2/TreeNode.tsx @@ -432,12 +432,20 @@ const Value = ({ {isElement && ","} ); - } else if (value instanceof Object && "min" in value && "max" in value) { + } else if (value instanceof Object) { return ( - - {JSON.stringify(value.min)}-{JSON.stringify(value.max)} - + <> + {Object.entries(value) + .filter(([, v]) => exists(v)) + .map(([k, v]) => ( + <> + {k}: + + ))} + ); + } else if (value === null) { + return ; } else { return {JSON.stringify(value)}; } From 063fa6d925a0b9f47e733fc2b858ea2f8d4527ef Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 30 May 2023 17:35:37 +0200 Subject: [PATCH 31/93] Fix boolean --- frontend/src/js/editor-v2/TreeNode.tsx | 2 +- frontend/src/localization/de.json | 2 +- frontend/src/localization/en.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/js/editor-v2/TreeNode.tsx b/frontend/src/js/editor-v2/TreeNode.tsx index b00e9e4d76..521dd68d39 100644 --- a/frontend/src/js/editor-v2/TreeNode.tsx +++ b/frontend/src/js/editor-v2/TreeNode.tsx @@ -409,7 +409,7 @@ const Value = ({ ); } else if (typeof value === "boolean") { - return {value ? "" : "false"}; + return {value ? "✔" : "✗"}; } else if (value instanceof Array) { return ( <> diff --git a/frontend/src/localization/de.json b/frontend/src/localization/de.json index d34decb337..d52333ef34 100644 --- a/frontend/src/localization/de.json +++ b/frontend/src/localization/de.json @@ -517,7 +517,7 @@ "pasted": "Importiert" }, "editorV2": { - "before": "VOR", + "before": "ZEIT", "and": "UND", "or": "ODER", "clear": "Leeren", diff --git a/frontend/src/localization/en.json b/frontend/src/localization/en.json index 88b7453bcd..f0f8c66a10 100644 --- a/frontend/src/localization/en.json +++ b/frontend/src/localization/en.json @@ -516,7 +516,7 @@ "pasted": "Imported" }, "editorV2": { - "before": "BEFORE", + "before": "TIME", "and": "AND", "or": "OR", "clear": "Clear", From 339a5d3da7486930a3c45ab4e681cad9ae0c18a9 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 5 Jun 2023 12:43:37 +0200 Subject: [PATCH 32/93] Add error boundary for forms tab --- frontend/src/js/app/RightPane.tsx | 5 +++- frontend/src/js/entity-history/History.tsx | 2 +- .../src/js/error-fallback/ErrorFallback.tsx | 25 ++++++++++++++++--- .../error-fallback/ResetableErrorBoundary.tsx | 25 +++++++++++++++++++ frontend/src/localization/de.json | 9 ++++--- 5 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 frontend/src/js/error-fallback/ResetableErrorBoundary.tsx diff --git a/frontend/src/js/app/RightPane.tsx b/frontend/src/js/app/RightPane.tsx index 801662bf27..1ce9a7b5ad 100644 --- a/frontend/src/js/app/RightPane.tsx +++ b/frontend/src/js/app/RightPane.tsx @@ -3,6 +3,7 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; +import { ResetableErrorBoundary } from "../error-fallback/ResetableErrorBoundary"; import FormsTab from "../external-forms/FormsTab"; import Pane from "../pane/Pane"; import { TabNavigationTab } from "../pane/TabNavigation"; @@ -59,7 +60,9 @@ const RightPane = () => { - + + + ); diff --git a/frontend/src/js/entity-history/History.tsx b/frontend/src/js/entity-history/History.tsx index 983b753deb..91e999409b 100644 --- a/frontend/src/js/entity-history/History.tsx +++ b/frontend/src/js/entity-history/History.tsx @@ -194,7 +194,7 @@ export const History = () => { onLoadFromFile={onLoadFromFile} onResetHistory={onResetEntityStatus} /> - + }>
diff --git a/frontend/src/js/error-fallback/ErrorFallback.tsx b/frontend/src/js/error-fallback/ErrorFallback.tsx index 8e31913064..febcedfd27 100644 --- a/frontend/src/js/error-fallback/ErrorFallback.tsx +++ b/frontend/src/js/error-fallback/ErrorFallback.tsx @@ -29,16 +29,33 @@ const ReloadButton = styled(TransparentButton)` margin-top: 10px; `; -const ErrorFallback = () => { +const ErrorFallback = ({ + allowFullRefresh, + onReset, +}: { + allowFullRefresh?: boolean; + onReset?: () => void; +}) => { const { t } = useTranslation(); return ( {t("error.sorry")} {t("error.description")} - window.location.reload()}> - {t("error.reload")} - + {allowFullRefresh && ( + <> + {t("error.reloadDescription")} + window.location.reload()}> + {t("error.reload")} + + + )} + {onReset && ( + <> + {t("error.resetDescription")} + {t("error.reset")} + + )} ); }; diff --git a/frontend/src/js/error-fallback/ResetableErrorBoundary.tsx b/frontend/src/js/error-fallback/ResetableErrorBoundary.tsx new file mode 100644 index 0000000000..a390b9d593 --- /dev/null +++ b/frontend/src/js/error-fallback/ResetableErrorBoundary.tsx @@ -0,0 +1,25 @@ +import { ReactNode, useCallback, useState } from "react"; +import { ErrorBoundary } from "react-error-boundary"; + +import ErrorFallback from "./ErrorFallback"; + +export const ResetableErrorBoundary = ({ + children, +}: { + children: ReactNode; +}) => { + const [resetKey, setResetKey] = useState(0); + const onReset = useCallback(() => setResetKey((key) => key + 1), []); + + return ( + ( + + )} + > + {children} + + ); +}; diff --git a/frontend/src/localization/de.json b/frontend/src/localization/de.json index 1b5843d915..8b21fe42c1 100644 --- a/frontend/src/localization/de.json +++ b/frontend/src/localization/de.json @@ -2,9 +2,12 @@ "locale": "de", "headline": "Anfragen und Analyse", "error": { - "sorry": "Das hat leider nicht geklappt", - "description": "Versuche es noch einmal. Falls es erneut nicht klappt, bitte hinterlasse uns eine Nachricht, damit wir dieses Problem beheben können.", - "reload": "Seite vollständig neu laden" + "sorry": "Da ist etwas schiefgelaufen!", + "description": "Aber Du hast nichts falsch gemacht. Das Problem liegt auf unserer Seite.", + "reset": "Zurücksetzen", + "resetDescription": "Versuche zurückzusetzen. Falls das Problem weiterhin besteht, bitte kontaktiere uns, damit wir das Problem schneller beheben können.", + "reload": "Seite vollständig neu laden", + "reloadDescription": "Bitte hinterlasse uns eine Nachricht, damit wir dieses Problem beheben können." }, "errorCodes": { "EXAMPLE_ERROR": "Dies ist eine Beispiel-Fehlermeldung", From 3a47f753a4e055ce310f15fb0f14188c312c3a54 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 5 Jun 2023 12:46:27 +0200 Subject: [PATCH 33/93] Add en localization --- frontend/src/localization/en.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/localization/en.json b/frontend/src/localization/en.json index 52d4c1f0ef..17e9a3595b 100644 --- a/frontend/src/localization/en.json +++ b/frontend/src/localization/en.json @@ -3,8 +3,11 @@ "headline": "Queries and Analyses", "error": { "sorry": "Sorry, something went wrong here", - "description": "Please try again. If this happens again, please leave us a message. That way, we can fix this issue.", - "reload": "Refresh page" + "description": "But it's not your fault. It's an issue on our side.", + "reset": "Reset", + "resetDescription": "Try to reset. If the issue happens again, please reach out to us. Then we can fix this issue sooner.", + "reload": "Refresh page", + "reloadDescription": "If this happens again, please leave us a message. That way, we can fix this issue sooner." }, "errorCodes": { "EXAMPLE_ERROR": "This is an example error", From ef1f49d6637871f307aa835f349628f3a6d4233d Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 5 Jun 2023 15:48:56 +0200 Subject: [PATCH 34/93] Remove unused imports --- frontend/src/js/standard-query-editor/QueryNode.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/js/standard-query-editor/QueryNode.tsx b/frontend/src/js/standard-query-editor/QueryNode.tsx index 353405fdd2..358c9f3e46 100644 --- a/frontend/src/js/standard-query-editor/QueryNode.tsx +++ b/frontend/src/js/standard-query-editor/QueryNode.tsx @@ -1,7 +1,6 @@ import styled from "@emotion/styled"; import { memo, useCallback, useRef } from "react"; import { useDrag } from "react-dnd"; -import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import type { QueryT } from "../api/types"; @@ -9,8 +8,6 @@ import { getWidthAndHeight } from "../app/DndProvider"; import type { StateT } from "../app/reducers"; import { getConceptById } from "../concept-trees/globalTreeStoreHelper"; import { - nodeHasNonDefaultSettings, - nodeHasFilterValues, nodeIsConceptQueryNode, canNodeBeDropped, useActiveState, From ecbd28cbb32283d60b0337cb3575e1d66c347d9e Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 5 Jun 2023 15:53:32 +0200 Subject: [PATCH 35/93] Add keys --- frontend/src/js/editor-v2/TreeNode.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/js/editor-v2/TreeNode.tsx b/frontend/src/js/editor-v2/TreeNode.tsx index 521dd68d39..efabf83689 100644 --- a/frontend/src/js/editor-v2/TreeNode.tsx +++ b/frontend/src/js/editor-v2/TreeNode.tsx @@ -415,7 +415,7 @@ const Value = ({ <> {value.slice(0, 10).map((v, idx) => ( <> - + ))} {value.length > 10 && {`... +${value.length - 10}`}} @@ -458,7 +458,7 @@ const Connection = memo(({ connection }: { connection?: ConnectionKind }) => { }); const SectionHeading = styled("h4")` - font-weight: 400; + font-weight: 700; color: ${(props) => props.theme.col.blueGrayDark}; margin: 0; text-transform: uppercase; From 61ce2a87fc0a3cbd17fcc34cf62160eb685c0f3e Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 6 Jun 2023 14:12:37 +0200 Subject: [PATCH 36/93] Start modeling time connection --- frontend/src/js/app/RightPane.tsx | 1 + frontend/src/js/editor-v2/EditorV2.tsx | 8 +- frontend/src/js/editor-v2/TimeConnection.tsx | 69 ++++++++ frontend/src/js/editor-v2/TreeNode.tsx | 152 +----------------- frontend/src/js/editor-v2/TreeNodeConcept.tsx | 145 +++++++++++++++++ .../connector-update/useConnectorRotation.ts | 41 ++++- frontend/src/js/editor-v2/types.ts | 31 +++- frontend/src/js/editor-v2/util.ts | 110 +++++++++++-- frontend/src/localization/de.json | 16 +- frontend/src/localization/en.json | 16 +- 10 files changed, 412 insertions(+), 177 deletions(-) create mode 100644 frontend/src/js/editor-v2/TimeConnection.tsx create mode 100644 frontend/src/js/editor-v2/TreeNodeConcept.tsx diff --git a/frontend/src/js/app/RightPane.tsx b/frontend/src/js/app/RightPane.tsx index 0764ff6aad..ad50815737 100644 --- a/frontend/src/js/app/RightPane.tsx +++ b/frontend/src/js/app/RightPane.tsx @@ -92,6 +92,7 @@ const RightPane = () => { featureConnectorRotate featureQueryNodeEdit featureContentInfos + featureTimebasedQueries /> ) : ( diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 721edc621d..1c81a6b264 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -35,7 +35,7 @@ import { useNegationEditing } from "./negation/useNegationEditing"; import { EditorV2QueryNodeEditor } from "./query-node-edit/EditorV2QueryNodeEditor"; import { useQueryNodeEditing } from "./query-node-edit/useQueryNodeEditing"; import { Tree } from "./types"; -import { findNodeById, useTranslatedConnection } from "./util"; +import { findNodeById, useGetTranslatedConnection } from "./util"; const Root = styled("div")` flex-grow: 1; @@ -120,6 +120,7 @@ export function EditorV2({ featureConnectorRotate, featureQueryNodeEdit, featureContentInfos, + featureTimebasedQueries, }: { featureDates: boolean; featureNegate: boolean; @@ -127,6 +128,7 @@ export function EditorV2({ featureConnectorRotate: boolean; featureQueryNodeEdit: boolean; featureContentInfos: boolean; + featureTimebasedQueries: boolean; }) { const { t } = useTranslation(); const { @@ -203,6 +205,7 @@ export function EditorV2({ const { onRotateConnector } = useConnectorEditing({ enabled: featureConnectorRotate, + timebasedQueriesEnabled: featureTimebasedQueries, hotkey: HOTKEYS.rotateConnector.keyname, selectedNode, updateTreeNode, @@ -218,7 +221,8 @@ export function EditorV2({ selectedNode, }); - const connection = useTranslatedConnection( + const getTranslatedConnection = useGetTranslatedConnection(); + const connection = getTranslatedConnection( selectedNode?.children?.connection, ); diff --git a/frontend/src/js/editor-v2/TimeConnection.tsx b/frontend/src/js/editor-v2/TimeConnection.tsx new file mode 100644 index 0000000000..e233ee511c --- /dev/null +++ b/frontend/src/js/editor-v2/TimeConnection.tsx @@ -0,0 +1,69 @@ +import styled from "@emotion/styled"; +import { memo } from "react"; +import { useTranslation } from "react-i18next"; + +import { TreeChildrenTime } from "./types"; +import { + useGetNodeLabel, + useGetTranslatedTimestamp, + useTranslatedInterval, + useTranslatedOperator, +} from "./util"; + +const TimeConnectionContainer = styled("div")` + display: flex; + align-items: center; + gap: 5px; + font-size: ${({ theme }) => theme.font.xs}; +`; + +const ConceptName = styled("span")` + font-weight: bold; + color: ${({ theme }) => theme.col.blueGrayDark}; +`; +const Timestamp = styled("span")` + font-weight: bold; + color: ${({ theme }) => theme.col.palette[6]}; +`; +const Interval = styled("span")` + font-weight: bold; + color: ${({ theme }) => theme.col.orange}; +`; +const Operator = styled("span")` + font-weight: bold; + color: ${({ theme }) => theme.col.green}; +`; + +export const TimeConnection = memo( + ({ conditions }: { conditions: TreeChildrenTime }) => { + const { t } = useTranslation(); + const getNodeLabel = useGetNodeLabel(); + const getTranslatedTimestamp = useGetTranslatedTimestamp(); + + const aTimestamp = getTranslatedTimestamp(conditions.timestamps[0]); + const bTimestamp = getTranslatedTimestamp(conditions.timestamps[1]); + const a = getNodeLabel(conditions.items[0]); + const b = getNodeLabel(conditions.items[1]); + const operator = useTranslatedOperator(conditions.operator); + const interval = useTranslatedInterval(conditions.interval); + + return ( +
+ + {aTimestamp} + {t("editorV2.dateRangeFrom")} + {a} + + + {interval} + {operator} + + + {bTimestamp} + {t("editorV2.dateRangeFrom")} + {b} + +
+ ); + }, +); diff --git a/frontend/src/js/editor-v2/TreeNode.tsx b/frontend/src/js/editor-v2/TreeNode.tsx index efabf83689..9d1fb34c15 100644 --- a/frontend/src/js/editor-v2/TreeNode.tsx +++ b/frontend/src/js/editor-v2/TreeNode.tsx @@ -5,19 +5,19 @@ import { DOMAttributes, memo } from "react"; import { useTranslation } from "react-i18next"; import { DNDType } from "../common/constants/dndTypes"; -import { exists } from "../common/helpers/exists"; import { Icon } from "../icon/FaIcon"; import { nodeIsConceptQueryNode, useActiveState } from "../model/node"; import { getRootNodeLabel } from "../standard-query-editor/helper"; -import { DragItemConceptTreeNode } from "../standard-query-editor/types"; import WithTooltip from "../tooltip/WithTooltip"; import Dropzone, { DropzoneProps } from "../ui-components/Dropzone"; import { Connector, Grid } from "./EditorLayout"; +import { TimeConnection } from "./TimeConnection"; +import { TreeNodeConcept } from "./TreeNodeConcept"; import { EDITOR_DROP_TYPES } from "./config"; import { DateRange } from "./date-restriction/DateRange"; import { ConnectionKind, Tree } from "./types"; -import { useTranslatedConnection } from "./util"; +import { useGetTranslatedConnection } from "./util"; const NodeContainer = styled("div")` display: grid; @@ -95,15 +95,6 @@ const Name = styled("div")` color: ${({ theme }) => theme.col.black}; `; -const Description = styled("div")` - font-size: ${({ theme }) => theme.font.xs}; - color: ${({ theme }) => theme.col.black}; - display: flex; - align-items: center; - gap: 0px 5px; - flex-wrap: wrap; -`; - const PreviousQueryLabel = styled("p")` margin: 0; line-height: 1.2; @@ -136,10 +127,6 @@ const Dates = styled("div")` font-weight: 400; `; -const Bold = styled("span")` - font-weight: 400; -`; - export function TreeNode({ tree, treeParent, @@ -275,6 +262,9 @@ export function TreeNode({ setSelectedNodeId(tree.id); }} > + {tree.children && tree.children.connection === "time" && ( + + )} {tree.dates?.restriction && ( @@ -394,134 +384,8 @@ export function TreeNode({ ); } -const Value = ({ - value, - isElement, -}: { - value: unknown; - isElement?: boolean; -}) => { - if (typeof value === "string" || typeof value === "number") { - return ( - - {value} - {isElement && ","} - - ); - } else if (typeof value === "boolean") { - return {value ? "✔" : "✗"}; - } else if (value instanceof Array) { - return ( - <> - {value.slice(0, 10).map((v, idx) => ( - <> - - - ))} - {value.length > 10 && {`... +${value.length - 10}`}} - - ); - } else if ( - value instanceof Object && - "label" in value && - typeof value.label === "string" - ) { - return ( - - {value.label} - {isElement && ","} - - ); - } else if (value instanceof Object) { - return ( - <> - {Object.entries(value) - .filter(([, v]) => exists(v)) - .map(([k, v]) => ( - <> - {k}: - - ))} - - ); - } else if (value === null) { - return ; - } else { - return {JSON.stringify(value)}; - } -}; - const Connection = memo(({ connection }: { connection?: ConnectionKind }) => { - const message = useTranslatedConnection(connection); + const getTranslatedConnection = useGetTranslatedConnection(); - return {message}; + return {getTranslatedConnection(connection)}; }); - -const SectionHeading = styled("h4")` - font-weight: 700; - color: ${(props) => props.theme.col.blueGrayDark}; - margin: 0; - text-transform: uppercase; - font-size: ${({ theme }) => theme.font.xs}; -`; - -const Appendix = styled("div")` - display: flex; - flex-direction: column; - gap: 6px; - margin-top: 8px; -`; - -const TreeNodeConcept = ({ - node, - featureContentInfos, -}: { - node: DragItemConceptTreeNode; - featureContentInfos?: boolean; -}) => { - const { t } = useTranslation(); - const selectedSelects = [ - ...node.selects, - ...node.tables.flatMap((t) => t.selects), - ].filter((s) => s.selected); - - const filtersWithValues = node.tables.flatMap((t) => - t.filters.filter( - (f) => - exists(f.value) && (!(f.value instanceof Array) || f.value.length > 0), - ), - ); - - const showAppendix = - featureContentInfos && - (selectedSelects.length > 0 || filtersWithValues.length > 0); - - return ( - <> - {node.description && {node.description}} - {showAppendix && ( - - {selectedSelects.length > 0 && ( -
- {t("editorV2.outputSection")} - - - -
- )} - {filtersWithValues.length > 0 && ( -
- {t("editorV2.filtersSection")} - {filtersWithValues.map((f) => ( - - {f.label}: - - - ))} -
- )} -
- )} - - ); -}; diff --git a/frontend/src/js/editor-v2/TreeNodeConcept.tsx b/frontend/src/js/editor-v2/TreeNodeConcept.tsx new file mode 100644 index 0000000000..72a81a71b0 --- /dev/null +++ b/frontend/src/js/editor-v2/TreeNodeConcept.tsx @@ -0,0 +1,145 @@ +import styled from "@emotion/styled"; +import { Fragment } from "react"; +import { useTranslation } from "react-i18next"; + +import { exists } from "../common/helpers/exists"; +import { DragItemConceptTreeNode } from "../standard-query-editor/types"; + +const Bold = styled("span")` + font-weight: 400; +`; + +const SectionHeading = styled("h4")` + font-weight: 700; + color: ${(props) => props.theme.col.blueGrayDark}; + margin: 0; + text-transform: uppercase; + font-size: ${({ theme }) => theme.font.xs}; +`; + +const Appendix = styled("div")` + display: flex; + flex-direction: column; + gap: 6px; + margin-top: 8px; +`; + +const Description = styled("div")` + font-size: ${({ theme }) => theme.font.xs}; + color: ${({ theme }) => theme.col.black}; + display: flex; + align-items: center; + gap: 0px 5px; + flex-wrap: wrap; +`; + +export const TreeNodeConcept = ({ + node, + featureContentInfos, +}: { + node: DragItemConceptTreeNode; + featureContentInfos?: boolean; +}) => { + const { t } = useTranslation(); + const selectedSelects = [ + ...node.selects, + ...node.tables.flatMap((t) => t.selects), + ].filter((s) => s.selected); + + const filtersWithValues = node.tables.flatMap((t) => + t.filters.filter( + (f) => + exists(f.value) && (!(f.value instanceof Array) || f.value.length > 0), + ), + ); + + const showAppendix = + featureContentInfos && + (selectedSelects.length > 0 || filtersWithValues.length > 0); + + return ( + <> + {node.description && {node.description}} + {showAppendix && ( + + {selectedSelects.length > 0 && ( +
+ {t("editorV2.outputSection")} + + + +
+ )} + {filtersWithValues.length > 0 && ( +
+ {t("editorV2.filtersSection")} + {filtersWithValues.map((f) => ( + + {f.label}: + + + ))} +
+ )} +
+ )} + + ); +}; + +const Value = ({ + value, + isElement, +}: { + value: unknown; + isElement?: boolean; +}) => { + if (typeof value === "string" || typeof value === "number") { + return ( + + {value} + {isElement && ","} + + ); + } else if (typeof value === "boolean") { + return {value ? "✔" : "✗"}; + } else if (value instanceof Array) { + return ( + <> + {value.slice(0, 10).map((v, idx) => ( + <> + + + ))} + {value.length > 10 && {`... +${value.length - 10}`}} + + ); + } else if ( + value instanceof Object && + "label" in value && + typeof value.label === "string" + ) { + return ( + + {value.label} + {isElement && ","} + + ); + } else if (value instanceof Object) { + return ( + <> + {Object.entries(value) + .filter(([, v]) => exists(v)) + .map(([k, v]) => ( + + {k}: + + ))} + + ); + } else if (value === null) { + return ; + } else { + return {JSON.stringify(value)}; + } +}; diff --git a/frontend/src/js/editor-v2/connector-update/useConnectorRotation.ts b/frontend/src/js/editor-v2/connector-update/useConnectorRotation.ts index 34b956702d..cc44406fc6 100644 --- a/frontend/src/js/editor-v2/connector-update/useConnectorRotation.ts +++ b/frontend/src/js/editor-v2/connector-update/useConnectorRotation.ts @@ -1,22 +1,49 @@ import { useCallback } from "react"; import { useHotkeys } from "react-hotkeys-hook"; -import { Tree } from "../types"; +import { ConnectionKind, Tree, TreeChildren } from "../types"; -const CONNECTORS = ["and", "or", "before"] as const; +const CONNECTIONS = ["and", "or", "time"] as ConnectionKind[]; -const getNextConnector = (connector: (typeof CONNECTORS)[number]) => { - const index = CONNECTORS.indexOf(connector); - return CONNECTORS[(index + 1) % CONNECTORS.length]; +const getNextConnector = ( + children: TreeChildren, + timebasedQueriesEnabled: boolean, +) => { + const allowedConnectors = timebasedQueriesEnabled + ? CONNECTIONS + : CONNECTIONS.filter((c) => c !== "time"); + + const index = allowedConnectors.indexOf(children.connection); + + const nextConnector = + allowedConnectors[(index + 1) % allowedConnectors.length]; + + if (nextConnector !== "time") { + return { + items: children.items, + direction: children.direction, + connection: nextConnector, + }; + } else { + return { + items: children.items, + direction: children.direction, + connection: "time" as const, + timestamps: children.items.map(() => "some" as const), + operator: "before" as const, + }; + } }; export const useConnectorEditing = ({ enabled, + timebasedQueriesEnabled, hotkey, selectedNode, updateTreeNode, }: { enabled: boolean; + timebasedQueriesEnabled: boolean; hotkey: string; selectedNode: Tree | undefined; updateTreeNode: (id: string, update: (node: Tree) => void) => void; @@ -27,9 +54,9 @@ export const useConnectorEditing = ({ updateTreeNode(selectedNode.id, (node) => { if (!node.children) return; - node.children.connection = getNextConnector(node.children.connection); + node.children = getNextConnector(node.children, timebasedQueriesEnabled); }); - }, [enabled, selectedNode, updateTreeNode]); + }, [enabled, selectedNode, updateTreeNode, timebasedQueriesEnabled]); useHotkeys(hotkey, onRotateConnector, [onRotateConnector]); diff --git a/frontend/src/js/editor-v2/types.ts b/frontend/src/js/editor-v2/types.ts index d4f7a82a11..c18470186d 100644 --- a/frontend/src/js/editor-v2/types.ts +++ b/frontend/src/js/editor-v2/types.ts @@ -4,7 +4,7 @@ import { DragItemQuery, } from "../standard-query-editor/types"; -export type ConnectionKind = "and" | "or" | "before"; +export type ConnectionKind = "and" | "or" | "time"; export type DirectionKind = "horizontal" | "vertical"; export interface Tree { @@ -16,12 +16,33 @@ export interface Tree { excluded?: boolean; }; data?: DragItemQuery | DragItemConceptTreeNode; - children?: { - connection: ConnectionKind; - direction: DirectionKind; - items: Tree[]; + children?: TreeChildren; +} + +export interface TreeChildrenBase { + direction: DirectionKind; + items: Tree[]; +} + +export interface TreeChildrenAnd extends TreeChildrenBase { + connection: "and"; +} +export interface TreeChildrenOr extends TreeChildrenBase { + connection: "or"; +} + +export type TimeTimestamp = "some" | "earliest" | "latest"; +export type TimeOperator = "before" | "after" | "while"; +export interface TreeChildrenTime extends TreeChildrenBase { + connection: "time"; + operator: TimeOperator; + timestamps: TimeTimestamp[]; // items.length + interval?: { + min?: number; + max?: number; }; } +export type TreeChildren = TreeChildrenAnd | TreeChildrenOr | TreeChildrenTime; export interface EditorV2Query { tree?: Tree; diff --git a/frontend/src/js/editor-v2/util.ts b/frontend/src/js/editor-v2/util.ts index e014f5327e..c133585556 100644 --- a/frontend/src/js/editor-v2/util.ts +++ b/frontend/src/js/editor-v2/util.ts @@ -1,7 +1,7 @@ -import { useMemo } from "react"; +import { useCallback } from "react"; import { useTranslation } from "react-i18next"; -import { ConnectionKind, Tree } from "./types"; +import { ConnectionKind, Tree, TreeChildrenTime } from "./types"; export const findNodeById = (tree: Tree, id: string): Tree | undefined => { if (tree.id === id) { @@ -18,20 +18,100 @@ export const findNodeById = (tree: Tree, id: string): Tree | undefined => { return undefined; }; -export const useTranslatedConnection = ( - connection: ConnectionKind | undefined, +const getNodeLabel = ( + node: Tree, + getTranslatedConnection: ReturnType, +): string => { + if (node.data?.label) { + return node.data.label; + } else if (node.children) { + return node.children.items + .map((n) => getNodeLabel(n, getTranslatedConnection)) + .join(" " + getTranslatedConnection(node.children.connection) + " "); + } else { + return ""; + } +}; + +export const useGetNodeLabel = (): ((node: Tree) => string) => { + const getTranslatedConnection = useGetTranslatedConnection(); + + return useCallback( + (node: Tree) => getNodeLabel(node, getTranslatedConnection), + [getTranslatedConnection], + ); +}; + +export const useGetTranslatedConnection = () => { + const { t } = useTranslation(); + + return useCallback( + (connection: ConnectionKind | undefined) => { + if (connection === "and") { + return t("editorV2.and"); + } else if (connection === "or") { + return t("editorV2.or"); + } else if (connection === "time") { + return t("editorV2.time"); + } else { + return ""; + } + }, + [t], + ); +}; + +export const useGetTranslatedTimestamp = () => { + const { t } = useTranslation(); + + return useCallback( + (timestamp: "every" | "some" | "earliest" | "latest") => { + if (timestamp === "every") { + return t("editorV2.every"); + } else if (timestamp === "some") { + return t("editorV2.some"); + } else if (timestamp === "earliest") { + return t("editorV2.earliest"); + } else if (timestamp === "latest") { + return t("editorV2.latest"); + } else { + return ""; + } + }, + [t], + ); +}; + +export const useTranslatedOperator = ( + operator: "before" | "after" | "while", ) => { const { t } = useTranslation(); - return useMemo(() => { - if (connection === "and") { - return t("editorV2.and"); - } else if (connection === "or") { - return t("editorV2.or"); - } else if (connection === "before") { - return t("editorV2.before"); - } else { - return ""; - } - }, [t, connection]); + if (operator === "before") { + return t("editorV2.before"); + } else if (operator === "after") { + return t("editorV2.after"); + } else if (operator === "while") { + return t("editorV2.while"); + } else { + return ""; + } +}; + +export const useTranslatedInterval = ( + interval: TreeChildrenTime["interval"], +) => { + const { t } = useTranslation(); + + if (!interval) return t("editorV2.intervalSome"); + + const { min, max } = interval; + + if (!min && !max) return t("editorV2.intervalSome"); + if (min && !max) return t("editorV2.intervalMinDays", { days: min }); + if (!min && max) return t("editorV2.intervalMaxDays", { days: max }); + if (min && max) + return t("editorV2.intervalMinMaxDays", { minDays: min, maxDays: max }); + + return t("editorV2.intervalSome"); }; diff --git a/frontend/src/localization/de.json b/frontend/src/localization/de.json index d52333ef34..8dc28f47c1 100644 --- a/frontend/src/localization/de.json +++ b/frontend/src/localization/de.json @@ -517,7 +517,7 @@ "pasted": "Importiert" }, "editorV2": { - "before": "ZEIT", + "time": "ZEIT", "and": "UND", "or": "ODER", "clear": "Leeren", @@ -531,6 +531,18 @@ "initialDropText": "Ziehe ein Konzept oder eine Anfrage hier hinein.", "datesExcluded": "Keine Datumswerte", "outputSection": "Ausgabewerte", - "filtersSection": "Filter" + "filtersSection": "Filter", + "every": "Jeder", + "some": "Irgend ein", + "earliest": "Der früheste", + "latest": "Der späteste", + "dateRangeFrom": "Zeitraum aus", + "intervalSome": "irgendwann", + "intervalMinDays": "mindestens {{days}} Tage", + "intervalMaxDays": "höchstens {{days}} Tage", + "intervalMinMaxDays": "zwischen {{minDays}} und {{maxDays}} Tagen", + "before": "zeitlich vor", + "after": "zeitlich nach", + "while": "zeitlich während" } } diff --git a/frontend/src/localization/en.json b/frontend/src/localization/en.json index f0f8c66a10..7f0bee2429 100644 --- a/frontend/src/localization/en.json +++ b/frontend/src/localization/en.json @@ -516,7 +516,7 @@ "pasted": "Imported" }, "editorV2": { - "before": "TIME", + "time": "TIME", "and": "AND", "or": "OR", "clear": "Clear", @@ -530,6 +530,18 @@ "initialDropText": "Drop a concept or query here.", "datesExcluded": "No dates", "outputSection": "Output values", - "filtersSection": "Filters" + "filtersSection": "Filters", + "every": "Every", + "some": "Some", + "earliest": "The earliest", + "latest": "The latest", + "dateRangeFrom": "date range from", + "intervalSome": "some time", + "intervalMinDays": "at least {{days}} days", + "intervalMaxDays": "at most {{days}} days", + "intervalMinMaxDays": "between {{minDays}} and {{maxDays}} days", + "before": "before", + "after": "after", + "while": "while" } } From 3abe5c403dd2f8246194936d3c808963ddf7e5e5 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 6 Jun 2023 16:23:32 +0200 Subject: [PATCH 37/93] Allow editing time connection --- frontend/src/js/editor-v2/EditorV2.tsx | 43 +++- frontend/src/js/editor-v2/TreeNode.tsx | 2 +- frontend/src/js/editor-v2/config.ts | 1 + .../connector-update/useConnectorRotation.ts | 2 +- .../{ => time-connection}/TimeConnection.tsx | 30 +-- .../time-connection/TimeConnectionModal.tsx | 203 ++++++++++++++++++ .../useTimeConnectionEditing.ts | 34 +++ frontend/src/js/editor-v2/types.ts | 6 +- frontend/src/js/editor-v2/util.ts | 2 - frontend/src/js/ui-components/BaseInput.tsx | 7 +- frontend/src/localization/de.json | 3 + frontend/src/localization/en.json | 3 + 12 files changed, 314 insertions(+), 22 deletions(-) rename frontend/src/js/editor-v2/{ => time-connection}/TimeConnection.tsx (81%) create mode 100644 frontend/src/js/editor-v2/time-connection/TimeConnectionModal.tsx create mode 100644 frontend/src/js/editor-v2/time-connection/useTimeConnectionEditing.ts diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 1c81a6b264..1935b1a668 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -5,6 +5,7 @@ import { faCircleNodes, faEdit, faExpandArrowsAlt, + faHourglass, faRefresh, faTrash, } from "@fortawesome/free-solid-svg-icons"; @@ -34,7 +35,9 @@ import { useExpandQuery } from "./expand/useExpandQuery"; import { useNegationEditing } from "./negation/useNegationEditing"; import { EditorV2QueryNodeEditor } from "./query-node-edit/EditorV2QueryNodeEditor"; import { useQueryNodeEditing } from "./query-node-edit/useQueryNodeEditing"; -import { Tree } from "./types"; +import { TimeConnectionModal } from "./time-connection/TimeConnectionModal"; +import { useTimeConnectionEditing } from "./time-connection/useTimeConnectionEditing"; +import { Tree, TreeChildrenTime } from "./types"; import { findNodeById, useGetTranslatedConnection } from "./util"; const Root = styled("div")` @@ -211,6 +214,16 @@ export function EditorV2({ updateTreeNode, }); + const { + showModal: showTimeModal, + onOpen: onOpenTimeModal, + onClose: onCloseTimeModal, + } = useTimeConnectionEditing({ + enabled: featureTimebasedQueries, + hotkey: HOTKEYS.editTimeConnection.keyname, + selectedNode, + }); + const { showModal: showQueryNodeEditor, onOpen: onOpenQueryNodeEditor, @@ -274,6 +287,17 @@ export function EditorV2({ }} /> )} + {showTimeModal && selectedNode && ( + { + updateTreeNode(selectedNode.id, (node) => { + node.children = nodeChildren; + }); + }} + /> + )} {tree && ( @@ -353,11 +377,26 @@ export function EditorV2({ onRotateConnector(); }} > - {t("editorV2.connector")} {connection} )} + {selectedNode?.children?.connection === "time" && ( + + { + e.stopPropagation(); + onOpenTimeModal(); + }} + > + {t("editorV2.timeConnection")} + + + )} {canExpand && ( "some" as const), + timestamps: children.items.map(() => "every" as const), operator: "before" as const, }; } diff --git a/frontend/src/js/editor-v2/TimeConnection.tsx b/frontend/src/js/editor-v2/time-connection/TimeConnection.tsx similarity index 81% rename from frontend/src/js/editor-v2/TimeConnection.tsx rename to frontend/src/js/editor-v2/time-connection/TimeConnection.tsx index e233ee511c..2e6e18176a 100644 --- a/frontend/src/js/editor-v2/TimeConnection.tsx +++ b/frontend/src/js/editor-v2/time-connection/TimeConnection.tsx @@ -2,15 +2,21 @@ import styled from "@emotion/styled"; import { memo } from "react"; import { useTranslation } from "react-i18next"; -import { TreeChildrenTime } from "./types"; +import { TreeChildrenTime } from "../types"; import { useGetNodeLabel, useGetTranslatedTimestamp, useTranslatedInterval, useTranslatedOperator, -} from "./util"; +} from "../util"; -const TimeConnectionContainer = styled("div")` +const Container = styled("div")` + margin: 0 auto; + display: inline-flex; + flex-direction: column; +`; + +const Row = styled("div")` display: flex; align-items: center; gap: 5px; @@ -48,22 +54,22 @@ export const TimeConnection = memo( const interval = useTranslatedInterval(conditions.interval); return ( -
- + + {aTimestamp} {t("editorV2.dateRangeFrom")} {a} - - - {interval} + + + {conditions.operator !== "while" && {interval}} {operator} - - + + {bTimestamp} {t("editorV2.dateRangeFrom")} {b} - -
+ + ); }, ); diff --git a/frontend/src/js/editor-v2/time-connection/TimeConnectionModal.tsx b/frontend/src/js/editor-v2/time-connection/TimeConnectionModal.tsx new file mode 100644 index 0000000000..d3a7a0f6d3 --- /dev/null +++ b/frontend/src/js/editor-v2/time-connection/TimeConnectionModal.tsx @@ -0,0 +1,203 @@ +import styled from "@emotion/styled"; +import { memo, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { exists } from "../../common/helpers/exists"; +import Modal from "../../modal/Modal"; +import BaseInput from "../../ui-components/BaseInput"; +import InputSelect from "../../ui-components/InputSelect/InputSelect"; +import { TimeOperator, TimeTimestamp, TreeChildrenTime } from "../types"; +import { useGetNodeLabel } from "../util"; + +const Content = styled("div")` + display: flex; + flex-direction: column; + gap: 15px; + min-width: 350px; +`; + +const Row = styled("div")` + display: flex; + align-items: center; + gap: 15px; +`; + +const SxBaseInput = styled(BaseInput)` + width: 100px; +`; + +const SxInputSelect = styled(InputSelect)<{ disabled?: boolean }>` + min-width: 150px; + flex-basis: 0; + opacity: ${({ disabled }) => (disabled ? 0.5 : 1)}; +`; + +const DateRangeFrom = styled("span")` + white-space: nowrap; +`; + +const ConceptName = styled("span")` + white-space: nowrap; + font-weight: bold; + color: ${({ theme }) => theme.col.blueGrayDark}; + flex-grow: 1; +`; + +export const TimeConnectionModal = memo( + ({ + conditions, + onChange, + onClose, + }: { + conditions: TreeChildrenTime; + onChange: (conditions: TreeChildrenTime) => void; + onClose: () => void; + }) => { + const conditionsRef = useRef(conditions); + conditionsRef.current = conditions; + + const onChangeRef = useRef(onChange); + onChangeRef.current = onChange; + + const { t } = useTranslation(); + const TIMESTAMP_OPTIONS = useMemo( + () => [ + { value: "every", label: t("editorV2.every") }, + { value: "some", label: t("editorV2.some") }, + { value: "latest", label: t("editorV2.latest") }, + { value: "earliest", label: t("editorV2.earliest") }, + ], + [t], + ); + const OPERATOR_OPTIONS = useMemo( + () => [ + { value: "before", label: t("editorV2.before") }, + { value: "after", label: t("editorV2.after") }, + { value: "while", label: t("editorV2.while") }, + ], + [t], + ); + + const INTERVAL_OPTIONS = useMemo( + () => [ + { value: "some", label: t("editorV2.intervalSome") }, + { value: "dayInterval", label: t("editorV2.dayInterval") }, + ], + [t], + ); + + const [aTimestamp, setATimestamp] = useState(conditions.timestamps[0]); + const [bTimestamp, setBTimestamp] = useState(conditions.timestamps[1]); + const [operator, setOperator] = useState(conditions.operator); + const [interval, setInterval] = useState(conditions.interval); + + const getNodeLabel = useGetNodeLabel(); + const a = getNodeLabel(conditions.items[0]); + const b = getNodeLabel(conditions.items[1]); + + useEffect(() => { + onChangeRef.current({ + ...conditionsRef.current, + timestamps: [aTimestamp, bTimestamp], + }); + }, [aTimestamp, bTimestamp]); + + useEffect(() => { + onChangeRef.current({ + ...conditionsRef.current, + operator, + }); + }, [operator]); + + useEffect(() => { + onChangeRef.current({ + ...conditionsRef.current, + interval, + }); + }, [interval]); + + return ( + + + + o.value === aTimestamp)!} + onChange={(opt) => { + if (opt) { + setATimestamp(opt.value as TimeTimestamp); + } + }} + /> + {t("editorV2.dateRangeFrom")} + {a} + + + { + setInterval({ min: val as number, max: interval?.max || null }); + }} + /> + + { + setInterval({ max: val as number, min: interval?.min || null }); + }} + /> + { + if (opt?.value === "some") { + setInterval(undefined); + } else { + setInterval({ min: 0, max: 0 }); + } + }} + /> + o.value === operator)!} + onChange={(opt) => { + if (opt) { + setOperator(opt.value as TimeOperator); + if (opt.value === "while") { + setInterval(undefined); + } + } + }} + /> + + + o.value === bTimestamp)!} + onChange={(opt) => { + if (opt) { + setBTimestamp(opt.value as TimeTimestamp); + } + }} + /> + {t("editorV2.dateRangeFrom")} + {b} + + + + ); + }, +); diff --git a/frontend/src/js/editor-v2/time-connection/useTimeConnectionEditing.ts b/frontend/src/js/editor-v2/time-connection/useTimeConnectionEditing.ts new file mode 100644 index 0000000000..d39e4afd65 --- /dev/null +++ b/frontend/src/js/editor-v2/time-connection/useTimeConnectionEditing.ts @@ -0,0 +1,34 @@ +import { useState, useCallback } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; + +import { Tree } from "../types"; + +export const useTimeConnectionEditing = ({ + enabled, + hotkey, + selectedNode, +}: { + enabled: boolean; + hotkey: string; + selectedNode: Tree | undefined; +}) => { + const [showModal, setShowModal] = useState(false); + + const onClose = useCallback(() => setShowModal(false), []); + const onOpen = useCallback(() => { + if (!enabled) return; + if (!selectedNode) return; + + setShowModal(true); + }, [enabled, selectedNode]); + + useHotkeys(hotkey, onOpen, [onOpen], { + preventDefault: true, + }); + + return { + showModal, + onClose, + onOpen, + }; +}; diff --git a/frontend/src/js/editor-v2/types.ts b/frontend/src/js/editor-v2/types.ts index c18470186d..9e1b1fe979 100644 --- a/frontend/src/js/editor-v2/types.ts +++ b/frontend/src/js/editor-v2/types.ts @@ -31,15 +31,15 @@ export interface TreeChildrenOr extends TreeChildrenBase { connection: "or"; } -export type TimeTimestamp = "some" | "earliest" | "latest"; +export type TimeTimestamp = "every" | "some" | "earliest" | "latest"; export type TimeOperator = "before" | "after" | "while"; export interface TreeChildrenTime extends TreeChildrenBase { connection: "time"; operator: TimeOperator; timestamps: TimeTimestamp[]; // items.length interval?: { - min?: number; - max?: number; + min: number | null; + max: number | null; }; } export type TreeChildren = TreeChildrenAnd | TreeChildrenOr | TreeChildrenTime; diff --git a/frontend/src/js/editor-v2/util.ts b/frontend/src/js/editor-v2/util.ts index c133585556..209b6f0cb8 100644 --- a/frontend/src/js/editor-v2/util.ts +++ b/frontend/src/js/editor-v2/util.ts @@ -93,8 +93,6 @@ export const useTranslatedOperator = ( return t("editorV2.after"); } else if (operator === "while") { return t("editorV2.while"); - } else { - return ""; } }; diff --git a/frontend/src/js/ui-components/BaseInput.tsx b/frontend/src/js/ui-components/BaseInput.tsx index 48416e4c29..79da15ae5f 100644 --- a/frontend/src/js/ui-components/BaseInput.tsx +++ b/frontend/src/js/ui-components/BaseInput.tsx @@ -20,9 +20,10 @@ const Root = styled("div")` position: relative; `; -const Input = styled("input")<{ large?: boolean }>` +const Input = styled("input")<{ large?: boolean; disabled?: boolean }>` outline: 0; width: 100%; + opacity: ${({ disabled }) => (disabled ? 0.5 : 1)}; border: 1px solid ${({ theme }) => theme.col.grayMediumLight}; padding: ${({ large }) => @@ -86,6 +87,7 @@ export interface Props { large?: boolean; inputProps?: InputProps; currencyConfig?: CurrencyConfigT; + disabled?: boolean; onFocus?: (e: FocusEvent) => void; onBlur?: (e: FocusEvent) => void; onClick?: (e: React.MouseEvent) => void; @@ -131,6 +133,7 @@ const BaseInput = forwardRef( valid, invalid, invalidText, + disabled, }, ref, ) => { @@ -179,6 +182,7 @@ const BaseInput = forwardRef( }} value={exists(value) ? value : ""} large={large} + disabled={disabled} onFocus={onFocus} onBlur={onBlur} onClick={onClick} @@ -204,6 +208,7 @@ const BaseInput = forwardRef( tiny icon={faTimes} tabIndex={-1} + disabled={disabled} title={t("common.clearValue")} aria-label={t("common.clearValue")} onClick={() => onChange(null)} diff --git a/frontend/src/localization/de.json b/frontend/src/localization/de.json index 8dc28f47c1..2fc7701c55 100644 --- a/frontend/src/localization/de.json +++ b/frontend/src/localization/de.json @@ -525,6 +525,8 @@ "dates": "Datum", "negate": "Nicht", "connector": "Verknüpfung", + "timeConnection": "Zeitverknüpfung", + "editTimeConnection": "Zeitverknüpfung bearbeiten", "delete": "Löschen", "expand": "Expandieren", "edit": "Details", @@ -537,6 +539,7 @@ "earliest": "Der früheste", "latest": "Der späteste", "dateRangeFrom": "Zeitraum aus", + "dayInterval": "Tage", "intervalSome": "irgendwann", "intervalMinDays": "mindestens {{days}} Tage", "intervalMaxDays": "höchstens {{days}} Tage", diff --git a/frontend/src/localization/en.json b/frontend/src/localization/en.json index 7f0bee2429..f24924e49e 100644 --- a/frontend/src/localization/en.json +++ b/frontend/src/localization/en.json @@ -524,6 +524,8 @@ "dates": "Dates", "negate": "Negate", "connector": "Connector", + "timeConnection": "Time connection", + "editTimeConnection": "Edit time connection", "delete": "Delete", "expand": "Expand", "edit": "Details", @@ -536,6 +538,7 @@ "earliest": "The earliest", "latest": "The latest", "dateRangeFrom": "date range from", + "dayInterval": "Days", "intervalSome": "some time", "intervalMinDays": "at least {{days}} days", "intervalMaxDays": "at most {{days}} days", From ed5939fefb9a065e88c58a9f2419552f8974dcb4 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 6 Jun 2023 16:35:03 +0200 Subject: [PATCH 38/93] Add missing translation --- frontend/src/localization/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/localization/en.json b/frontend/src/localization/en.json index f24924e49e..aa01d74e19 100644 --- a/frontend/src/localization/en.json +++ b/frontend/src/localization/en.json @@ -444,6 +444,7 @@ "tabQueryEditor": "In the Editor, a query may be built and sent.", "tabTimebasedEditor": "In the Timebased Editor, a time-based query may be built and sent. That means, previous queries may be combined using time based relations, such as 'before' and 'after'.", "tabFormEditor": "The Form Editor allows for further analysis and statistics of previous queries.", + "tabEditorV2": "An extended editor that allows advanced queries.", "datasetSelector": "Select the dataset – concept trees, previous queries and forms will be loaded.", "excludeTimestamps": "If selected, will avoid using the date values from this concept within a query.", "excludeFromSecondaryId": "If selected, will avoid using this concept in the analysis layer, should an analysis layer be selected.", From 80b3b1eb99583bbddb603485279376fa83bdee9b Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 6 Jun 2023 16:41:47 +0200 Subject: [PATCH 39/93] Transform to timebased api (WIP) --- frontend/src/js/api/apiHelper.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/frontend/src/js/api/apiHelper.ts b/frontend/src/js/api/apiHelper.ts index 815a3020f0..5320441c71 100644 --- a/frontend/src/js/api/apiHelper.ts +++ b/frontend/src/js/api/apiHelper.ts @@ -238,17 +238,20 @@ const transformTreeToApi = (tree: Tree): unknown => { case "or": node = createOr(tree.children.items.map(transformTreeToApi)); break; - case "before": + case "time": node = { - type: "BEFORE", - // TODO: - // ...days, + type: "BEFORE", // SHOULD BE: tree.children.operator, + days: { + ...(tree.children.interval || {}), + }, + // TODO: improve this to be more flexible with the "preceding" and "index" keys + // based on the operator, which would be "before" | "after" | "while" preceding: { - sampler: "EARLIEST", + sampler: "EARLIEST", // SHOULD BE: tree.children.timestamps[0], child: transformTreeToApi(tree.children.items[0]), }, index: { - sampler: "EARLIEST", + sampler: "EARLIEST", // SHOULD BE: tree.children.timestamps[1] child: transformTreeToApi(tree.children.items[1]), }, }; From 0bc81237e8edd07e939cf6821a37ee3749861140 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 6 Jun 2023 16:48:52 +0200 Subject: [PATCH 40/93] Fix hotkeys --- frontend/src/js/editor-v2/EditorV2.tsx | 7 ++++--- frontend/src/js/editor-v2/config.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 1935b1a668..3e4101d620 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -179,8 +179,7 @@ export function EditorV2({ } }, [selectedNode, setTree, updateTreeNode]); - useHotkeys(HOTKEYS.delete[0].keyname, onDelete, [onDelete]); - useHotkeys(HOTKEYS.delete[1].keyname, onDelete, [onDelete]); + useHotkeys(HOTKEYS.delete.keyname, onDelete, [onDelete]); useHotkeys(HOTKEYS.flip.keyname, onFlip, [onFlip]); useHotkeys(HOTKEYS.reset.keyname, onReset, [onReset]); @@ -412,7 +411,9 @@ export function EditorV2({
)} {selectedNode && ( - + Date: Tue, 13 Jun 2023 11:10:51 +0200 Subject: [PATCH 41/93] print conceptElementId with dataset --- .../bakdata/conquery/apiv1/query/TableExportQuery.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/query/TableExportQuery.java b/backend/src/main/java/com/bakdata/conquery/apiv1/query/TableExportQuery.java index 8ab94340d9..63a798787c 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/query/TableExportQuery.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/query/TableExportQuery.java @@ -143,7 +143,7 @@ public void resolve(QueryResolveContext context) { query.resolve(context); // First is dates, second is source id - AtomicInteger currentPosition = new AtomicInteger(2); + final AtomicInteger currentPosition = new AtomicInteger(2); final Map secondaryIdPositions = calculateSecondaryIdPositions(currentPosition); @@ -153,7 +153,7 @@ public void resolve(QueryResolveContext context) { } private Map calculateSecondaryIdPositions(AtomicInteger currentPosition) { - Map secondaryIdPositions = new HashMap<>(); + final Map secondaryIdPositions = new HashMap<>(); // SecondaryIds are pulled to the front and grouped over all tables tables.stream() @@ -303,12 +303,12 @@ public static String printValue(Concept concept, Object rawValue, PrintSettings final TreeConcept tree = (TreeConcept) concept; - int localId = (int) rawValue; + final int localId = (int) rawValue; final ConceptTreeNode node = tree.getElementByLocalId(localId); if (!printSettings.isPrettyPrint()) { - return node.getId().toStringWithoutDataset(); + return node.getId().toString(); } if (node.getDescription() == null) { From 19675b35ccadffaa591a57144b47ed09738411b4 Mon Sep 17 00:00:00 2001 From: Fabian Blank Date: Tue, 13 Jun 2023 12:05:58 +0200 Subject: [PATCH 42/93] add colored labels and increase size of download text --- frontend/src/app-theme.ts | 6 +++++ frontend/src/js/button/DownloadButton.tsx | 27 ++++++++++++++++------- frontend/src/js/button/IconButton.tsx | 14 +++++++++--- frontend/src/react-app-env.d.ts | 6 +++++ 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/frontend/src/app-theme.ts b/frontend/src/app-theme.ts index d7f0b9dd2c..f73533cd18 100644 --- a/frontend/src/app-theme.ts +++ b/frontend/src/app-theme.ts @@ -27,6 +27,12 @@ export const theme: Theme = { "#777", "#fff", ], + files: { + csv: "#5aa86f", + pdf: "#c9181e", + zip: "#e5c527", + xlsx: "#3B843C", + }, bgAlt: "#f4f6f5", blueGrayDark: "#1f5f30", blueGray: "#98b099", diff --git a/frontend/src/js/button/DownloadButton.tsx b/frontend/src/js/button/DownloadButton.tsx index 6fb2d32473..bc487beea1 100644 --- a/frontend/src/js/button/DownloadButton.tsx +++ b/frontend/src/js/button/DownloadButton.tsx @@ -10,6 +10,7 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { ReactNode, useContext, forwardRef } from "react"; +import { theme } from "../../app-theme"; import { ResultUrlWithLabel } from "../api/types"; import { AuthTokenContext } from "../authorization/AuthTokenProvider"; import { getEnding } from "../query-runner/DownloadResultsDropdownButton"; @@ -24,13 +25,19 @@ const Link = styled("a")` line-height: 1; `; -const fileTypeToIcon: Record = { - ZIP: faFileArchive, - XLSX: faFileExcel, - PDF: faFilePdf, - CSV: faFileCsv, +interface FileIcon { + icon: IconProp; + color?: string; +} + +const fileTypeToIcon: Record = { + ZIP: { icon: faFileArchive, color: theme.col.files.zip }, + XLSX: { icon: faFileExcel, color: theme.col.files.xlsx }, + PDF: { icon: faFilePdf, color: theme.col.files.pdf }, + CSV: { icon: faFileCsv, color: theme.col.files.csv }, }; -function getFileIcon(url: string): IconProp { + +function getFileInfo(url: string): FileIcon { // Forms if (url.includes(".")) { const ext = getEnding(url); @@ -38,7 +45,7 @@ function getFileIcon(url: string): IconProp { return fileTypeToIcon[ext]; } } - return faFileDownload; + return { icon: faFileDownload }; } interface Props extends Omit { @@ -60,12 +67,16 @@ const DownloadButton = forwardRef( authToken, )}&charset=ISO_8859_1`; + const fileInfo = getFileInfo(resultUrl.url); + return ( {children} diff --git a/frontend/src/js/button/IconButton.tsx b/frontend/src/js/button/IconButton.tsx index 2de24b10fb..fef7a99ed2 100644 --- a/frontend/src/js/button/IconButton.tsx +++ b/frontend/src/js/button/IconButton.tsx @@ -11,11 +11,14 @@ interface StyledFaIconProps extends FaIconPropsT { red?: boolean; secondary?: boolean; hasChildren: boolean; + iconColor?: string; } const SxFaIcon = styled(FaIcon)` - color: ${({ theme, active, red, secondary, light }) => - red + color: ${({ theme, active, red, secondary, light, iconColor }) => + iconColor + ? iconColor + : red ? theme.col.red : active ? theme.col.blueGrayDark @@ -42,6 +45,7 @@ const SxBasicButton = styled(BasicButton)<{ tight?: boolean; bgHover?: boolean; red?: boolean; + large?: boolean; }>` background-color: transparent; color: ${({ theme, active, secondary, red }) => @@ -62,7 +66,7 @@ const SxBasicButton = styled(BasicButton)<{ display: inline-flex; align-items: center; gap: ${({ tight }) => (tight ? "5px" : "10px")}; - + font-size: ${({ theme, large }) => (large ? theme.font.md : theme.font.sm)}; &:hover { opacity: 1; @@ -98,6 +102,7 @@ export interface IconButtonPropsT extends BasicButtonProps { light?: boolean; fixedIconWidth?: number; bgHover?: boolean; + iconColor?: string; } // A button that is prefixed by an icon @@ -117,6 +122,7 @@ const IconButton = forwardRef( light, fixedIconWidth, bgHover, + iconColor, ...restProps }, ref, @@ -135,6 +141,7 @@ const IconButton = forwardRef( tight={tight} small={small} light={light} + iconColor={iconColor} {...iconProps} /> ); @@ -169,6 +176,7 @@ const IconButton = forwardRef( red={red} {...restProps} ref={ref} + large={large} > {iconElement} {children && {children}} diff --git a/frontend/src/react-app-env.d.ts b/frontend/src/react-app-env.d.ts index 9cf631a06b..f2bf4b86c6 100644 --- a/frontend/src/react-app-env.d.ts +++ b/frontend/src/react-app-env.d.ts @@ -31,6 +31,12 @@ declare module "@emotion/react" { green: string; orange: string; palette: string[]; + files: { + csv: string; + pdf: string; + zip: string; + xlsx: string; + }; }; img: { logo: string; From 4b0f6dd2772feb64b0e8a9846d657cebf706e191 Mon Sep 17 00:00:00 2001 From: Fabian Blank Date: Tue, 13 Jun 2023 12:08:12 +0200 Subject: [PATCH 43/93] fix linter error --- frontend/src/js/button/IconButton.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/js/button/IconButton.tsx b/frontend/src/js/button/IconButton.tsx index fef7a99ed2..2b7f5e076a 100644 --- a/frontend/src/js/button/IconButton.tsx +++ b/frontend/src/js/button/IconButton.tsx @@ -166,6 +166,7 @@ const IconButton = forwardRef( secondary, light, fixedIconWidth, + iconColor, ]); return ( Date: Tue, 13 Jun 2023 12:16:59 +0200 Subject: [PATCH 44/93] Support concept columns in time stratified infos --- frontend/src/js/api/types.ts | 14 ++- frontend/src/js/entity-history/Timeline.tsx | 47 +++++---- .../js/entity-history/timeline/Quarter.tsx | 2 +- .../js/entity-history/timeline/YearHead.tsx | 97 ++++++++++++++++--- frontend/src/localization/de.json | 8 +- 5 files changed, 124 insertions(+), 44 deletions(-) diff --git a/frontend/src/js/api/types.ts b/frontend/src/js/api/types.ts index 55d19c2008..82eb4a1d50 100644 --- a/frontend/src/js/api/types.ts +++ b/frontend/src/js/api/types.ts @@ -326,7 +326,8 @@ export type ColumnDescriptionKind = | "MONEY" | "DATE" | "DATE_RANGE" - | "LIST[DATE_RANGE]"; + | "LIST[DATE_RANGE]" + | "LIST[STRING]"; export interface ColumnDescriptionSemanticColumn { type: "COLUMN"; @@ -384,6 +385,7 @@ export interface ColumnDescription { // `label` matches column name in CSV // So it's more of an id, TODO: rename this to 'id', label: string; + description: string | null; type: ColumnDescriptionKind; semantics: ColumnDescriptionSemantic[]; @@ -564,13 +566,9 @@ export interface TimeStratifiedInfo { totals: { [label: string]: number | string[]; }; - columns: { - label: string; // Matches `label` with `year.values` and `year.quarters[].values` - defaultLabel: string; // Probably not used by us - description: string | null; - type: ColumnDescriptionKind; // Relevant to show e.g. € for money - semantics: ColumnDescriptionSemantic[]; // Probably not used by us - }[]; + // `columns[].label` matches with + // `year.values` and `year.quarters[].values` + columns: ColumnDescription[]; years: TimeStratifiedInfoYear[]; } diff --git a/frontend/src/js/entity-history/Timeline.tsx b/frontend/src/js/entity-history/Timeline.tsx index c8e9b67501..ee6a19fce8 100644 --- a/frontend/src/js/entity-history/Timeline.tsx +++ b/frontend/src/js/entity-history/Timeline.tsx @@ -31,14 +31,20 @@ import { const Root = styled("div")` overflow-y: auto; -webkit-overflow-scrolling: touch; - padding: 0 20px 0 10px; + padding: 0 20px 20px 10px; display: inline-grid; grid-template-columns: 200px auto; grid-auto-rows: minmax(min-content, max-content); - gap: 12px 4px; + gap: 20px 4px; width: 100%; `; +const Divider = styled("div")` + grid-column: 1 / span 2; + height: 1px; + background: ${({ theme }) => theme.col.grayLight}; +`; + const SxEntityCard = styled(EntityCard)` grid-column: span 2; `; @@ -94,23 +100,26 @@ const Timeline = ({ infos={currentEntityInfos} timeStratifiedInfos={currentEntityTimeStratifiedInfos} /> - {eventsByQuarterWithGroups.map(({ year, quarterwiseData }) => ( - + {eventsByQuarterWithGroups.map(({ year, quarterwiseData }, i) => ( + <> + + {i < eventsByQuarterWithGroups.length - 1 && } + ))} ); diff --git a/frontend/src/js/entity-history/timeline/Quarter.tsx b/frontend/src/js/entity-history/timeline/Quarter.tsx index 1042b68c03..369f03b2c3 100644 --- a/frontend/src/js/entity-history/timeline/Quarter.tsx +++ b/frontend/src/js/entity-history/timeline/Quarter.tsx @@ -55,7 +55,7 @@ const InlineGrid = styled("div")` cursor: pointer; border: 1px solid transparent; border-radius: ${({ theme }) => theme.borderRadius}; - padding: 5px; + padding: 6px 10px; &:hover { border: 1px solid ${({ theme }) => theme.col.blueGray}; } diff --git a/frontend/src/js/entity-history/timeline/YearHead.tsx b/frontend/src/js/entity-history/timeline/YearHead.tsx index 94ad76ebd0..e6b93b8a04 100644 --- a/frontend/src/js/entity-history/timeline/YearHead.tsx +++ b/frontend/src/js/entity-history/timeline/YearHead.tsx @@ -4,12 +4,18 @@ import { Fragment, memo } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; -import { TimeStratifiedInfo } from "../../api/types"; +import { + ColumnDescriptionSemanticConceptColumn, + TimeStratifiedInfo, +} from "../../api/types"; import { StateT } from "../../app/reducers"; +import { exists } from "../../common/helpers/exists"; +import { getConceptById } from "../../concept-trees/globalTreeStoreHelper"; import FaIcon from "../../icon/FaIcon"; +import WithTooltip from "../../tooltip/WithTooltip"; import { SmallHeading } from "./SmallHeading"; -import { getColumnType } from "./util"; +import { isConceptColumn, isMoneyColumn } from "./util"; const Root = styled("div")` font-size: ${({ theme }) => theme.font.xs}; @@ -20,7 +26,7 @@ const StickyWrap = styled("div")` position: sticky; top: 0; left: 0; - padding: 5px; + padding: 6px 10px; cursor: pointer; display: grid; grid-template-columns: 16px 1fr; @@ -44,6 +50,22 @@ const Grid = styled("div")` gap: 0px 10px; `; +const ConceptRow = styled("div")` + grid-column: span 2; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 2px; +`; + +const ConceptBubble = styled("span")` + padding: 0 2px; + border-radius: ${({ theme }) => theme.borderRadius}; + color: ${({ theme }) => theme.col.black}; + border: 1px solid ${({ theme }) => theme.col.blueGrayLight}; + font-size: ${({ theme }) => theme.font.tiny}; +`; + const Value = styled("div")` font-size: ${({ theme }) => theme.font.tiny}; font-weight: 400; @@ -102,20 +124,71 @@ const TimeStratifiedInfos = ({ info.columns.findIndex((c) => c.label === l2), ) .map(([label, value]) => { - const columnType = getColumnType(info, label); - const valueFormatted = - typeof value === "number" - ? Math.round(value) - : value instanceof Array - ? value.join(", ") - : value; + const column = info.columns.find((c) => c.label === label); + + if (!column) { + return null; + } + + if (isConceptColumn(column)) { + const semantic = column.semantics.find( + (s): s is ColumnDescriptionSemanticConceptColumn => + s.type === "CONCEPT_COLUMN", + ); + + if (value instanceof Array) { + const concepts = value + .map((v) => + getConceptById( + // TODO: should be just v + semantic?.concept.split(".")[0] + "." + v, + semantic!.concept, + ), + ) + .filter(exists); + + return ( + + + + {concepts.map((concept) => ( + + {concept.label} + + ))} + + + ); + } + // else if (typeof value === "string") { + // const concept = getConceptById(semantic!.concept, value); + + // if (concept) valueFormatted = concept.label; + // } + } + + let valueFormatted: string | number | string[] = value; + if (typeof value === "number") { + valueFormatted = Math.round(value); + } else if (value instanceof Array) { + valueFormatted = value.join(", "); + } return ( - + {valueFormatted} - {columnType === "MONEY" ? " " + currencyUnit : ""} + {isMoneyColumn(column) ? " " + currencyUnit : ""} ); diff --git a/frontend/src/localization/de.json b/frontend/src/localization/de.json index 3c09170e1f..6aa4c25e4a 100644 --- a/frontend/src/localization/de.json +++ b/frontend/src/localization/de.json @@ -469,13 +469,13 @@ "backButtonWarning": "Achtung: Wenn Du zurück gehst, werden Änderungen an der Liste verworfen. Lade die Liste vorher herunter, um sie zu speichern.", "history": "Historie", "marked": "mit Status", - "events_one": "Datenpunkt", - "events_other": "Datenpunkte", + "events_one": "Eintrag", + "events_other": "Einträge", "downloadButtonLabel": "Liste mit Status-Einträgen herunterladen", "nextButtonLabel": "Weiterblättern", "prevButtonLabel": "Zurückblättern", "downloadEntityData": "Einzelhistorie herunterladen", - "differencesTooltip": "Unterschiede aus den einzelnen Datenpunkten", + "differencesTooltip": "Unterschiede aus den einzelnen Einträgen", "closeAll": "Alle schließen", "openAll": "Alle aufklappen", "dates": "Datumswerte", @@ -488,7 +488,7 @@ "detail": { "summary": "Zusammenfassung", "detail": "Gruppiert", - "full": "Alle Einzeldatenpunkte" + "full": "Alle Einzeleinträge" }, "content": { "money": "Geldbeträge", From 47ea7f7a16699ca6884732c2c049e4611ca0f0f1 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 13 Jun 2023 12:18:10 +0200 Subject: [PATCH 45/93] Fix broken date columns --- frontend/src/js/entity-history/Timeline.tsx | 7 +++---- frontend/src/js/entity-history/timeline/GroupedContent.tsx | 3 ++- frontend/src/js/entity-history/timeline/util.ts | 3 +++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/src/js/entity-history/Timeline.tsx b/frontend/src/js/entity-history/Timeline.tsx index ee6a19fce8..2394f746d7 100644 --- a/frontend/src/js/entity-history/Timeline.tsx +++ b/frontend/src/js/entity-history/Timeline.tsx @@ -1,5 +1,5 @@ import styled from "@emotion/styled"; -import { memo, useMemo } from "react"; +import { Fragment, memo, useMemo } from "react"; import { useSelector } from "react-redux"; import { @@ -101,9 +101,8 @@ const Timeline = ({ timeStratifiedInfos={currentEntityTimeStratifiedInfos} /> {eventsByQuarterWithGroups.map(({ year, quarterwiseData }, i) => ( - <> + {i < eventsByQuarterWithGroups.length - 1 && } - + ))} ); diff --git a/frontend/src/js/entity-history/timeline/GroupedContent.tsx b/frontend/src/js/entity-history/timeline/GroupedContent.tsx index b6248455d7..7a4414f4b9 100644 --- a/frontend/src/js/entity-history/timeline/GroupedContent.tsx +++ b/frontend/src/js/entity-history/timeline/GroupedContent.tsx @@ -18,6 +18,7 @@ import ConceptName from "./ConceptName"; import { TinyLabel } from "./TinyLabel"; import { isConceptColumn, + isDateColumn, isMoneyColumn, isSecondaryIdColumn, isVisibleColumn, @@ -154,7 +155,7 @@ const Cell = memo( datasetId: DatasetT["id"]; rootConceptIdsByColumn: Record; }) => { - if (!columnDescription) { + if (isDateColumn(columnDescription)) { return cell.from === cell.to ? ( {formatHistoryDayRange(cell.from)} ) : ( diff --git a/frontend/src/js/entity-history/timeline/util.ts b/frontend/src/js/entity-history/timeline/util.ts index eef88d1c25..422ed415cd 100644 --- a/frontend/src/js/entity-history/timeline/util.ts +++ b/frontend/src/js/entity-history/timeline/util.ts @@ -3,6 +3,9 @@ import { ColumnDescription, TimeStratifiedInfo } from "../../api/types"; export const isIdColumn = (columnDescription: ColumnDescription) => columnDescription.semantics.some((s) => s.type === "ID"); +export const isDateColumn = (columnDescription: ColumnDescription) => + columnDescription.semantics.some((s) => s.type === "EVENT_DATE"); + export const isGroupableColumn = (columnDescription: ColumnDescription) => columnDescription.semantics.some((s) => s.type === "GROUP"); From b834c859b1b7df516d801d251f86c5dd6c6d0d8a Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Tue, 13 Jun 2023 11:27:34 +0200 Subject: [PATCH 46/93] fixes format for full id with TableExportQuery --- .../integration/tests/EntityExportTest.java | 6 +++--- .../CONCEPT_VALUES_RESOLVED.test.json | 8 ++++---- .../CONCEPT_COLUMN_SELECTS/expected_resolved.csv | 8 ++++---- .../query/TABLE_EXPORT/RAW_TABLE_EXPORT.test.json | 2 +- .../query/TABLE_EXPORT/TABLE_EXPORT.test.json | 2 +- .../tests/query/TABLE_EXPORT/expected.csv | 14 +++++++------- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/backend/src/test/java/com/bakdata/conquery/integration/tests/EntityExportTest.java b/backend/src/test/java/com/bakdata/conquery/integration/tests/EntityExportTest.java index ab862e3de4..309141f5c2 100644 --- a/backend/src/test/java/com/bakdata/conquery/integration/tests/EntityExportTest.java +++ b/backend/src/test/java/com/bakdata/conquery/integration/tests/EntityExportTest.java @@ -245,9 +245,9 @@ public void execute(String name, TestConquery testConquery) throws Exception { assertThat(resultLines.readEntity(String.class).lines().collect(Collectors.toList())) .containsExactlyInAnyOrder( "result,dates,source,secondaryid,table1 column,table2 column", - "1,{2013-11-10/2013-11-10},table1,External: oneone,tree1.child_a,", - "1,{2012-01-01/2012-01-01},table2,2222,,tree2", - "1,{2010-07-15/2010-07-15},table2,External: threethree,,tree2" + "1,{2013-11-10/2013-11-10},table1,External: oneone,EntityExportTest.tree1.child_a,", + "1,{2012-01-01/2012-01-01},table2,2222,,EntityExportTest.tree2", + "1,{2010-07-15/2010-07-15},table2,External: threethree,,EntityExportTest.tree2" ); } diff --git a/backend/src/test/resources/tests/aggregator/CONCEPT_COLUMN_SELECTS/CONCEPT_VALUES_RESOLVED.test.json b/backend/src/test/resources/tests/aggregator/CONCEPT_COLUMN_SELECTS/CONCEPT_VALUES_RESOLVED.test.json index fc85a046d6..a98482ec1c 100644 --- a/backend/src/test/resources/tests/aggregator/CONCEPT_COLUMN_SELECTS/CONCEPT_VALUES_RESOLVED.test.json +++ b/backend/src/test/resources/tests/aggregator/CONCEPT_COLUMN_SELECTS/CONCEPT_VALUES_RESOLVED.test.json @@ -6,7 +6,7 @@ "type": "CONCEPT_QUERY", "root": { "type": "CONCEPT", - "selects" : [ + "selects": [ "tree.select" ], "ids": [ @@ -28,9 +28,9 @@ "type": "TREE", "selects": [ { - "type" : "CONCEPT_VALUES", - "name" : "select", - "asIds" : true + "type": "CONCEPT_VALUES", + "name": "select", + "asIds": true } ], "connectors": [ diff --git a/backend/src/test/resources/tests/aggregator/CONCEPT_COLUMN_SELECTS/expected_resolved.csv b/backend/src/test/resources/tests/aggregator/CONCEPT_COLUMN_SELECTS/expected_resolved.csv index 36a1e899b1..d99355deeb 100644 --- a/backend/src/test/resources/tests/aggregator/CONCEPT_COLUMN_SELECTS/expected_resolved.csv +++ b/backend/src/test/resources/tests/aggregator/CONCEPT_COLUMN_SELECTS/expected_resolved.csv @@ -1,5 +1,5 @@ result,dates,tree select -1,{2012-01-01/2012-01-02},"{tree.test_child2,tree.test_child1}" -2,{2010-07-15/2010-07-15},{tree.test_child2} -3,{2013-11-10/2013-11-10},{tree.test_child1} -4,{2012-11-11/2012-11-11},{tree.test_child2} \ No newline at end of file +1,{2012-01-01/2012-01-02},"{CONCEPT_VALUES$20$28Resolved$29$20Test.tree.test_child2,CONCEPT_VALUES$20$28Resolved$29$20Test.tree.test_child1}" +2,{2010-07-15/2010-07-15},{CONCEPT_VALUES$20$28Resolved$29$20Test.tree.test_child2} +3,{2013-11-10/2013-11-10},{CONCEPT_VALUES$20$28Resolved$29$20Test.tree.test_child1} +4,{2012-11-11/2012-11-11},{CONCEPT_VALUES$20$28Resolved$29$20Test.tree.test_child2} \ No newline at end of file diff --git a/backend/src/test/resources/tests/query/TABLE_EXPORT/RAW_TABLE_EXPORT.test.json b/backend/src/test/resources/tests/query/TABLE_EXPORT/RAW_TABLE_EXPORT.test.json index c3c321cdc0..bf49129fca 100644 --- a/backend/src/test/resources/tests/query/TABLE_EXPORT/RAW_TABLE_EXPORT.test.json +++ b/backend/src/test/resources/tests/query/TABLE_EXPORT/RAW_TABLE_EXPORT.test.json @@ -1,6 +1,6 @@ { "type": "QUERY_TEST", - "label": "TABLE_EXPORT Test", + "label": "RAW_TABLE_EXPORT Test", "expectedCsv": "tests/query/TABLE_EXPORT/raw_expected.csv", "query": { "type": "TABLE_EXPORT", diff --git a/backend/src/test/resources/tests/query/TABLE_EXPORT/TABLE_EXPORT.test.json b/backend/src/test/resources/tests/query/TABLE_EXPORT/TABLE_EXPORT.test.json index 7107f3904a..4b78d2cf0b 100644 --- a/backend/src/test/resources/tests/query/TABLE_EXPORT/TABLE_EXPORT.test.json +++ b/backend/src/test/resources/tests/query/TABLE_EXPORT/TABLE_EXPORT.test.json @@ -34,7 +34,7 @@ "min": "2000-01-01", "max": "2020-12-31" }, - "rawConceptValues" : false, + "rawConceptValues": false, "query": { "type": "CONCEPT_QUERY", "root": { diff --git a/backend/src/test/resources/tests/query/TABLE_EXPORT/expected.csv b/backend/src/test/resources/tests/query/TABLE_EXPORT/expected.csv index 9dab1a4faa..88e9196f5d 100644 --- a/backend/src/test/resources/tests/query/TABLE_EXPORT/expected.csv +++ b/backend/src/test/resources/tests/query/TABLE_EXPORT/expected.csv @@ -1,8 +1,8 @@ result,dates,source,SecondaryId,table1 value,table1 code,table2 value,table2 code -a,{2020-06-30/2020-06-30},table2,,,,13.0,concept.a_child -a,{2014-06-30/2015-06-30},table1,f_a1,1.0,concept.a_child,, -a,{2016-06-30/2016-06-30},table1,f_a1,1.0,concept.a_child,, -a,{2014-06-30/2015-06-30},table1,f_a2,1.0,concept.a_child,, -a,{2010-06-30/2010-06-30},table1,,1.0,concept.a_child,, -b,{2015-02-03/2015-06-30},table1,f_b1,1.0,concept,, -a,{2020-06-30/2020-06-30},table2,,,,13.0,concept \ No newline at end of file +a,{2020-06-30/2020-06-30},table2,,,,13.0,TABLE_EXPORT$20Test.concept.a_child +a,{2014-06-30/2015-06-30},table1,f_a1,1.0,TABLE_EXPORT$20Test.concept.a_child,, +a,{2016-06-30/2016-06-30},table1,f_a1,1.0,TABLE_EXPORT$20Test.concept.a_child,, +a,{2014-06-30/2015-06-30},table1,f_a2,1.0,TABLE_EXPORT$20Test.concept.a_child,, +a,{2010-06-30/2010-06-30},table1,,1.0,TABLE_EXPORT$20Test.concept.a_child,, +b,{2015-02-03/2015-06-30},table1,f_b1,1.0,TABLE_EXPORT$20Test.concept,, +a,{2020-06-30/2020-06-30},table2,,,,13.0,TABLE_EXPORT$20Test.concept \ No newline at end of file From 828785be02d2fedc821c1f0cb480b4830c9b2f5f Mon Sep 17 00:00:00 2001 From: Fabian Blank Date: Tue, 13 Jun 2023 12:59:58 +0200 Subject: [PATCH 47/93] change csv color --- frontend/src/app-theme.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app-theme.ts b/frontend/src/app-theme.ts index f73533cd18..63f6825b57 100644 --- a/frontend/src/app-theme.ts +++ b/frontend/src/app-theme.ts @@ -28,7 +28,7 @@ export const theme: Theme = { "#fff", ], files: { - csv: "#5aa86f", + csv: "#279e47", pdf: "#c9181e", zip: "#e5c527", xlsx: "#3B843C", From 7612b00fd55cfff7c655f8979c5375c162a80622 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 13 Jun 2023 13:35:47 +0200 Subject: [PATCH 48/93] Remove unnecessary fn --- frontend/src/js/entity-history/EntityCard.tsx | 9 ++------- frontend/src/js/entity-history/timeline/util.ts | 9 +-------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/frontend/src/js/entity-history/EntityCard.tsx b/frontend/src/js/entity-history/EntityCard.tsx index d91f481be7..04a78393bc 100644 --- a/frontend/src/js/entity-history/EntityCard.tsx +++ b/frontend/src/js/entity-history/EntityCard.tsx @@ -9,7 +9,7 @@ import { exists } from "../common/helpers/exists"; import EntityInfos from "./EntityInfos"; import { TimeStratifiedChart } from "./TimeStratifiedChart"; -import { getColumnType } from "./timeline/util"; +import { getColumnType, isMoneyColumn } from "./timeline/util"; const Container = styled("div")` display: grid; @@ -60,11 +60,6 @@ const Table = ({ return ( {timeStratifiedInfo.columns.map((column) => { - const columnType = getColumnType( - timeStratifiedInfo, - column.label, - ); - const label = column.label; const value = timeStratifiedInfo.totals[column.label]; @@ -81,7 +76,7 @@ const Table = ({ - {columnType === "MONEY" && typeof value === "number" ? ( + {isMoneyColumn(column) && typeof value === "number" ? ( columnDescription.semantics.some((s) => s.type === "ID"); @@ -21,10 +21,3 @@ export const isMoneyColumn = (columnDescription: ColumnDescription) => export const isSecondaryIdColumn = (columnDescription: ColumnDescription) => columnDescription.semantics.some((s) => s.type === "SECONDARY_ID"); - -export const getColumnType = ( - timeStratifiedInfo: TimeStratifiedInfo, - label: string, -) => { - return timeStratifiedInfo.columns.find((c) => c.label === label)?.type; -}; From b536b3f0c3a1223821e881a49182b02669d830b3 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 13 Jun 2023 13:48:36 +0200 Subject: [PATCH 49/93] Remove dataset / concept id concatenation --- frontend/src/js/entity-history/Timeline.tsx | 5 ---- .../entity-history/timeline/ConceptName.tsx | 20 +++----------- .../js/entity-history/timeline/EventCard.tsx | 26 +++++++------------ .../timeline/GroupedContent.tsx | 7 ----- .../js/entity-history/timeline/Quarter.tsx | 5 ---- .../src/js/entity-history/timeline/Year.tsx | 4 --- .../js/entity-history/timeline/YearHead.tsx | 13 ++-------- 7 files changed, 16 insertions(+), 64 deletions(-) diff --git a/frontend/src/js/entity-history/Timeline.tsx b/frontend/src/js/entity-history/Timeline.tsx index 2394f746d7..42515cc6e5 100644 --- a/frontend/src/js/entity-history/Timeline.tsx +++ b/frontend/src/js/entity-history/Timeline.tsx @@ -11,7 +11,6 @@ import { TimeStratifiedInfo, } from "../api/types"; import type { StateT } from "../app/reducers"; -import { useDatasetId } from "../dataset/selectors"; import { ContentFilterValue } from "./ContentControl"; import type { DetailLevel } from "./DetailControl"; @@ -72,7 +71,6 @@ const Timeline = ({ toggleOpenYear, toggleOpenQuarter, }: Props) => { - const datasetId = useDatasetId(); const data = useSelector( (state) => state.entityHistory.currentEntityData, ); @@ -88,8 +86,6 @@ const Timeline = ({ secondaryIds: columnBuckets.secondaryIds, }); - if (!datasetId) return null; - if (eventsByQuarterWithGroups.length === 0) { return ; } @@ -104,7 +100,6 @@ const Timeline = ({ { - // TODO: refactor. It's very implicit that the id is - // somehow containing the datasetId. - if (!datasetId) return null; - - const fullConceptId = `${datasetId}.${conceptId}`; - const concept = getConceptById(fullConceptId, rootConceptId); +const ConceptName = ({ className, title, rootConceptId, conceptId }: Props) => { + const concept = getConceptById(conceptId, rootConceptId); if (!concept) { return ( @@ -55,7 +43,7 @@ const ConceptName = ({ ); - if (fullConceptId === rootConceptId) { + if (conceptId === rootConceptId) { return (
{conceptName} diff --git a/frontend/src/js/entity-history/timeline/EventCard.tsx b/frontend/src/js/entity-history/timeline/EventCard.tsx index 60e3b85868..11bdb01202 100644 --- a/frontend/src/js/entity-history/timeline/EventCard.tsx +++ b/frontend/src/js/entity-history/timeline/EventCard.tsx @@ -11,7 +11,6 @@ import type { ColumnDescription, ConceptIdT, CurrencyConfigT, - DatasetT, } from "../../api/types"; import { exists } from "../../common/helpers/exists"; import FaIcon from "../../icon/FaIcon"; @@ -93,29 +92,25 @@ const Bullet = styled("div")` flex-shrink: 0; `; -interface Props { - row: EntityEvent; - columns: Record; - columnBuckets: ColumnBuckets; - datasetId: DatasetT["id"]; - contentFilter: ContentFilterValue; - currencyConfig: CurrencyConfigT; - rootConceptIdsByColumn: Record; - groupedRows?: EntityEvent[]; - groupedRowsKeysWithDifferentValues?: string[]; -} - const EventCard = ({ row, columns, columnBuckets, - datasetId, currencyConfig, contentFilter, rootConceptIdsByColumn, groupedRows, groupedRowsKeysWithDifferentValues, -}: Props) => { +}: { + row: EntityEvent; + columns: Record; + columnBuckets: ColumnBuckets; + contentFilter: ContentFilterValue; + currencyConfig: CurrencyConfigT; + rootConceptIdsByColumn: Record; + groupedRows?: EntityEvent[]; + groupedRowsKeysWithDifferentValues?: string[]; +}) => { const { t } = useTranslation(); const applicableGroupableIds = columnBuckets.groupableIds.filter( @@ -168,7 +163,6 @@ const EventCard = ({ )} {groupedRowsKeysWithDifferentValues && groupedRows && ( ; groupedRows: EntityEvent[]; groupedRowsKeysWithDifferentValues: string[]; @@ -65,7 +63,6 @@ interface Props { } const GroupedContent = ({ - datasetId, columns, groupedRows, groupedRowsKeysWithDifferentValues, @@ -115,7 +112,6 @@ const GroupedContent = ({ differencesKeys.map((key) => ( ; }) => { if (isDateColumn(columnDescription)) { @@ -170,7 +164,6 @@ const Cell = memo( ); diff --git a/frontend/src/js/entity-history/timeline/Quarter.tsx b/frontend/src/js/entity-history/timeline/Quarter.tsx index 369f03b2c3..178897552c 100644 --- a/frontend/src/js/entity-history/timeline/Quarter.tsx +++ b/frontend/src/js/entity-history/timeline/Quarter.tsx @@ -7,7 +7,6 @@ import { ColumnDescription, ConceptIdT, CurrencyConfigT, - DatasetT, } from "../../api/types"; import FaIcon from "../../icon/FaIcon"; import { ContentFilterValue } from "../ContentControl"; @@ -90,7 +89,6 @@ const Quarter = ({ currencyConfig, rootConceptIdsByColumn, contentFilter, - datasetId, }: { year: number; quarter: number; @@ -100,7 +98,6 @@ const Quarter = ({ detailLevel: DetailLevel; toggleOpenQuarter: (year: number, quarter: number) => void; differences: string[][]; - datasetId: DatasetT["id"]; columns: Record; columnBuckets: ColumnBuckets; contentFilter: ContentFilterValue; @@ -151,7 +148,6 @@ const Quarter = ({ key={`${index}-${evtIdx}`} columns={columns} columnBuckets={columnBuckets} - datasetId={datasetId} contentFilter={contentFilter} rootConceptIdsByColumn={rootConceptIdsByColumn} row={evt} @@ -174,7 +170,6 @@ const Quarter = ({ key={index} columns={columns} columnBuckets={columnBuckets} - datasetId={datasetId} contentFilter={contentFilter} rootConceptIdsByColumn={rootConceptIdsByColumn} row={firstRowWithoutDifferences} diff --git a/frontend/src/js/entity-history/timeline/Year.tsx b/frontend/src/js/entity-history/timeline/Year.tsx index ccc43d5250..4ed30bbc67 100644 --- a/frontend/src/js/entity-history/timeline/Year.tsx +++ b/frontend/src/js/entity-history/timeline/Year.tsx @@ -5,7 +5,6 @@ import { ColumnDescription, ConceptIdT, CurrencyConfigT, - DatasetT, TimeStratifiedInfo, } from "../../api/types"; import { ContentFilterValue } from "../ContentControl"; @@ -22,7 +21,6 @@ const YearGroup = styled("div")` `; const Year = ({ - datasetId, year, getIsOpen, toggleOpenYear, @@ -36,7 +34,6 @@ const Year = ({ rootConceptIdsByColumn, timeStratifiedInfos, }: { - datasetId: DatasetT["id"]; year: number; getIsOpen: (year: number, quarter?: number) => boolean; toggleOpenYear: (year: number) => void; @@ -78,7 +75,6 @@ const Year = ({ - getConceptById( - // TODO: should be just v - semantic?.concept.split(".")[0] + "." + v, - semantic!.concept, - ), - ) + .map((v) => getConceptById(v, semantic!.concept)) .filter(exists); return ( @@ -169,11 +163,8 @@ const TimeStratifiedInfos = ({ ); } - // else if (typeof value === "string") { - // const concept = getConceptById(semantic!.concept, value); - // if (concept) valueFormatted = concept.label; - // } + // TOOD: Potentially support single-value concepts } let valueFormatted: string | number | string[] = value; From 96b12a3aff31c46e1399b1bf87660fec2980ef7b Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 13 Jun 2023 13:52:47 +0200 Subject: [PATCH 50/93] Fix unused import --- frontend/src/js/entity-history/EntityCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/js/entity-history/EntityCard.tsx b/frontend/src/js/entity-history/EntityCard.tsx index 04a78393bc..2e26f237d2 100644 --- a/frontend/src/js/entity-history/EntityCard.tsx +++ b/frontend/src/js/entity-history/EntityCard.tsx @@ -9,7 +9,7 @@ import { exists } from "../common/helpers/exists"; import EntityInfos from "./EntityInfos"; import { TimeStratifiedChart } from "./TimeStratifiedChart"; -import { getColumnType, isMoneyColumn } from "./timeline/util"; +import { isMoneyColumn } from "./timeline/util"; const Container = styled("div")` display: grid; From 40deac363097a42407e73068f61ac2e6e93385d5 Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Tue, 13 Jun 2023 15:53:24 +0200 Subject: [PATCH 51/93] move observationPeriodStart to FrontendConfig and map it on FrontendConfiguration level --- .../apiv1/frontend/FrontendConfiguration.java | 4 +++- .../apiv1/frontend/FrontendPreviewConfig.java | 3 --- .../conquery/models/config/FrontendConfig.java | 14 ++++++++------ .../conquery/models/datasets/PreviewConfig.java | 6 ------ .../conquery/resources/api/ConceptsProcessor.java | 1 - .../conquery/resources/api/ConfigResource.java | 3 ++- .../integration/tests/EntityExportTest.java | 8 -------- 7 files changed, 13 insertions(+), 26 deletions(-) diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/frontend/FrontendConfiguration.java b/backend/src/main/java/com/bakdata/conquery/apiv1/frontend/FrontendConfiguration.java index 8aa6ce070a..57fcfa27ce 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/frontend/FrontendConfiguration.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/frontend/FrontendConfiguration.java @@ -1,6 +1,7 @@ package com.bakdata.conquery.apiv1.frontend; import java.net.URL; +import java.time.LocalDate; import com.bakdata.conquery.models.config.FrontendConfig; import com.bakdata.conquery.models.config.IdColumnConfig; @@ -19,6 +20,7 @@ public record FrontendConfiguration( FrontendConfig.CurrencyConfig currency, IdColumnConfig queryUpload, URL manualUrl, - String contactEmail + String contactEmail, + LocalDate observationPeriodStart ) { } diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/frontend/FrontendPreviewConfig.java b/backend/src/main/java/com/bakdata/conquery/apiv1/frontend/FrontendPreviewConfig.java index 6fe30b9994..9769140747 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/frontend/FrontendPreviewConfig.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/frontend/FrontendPreviewConfig.java @@ -1,6 +1,5 @@ package com.bakdata.conquery.apiv1.frontend; -import java.time.LocalDate; import java.util.Collection; import java.util.List; @@ -19,8 +18,6 @@ public static class Labelled { private final String label; } - private final LocalDate observationPeriodMin; - private final Collection all; @JsonProperty("default") private final Collection defaultConnectors; diff --git a/backend/src/main/java/com/bakdata/conquery/models/config/FrontendConfig.java b/backend/src/main/java/com/bakdata/conquery/models/config/FrontendConfig.java index e50fb7008f..66f6e7b293 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/config/FrontendConfig.java +++ b/backend/src/main/java/com/bakdata/conquery/models/config/FrontendConfig.java @@ -2,6 +2,7 @@ import java.net.URI; import java.net.URL; +import java.time.LocalDate; import javax.annotation.Nullable; import javax.validation.Valid; @@ -10,27 +11,28 @@ import com.bakdata.conquery.models.forms.frontendconfiguration.FormScanner; import com.fasterxml.jackson.annotation.JsonAlias; -import groovy.transform.ToString; import lombok.AllArgsConstructor; import lombok.Data; -import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; import lombok.With; import lombok.extern.slf4j.Slf4j; -@ToString -@Getter -@Setter @NoArgsConstructor @AllArgsConstructor @Slf4j @With +@Data public class FrontendConfig { @Valid @NotNull private CurrencyConfig currency = new CurrencyConfig(); + /** + * Default start-date for EntityPreview and DatePicker. + */ + @NotNull + private LocalDate observationStart; + /** * The url that points a manual. This is also used by the {@link FormScanner} * as the base url for forms that specify a relative url. Internally {@link URI#resolve(URI)} diff --git a/backend/src/main/java/com/bakdata/conquery/models/datasets/PreviewConfig.java b/backend/src/main/java/com/bakdata/conquery/models/datasets/PreviewConfig.java index c08a23458c..9deccb3249 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/datasets/PreviewConfig.java +++ b/backend/src/main/java/com/bakdata/conquery/models/datasets/PreviewConfig.java @@ -1,6 +1,5 @@ package com.bakdata.conquery.models.datasets; -import java.time.LocalDate; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -46,11 +45,6 @@ @NoArgsConstructor public class PreviewConfig { - /** - * Default start-date for EntityPreview, end date will always be LocalDate.now() - */ - @NotNull - private LocalDate observationStart; /** * Selects to be used in {@link com.bakdata.conquery.apiv1.QueryProcessor#getSingleEntityExport(Subject, UriBuilder, String, String, List, Dataset, Range)}. diff --git a/backend/src/main/java/com/bakdata/conquery/resources/api/ConceptsProcessor.java b/backend/src/main/java/com/bakdata/conquery/resources/api/ConceptsProcessor.java index c59be20ec6..a3ff393d83 100644 --- a/backend/src/main/java/com/bakdata/conquery/resources/api/ConceptsProcessor.java +++ b/backend/src/main/java/com/bakdata/conquery/resources/api/ConceptsProcessor.java @@ -139,7 +139,6 @@ public FrontendPreviewConfig getEntityPreviewFrontendConfig(Dataset dataset) { // Connectors only act as bridge to table for the fronted, but also provide ConceptColumnT semantic return new FrontendPreviewConfig( - previewConfig.getObservationStart(), previewConfig.getAllConnectors() .stream() .map(id -> new FrontendPreviewConfig.Labelled(id.toString(), namespace.getCentralRegistry().resolve(id).getTable().getLabel())) diff --git a/backend/src/main/java/com/bakdata/conquery/resources/api/ConfigResource.java b/backend/src/main/java/com/bakdata/conquery/resources/api/ConfigResource.java index 8f1f695329..0c2432c762 100644 --- a/backend/src/main/java/com/bakdata/conquery/resources/api/ConfigResource.java +++ b/backend/src/main/java/com/bakdata/conquery/resources/api/ConfigResource.java @@ -37,7 +37,8 @@ public FrontendConfiguration getFrontendConfig() { frontendConfig.getCurrency(), idColumns, frontendConfig.getManualUrl(), - frontendConfig.getContactEmail() + frontendConfig.getContactEmail(), + frontendConfig.getObservationStart() ); } diff --git a/backend/src/test/java/com/bakdata/conquery/integration/tests/EntityExportTest.java b/backend/src/test/java/com/bakdata/conquery/integration/tests/EntityExportTest.java index ab862e3de4..6f21b6f6c4 100644 --- a/backend/src/test/java/com/bakdata/conquery/integration/tests/EntityExportTest.java +++ b/backend/src/test/java/com/bakdata/conquery/integration/tests/EntityExportTest.java @@ -13,7 +13,6 @@ import java.util.Set; import java.util.stream.Collectors; -import javax.servlet.http.HttpServletRequest; import javax.ws.rs.client.Entity; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -24,7 +23,6 @@ import com.bakdata.conquery.integration.common.RequiredData; import com.bakdata.conquery.integration.json.JsonIntegrationTest; import com.bakdata.conquery.integration.json.QueryTest; -import com.bakdata.conquery.models.auth.entities.Subject; import com.bakdata.conquery.models.common.Range; import com.bakdata.conquery.models.datasets.Dataset; import com.bakdata.conquery.models.datasets.PreviewConfig; @@ -42,7 +40,6 @@ import com.bakdata.conquery.resources.admin.rest.AdminDatasetResource; import com.bakdata.conquery.resources.api.DatasetQueryResource; import com.bakdata.conquery.resources.api.EntityPreviewRequest; -import com.bakdata.conquery.resources.api.QueryResource; import com.bakdata.conquery.resources.hierarchies.HierarchyHelper; import com.bakdata.conquery.util.support.StandaloneSupport; import com.bakdata.conquery.util.support.TestConquery; @@ -50,9 +47,6 @@ import lombok.extern.slf4j.Slf4j; import org.assertj.core.description.LazyTextDescription; -/** - * Adapted from {@link com.bakdata.conquery.integration.tests.deletion.ImportDeletionTest}, tests {@link QueryResource#getEntityData(Subject, QueryResource.EntityPreview, HttpServletRequest)}. - */ @Slf4j public class EntityExportTest implements ProgrammaticIntegrationTest { @@ -100,8 +94,6 @@ public void execute(String name, TestConquery testConquery) throws Exception { final PreviewConfig previewConfig = new PreviewConfig(); - previewConfig.setObservationStart(LocalDate.of(2010,1,1)); - previewConfig.setInfoCardSelects(List.of( new PreviewConfig.InfoCardSelect("Age", SelectId.Parser.INSTANCE.parsePrefixed(dataset.getName(), "tree1.connector.age"), null), new PreviewConfig.InfoCardSelect("Values", valuesSelectId, null) From 41a89962780e404e73bd0e357a9b2d907ec6977e Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 13 Jun 2023 16:17:33 +0200 Subject: [PATCH 52/93] Fix unused import --- frontend/src/js/entity-history/timeline/ConceptName.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/js/entity-history/timeline/ConceptName.tsx b/frontend/src/js/entity-history/timeline/ConceptName.tsx index d3324e6b09..d49facb336 100644 --- a/frontend/src/js/entity-history/timeline/ConceptName.tsx +++ b/frontend/src/js/entity-history/timeline/ConceptName.tsx @@ -2,7 +2,7 @@ import styled from "@emotion/styled"; import { faFolder } from "@fortawesome/free-solid-svg-icons"; import { memo } from "react"; -import { ConceptIdT, DatasetT } from "../../api/types"; +import { ConceptIdT } from "../../api/types"; import { getConceptById } from "../../concept-trees/globalTreeStoreHelper"; import FaIcon from "../../icon/FaIcon"; From b9d4768fed36107be1510fedccbe35c97024a476 Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Tue, 13 Jun 2023 16:33:20 +0200 Subject: [PATCH 53/93] adds default to FrontendConfig#observationStart --- .../com/bakdata/conquery/models/config/FrontendConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/bakdata/conquery/models/config/FrontendConfig.java b/backend/src/main/java/com/bakdata/conquery/models/config/FrontendConfig.java index 66f6e7b293..599b78a222 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/config/FrontendConfig.java +++ b/backend/src/main/java/com/bakdata/conquery/models/config/FrontendConfig.java @@ -3,6 +3,7 @@ import java.net.URI; import java.net.URL; import java.time.LocalDate; +import java.time.temporal.ChronoUnit; import javax.annotation.Nullable; import javax.validation.Valid; @@ -31,7 +32,7 @@ public class FrontendConfig { * Default start-date for EntityPreview and DatePicker. */ @NotNull - private LocalDate observationStart; + private LocalDate observationStart = LocalDate.now().minus(10, ChronoUnit.YEARS); /** * The url that points a manual. This is also used by the {@link FormScanner} From 68570e64de983f0eb691b24da9370b715c02a478 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 13 Jun 2023 17:02:57 +0200 Subject: [PATCH 54/93] Use observation period start from frontend config --- frontend/src/js/api/types.ts | 2 +- frontend/src/js/entity-history/actions.ts | 10 +++++++++- frontend/src/js/entity-history/reducer.ts | 3 --- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/src/js/api/types.ts b/frontend/src/js/api/types.ts index 82eb4a1d50..ef0fc4d964 100644 --- a/frontend/src/js/api/types.ts +++ b/frontend/src/js/api/types.ts @@ -296,6 +296,7 @@ export interface GetFrontendConfigResponseT { queryUpload: QueryUploadConfigT; manualUrl?: string; contactEmail?: string; + observationPeriodStart?: string; // yyyy-mm-dd format, start of the data } export type GetConceptResponseT = Record; @@ -535,7 +536,6 @@ export interface HistorySources { export type GetEntityHistoryDefaultParamsResponse = HistorySources & { searchConcept: string | null; // concept id searchFilters?: string[]; // allowlisted filter ids within the searchConcept - observationPeriodMin: string; // yyyy-MM-dd }; export interface EntityInfo { diff --git a/frontend/src/js/entity-history/actions.ts b/frontend/src/js/entity-history/actions.ts index fb7b8d18f6..2f72407754 100644 --- a/frontend/src/js/entity-history/actions.ts +++ b/frontend/src/js/entity-history/actions.ts @@ -1,3 +1,5 @@ +import startOfYear from "date-fns/startOfYear"; +import subYears from "date-fns/subYears"; import { useCallback } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; @@ -180,6 +182,12 @@ export function useUpdateHistorySession() { StateT, StateT["entityHistory"]["defaultParams"] >((state) => state.entityHistory.defaultParams); + const observationPeriodMin = useSelector((state) => { + return ( + state.startup.config.observationPeriodStart || + formatStdDate(subYears(startOfYear(new Date()), 1)) + ); + }); return useCallback( async ({ @@ -203,7 +211,7 @@ export function useUpdateHistorySession() { entityId, defaultEntityHistoryParams.sources, { - min: defaultEntityHistoryParams.observationPeriodMin, + min: observationPeriodMin, max: formatStdDate(new Date()), }, ); diff --git a/frontend/src/js/entity-history/reducer.ts b/frontend/src/js/entity-history/reducer.ts index 11a577b2ee..f6d59e45d5 100644 --- a/frontend/src/js/entity-history/reducer.ts +++ b/frontend/src/js/entity-history/reducer.ts @@ -35,7 +35,6 @@ export interface EntityId { export type EntityHistoryStateT = { defaultParams: { - observationPeriodMin: string; sources: HistorySources; searchConcept: string | null; searchFilters: string[]; @@ -57,7 +56,6 @@ export type EntityHistoryStateT = { const initialState: EntityHistoryStateT = { defaultParams: { - observationPeriodMin: "2020-01-01", sources: { all: [], default: [] }, searchConcept: null, searchFilters: [], @@ -86,7 +84,6 @@ export default function reducer( return { ...state, defaultParams: { - observationPeriodMin: action.payload.observationPeriodMin, sources: { all: action.payload.all, default: action.payload.default }, searchConcept: action.payload.searchConcept, searchFilters: action.payload.searchFilters || [], From ddda5aa1f04ca9293b7fe3bf2a0b7ec1d3b30141 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 13 Jun 2023 17:14:37 +0200 Subject: [PATCH 55/93] Use the observation period start for date picker --- frontend/src/js/common/helpers/dateHelper.ts | 2 ++ frontend/src/js/entity-history/actions.ts | 1 + .../ui-components/InputDate/CustomHeader.tsx | 20 +++++++++++++------ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/frontend/src/js/common/helpers/dateHelper.ts b/frontend/src/js/common/helpers/dateHelper.ts index c5fc13fc8c..f7d7a566ad 100644 --- a/frontend/src/js/common/helpers/dateHelper.ts +++ b/frontend/src/js/common/helpers/dateHelper.ts @@ -216,11 +216,13 @@ export function getFirstAndLastDateOfRange(dateRangeStr: string): { export function useMonthName(date: Date): string { const locale = useDateLocale(); + return format(date, "LLLL", { locale }); } export function useMonthNames(): string[] { const locale = useDateLocale(); + return [...Array(12).keys()].map((month) => { const date = new Date(); date.setMonth(month); diff --git a/frontend/src/js/entity-history/actions.ts b/frontend/src/js/entity-history/actions.ts index 2f72407754..a8e416ce0a 100644 --- a/frontend/src/js/entity-history/actions.ts +++ b/frontend/src/js/entity-history/actions.ts @@ -278,6 +278,7 @@ export function useUpdateHistorySession() { dispatch, getAuthorizedUrl, getEntityHistory, + observationPeriodMin, ], ); } diff --git a/frontend/src/js/ui-components/InputDate/CustomHeader.tsx b/frontend/src/js/ui-components/InputDate/CustomHeader.tsx index ca1269e39f..83b0b594c0 100644 --- a/frontend/src/js/ui-components/InputDate/CustomHeader.tsx +++ b/frontend/src/js/ui-components/InputDate/CustomHeader.tsx @@ -5,8 +5,10 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { useState } from "react"; import { ReactDatePickerCustomHeaderProps } from "react-datepicker"; +import { useSelector } from "react-redux"; import { SelectOptionT } from "../../api/types"; +import { StateT } from "../../app/reducers"; import IconButton from "../../button/IconButton"; import { TransparentButton } from "../../button/TransparentButton"; import { useMonthName, useMonthNames } from "../../common/helpers/dateHelper"; @@ -79,13 +81,19 @@ const YearMonthSelect = ({ ReactDatePickerCustomHeaderProps, "date" | "changeYear" | "changeMonth" >) => { - const yearSelectionSpan = 10; - const yearOptions: SelectOptionT[] = [...Array(yearSelectionSpan).keys()] + const numLastYearsToShow = useSelector((state) => { + if (state.startup.config.observationPeriodStart) { + return ( + new Date().getFullYear() - + new Date(state.startup.config.observationPeriodStart).getFullYear() + ); + } else { + return 10; + } + }); + const yearOptions: SelectOptionT[] = [...Array(numLastYearsToShow).keys()] .map((n) => new Date().getFullYear() - n) - .map((year) => ({ - label: String(year), - value: year, - })) + .map((year) => ({ label: String(year), value: year })) .reverse(); const monthNames = useMonthNames(); From 3bc01dee4afb8ce7823003f10090b70fcfcbf0aa Mon Sep 17 00:00:00 2001 From: Fabian Blank Date: Tue, 13 Jun 2023 17:40:14 +0200 Subject: [PATCH 56/93] change colors and rename variables --- frontend/src/app-theme.ts | 10 +++++----- frontend/src/js/button/DownloadButton.tsx | 15 ++++++++------- frontend/src/react-app-env.d.ts | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/frontend/src/app-theme.ts b/frontend/src/app-theme.ts index 63f6825b57..572c8b3fd2 100644 --- a/frontend/src/app-theme.ts +++ b/frontend/src/app-theme.ts @@ -27,11 +27,11 @@ export const theme: Theme = { "#777", "#fff", ], - files: { - csv: "#279e47", - pdf: "#c9181e", - zip: "#e5c527", - xlsx: "#3B843C", + fileTypes: { + csv: "#1c8e3b", + pdf: "#b50a10", + zip: "#c1a515", + xlsx: "#2b602c", }, bgAlt: "#f4f6f5", blueGrayDark: "#1f5f30", diff --git a/frontend/src/js/button/DownloadButton.tsx b/frontend/src/js/button/DownloadButton.tsx index bc487beea1..6d5df34a08 100644 --- a/frontend/src/js/button/DownloadButton.tsx +++ b/frontend/src/js/button/DownloadButton.tsx @@ -30,19 +30,20 @@ interface FileIcon { color?: string; } -const fileTypeToIcon: Record = { - ZIP: { icon: faFileArchive, color: theme.col.files.zip }, - XLSX: { icon: faFileExcel, color: theme.col.files.xlsx }, - PDF: { icon: faFilePdf, color: theme.col.files.pdf }, - CSV: { icon: faFileCsv, color: theme.col.files.csv }, +const fileTypeToFileIcon: Record = { + ZIP: { icon: faFileArchive, color: theme.col.fileTypes.zip }, + XLSX: { icon: faFileExcel, color: theme.col.fileTypes.xlsx }, + PDF: { icon: faFilePdf, color: theme.col.fileTypes.pdf }, + CSV: { icon: faFileCsv, color: theme.col.fileTypes.csv }, }; function getFileInfo(url: string): FileIcon { // Forms + if (url.includes(".")) { const ext = getEnding(url); - if (ext in fileTypeToIcon) { - return fileTypeToIcon[ext]; + if (ext in fileTypeToFileIcon) { + return fileTypeToFileIcon[ext]; } } return { icon: faFileDownload }; diff --git a/frontend/src/react-app-env.d.ts b/frontend/src/react-app-env.d.ts index f2bf4b86c6..16604e8489 100644 --- a/frontend/src/react-app-env.d.ts +++ b/frontend/src/react-app-env.d.ts @@ -31,7 +31,7 @@ declare module "@emotion/react" { green: string; orange: string; palette: string[]; - files: { + fileTypes: { csv: string; pdf: string; zip: string; From 8de2c071bc80d307e09227bd3e009c623490f3c0 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 19 Jun 2023 11:33:54 +0200 Subject: [PATCH 57/93] Reset results on dataset select --- frontend/src/js/dataset/actions.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/js/dataset/actions.ts b/frontend/src/js/dataset/actions.ts index 3e7287e561..caa4aee6f3 100644 --- a/frontend/src/js/dataset/actions.ts +++ b/frontend/src/js/dataset/actions.ts @@ -9,8 +9,12 @@ import { StateT } from "../app/reducers"; import { ErrorObject } from "../common/actions/genericActions"; import { exists } from "../common/helpers/exists"; import { useLoadTrees } from "../concept-trees/actions"; -import { useLoadDefaultHistoryParams } from "../entity-history/actions"; +import { + resetHistory, + useLoadDefaultHistoryParams, +} from "../entity-history/actions"; import { useLoadQueries } from "../previous-queries/list/actions"; +import { queryResultReset } from "../query-runner/actions"; import { setMessage } from "../snack-message/actions"; import { SnackMessageType } from "../snack-message/reducer"; import { clearQuery, loadSavedQuery } from "../standard-query-editor/actions"; @@ -109,6 +113,12 @@ export const useSelectDataset = () => { dispatch(selectDatasetInput({ id: datasetId })); + dispatch(resetHistory()); + dispatch(queryResultReset({ queryType: "standard" })); + dispatch(queryResultReset({ queryType: "timebased" })); + dispatch(queryResultReset({ queryType: "editorV2" })); + dispatch(queryResultReset({ queryType: "externalForms" })); + // To allow loading trees to check whether they should abort or not setDatasetId(datasetId); From e3429bfc4ae62dc0922773528d1d0ed5d9f64dc0 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 19 Jun 2023 12:13:54 +0200 Subject: [PATCH 58/93] Avoid importing theme directly, use hook instead --- frontend/src/js/button/DownloadButton.tsx | 25 ++++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/frontend/src/js/button/DownloadButton.tsx b/frontend/src/js/button/DownloadButton.tsx index 6d5df34a08..0862be59eb 100644 --- a/frontend/src/js/button/DownloadButton.tsx +++ b/frontend/src/js/button/DownloadButton.tsx @@ -1,3 +1,4 @@ +import { useTheme } from "@emotion/react"; import styled from "@emotion/styled"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { @@ -8,9 +9,8 @@ import { faFileExcel, faFilePdf, } from "@fortawesome/free-solid-svg-icons"; -import { ReactNode, useContext, forwardRef } from "react"; +import { ReactNode, useContext, forwardRef, useMemo } from "react"; -import { theme } from "../../app-theme"; import { ResultUrlWithLabel } from "../api/types"; import { AuthTokenContext } from "../authorization/AuthTokenProvider"; import { getEnding } from "../query-runner/DownloadResultsDropdownButton"; @@ -30,22 +30,27 @@ interface FileIcon { color?: string; } -const fileTypeToFileIcon: Record = { - ZIP: { icon: faFileArchive, color: theme.col.fileTypes.zip }, - XLSX: { icon: faFileExcel, color: theme.col.fileTypes.xlsx }, - PDF: { icon: faFilePdf, color: theme.col.fileTypes.pdf }, - CSV: { icon: faFileCsv, color: theme.col.fileTypes.csv }, -}; +function useFileIcon(url: string): FileIcon { + const theme = useTheme(); -function getFileInfo(url: string): FileIcon { - // Forms + const fileTypeToFileIcon: Record = useMemo( + () => ({ + ZIP: { icon: faFileArchive, color: theme.col.fileTypes.zip }, + XLSX: { icon: faFileExcel, color: theme.col.fileTypes.xlsx }, + PDF: { icon: faFilePdf, color: theme.col.fileTypes.pdf }, + CSV: { icon: faFileCsv, color: theme.col.fileTypes.csv }, + }), + [theme], + ); if (url.includes(".")) { const ext = getEnding(url); + if (ext in fileTypeToFileIcon) { return fileTypeToFileIcon[ext]; } } + return { icon: faFileDownload }; } From 70c4dc0b0b7054d2e30fcbfee5b44ae12eeb2571 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 19 Jun 2023 12:14:18 +0200 Subject: [PATCH 59/93] Iterate file type colors based on best practices + ChatGPT suggestions --- frontend/src/app-theme.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/app-theme.ts b/frontend/src/app-theme.ts index 572c8b3fd2..fb8cfc19c4 100644 --- a/frontend/src/app-theme.ts +++ b/frontend/src/app-theme.ts @@ -28,10 +28,10 @@ export const theme: Theme = { "#fff", ], fileTypes: { - csv: "#1c8e3b", - pdf: "#b50a10", - zip: "#c1a515", - xlsx: "#2b602c", + csv: "#007BFF", + pdf: "#d73a49", + zip: "#6f42c1", + xlsx: "#28a745", }, bgAlt: "#f4f6f5", blueGrayDark: "#1f5f30", From 5b3dc08c1ad97e65f3a879e1914bf7eb27ad2264 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 19 Jun 2023 12:14:38 +0200 Subject: [PATCH 60/93] Simplify props passing --- frontend/src/js/button/DownloadButton.tsx | 8 ++++---- frontend/src/js/button/IconButton.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/js/button/DownloadButton.tsx b/frontend/src/js/button/DownloadButton.tsx index 0862be59eb..2868f260bf 100644 --- a/frontend/src/js/button/DownloadButton.tsx +++ b/frontend/src/js/button/DownloadButton.tsx @@ -73,16 +73,16 @@ const DownloadButton = forwardRef( authToken, )}&charset=ISO_8859_1`; - const fileInfo = getFileInfo(resultUrl.url); + const { icon, color } = useFileIcon(resultUrl.url); return ( {children} diff --git a/frontend/src/js/button/IconButton.tsx b/frontend/src/js/button/IconButton.tsx index 2b7f5e076a..772e52b4a2 100644 --- a/frontend/src/js/button/IconButton.tsx +++ b/frontend/src/js/button/IconButton.tsx @@ -175,9 +175,9 @@ const IconButton = forwardRef( tight={tight} bgHover={bgHover} red={red} + large={large} {...restProps} ref={ref} - large={large} > {iconElement} {children && {children}} From f05722ca95f16a54f5008ada24e98ea6dac9e6a5 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 19 Jun 2023 13:09:47 +0200 Subject: [PATCH 61/93] Add tabbable money column charts --- frontend/src/js/entity-history/EntityCard.tsx | 92 ++----------------- .../TabbableTimeStratifiedCharts.tsx | 42 +++++++++ .../js/entity-history/TimeStratifiedChart.tsx | 17 ++-- 3 files changed, 59 insertions(+), 92 deletions(-) create mode 100644 frontend/src/js/entity-history/TabbableTimeStratifiedCharts.tsx diff --git a/frontend/src/js/entity-history/EntityCard.tsx b/frontend/src/js/entity-history/EntityCard.tsx index 2e26f237d2..1654920b5e 100644 --- a/frontend/src/js/entity-history/EntityCard.tsx +++ b/frontend/src/js/entity-history/EntityCard.tsx @@ -1,24 +1,20 @@ import styled from "@emotion/styled"; -import { Fragment } from "react"; -import { NumericFormat } from "react-number-format"; -import { useSelector } from "react-redux"; -import { CurrencyConfigT, EntityInfo, TimeStratifiedInfo } from "../api/types"; -import { StateT } from "../app/reducers"; -import { exists } from "../common/helpers/exists"; +import { EntityInfo, TimeStratifiedInfo } from "../api/types"; import EntityInfos from "./EntityInfos"; -import { TimeStratifiedChart } from "./TimeStratifiedChart"; +import { TabbableTimeStratifiedCharts } from "./TabbableTimeStratifiedCharts"; import { isMoneyColumn } from "./timeline/util"; const Container = styled("div")` display: grid; grid-template-columns: 1fr 1fr; gap: 10px; - padding: 20px; + padding: 20px 24px; background-color: ${({ theme }) => theme.col.bg}; border-radius: ${({ theme }) => theme.borderRadius}; border: 1px solid ${({ theme }) => theme.col.grayLight}; + align-items: center; `; const Centered = styled("div")` @@ -28,76 +24,6 @@ const Centered = styled("div")` gap: 10px; `; -const Grid = styled("div")` - display: grid; - gap: 0 20px; - grid-template-columns: auto auto; -`; - -const Label = styled("div")` - font-size: ${({ theme }) => theme.font.xs}; -`; - -const Value = styled("div")` - font-size: ${({ theme }) => theme.font.sm}; - font-weight: 400; - justify-self: end; -`; - -// @ts-ignore EVALUATE IF WE WANT TO SHOW THIS TABLE WITH FUTURE DATA -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const Table = ({ - timeStratifiedInfos, -}: { - timeStratifiedInfos: TimeStratifiedInfo[]; -}) => { - const currencyConfig = useSelector( - (state) => state.startup.config.currency, - ); - return ( - <> - {timeStratifiedInfos.map((timeStratifiedInfo) => { - return ( - - {timeStratifiedInfo.columns.map((column) => { - const label = column.label; - const value = timeStratifiedInfo.totals[column.label]; - - if (!exists(value)) return <>; - - const valueFormatted = - typeof value === "number" - ? Math.round(value) - : value instanceof Array - ? value.join(", ") - : value; - - return ( - - - - {isMoneyColumn(column) && typeof value === "number" ? ( - - ) : ( - valueFormatted - )} - - - ); - })} - - ); - })} - - ); -}; - export const EntityCard = ({ className, infos, @@ -107,16 +33,16 @@ export const EntityCard = ({ infos: EntityInfo[]; timeStratifiedInfos: TimeStratifiedInfo[]; }) => { + const infosWithOnlyMoneyColumns = timeStratifiedInfos.filter((info) => + info.columns.every(isMoneyColumn), + ); + return ( - {/* TODO: EVALUATE IF WE WANT TO SHOW THIS TABLE WITH FUTURE DATA - */} - + ); }; diff --git a/frontend/src/js/entity-history/TabbableTimeStratifiedCharts.tsx b/frontend/src/js/entity-history/TabbableTimeStratifiedCharts.tsx new file mode 100644 index 0000000000..49c248d070 --- /dev/null +++ b/frontend/src/js/entity-history/TabbableTimeStratifiedCharts.tsx @@ -0,0 +1,42 @@ +import styled from "@emotion/styled"; +import { useState, useMemo } from "react"; + +import { TimeStratifiedInfo } from "../api/types"; +import SmallTabNavigation from "../small-tab-navigation/SmallTabNavigation"; + +import { TimeStratifiedChart } from "./TimeStratifiedChart"; + +const Container = styled("div")` + display: flex; + flex-direction: column; + align-items: flex-end; +`; + +export const TabbableTimeStratifiedCharts = ({ + infos, +}: { + infos: TimeStratifiedInfo[]; +}) => { + const [activeTab, setActiveTab] = useState(infos[0].label); + const options = useMemo(() => { + return infos.map((info) => ({ + value: info.label, + label: () => info.label, + })); + }, [infos]); + + const activeInfos = useMemo(() => { + return infos.find((info) => info.label === activeTab); + }, [infos, activeTab]); + + return ( + + + {activeInfos && } + + ); +}; diff --git a/frontend/src/js/entity-history/TimeStratifiedChart.tsx b/frontend/src/js/entity-history/TimeStratifiedChart.tsx index 8bfdb576d9..2bd08588f2 100644 --- a/frontend/src/js/entity-history/TimeStratifiedChart.tsx +++ b/frontend/src/js/entity-history/TimeStratifiedChart.tsx @@ -19,7 +19,7 @@ import { exists } from "../common/helpers/exists"; ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip); const ChartContainer = styled("div")` - height: 185px; + height: 190px; width: 100%; display: flex; justify-content: flex-end; @@ -65,16 +65,15 @@ const useFormatCurrency = () => { }; export const TimeStratifiedChart = ({ - timeStratifiedInfos, + timeStratifiedInfo, }: { - timeStratifiedInfos: TimeStratifiedInfo[]; + timeStratifiedInfo: TimeStratifiedInfo; }) => { const theme = useTheme(); - const infosToVisualize = timeStratifiedInfos[0]; - const labels = infosToVisualize.columns.map((col) => col.label); + const labels = timeStratifiedInfo.columns.map((col) => col.label); const datasets = useMemo(() => { - const sortedYears = [...infosToVisualize.years].sort( + const sortedYears = [...timeStratifiedInfo.years].sort( (a, b) => b.year - a.year, ); @@ -87,7 +86,7 @@ export const TimeStratifiedChart = ({ )}, ${interpolateDecreasingOpacity(i)})`, }; }); - }, [theme, infosToVisualize, labels]); + }, [theme, timeStratifiedInfo, labels]); const data = { labels, @@ -101,7 +100,7 @@ export const TimeStratifiedChart = ({ plugins: { title: { display: true, - text: infosToVisualize.label, + text: timeStratifiedInfo.label, }, tooltip: { usePointStyle: true, @@ -159,7 +158,7 @@ export const TimeStratifiedChart = ({ }, }, }; - }, [infosToVisualize, labels, formatCurrency]); + }, [timeStratifiedInfo, labels, formatCurrency]); return ( From 9af584733c77688c130327229bd7a7dffb10e173 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 19 Jun 2023 15:39:29 +0200 Subject: [PATCH 62/93] Blur history entity data by default --- frontend/src/js/entity-history/EntityCard.tsx | 4 +- .../src/js/entity-history/EntityInfos.tsx | 13 ++++-- frontend/src/js/entity-history/History.tsx | 10 +++++ frontend/src/js/entity-history/Timeline.tsx | 27 +++++++------ .../js/entity-history/VisibilityControl.tsx | 40 +++++++++++++++++++ frontend/src/localization/de.json | 1 + frontend/src/localization/en.json | 1 + 7 files changed, 79 insertions(+), 17 deletions(-) create mode 100644 frontend/src/js/entity-history/VisibilityControl.tsx diff --git a/frontend/src/js/entity-history/EntityCard.tsx b/frontend/src/js/entity-history/EntityCard.tsx index 1654920b5e..f4633d1468 100644 --- a/frontend/src/js/entity-history/EntityCard.tsx +++ b/frontend/src/js/entity-history/EntityCard.tsx @@ -25,10 +25,12 @@ const Centered = styled("div")` `; export const EntityCard = ({ + blurred, className, infos, timeStratifiedInfos, }: { + blurred?: boolean; className?: string; infos: EntityInfo[]; timeStratifiedInfos: TimeStratifiedInfo[]; @@ -40,7 +42,7 @@ export const EntityCard = ({ return ( - + diff --git a/frontend/src/js/entity-history/EntityInfos.tsx b/frontend/src/js/entity-history/EntityInfos.tsx index 7c8786b704..d425d3032a 100644 --- a/frontend/src/js/entity-history/EntityInfos.tsx +++ b/frontend/src/js/entity-history/EntityInfos.tsx @@ -12,18 +12,25 @@ const Grid = styled("div")` const Label = styled("div")` font-size: ${({ theme }) => theme.font.xs}; `; -const Value = styled("div")` +const Value = styled("div")<{ blurred?: boolean }>` font-size: ${({ theme }) => theme.font.sm}; font-weight: 400; + ${({ blurred }) => blurred && "filter: blur(6px);"} `; -const EntityInfos = ({ infos }: { infos: EntityInfo[] }) => { +const EntityInfos = ({ + infos, + blurred, +}: { + infos: EntityInfo[]; + blurred?: boolean; +}) => { return ( {infos.map((info) => ( - {info.value} + {info.value} ))} diff --git a/frontend/src/js/entity-history/History.tsx b/frontend/src/js/entity-history/History.tsx index 91e999409b..e134ca0b21 100644 --- a/frontend/src/js/entity-history/History.tsx +++ b/frontend/src/js/entity-history/History.tsx @@ -25,6 +25,7 @@ import type { LoadingPayload } from "./LoadHistoryDropzone"; import { Navigation } from "./Navigation"; import SourcesControl from "./SourcesControl"; import Timeline from "./Timeline"; +import VisibilityControl from "./VisibilityControl"; import { useUpdateHistorySession } from "./actions"; import { EntityId } from "./reducer"; @@ -120,6 +121,10 @@ export const History = () => { (state) => state.entityHistory.resultUrls, ); + const [blurred, setBlurred] = useState(true); + const toggleBlurred = useCallback(() => setBlurred((v) => !v), []); + useHotkeys("p", toggleBlurred, [toggleBlurred]); + const [showAdvancedControls, setShowAdvancedControls] = useState(false); useHotkeys("shift+alt+h", () => { @@ -216,6 +221,10 @@ export const History = () => { + {showAdvancedControls && ( { ; - contentFilter: ContentFilterValue; - getIsOpen: (year: number, quarter?: number) => boolean; - toggleOpenYear: (year: number) => void; - toggleOpenQuarter: (year: number, quarter: number) => void; -} - const Timeline = ({ className, currentEntityInfos, @@ -70,7 +58,19 @@ const Timeline = ({ getIsOpen, toggleOpenYear, toggleOpenQuarter, -}: Props) => { + blurred, +}: { + className?: string; + currentEntityInfos: EntityInfo[]; + currentEntityTimeStratifiedInfos: TimeStratifiedInfo[]; + detailLevel: DetailLevel; + sources: Set; + contentFilter: ContentFilterValue; + getIsOpen: (year: number, quarter?: number) => boolean; + toggleOpenYear: (year: number) => void; + toggleOpenQuarter: (year: number, quarter: number) => void; + blurred?: boolean; +}) => { const data = useSelector( (state) => state.entityHistory.currentEntityData, ); @@ -93,6 +93,7 @@ const Timeline = ({ return ( diff --git a/frontend/src/js/entity-history/VisibilityControl.tsx b/frontend/src/js/entity-history/VisibilityControl.tsx new file mode 100644 index 0000000000..f1bfed7ce6 --- /dev/null +++ b/frontend/src/js/entity-history/VisibilityControl.tsx @@ -0,0 +1,40 @@ +import styled from "@emotion/styled"; +import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons"; +import { memo } from "react"; +import { useTranslation } from "react-i18next"; + +import IconButton from "../button/IconButton"; +import WithTooltip from "../tooltip/WithTooltip"; + +const Root = styled("div")` + display: flex; + flex-direction: column; + align-items: center; +`; + +const SxIconButton = styled(IconButton)` + padding: 8px 10px; +`; + +const VisibilityControl = ({ + blurred, + toggleBlurred, +}: { + blurred?: boolean; + toggleBlurred: () => void; +}) => { + const { t } = useTranslation(); + + return ( + + + + + + ); +}; + +export default memo(VisibilityControl); diff --git a/frontend/src/localization/de.json b/frontend/src/localization/de.json index 6aa4c25e4a..da050656ef 100644 --- a/frontend/src/localization/de.json +++ b/frontend/src/localization/de.json @@ -459,6 +459,7 @@ "queryNodeDetails": "Detail-Einstellungen bearbeiten" }, "history": { + "blurred": "Daten-Sichtbarkeit", "emptyTimeline": { "headline": "Historie", "description": "Hier werden Informationen chronologisch dargestellt.", diff --git a/frontend/src/localization/en.json b/frontend/src/localization/en.json index aa5462a49a..007499910f 100644 --- a/frontend/src/localization/en.json +++ b/frontend/src/localization/en.json @@ -459,6 +459,7 @@ "queryNodeDetails": "Detail-Einstellungen bearbeiten" }, "history": { + "blurred": "Data visibility", "emptyTimeline": { "headline": "History", "description": "Exploring individual events chronologically.", From 8b6edd70e9ad6ecb8a98f3c18088ae47916f7500 Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Mon, 19 Jun 2023 16:33:14 +0200 Subject: [PATCH 63/93] fixes rasining an NPE when unknown form was submitted --- .../conquery/apiv1/forms/ExternalForm.java | 2 +- .../bakdata/conquery/apiv1/forms/Form.java | 10 +- .../frontendconfiguration/FormProcessor.java | 4 +- .../frontendconfiguration/FormScanner.java | 121 ++++++++++-------- 4 files changed, 81 insertions(+), 56 deletions(-) diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/forms/ExternalForm.java b/backend/src/main/java/com/bakdata/conquery/apiv1/forms/ExternalForm.java index 49ec7b6ca2..6416184df7 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/forms/ExternalForm.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/forms/ExternalForm.java @@ -79,7 +79,7 @@ public String getLocalizedTypeLabel() { // Form had no specific title set. Try localized lookup in FormConfig final Locale preferredLocale = I18n.LOCALE.get(); - final FormType frontendConfig = FormScanner.FRONTEND_FORM_CONFIGS.get(getFormType()); + final FormType frontendConfig = FormScanner.resolveFormType(getFormType()); if (frontendConfig == null) { return getSubType(); diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/forms/Form.java b/backend/src/main/java/com/bakdata/conquery/apiv1/forms/Form.java index 11bcd5f3e6..c9844ce5c4 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/forms/Form.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/forms/Form.java @@ -8,7 +8,9 @@ import com.bakdata.conquery.models.auth.entities.Subject; import com.bakdata.conquery.models.auth.permissions.Ability; import com.bakdata.conquery.models.datasets.Dataset; +import com.bakdata.conquery.models.error.ConqueryError; import com.bakdata.conquery.models.forms.frontendconfiguration.FormScanner; +import com.bakdata.conquery.models.forms.frontendconfiguration.FormType; import com.bakdata.conquery.models.forms.managed.ManagedForm; import com.bakdata.conquery.models.query.visitor.QueryVisitor; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -42,7 +44,13 @@ public String getFormType() { public void authorize(Subject subject, Dataset submittedDataset, @NonNull ClassToInstanceMap visitors, MetaStorage storage) { QueryDescription.super.authorize(subject, submittedDataset, visitors, storage); // Check if subject is allowed to create this form - subject.authorize(FormScanner.FRONTEND_FORM_CONFIGS.get(getFormType()), Ability.CREATE); + final FormType formType = FormScanner.resolveFormType(getFormType()); + + if (formType == null) { + throw new ConqueryError.ExecutionCreationErrorUnspecified(); + } + + subject.authorize(formType, Ability.CREATE); } diff --git a/backend/src/main/java/com/bakdata/conquery/models/forms/frontendconfiguration/FormProcessor.java b/backend/src/main/java/com/bakdata/conquery/models/forms/frontendconfiguration/FormProcessor.java index 1a202425aa..df03966ba2 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/forms/frontendconfiguration/FormProcessor.java +++ b/backend/src/main/java/com/bakdata/conquery/models/forms/frontendconfiguration/FormProcessor.java @@ -1,7 +1,5 @@ package com.bakdata.conquery.models.forms.frontendconfiguration; -import static com.bakdata.conquery.models.forms.frontendconfiguration.FormScanner.FRONTEND_FORM_CONFIGS; - import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -15,7 +13,7 @@ public class FormProcessor { public Collection getFormsForUser(Subject subject) { List allowedForms = new ArrayList<>(); - for (FormType formMapping : FRONTEND_FORM_CONFIGS.values()) { + for (FormType formMapping : FormScanner.getAllFormTypes()) { if (!subject.isPermitted(formMapping, Ability.CREATE)) { continue; } diff --git a/backend/src/main/java/com/bakdata/conquery/models/forms/frontendconfiguration/FormScanner.java b/backend/src/main/java/com/bakdata/conquery/models/forms/frontendconfiguration/FormScanner.java index a5ca29059f..ee88e86290 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/forms/frontendconfiguration/FormScanner.java +++ b/backend/src/main/java/com/bakdata/conquery/models/forms/frontendconfiguration/FormScanner.java @@ -9,6 +9,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Consumer; import com.bakdata.conquery.apiv1.forms.Form; @@ -36,15 +37,13 @@ public class FormScanner extends Task { public static final String MANUAL_URL_KEY = "manualUrl"; public static Map FRONTEND_FORM_CONFIGS = Collections.emptyMap(); - - private Consumer> providerChain = QueryUtils.getNoOpEntryPoint(); - /** * The config is used to look up the base url for manuals see {@link FrontendConfig#getManualUrl()}. * If the url was changed (e.g. using {@link AdminProcessor#executeScript(String)}) an execution of this * task accounts the change. */ private final ConqueryConfig config; + private Consumer> providerChain = QueryUtils.getNoOpEntryPoint(); public FormScanner(ConqueryConfig config) { super("form-scanner"); @@ -52,68 +51,48 @@ public FormScanner(ConqueryConfig config) { registerFrontendFormConfigProvider(ResourceFormConfigProvider::accept); } - private static Map> findBackendMappingClasses() { - Builder> backendClasses = ImmutableMap.builder(); - // Gather form implementations first - for (Class subclass : CPSTypeIdResolver.SCAN_RESULT.getSubclasses(Form.class.getName()).loadClasses()) { - if (Modifier.isAbstract(subclass.getModifiers())) { - continue; - } - CPSType[] cpsAnnotations = subclass.getAnnotationsByType(CPSType.class); - - if (cpsAnnotations.length == 0) { - log.warn("Implemented Form {} has no CPSType annotation", subclass); - continue; - } - for (CPSType cpsType : cpsAnnotations) { - backendClasses.put(cpsType.id(), (Class) subclass); - } - } - return backendClasses.build(); + public synchronized void registerFrontendFormConfigProvider(Consumer> provider) { + providerChain = providerChain.andThen(provider); } - public synchronized void registerFrontendFormConfigProvider(Consumer> provider){ - providerChain = providerChain.andThen(provider); + public static FormType resolveFormType(String formType) { + return FRONTEND_FORM_CONFIGS.get(formType); } - /** - * Frontend form configurations can be provided from different sources. - * Each source must register a provider with {@link FormScanner#registerFrontendFormConfigProvider(Consumer)} beforehand. - */ - @SneakyThrows - private List findFrontendFormConfigs() { + public static Set getAllFormTypes() { + return Set.copyOf(FRONTEND_FORM_CONFIGS.values()); + } - ImmutableList.Builder frontendConfigs = ImmutableList.builder(); - try { - providerChain.accept(frontendConfigs); - } catch (Exception e) { - log.error("Unable to collect all frontend form configurations.", e); - } - return frontendConfigs.build(); + @Override + public void execute(Map> parameters, PrintWriter output) throws Exception { + FRONTEND_FORM_CONFIGS = generateFEFormConfigMap(); } private Map generateFEFormConfigMap() { // Collect backend implementations for specific forms - Map> forms = findBackendMappingClasses(); + final Map> forms = findBackendMappingClasses(); // Collect frontend form configurations for the specific forms - List frontendConfigs = findFrontendFormConfigs(); + final List frontendConfigs = findFrontendFormConfigs(); // Match frontend form configurations to backend implementations final ImmutableMap.Builder result = ImmutableMap.builderWithExpectedSize(frontendConfigs.size()); for (FormFrontendConfigInformation configInfo : frontendConfigs) { - ObjectNode configTree = configInfo.getConfigTree(); - JsonNode type = configTree.get("type"); + + final ObjectNode configTree = configInfo.getConfigTree(); + final JsonNode type = configTree.get("type"); + if (!validTypeId(type)) { log.warn("Found invalid type id in {}. Was: {}", configInfo.getOrigin(), type); continue; } // Extract complete type information (type@subtype) and type information - String fullTypeIdentifier = type.asText(); - String typeIdentifier = CPSTypeIdResolver.truncateSubTypeInformation(fullTypeIdentifier); + final String fullTypeIdentifier = type.asText(); + final String typeIdentifier = CPSTypeIdResolver.truncateSubTypeInformation(fullTypeIdentifier); + if (!forms.containsKey(typeIdentifier)) { log.error("Frontend form config {} (type = {}) does not map to a backend class.", configInfo, type); continue; @@ -139,14 +118,19 @@ private Map generateFEFormConfigMap() { return URI.create(manualUrl.textValue()); }); + + final URL manualBaseUrl = config.getFrontend().getManualUrl(); + if (manualBaseUrl != null && manualURL != null) { final TextNode manualNode = relativizeManualUrl(fullTypeIdentifier, manualURL, manualBaseUrl); + if (manualNode == null) { log.warn("Manual url relativization did not succeed for {}. Skipping registration.", fullTypeIdentifier); continue; } + configTree.set(MANUAL_URL_KEY, manualNode); } @@ -158,6 +142,50 @@ private Map generateFEFormConfigMap() { return result.build(); } + private static Map> findBackendMappingClasses() { + final Builder> backendClasses = ImmutableMap.builder(); + // Gather form implementations first + for (Class subclass : CPSTypeIdResolver.SCAN_RESULT.getSubclasses(Form.class.getName()).loadClasses()) { + if (Modifier.isAbstract(subclass.getModifiers())) { + continue; + } + + final CPSType[] cpsAnnotations = subclass.getAnnotationsByType(CPSType.class); + + if (cpsAnnotations.length == 0) { + log.warn("Implemented Form {} has no CPSType annotation", subclass); + continue; + } + + for (CPSType cpsType : cpsAnnotations) { + backendClasses.put(cpsType.id(), (Class) subclass); + } + } + return backendClasses.build(); + } + + /** + * Frontend form configurations can be provided from different sources. + * Each source must register a provider with {@link FormScanner#registerFrontendFormConfigProvider(Consumer)} beforehand. + */ + @SneakyThrows + private List findFrontendFormConfigs() { + + final ImmutableList.Builder frontendConfigs = ImmutableList.builder(); + + try { + providerChain.accept(frontendConfigs); + } + catch (Exception e) { + log.error("Unable to collect all frontend form configurations.", e); + } + return frontendConfigs.build(); + } + + private static boolean validTypeId(JsonNode node) { + return node != null && node.isTextual() && !node.asText().isEmpty(); + } + private TextNode relativizeManualUrl(@NonNull String formTypeIdentifier, @NonNull URI manualUri, @NonNull URL manualBaseUrl) { try { @@ -182,13 +210,4 @@ private TextNode relativizeManualUrl(@NonNull String formTypeIdentifier, @NonNul } } - private static boolean validTypeId(JsonNode node) { - return node != null && node.isTextual() && !node.asText().isEmpty(); - } - - @Override - public void execute(Map> parameters, PrintWriter output) throws Exception { - FRONTEND_FORM_CONFIGS = generateFEFormConfigMap(); - } - } From ff69232adf6a80073b09988f9368a750347d20e0 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 20 Jun 2023 11:21:34 +0200 Subject: [PATCH 64/93] Support default time aggregation settings --- frontend/src/js/api/types.ts | 1 + .../src/js/concept-trees/ConceptTreeNode.tsx | 57 +++++-------------- frontend/src/js/model/node.ts | 11 +++- .../src/js/standard-query-editor/helper.ts | 2 +- 4 files changed, 26 insertions(+), 45 deletions(-) diff --git a/frontend/src/js/api/types.ts b/frontend/src/js/api/types.ts index ef0fc4d964..2820fcc75e 100644 --- a/frontend/src/js/api/types.ts +++ b/frontend/src/js/api/types.ts @@ -163,6 +163,7 @@ export interface ConceptBaseT { description?: string; // Empty array: key not defined additionalInfos?: InfoT[]; // Empty array: key not defined dateRange?: DateRangeT; + excludeFromTimeAggregation?: boolean; // To default-exclude some concepts from time aggregation } export type ConceptStructT = ConceptBaseT; diff --git a/frontend/src/js/concept-trees/ConceptTreeNode.tsx b/frontend/src/js/concept-trees/ConceptTreeNode.tsx index ffe51d382a..5095f42d27 100644 --- a/frontend/src/js/concept-trees/ConceptTreeNode.tsx +++ b/frontend/src/js/concept-trees/ConceptTreeNode.tsx @@ -1,13 +1,6 @@ import styled from "@emotion/styled"; -import { FC } from "react"; - -import type { - ConceptIdT, - InfoT, - DateRangeT, - ConceptT, - ConceptElementT, -} from "../api/types"; + +import type { ConceptIdT, ConceptT, ConceptElementT } from "../api/types"; import { useOpenableConcept } from "../concept-trees-open/useOpenableConcept"; import { resetSelects } from "../model/select"; import { resetTables } from "../model/table"; @@ -22,44 +15,18 @@ const Root = styled("div")` font-size: ${({ theme }) => theme.font.sm}; `; -// Concept data that is necessary to display tree nodes. Includes additional infos -// for the tooltip as well as the id of the corresponding tree -interface TreeNodeData { - label: string; - description?: string; - active?: boolean; - matchingEntries: number | null; - matchingEntities: number | null; - dateRange?: DateRangeT; - additionalInfos?: InfoT[]; - children?: ConceptIdT[]; -} - -interface PropsT { - rootConceptId: ConceptIdT; - conceptId: ConceptIdT; - data: TreeNodeData; - depth: number; - search: SearchT; -} - -const selectTreeNodeData = (concept: ConceptT) => ({ - label: concept.label, - description: concept.description, - active: concept.active, - matchingEntries: concept.matchingEntries, - matchingEntities: concept.matchingEntities, - dateRange: concept.dateRange, - additionalInfos: concept.additionalInfos, - children: concept.children, -}); - -const ConceptTreeNode: FC = ({ +const ConceptTreeNode = ({ data, rootConceptId, conceptId, depth, search, +}: { + rootConceptId: ConceptIdT; + conceptId: ConceptIdT; + data: ConceptT; + depth: number; + search: SearchT; }) => { const { open, onToggleOpen } = useOpenableConcept({ conceptId, @@ -121,6 +88,10 @@ const ConceptTreeNode: FC = ({ matchingEntities: data.matchingEntities, dateRange: data.dateRange, + excludeTimestamps: + root.excludeFromTimeAggregation || + data.excludeFromTimeAggregation, + tree: rootConceptId, }; }} @@ -140,7 +111,7 @@ const ConceptTreeNode: FC = ({ key={childId} rootConceptId={rootConceptId} conceptId={childId} - data={selectTreeNodeData(child)} + data={child} depth={depth + 1} search={search} /> diff --git a/frontend/src/js/model/node.ts b/frontend/src/js/model/node.ts index 4cf533d58e..2f3d01a8fb 100644 --- a/frontend/src/js/model/node.ts +++ b/frontend/src/js/model/node.ts @@ -11,6 +11,7 @@ import { useTranslation } from "react-i18next"; import { ConceptElementT, ConceptT } from "../api/types"; import { DNDType } from "../common/constants/dndTypes"; +import { getConceptById } from "../concept-trees/globalTreeStoreHelper"; import type { ConceptQueryNodeType, DragItemConceptTreeNode, @@ -55,8 +56,16 @@ export const nodeHasEmptySettings = (node: StandardQueryNodeT) => { export const nodeHasFilterValues = (node: StandardQueryNodeT) => nodeIsConceptQueryNode(node) && tablesHaveFilterValues(node.tables); +const nodeHasNonDefaultExcludeTimestamps = (node: StandardQueryNodeT) => { + if (!nodeIsConceptQueryNode(node)) return node.excludeTimestamps; + + const root = getConceptById(node.tree, node.tree); + + return node.excludeTimestamps !== root?.excludeFromTimeAggregation; +}; + export const nodeHasNonDefaultSettings = (node: StandardQueryNodeT) => - node.excludeTimestamps || + nodeHasNonDefaultExcludeTimestamps(node) || node.excludeFromSecondaryId || (nodeIsConceptQueryNode(node) && (objectHasNonDefaultSelects(node) || diff --git a/frontend/src/js/standard-query-editor/helper.ts b/frontend/src/js/standard-query-editor/helper.ts index 06efce90db..bb2c20a483 100644 --- a/frontend/src/js/standard-query-editor/helper.ts +++ b/frontend/src/js/standard-query-editor/helper.ts @@ -7,7 +7,7 @@ export function getRootNodeLabel(node: StandardQueryNodeT) { if (!nodeIsConceptQueryNode(node) || !node.ids || !node.tree) return null; const nodeIsRootNode = node.ids.includes(node.tree); - const root = getConceptById(node.tree); + const root = getConceptById(node.tree, node.tree); if (nodeIsRootNode) { const noRootOrSameLabel = !root || root.label === node.label; From 63c8f5433bbb4f344f6947053ae92885be97b7cd Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Tue, 20 Jun 2023 11:37:35 +0200 Subject: [PATCH 65/93] adds nullable annotation --- .../models/forms/frontendconfiguration/FormScanner.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/src/main/java/com/bakdata/conquery/models/forms/frontendconfiguration/FormScanner.java b/backend/src/main/java/com/bakdata/conquery/models/forms/frontendconfiguration/FormScanner.java index ee88e86290..c6f2ffc4e0 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/forms/frontendconfiguration/FormScanner.java +++ b/backend/src/main/java/com/bakdata/conquery/models/forms/frontendconfiguration/FormScanner.java @@ -12,6 +12,8 @@ import java.util.Set; import java.util.function.Consumer; +import javax.annotation.Nullable; + import com.bakdata.conquery.apiv1.forms.Form; import com.bakdata.conquery.io.cps.CPSType; import com.bakdata.conquery.io.cps.CPSTypeIdResolver; @@ -55,6 +57,8 @@ public synchronized void registerFrontendFormConfigProvider(Consumer Date: Tue, 20 Jun 2023 16:19:33 +0200 Subject: [PATCH 66/93] don't merge CONCEPT_COLUMN into SECONDARY_ID --- .../conquery/apiv1/query/TableExportQuery.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/query/TableExportQuery.java b/backend/src/main/java/com/bakdata/conquery/apiv1/query/TableExportQuery.java index 63a798787c..1b9d71b4f1 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/query/TableExportQuery.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/query/TableExportQuery.java @@ -2,6 +2,7 @@ import java.time.LocalDate; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -172,6 +173,15 @@ private Map calculateSecondaryIdPositions(Atomi private static Map calculateColumnPositions(AtomicInteger currentPosition, List tables, Map secondaryIdPositions) { final Map positions = new HashMap<>(); + // We need to know if a column is a concept column so we can prioritize it if it is also a SecondaryId + final Set conceptColumns = tables.stream() + .map(CQConcept::getTables) + .flatMap(Collection::stream) + .map(CQTable::getConnector) + .map(Connector::getColumn) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + for (CQConcept concept : tables) { for (CQTable table : concept.getTables()) { @@ -188,7 +198,8 @@ private static Map calculateColumnPositions(AtomicInteger curre continue; } - if (column.getSecondaryId() != null) { + // We want to have ConceptColumns separate here. + if (column.getSecondaryId() != null && !conceptColumns.contains(column)) { positions.putIfAbsent(column, secondaryIdPositions.get(column.getSecondaryId())); continue; } From d368ef018aefa3f438ed2e9e11b7f4ad866018d3 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 20 Jun 2023 16:55:42 +0200 Subject: [PATCH 67/93] Iterate blurification --- frontend/.env.e2e | 2 ++ frontend/.env.example | 5 +++- frontend/Dockerfile | 1 + frontend/scripts/replace-env-at-runtime.sh | 1 + .../src/js/entity-history/EntityHeader.tsx | 25 ++++++++++--------- .../src/js/entity-history/EntityIdsList.tsx | 21 ++++++++++------ frontend/src/js/entity-history/History.tsx | 5 +++- frontend/src/js/entity-history/Navigation.tsx | 3 +++ frontend/src/js/environment/index.ts | 4 +++ 9 files changed, 45 insertions(+), 22 deletions(-) diff --git a/frontend/.env.e2e b/frontend/.env.e2e index 1e786c501c..f85d08b732 100644 --- a/frontend/.env.e2e +++ b/frontend/.env.e2e @@ -18,3 +18,5 @@ REACT_APP_IDP_ENABLE=false REACT_APP_IDP_URL=http://localhost:8080/auth REACT_APP_IDP_REALM=Myrealm REACT_APP_IDP_CLIENT_ID=frontend + +REACT_APP_BLURRED_ENTITY_DEFAULT=true \ No newline at end of file diff --git a/frontend/.env.example b/frontend/.env.example index 40734bdcc7..fd6878cafc 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -22,4 +22,7 @@ REACT_APP_IDP_URL=http://localhost:8080/auth # NOTE: You will need to create this realm in keycloak REACT_APP_IDP_REALM=Myrealm # NOTE: You will need to create this client in keycloak -REACT_APP_IDP_CLIENT_ID=frontend \ No newline at end of file +REACT_APP_IDP_CLIENT_ID=frontend + +# Application features +REACT_APP_BLURRED_ENTITY_DEFAULT=true \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile index d1d6e8ceb8..3245718e0b 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -24,6 +24,7 @@ ENV REACT_APP_IDP_ENABLE=$REACT_APP_IDP_ENABLE ENV REACT_APP_IDP_URL=$REACT_APP_IDP_URL ENV REACT_APP_IDP_REALM=$REACT_APP_IDP_REALM ENV REACT_APP_IDP_CLIENT_ID=$REACT_APP_IDP_CLIENT_ID +ENV REACT_APP_BLURRED_ENTITY_DEFAULT=$REACT_APP_BLURRED_ENTITY_DEFAULT # Copy the build artifacts from the builder phase COPY --from=builder /app/dist /usr/share/nginx/html diff --git a/frontend/scripts/replace-env-at-runtime.sh b/frontend/scripts/replace-env-at-runtime.sh index 5b359112f0..df87e7f85b 100755 --- a/frontend/scripts/replace-env-at-runtime.sh +++ b/frontend/scripts/replace-env-at-runtime.sh @@ -25,6 +25,7 @@ ENVSTRING="${ENVSTRING}REACT_APP_IDP_ENABLE: \"${REACT_APP_IDP_ENABLE:-null}\"," ENVSTRING="${ENVSTRING}REACT_APP_IDP_URL: \"${REACT_APP_IDP_URL:-null}\"," ENVSTRING="${ENVSTRING}REACT_APP_IDP_REALM: \"${REACT_APP_IDP_REALM:-null}\"," ENVSTRING="${ENVSTRING}REACT_APP_IDP_CLIENT_ID: \"${REACT_APP_IDP_CLIENT_ID:-null}\"," +ENVSTRING="${ENVSTRING}REACT_APP_BLURRED_ENTITY_DEFAULT: \"${REACT_APP_BLURRED_ENTITY_DEFAULT:-null}\"," # Replace the marker diff --git a/frontend/src/js/entity-history/EntityHeader.tsx b/frontend/src/js/entity-history/EntityHeader.tsx index d3caa665cf..2012de8306 100644 --- a/frontend/src/js/entity-history/EntityHeader.tsx +++ b/frontend/src/js/entity-history/EntityHeader.tsx @@ -29,9 +29,10 @@ const Buttons = styled("div")` gap: 5px; `; -const SxHeading3 = styled(Heading3)` +const SxHeading3 = styled(Heading3)<{ blurred?: boolean }>` flex-shrink: 0; margin: 0; + ${({ blurred }) => blurred && "filter: blur(6px);"} `; const Subtitle = styled("div")` font-size: ${({ theme }) => theme.font.xs}; @@ -47,23 +48,23 @@ const Avatar = styled(SxHeading3)` font-weight: 300; `; -interface Props { - className?: string; - currentEntityIndex: number; - currentEntityId: EntityId; - status: SelectOptionT[]; - setStatus: (value: SelectOptionT[]) => void; - entityStatusOptions: SelectOptionT[]; -} - export const EntityHeader = ({ + blurred, className, currentEntityIndex, currentEntityId, status, setStatus, entityStatusOptions, -}: Props) => { +}: { + blurred?: boolean; + className?: string; + currentEntityIndex: number; + currentEntityId: EntityId; + status: SelectOptionT[]; + setStatus: (value: SelectOptionT[]) => void; + entityStatusOptions: SelectOptionT[]; +}) => { const totalEvents = useSelector( (state) => state.entityHistory.currentEntityData.length, ); @@ -87,7 +88,7 @@ export const EntityHeader = ({
#{currentEntityIndex + 1} - {currentEntityId.id} + {currentEntityId.id} {totalEvents} {t("history.events", { count: totalEvents })} diff --git a/frontend/src/js/entity-history/EntityIdsList.tsx b/frontend/src/js/entity-history/EntityIdsList.tsx index 1d7da5ae22..316db42c52 100644 --- a/frontend/src/js/entity-history/EntityIdsList.tsx +++ b/frontend/src/js/entity-history/EntityIdsList.tsx @@ -54,19 +54,23 @@ const Gray = styled("span")` color: ${({ theme }) => theme.col.gray}; `; -interface Props { - currentEntityId: EntityId | null; - entityIds: EntityId[]; - updateHistorySession: ReturnType; - entityIdsStatus: EntityIdsStatus; -} +const Blurred = styled("span")<{ blurred?: boolean }>` + ${({ blurred }) => blurred && "filter: blur(6px);"} +`; export const EntityIdsList = ({ + blurred, currentEntityId, entityIds, entityIdsStatus, updateHistorySession, -}: Props) => { +}: { + blurred?: boolean; + currentEntityId: EntityId | null; + entityIds: EntityId[]; + updateHistorySession: ReturnType; + entityIdsStatus: EntityIdsStatus; +}) => { const numberWidth = useMemo(() => { const magnitude = Math.ceil(Math.log(entityIds.length) / Math.log(10)); @@ -85,7 +89,8 @@ export const EntityIdsList = ({ > #{index + 1} - {entityId.id} ({entityId.kind}) + {entityId.id}{" "} + ({entityId.kind}) {entityIdsStatus[entityId.id] && diff --git a/frontend/src/js/entity-history/History.tsx b/frontend/src/js/entity-history/History.tsx index e134ca0b21..a083589bc0 100644 --- a/frontend/src/js/entity-history/History.tsx +++ b/frontend/src/js/entity-history/History.tsx @@ -14,6 +14,7 @@ import type { TimeStratifiedInfo, } from "../api/types"; import type { StateT } from "../app/reducers"; +import { isEntityBlurredByDefault } from "../environment"; import ErrorFallback from "../error-fallback/ErrorFallback"; import DownloadResultsDropdownButton from "../query-runner/DownloadResultsDropdownButton"; @@ -121,7 +122,7 @@ export const History = () => { (state) => state.entityHistory.resultUrls, ); - const [blurred, setBlurred] = useState(true); + const [blurred, setBlurred] = useState(isEntityBlurredByDefault); const toggleBlurred = useCallback(() => setBlurred((v) => !v), []); useHotkeys("p", toggleBlurred, [toggleBlurred]); @@ -190,6 +191,7 @@ export const History = () => { defaultSize="400px" > { {currentEntityId && ( )} Date: Tue, 20 Jun 2023 17:36:29 +0200 Subject: [PATCH 68/93] Increase truncation len, round money to 0 decimals --- .../js/entity-history/TimeStratifiedChart.tsx | 45 +++++++------------ 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/frontend/src/js/entity-history/TimeStratifiedChart.tsx b/frontend/src/js/entity-history/TimeStratifiedChart.tsx index 2bd08588f2..4eeb6c951f 100644 --- a/frontend/src/js/entity-history/TimeStratifiedChart.tsx +++ b/frontend/src/js/entity-history/TimeStratifiedChart.tsx @@ -8,14 +8,14 @@ import { Tooltip, ChartOptions, } from "chart.js"; -import { useCallback, useMemo } from "react"; +import { useMemo } from "react"; import { Bar } from "react-chartjs-2"; -import { useSelector } from "react-redux"; -import { CurrencyConfigT, TimeStratifiedInfo } from "../api/types"; -import { StateT } from "../app/reducers"; +import { TimeStratifiedInfo } from "../api/types"; import { exists } from "../common/helpers/exists"; +const TRUNCATE_X_AXIS_LABELS_LEN = 18; + ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip); const ChartContainer = styled("div")` @@ -42,27 +42,13 @@ function interpolateDecreasingOpacity(index: number) { return Math.min(1, 1 / (index + 0.3)); } -const useFormatCurrency = () => { - const currencyConfig = useSelector( - (state) => state.startup.config.currency, - ); - - const formatCurrency = useCallback( - (value: number) => { - return value.toLocaleString("de-DE", { - style: "currency", - currency: "EUR", - minimumFractionDigits: currencyConfig.decimalScale, - maximumFractionDigits: currencyConfig.decimalScale, - }); - }, - [currencyConfig], - ); - - return { - formatCurrency, - }; -}; +const formatCurrency = (value: number) => + value.toLocaleString("de-DE", { + style: "currency", + currency: "EUR", + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }); export const TimeStratifiedChart = ({ timeStratifiedInfo, @@ -93,8 +79,6 @@ export const TimeStratifiedChart = ({ datasets, }; - const { formatCurrency } = useFormatCurrency(); - const options: ChartOptions<"bar"> = useMemo(() => { return { plugins: { @@ -144,8 +128,9 @@ export const TimeStratifiedChart = ({ x: { ticks: { callback: (idx: any) => { - return labels[idx].length > 12 - ? labels[idx].substring(0, 9) + "..." + return labels[idx].length > TRUNCATE_X_AXIS_LABELS_LEN + ? labels[idx].substring(0, TRUNCATE_X_AXIS_LABELS_LEN - 3) + + "..." : labels[idx]; }, }, @@ -158,7 +143,7 @@ export const TimeStratifiedChart = ({ }, }, }; - }, [timeStratifiedInfo, labels, formatCurrency]); + }, [timeStratifiedInfo, labels]); return ( From ac0d699130e60d8943cb1a02dd0a691320a10ed7 Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Wed, 21 Jun 2023 16:07:05 +0200 Subject: [PATCH 69/93] fixes not applying same logic to result infos --- .../apiv1/query/TableExportQuery.java | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/query/TableExportQuery.java b/backend/src/main/java/com/bakdata/conquery/apiv1/query/TableExportQuery.java index 1b9d71b4f1..993cee86fc 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/query/TableExportQuery.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/query/TableExportQuery.java @@ -148,9 +148,18 @@ public void resolve(QueryResolveContext context) { final Map secondaryIdPositions = calculateSecondaryIdPositions(currentPosition); - positions = calculateColumnPositions(currentPosition, tables, secondaryIdPositions); + // We need to know if a column is a concept column so we can prioritize it if it is also a SecondaryId + final Set conceptColumns = tables.stream() + .map(CQConcept::getTables) + .flatMap(Collection::stream) + .map(CQTable::getConnector) + .map(Connector::getColumn) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + positions = calculateColumnPositions(currentPosition, tables, secondaryIdPositions, conceptColumns); - resultInfos = createResultInfos(secondaryIdPositions); + resultInfos = createResultInfos(secondaryIdPositions, conceptColumns); } private Map calculateSecondaryIdPositions(AtomicInteger currentPosition) { @@ -170,17 +179,9 @@ private Map calculateSecondaryIdPositions(Atomi return secondaryIdPositions; } - private static Map calculateColumnPositions(AtomicInteger currentPosition, List tables, Map secondaryIdPositions) { + private static Map calculateColumnPositions(AtomicInteger currentPosition, List tables, Map secondaryIdPositions, Set conceptColumns) { final Map positions = new HashMap<>(); - // We need to know if a column is a concept column so we can prioritize it if it is also a SecondaryId - final Set conceptColumns = tables.stream() - .map(CQConcept::getTables) - .flatMap(Collection::stream) - .map(CQTable::getConnector) - .map(Connector::getColumn) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); for (CQConcept concept : tables) { for (CQTable table : concept.getTables()) { @@ -212,7 +213,7 @@ private static Map calculateColumnPositions(AtomicInteger curre return positions; } - private List createResultInfos(Map secondaryIdPositions) { + private List createResultInfos(Map secondaryIdPositions, Set conceptColumns) { final int size = positions.values().stream().mapToInt(i -> i).max().getAsInt() + 1; @@ -263,7 +264,7 @@ private List createResultInfos(Map } // SecondaryIds and date columns are pulled to the front, thus already covered. - if (column.getSecondaryId() != null) { + if (column.getSecondaryId() != null && !conceptColumns.contains(column)) { infos[secondaryIdPositions.get(column.getSecondaryId())].getSemantics() .add(new SemanticType.ColumnT(column)); continue; From 272927c80a28ab4b2726b4647a56f1e457cf91f0 Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Thu, 22 Jun 2023 10:00:39 +0200 Subject: [PATCH 70/93] log NoSuchElementException when caught in REST context --- .../bakdata/conquery/io/jetty/NoSuchElementExceptionMapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/bakdata/conquery/io/jetty/NoSuchElementExceptionMapper.java b/backend/src/main/java/com/bakdata/conquery/io/jetty/NoSuchElementExceptionMapper.java index 531786b0c5..f751f9f31f 100644 --- a/backend/src/main/java/com/bakdata/conquery/io/jetty/NoSuchElementExceptionMapper.java +++ b/backend/src/main/java/com/bakdata/conquery/io/jetty/NoSuchElementExceptionMapper.java @@ -12,7 +12,7 @@ public class NoSuchElementExceptionMapper implements ExceptionMapper { @Override public Response toResponse(NoSuchElementException exception) { - log.trace("Mapping exception:", exception); + log.warn("Uncaught NoSuchElementException", exception); return Response.status(Response.Status.NOT_FOUND).type(MediaType.APPLICATION_JSON_TYPE).entity(exception.getMessage()).build(); } } From bbde558b1265b5f533b89db616c1da61474f22eb Mon Sep 17 00:00:00 2001 From: Fabian Blank Date: Fri, 23 Jun 2023 11:43:14 +0200 Subject: [PATCH 71/93] introduce showColoredIcon and apply it to results dropdown --- frontend/src/js/button/DownloadButton.tsx | 5 +++-- .../src/js/query-runner/DownloadResultsDropdownButton.tsx | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/js/button/DownloadButton.tsx b/frontend/src/js/button/DownloadButton.tsx index 2868f260bf..5c382da729 100644 --- a/frontend/src/js/button/DownloadButton.tsx +++ b/frontend/src/js/button/DownloadButton.tsx @@ -60,11 +60,12 @@ interface Props extends Omit { children?: ReactNode; simpleIcon?: boolean; onClick?: () => void; + showColoredIcon?: boolean; } const DownloadButton = forwardRef( ( - { simpleIcon, resultUrl, className, children, onClick, ...restProps }, + { simpleIcon, resultUrl, className, children, onClick, showColoredIcon, ...restProps }, ref, ) => { const { authToken } = useContext(AuthTokenContext); @@ -82,7 +83,7 @@ const DownloadButton = forwardRef( large icon={simpleIcon ? faDownload : icon} onClick={onClick} - iconColor={color} + iconColor={showColoredIcon ? color : undefined} > {children} diff --git a/frontend/src/js/query-runner/DownloadResultsDropdownButton.tsx b/frontend/src/js/query-runner/DownloadResultsDropdownButton.tsx index a59e91f7ed..ca5fa21e89 100644 --- a/frontend/src/js/query-runner/DownloadResultsDropdownButton.tsx +++ b/frontend/src/js/query-runner/DownloadResultsDropdownButton.tsx @@ -124,6 +124,7 @@ const DownloadResultsDropdownButton = ({ resultUrl={resultUrl} onClick={() => setFileChoice({ label: resultUrl.label, ending })} bgHover + showColoredIcon > {truncate(resultUrl.label)} @@ -137,7 +138,7 @@ const DownloadResultsDropdownButton = ({ {!tiny && ( <> - + {truncChosenLabel} From c6585d839ea3c195c4c7af54ece6c3187f5d5ec3 Mon Sep 17 00:00:00 2001 From: Fabian Blank Date: Fri, 23 Jun 2023 11:44:03 +0200 Subject: [PATCH 72/93] format --- frontend/src/js/button/DownloadButton.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/js/button/DownloadButton.tsx b/frontend/src/js/button/DownloadButton.tsx index 5c382da729..fb1a2d6872 100644 --- a/frontend/src/js/button/DownloadButton.tsx +++ b/frontend/src/js/button/DownloadButton.tsx @@ -65,7 +65,15 @@ interface Props extends Omit { const DownloadButton = forwardRef( ( - { simpleIcon, resultUrl, className, children, onClick, showColoredIcon, ...restProps }, + { + simpleIcon, + resultUrl, + className, + children, + onClick, + showColoredIcon, + ...restProps + }, ref, ) => { const { authToken } = useContext(AuthTokenContext); @@ -83,7 +91,7 @@ const DownloadButton = forwardRef( large icon={simpleIcon ? faDownload : icon} onClick={onClick} - iconColor={showColoredIcon ? color : undefined} + iconColor={showColoredIcon ? color : undefined} > {children} From 9526f4851f572055257bddffcefa52b10741070d Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 27 Jun 2023 10:13:39 +0200 Subject: [PATCH 73/93] Remove env variable again --- frontend/.env.e2e | 4 +--- frontend/.env.example | 3 --- frontend/Dockerfile | 1 - frontend/scripts/replace-env-at-runtime.sh | 1 - frontend/src/js/entity-history/History.tsx | 3 +-- frontend/src/js/environment/index.ts | 4 ---- 6 files changed, 2 insertions(+), 14 deletions(-) diff --git a/frontend/.env.e2e b/frontend/.env.e2e index f85d08b732..1e5b246a4f 100644 --- a/frontend/.env.e2e +++ b/frontend/.env.e2e @@ -17,6 +17,4 @@ REACT_APP_LANG=de REACT_APP_IDP_ENABLE=false REACT_APP_IDP_URL=http://localhost:8080/auth REACT_APP_IDP_REALM=Myrealm -REACT_APP_IDP_CLIENT_ID=frontend - -REACT_APP_BLURRED_ENTITY_DEFAULT=true \ No newline at end of file +REACT_APP_IDP_CLIENT_ID=frontend \ No newline at end of file diff --git a/frontend/.env.example b/frontend/.env.example index fd6878cafc..f3763b5599 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -23,6 +23,3 @@ REACT_APP_IDP_URL=http://localhost:8080/auth REACT_APP_IDP_REALM=Myrealm # NOTE: You will need to create this client in keycloak REACT_APP_IDP_CLIENT_ID=frontend - -# Application features -REACT_APP_BLURRED_ENTITY_DEFAULT=true \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 3245718e0b..d1d6e8ceb8 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -24,7 +24,6 @@ ENV REACT_APP_IDP_ENABLE=$REACT_APP_IDP_ENABLE ENV REACT_APP_IDP_URL=$REACT_APP_IDP_URL ENV REACT_APP_IDP_REALM=$REACT_APP_IDP_REALM ENV REACT_APP_IDP_CLIENT_ID=$REACT_APP_IDP_CLIENT_ID -ENV REACT_APP_BLURRED_ENTITY_DEFAULT=$REACT_APP_BLURRED_ENTITY_DEFAULT # Copy the build artifacts from the builder phase COPY --from=builder /app/dist /usr/share/nginx/html diff --git a/frontend/scripts/replace-env-at-runtime.sh b/frontend/scripts/replace-env-at-runtime.sh index df87e7f85b..5b359112f0 100755 --- a/frontend/scripts/replace-env-at-runtime.sh +++ b/frontend/scripts/replace-env-at-runtime.sh @@ -25,7 +25,6 @@ ENVSTRING="${ENVSTRING}REACT_APP_IDP_ENABLE: \"${REACT_APP_IDP_ENABLE:-null}\"," ENVSTRING="${ENVSTRING}REACT_APP_IDP_URL: \"${REACT_APP_IDP_URL:-null}\"," ENVSTRING="${ENVSTRING}REACT_APP_IDP_REALM: \"${REACT_APP_IDP_REALM:-null}\"," ENVSTRING="${ENVSTRING}REACT_APP_IDP_CLIENT_ID: \"${REACT_APP_IDP_CLIENT_ID:-null}\"," -ENVSTRING="${ENVSTRING}REACT_APP_BLURRED_ENTITY_DEFAULT: \"${REACT_APP_BLURRED_ENTITY_DEFAULT:-null}\"," # Replace the marker diff --git a/frontend/src/js/entity-history/History.tsx b/frontend/src/js/entity-history/History.tsx index a083589bc0..94da6be0ec 100644 --- a/frontend/src/js/entity-history/History.tsx +++ b/frontend/src/js/entity-history/History.tsx @@ -14,7 +14,6 @@ import type { TimeStratifiedInfo, } from "../api/types"; import type { StateT } from "../app/reducers"; -import { isEntityBlurredByDefault } from "../environment"; import ErrorFallback from "../error-fallback/ErrorFallback"; import DownloadResultsDropdownButton from "../query-runner/DownloadResultsDropdownButton"; @@ -122,7 +121,7 @@ export const History = () => { (state) => state.entityHistory.resultUrls, ); - const [blurred, setBlurred] = useState(isEntityBlurredByDefault); + const [blurred, setBlurred] = useState(false); const toggleBlurred = useCallback(() => setBlurred((v) => !v), []); useHotkeys("p", toggleBlurred, [toggleBlurred]); diff --git a/frontend/src/js/environment/index.ts b/frontend/src/js/environment/index.ts index 63df1b96d2..9328fbbe10 100644 --- a/frontend/src/js/environment/index.ts +++ b/frontend/src/js/environment/index.ts @@ -28,9 +28,6 @@ const idpRealmEnv = const idpClientIdEnv = runtimeVar("REACT_APP_IDP_CLIENT_ID") || import.meta.env.REACT_APP_IDP_CLIENT_ID; -const entityBlurredByDefaultEnv = - runtimeVar("REACT_APP_BLURRED_ENTITY_DEFAULT") || - import.meta.env.REACT_APP_BLURRED_ENTITY_DEFAULT; export const isProduction = isProductionEnv === "production" || true; export const language = languageEnv === "de" ? "de" : "en"; @@ -41,7 +38,6 @@ export const basename = basenameEnv || ""; export const idpUrl = idpUrlEnv || ""; export const idpRealm = idpRealmEnv || ""; export const idpClientId = idpClientIdEnv || ""; -export const isEntityBlurredByDefault = entityBlurredByDefaultEnv === "true"; export interface CustomEnvironment { getExternalSupportedErrorMessage?: ( From 832ceeb9183e8d506e6bdebe407249391b2f6f85 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 27 Jun 2023 10:48:12 +0200 Subject: [PATCH 74/93] Show history loading indication when it takes longer than 300ms --- .../src/js/entity-history/EntityIdsList.tsx | 24 +++++++++++----- frontend/src/js/entity-history/History.tsx | 2 +- frontend/src/js/entity-history/Navigation.tsx | 3 +- frontend/src/js/entity-history/actions.ts | 28 +++++++++++++++++-- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/frontend/src/js/entity-history/EntityIdsList.tsx b/frontend/src/js/entity-history/EntityIdsList.tsx index 1d7da5ae22..1609c71654 100644 --- a/frontend/src/js/entity-history/EntityIdsList.tsx +++ b/frontend/src/js/entity-history/EntityIdsList.tsx @@ -1,7 +1,10 @@ import styled from "@emotion/styled"; +import { faSpinner } from "@fortawesome/free-solid-svg-icons"; import { useMemo } from "react"; import ReactList from "react-list"; +import FaIcon from "../icon/FaIcon"; + import type { EntityIdsStatus } from "./History"; import { useUpdateHistorySession } from "./actions"; import { EntityId } from "./reducer"; @@ -54,19 +57,25 @@ const Gray = styled("span")` color: ${({ theme }) => theme.col.gray}; `; -interface Props { - currentEntityId: EntityId | null; - entityIds: EntityId[]; - updateHistorySession: ReturnType; - entityIdsStatus: EntityIdsStatus; -} +const SxFaIcon = styled(FaIcon)` + margin: 3px 6px; +`; export const EntityIdsList = ({ currentEntityId, entityIds, entityIdsStatus, updateHistorySession, -}: Props) => { + loadingId, +}: { + currentEntityId: EntityId | null; + entityIds: EntityId[]; + updateHistorySession: ReturnType< + typeof useUpdateHistorySession + >["updateHistorySession"]; + entityIdsStatus: EntityIdsStatus; + loadingId?: string; +}) => { const numberWidth = useMemo(() => { const magnitude = Math.ceil(Math.log(entityIds.length) / Math.log(10)); @@ -87,6 +96,7 @@ export const EntityIdsList = ({ {entityId.id} ({entityId.kind}) + {loadingId === entityId.id && } {entityIdsStatus[entityId.id] && entityIdsStatus[entityId.id].map((val) => ( diff --git a/frontend/src/js/entity-history/History.tsx b/frontend/src/js/entity-history/History.tsx index e134ca0b21..85ed15c9cb 100644 --- a/frontend/src/js/entity-history/History.tsx +++ b/frontend/src/js/entity-history/History.tsx @@ -132,7 +132,7 @@ export const History = () => { }); const [detailLevel, setDetailLevel] = useState("summary"); - const updateHistorySession = useUpdateHistorySession(); + const { updateHistorySession } = useUpdateHistorySession(); const { options, sourcesSet, sourcesFilter, setSourcesFilter } = useSourcesControl(); diff --git a/frontend/src/js/entity-history/Navigation.tsx b/frontend/src/js/entity-history/Navigation.tsx index f125a93589..4631ba22ad 100644 --- a/frontend/src/js/entity-history/Navigation.tsx +++ b/frontend/src/js/entity-history/Navigation.tsx @@ -105,7 +105,7 @@ export const Navigation = memo( }) => { const { t } = useTranslation(); const dispatch = useDispatch(); - const updateHistorySession = useUpdateHistorySession(); + const { loadingId, updateHistorySession } = useUpdateHistorySession(); const onCloseHistory = useCallback(() => { dispatch(closeHistory()); }, [dispatch]); @@ -204,6 +204,7 @@ export const Navigation = memo( entityIds={entityIds} updateHistorySession={updateHistorySession} entityIdsStatus={entityIdsStatus} + loadingId={loadingId} /> {!empty && ( diff --git a/frontend/src/js/entity-history/actions.ts b/frontend/src/js/entity-history/actions.ts index a8e416ce0a..1ccb5c82e5 100644 --- a/frontend/src/js/entity-history/actions.ts +++ b/frontend/src/js/entity-history/actions.ts @@ -1,6 +1,6 @@ import startOfYear from "date-fns/startOfYear"; import subYears from "date-fns/subYears"; -import { useCallback } from "react"; +import { useCallback, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { ActionType, createAction, createAsyncAction } from "typesafe-actions"; @@ -119,7 +119,7 @@ function getPreferredIdColumns(columns: ColumnDescription[]) { export function useNewHistorySession() { const dispatch = useDispatch(); const loadPreviewData = useLoadPreviewData(); - const updateHistorySession = useUpdateHistorySession(); + const { updateHistorySession } = useUpdateHistorySession(); return async (url: string, columns: ColumnDescription[], label: string) => { dispatch(loadHistoryData.request()); @@ -171,6 +171,8 @@ export function useNewHistorySession() { }; } +const SHOW_LOADING_DELAY = 300; + export function useUpdateHistorySession() { const dispatch = useDispatch(); const datasetId = useDatasetId(); @@ -178,6 +180,9 @@ export function useUpdateHistorySession() { const getAuthorizedUrl = useGetAuthorizedUrl(); const { t } = useTranslation(); + const loadingIdTimeout = useRef(); + const [loadingId, setLoadingId] = useState(); + const defaultEntityHistoryParams = useSelector< StateT, StateT["entityHistory"]["defaultParams"] @@ -189,7 +194,7 @@ export function useUpdateHistorySession() { ); }); - return useCallback( + const updateHistorySession = useCallback( async ({ entityId, entityIds, @@ -202,6 +207,13 @@ export function useUpdateHistorySession() { }) => { if (!datasetId) return; + if (loadingIdTimeout.current) { + clearTimeout(loadingIdTimeout.current); + } + loadingIdTimeout.current = setTimeout(() => { + setLoadingId(entityId.id); + }, SHOW_LOADING_DELAY); + try { dispatch(loadHistoryData.request()); @@ -270,6 +282,11 @@ export function useUpdateHistorySession() { }), ); } + + if (loadingIdTimeout.current) { + clearTimeout(loadingIdTimeout.current); + } + setLoadingId(undefined); }, [ t, @@ -281,6 +298,11 @@ export function useUpdateHistorySession() { observationPeriodMin, ], ); + + return { + loadingId, + updateHistorySession, + }; } const transformEntityData = (data: { [key: string]: any }[]): EntityEvent[] => { From 2e6ad30d274c4a77fdfe0676966414d525cfeab0 Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Tue, 27 Jun 2023 15:08:27 +0200 Subject: [PATCH 75/93] cleanup ShutdownTask --- .../resources/admin/ShutdownTask.java | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/backend/src/main/java/com/bakdata/conquery/resources/admin/ShutdownTask.java b/backend/src/main/java/com/bakdata/conquery/resources/admin/ShutdownTask.java index e87cff242b..0965112c78 100644 --- a/backend/src/main/java/com/bakdata/conquery/resources/admin/ShutdownTask.java +++ b/backend/src/main/java/com/bakdata/conquery/resources/admin/ShutdownTask.java @@ -25,24 +25,25 @@ public void serverStarted(Server server) { @Override public void execute(Map> parameters, PrintWriter output) throws Exception { + log.info("Received Shutdown command"); + if(server == null) { output.print("Server not yet started"); + return; } - else { - output.print("Shutting down"); - log.info("Received Shutdown command"); - //this must be done in an extra step or the shutdown will wait for this request to be resolved - new Thread("shutdown waiter thread") { - @Override - public void run() { - try { - server.stop(); - } catch (Exception e) { - log.error("Failed while shutting down", e); - } + + output.print("Shutting down"); + //this must be done in an extra step or the shutdown will wait for this request to be resolved + new Thread("shutdown waiter thread") { + @Override + public void run() { + try { + server.stop(); + } catch (Exception e) { + log.error("Failed while shutting down", e); } - }.start(); - } + } + }.start(); } } From c73bd293170525a0f1ed1f3498c0aa4d0665eb4d Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Tue, 27 Jun 2023 15:18:45 +0200 Subject: [PATCH 76/93] adds Task to reload MetaStorage from disk --- .../conquery/commands/ManagerNode.java | 4 +- .../conquery/tasks/ReloadMetaStorageTask.java | 56 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 backend/src/main/java/com/bakdata/conquery/tasks/ReloadMetaStorageTask.java diff --git a/backend/src/main/java/com/bakdata/conquery/commands/ManagerNode.java b/backend/src/main/java/com/bakdata/conquery/commands/ManagerNode.java index 543e7723f6..7d7d2176ef 100644 --- a/backend/src/main/java/com/bakdata/conquery/commands/ManagerNode.java +++ b/backend/src/main/java/com/bakdata/conquery/commands/ManagerNode.java @@ -46,6 +46,7 @@ import com.bakdata.conquery.resources.unprotected.AuthServlet; import com.bakdata.conquery.tasks.PermissionCleanupTask; import com.bakdata.conquery.tasks.QueryCleanupTask; +import com.bakdata.conquery.tasks.ReloadMetaStorageTask; import com.bakdata.conquery.tasks.ReportConsistencyTask; import com.bakdata.conquery.util.io.ConqueryMDC; import com.fasterxml.jackson.databind.DeserializationConfig; @@ -194,8 +195,9 @@ public void run(ConqueryConfig config, Environment environment) throws Interrupt ))); environment.admin().addTask(new PermissionCleanupTask(storage)); environment.admin().addTask(new ReportConsistencyTask(datasetRegistry)); + environment.admin().addTask(new ReloadMetaStorageTask(storage)); - ShutdownTask shutdown = new ShutdownTask(); + final ShutdownTask shutdown = new ShutdownTask(); environment.admin().addTask(shutdown); environment.lifecycle().addServerLifecycleListener(shutdown); } diff --git a/backend/src/main/java/com/bakdata/conquery/tasks/ReloadMetaStorageTask.java b/backend/src/main/java/com/bakdata/conquery/tasks/ReloadMetaStorageTask.java new file mode 100644 index 0000000000..fb5d423dc2 --- /dev/null +++ b/backend/src/main/java/com/bakdata/conquery/tasks/ReloadMetaStorageTask.java @@ -0,0 +1,56 @@ +package com.bakdata.conquery.tasks; + +import java.io.PrintWriter; +import java.util.List; +import java.util.Map; + +import com.bakdata.conquery.io.storage.MetaStorage; +import com.google.common.base.Stopwatch; +import io.dropwizard.servlets.tasks.Task; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ReloadMetaStorageTask extends Task { + + private final MetaStorage storage; + + public ReloadMetaStorageTask(MetaStorage storage) { + super("reload-meta-storage"); + this.storage = storage; + } + + @Override + public void execute(Map> parameters, PrintWriter output) throws Exception { + final Stopwatch timer = Stopwatch.createStarted(); + + output.println("BEGIN reloading MetaStorage."); + + { + final int allUsers = storage.getAllUsers().size(); + final int allExecutions = storage.getAllExecutions().size(); + final int allFormConfigs = storage.getAllFormConfigs().size(); + final int allGroups = storage.getAllGroups().size(); + final int allRoles = storage.getAllRoles().size(); + + log.debug("BEFORE: Have {} Users, {} Groups, {} Roles, {} Executions, {} FormConfigs.", + allUsers, allGroups, allRoles, allExecutions, allFormConfigs); + } + + storage.loadData(); + output.println("DONE reloading MetaStorage within %s.".formatted(timer.elapsed())); + + { + final int allUsers = storage.getAllUsers().size(); + final int allExecutions = storage.getAllExecutions().size(); + final int allFormConfigs = storage.getAllFormConfigs().size(); + final int allGroups = storage.getAllGroups().size(); + final int allRoles = storage.getAllRoles().size(); + + log.debug("AFTER: Have {} Users, {} Groups, {} Roles, {} Executions, {} FormConfigs.", + allUsers, allGroups, allRoles, allExecutions, allFormConfigs); + } + + + + } +} From 96466ef9c4c5f07b6d3026733c60a8d4865af15f Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 27 Jun 2023 15:30:40 +0200 Subject: [PATCH 77/93] Increase year head size, format currency --- .../js/entity-history/TimeStratifiedChart.tsx | 10 +---- frontend/src/js/entity-history/Timeline.tsx | 2 +- .../entity-history/timeline/SmallHeading.tsx | 1 + .../js/entity-history/timeline/YearHead.tsx | 37 ++++++++++--------- .../src/js/entity-history/timeline/util.ts | 10 +++++ 5 files changed, 34 insertions(+), 26 deletions(-) diff --git a/frontend/src/js/entity-history/TimeStratifiedChart.tsx b/frontend/src/js/entity-history/TimeStratifiedChart.tsx index 4eeb6c951f..718d4d1c4c 100644 --- a/frontend/src/js/entity-history/TimeStratifiedChart.tsx +++ b/frontend/src/js/entity-history/TimeStratifiedChart.tsx @@ -14,6 +14,8 @@ import { Bar } from "react-chartjs-2"; import { TimeStratifiedInfo } from "../api/types"; import { exists } from "../common/helpers/exists"; +import { formatCurrency } from "./timeline/util"; + const TRUNCATE_X_AXIS_LABELS_LEN = 18; ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip); @@ -42,14 +44,6 @@ function interpolateDecreasingOpacity(index: number) { return Math.min(1, 1 / (index + 0.3)); } -const formatCurrency = (value: number) => - value.toLocaleString("de-DE", { - style: "currency", - currency: "EUR", - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }); - export const TimeStratifiedChart = ({ timeStratifiedInfo, }: { diff --git a/frontend/src/js/entity-history/Timeline.tsx b/frontend/src/js/entity-history/Timeline.tsx index 518180033e..84c60d327e 100644 --- a/frontend/src/js/entity-history/Timeline.tsx +++ b/frontend/src/js/entity-history/Timeline.tsx @@ -32,7 +32,7 @@ const Root = styled("div")` -webkit-overflow-scrolling: touch; padding: 0 20px 20px 10px; display: inline-grid; - grid-template-columns: 200px auto; + grid-template-columns: 280px auto; grid-auto-rows: minmax(min-content, max-content); gap: 20px 4px; width: 100%; diff --git a/frontend/src/js/entity-history/timeline/SmallHeading.tsx b/frontend/src/js/entity-history/timeline/SmallHeading.tsx index e72a52de23..51d5a8a06f 100644 --- a/frontend/src/js/entity-history/timeline/SmallHeading.tsx +++ b/frontend/src/js/entity-history/timeline/SmallHeading.tsx @@ -6,4 +6,5 @@ export const SmallHeading = styled(Heading4)` flex-shrink: 0; margin: 0; color: ${({ theme }) => theme.col.black}; + font-size: ${({ theme }) => theme.font.md}; `; diff --git a/frontend/src/js/entity-history/timeline/YearHead.tsx b/frontend/src/js/entity-history/timeline/YearHead.tsx index f05bd5a59c..1d35d4660c 100644 --- a/frontend/src/js/entity-history/timeline/YearHead.tsx +++ b/frontend/src/js/entity-history/timeline/YearHead.tsx @@ -2,24 +2,21 @@ import styled from "@emotion/styled"; import { faCaretDown, faCaretRight } from "@fortawesome/free-solid-svg-icons"; import { Fragment, memo } from "react"; import { useTranslation } from "react-i18next"; -import { useSelector } from "react-redux"; import { ColumnDescriptionSemanticConceptColumn, TimeStratifiedInfo, } from "../../api/types"; -import { StateT } from "../../app/reducers"; import { exists } from "../../common/helpers/exists"; import { getConceptById } from "../../concept-trees/globalTreeStoreHelper"; import FaIcon from "../../icon/FaIcon"; import WithTooltip from "../../tooltip/WithTooltip"; import { SmallHeading } from "./SmallHeading"; -import { isConceptColumn, isMoneyColumn } from "./util"; +import { formatCurrency, isConceptColumn, isMoneyColumn } from "./util"; const Root = styled("div")` font-size: ${({ theme }) => theme.font.xs}; - color: ${({ theme }) => theme.col.gray}; padding: 0 10px 0 0; `; const StickyWrap = styled("div")` @@ -55,19 +52,20 @@ const ConceptRow = styled("div")` display: flex; flex-wrap: wrap; align-items: center; - gap: 2px; + gap: 4px; `; const ConceptBubble = styled("span")` - padding: 0 2px; + padding: 0 3px; border-radius: ${({ theme }) => theme.borderRadius}; color: ${({ theme }) => theme.col.black}; - border: 1px solid ${({ theme }) => theme.col.blueGrayLight}; - font-size: ${({ theme }) => theme.font.tiny}; + border: 1px solid ${({ theme }) => theme.col.gray}; + background-color: white; + font-size: ${({ theme }) => theme.font.sm}; `; const Value = styled("div")` - font-size: ${({ theme }) => theme.font.tiny}; + font-size: ${({ theme }) => theme.font.sm}; font-weight: 400; justify-self: end; white-space: nowrap; @@ -78,7 +76,7 @@ const Value = styled("div")` `; const Label = styled("div")` - font-size: ${({ theme }) => theme.font.tiny}; + font-size: ${({ theme }) => theme.font.sm}; max-width: 100%; white-space: nowrap; overflow: hidden; @@ -92,10 +90,6 @@ const TimeStratifiedInfos = ({ year: number; timeStratifiedInfos: TimeStratifiedInfo[]; }) => { - const currencyUnit = useSelector( - (state) => state.startup.config.currency.unit, - ); - const infos = timeStratifiedInfos .map((info) => { return { @@ -139,7 +133,15 @@ const TimeStratifiedInfos = ({ if (value instanceof Array) { const concepts = value .map((v) => getConceptById(v, semantic!.concept)) - .filter(exists); + .filter(exists) + .sort((c1, c2) => { + const n1 = Number(c1.label); + const n2 = Number(c2.label); + if (!isNaN(n1) && !isNaN(n2)) { + return n1 - n2; + } + return c1.label.localeCompare(c2.label); + }); return ( @@ -169,7 +171,9 @@ const TimeStratifiedInfos = ({ let valueFormatted: string | number | string[] = value; if (typeof value === "number") { - valueFormatted = Math.round(value); + valueFormatted = isMoneyColumn(column) + ? formatCurrency(value) + : Math.round(value); } else if (value instanceof Array) { valueFormatted = value.join(", "); } @@ -179,7 +183,6 @@ const TimeStratifiedInfos = ({ {valueFormatted} - {isMoneyColumn(column) ? " " + currencyUnit : ""} ); diff --git a/frontend/src/js/entity-history/timeline/util.ts b/frontend/src/js/entity-history/timeline/util.ts index bcbac7f1ae..b3f7db5013 100644 --- a/frontend/src/js/entity-history/timeline/util.ts +++ b/frontend/src/js/entity-history/timeline/util.ts @@ -21,3 +21,13 @@ export const isMoneyColumn = (columnDescription: ColumnDescription) => export const isSecondaryIdColumn = (columnDescription: ColumnDescription) => columnDescription.semantics.some((s) => s.type === "SECONDARY_ID"); + +export const formatCurrency = (value: number) => + value.toLocaleString(navigator.language, { + style: "currency", + + currency: "EUR", + unitDisplay: "short", + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }); From fed99f14a6c957ac9ae2096352b405b711e683fc Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 27 Jun 2023 15:33:51 +0200 Subject: [PATCH 78/93] Also increase size of entity card labels --- frontend/src/js/entity-history/EntityInfos.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/js/entity-history/EntityInfos.tsx b/frontend/src/js/entity-history/EntityInfos.tsx index d425d3032a..8b7e4a12b3 100644 --- a/frontend/src/js/entity-history/EntityInfos.tsx +++ b/frontend/src/js/entity-history/EntityInfos.tsx @@ -10,7 +10,7 @@ const Grid = styled("div")` place-items: center start; `; const Label = styled("div")` - font-size: ${({ theme }) => theme.font.xs}; + font-size: ${({ theme }) => theme.font.sm}; `; const Value = styled("div")<{ blurred?: boolean }>` font-size: ${({ theme }) => theme.font.sm}; From ac59b7d200ff42a0ef00476741b68b2be45dc88e Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 27 Jun 2023 16:46:03 +0200 Subject: [PATCH 79/93] Start iterating towards displaying concept data --- frontend/src/js/entity-history/EntityCard.tsx | 9 +-- .../TabbableTimeStratifiedCharts.tsx | 42 ------------ .../TabbableTimeStratifiedInfos.tsx | 68 +++++++++++++++++++ 3 files changed, 70 insertions(+), 49 deletions(-) delete mode 100644 frontend/src/js/entity-history/TabbableTimeStratifiedCharts.tsx create mode 100644 frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx diff --git a/frontend/src/js/entity-history/EntityCard.tsx b/frontend/src/js/entity-history/EntityCard.tsx index f4633d1468..d20e7ee7fc 100644 --- a/frontend/src/js/entity-history/EntityCard.tsx +++ b/frontend/src/js/entity-history/EntityCard.tsx @@ -3,8 +3,7 @@ import styled from "@emotion/styled"; import { EntityInfo, TimeStratifiedInfo } from "../api/types"; import EntityInfos from "./EntityInfos"; -import { TabbableTimeStratifiedCharts } from "./TabbableTimeStratifiedCharts"; -import { isMoneyColumn } from "./timeline/util"; +import { TabbableTimeStratifiedInfos } from "./TabbableTimeStratifiedInfos"; const Container = styled("div")` display: grid; @@ -35,16 +34,12 @@ export const EntityCard = ({ infos: EntityInfo[]; timeStratifiedInfos: TimeStratifiedInfo[]; }) => { - const infosWithOnlyMoneyColumns = timeStratifiedInfos.filter((info) => - info.columns.every(isMoneyColumn), - ); - return ( - + ); }; diff --git a/frontend/src/js/entity-history/TabbableTimeStratifiedCharts.tsx b/frontend/src/js/entity-history/TabbableTimeStratifiedCharts.tsx deleted file mode 100644 index 49c248d070..0000000000 --- a/frontend/src/js/entity-history/TabbableTimeStratifiedCharts.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import styled from "@emotion/styled"; -import { useState, useMemo } from "react"; - -import { TimeStratifiedInfo } from "../api/types"; -import SmallTabNavigation from "../small-tab-navigation/SmallTabNavigation"; - -import { TimeStratifiedChart } from "./TimeStratifiedChart"; - -const Container = styled("div")` - display: flex; - flex-direction: column; - align-items: flex-end; -`; - -export const TabbableTimeStratifiedCharts = ({ - infos, -}: { - infos: TimeStratifiedInfo[]; -}) => { - const [activeTab, setActiveTab] = useState(infos[0].label); - const options = useMemo(() => { - return infos.map((info) => ({ - value: info.label, - label: () => info.label, - })); - }, [infos]); - - const activeInfos = useMemo(() => { - return infos.find((info) => info.label === activeTab); - }, [infos, activeTab]); - - return ( - - - {activeInfos && } - - ); -}; diff --git a/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx b/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx new file mode 100644 index 0000000000..bdbe9ed57f --- /dev/null +++ b/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx @@ -0,0 +1,68 @@ +import styled from "@emotion/styled"; +import { useState, useMemo } from "react"; + +import { TimeStratifiedInfo } from "../api/types"; +import SmallTabNavigation from "../small-tab-navigation/SmallTabNavigation"; + +import { TimeStratifiedChart } from "./TimeStratifiedChart"; +import { isConceptColumn, isMoneyColumn } from "./timeline/util"; + +const Container = styled("div")` + display: flex; + flex-direction: column; + align-items: flex-end; +`; + +export const TabbableTimeStratifiedInfos = ({ + infos, +}: { + infos: TimeStratifiedInfo[]; +}) => { + const [activeTab, setActiveTab] = useState(infos[0].label); + const options = useMemo(() => { + return infos.map((info) => ({ + value: info.label, + label: () => info.label, + })); + }, [infos]); + + const { data, type } = useMemo(() => { + let infoType = "money"; + let infoData = infos.find((info) => info.label === activeTab); + + if (infoData?.columns.some((c) => !isMoneyColumn(c))) { + const columns = infoData?.columns.filter(isMoneyColumn); + + infoData = { + ...infoData, + totals: Object.fromEntries( + Object.entries(infoData?.totals).filter(([k]) => + columns?.map((c) => c.label).includes(k), + ), + ), + columns: columns ?? [], + }; + } else if (infoData?.columns.some(isConceptColumn)) { + // TODO: Handle concept data + infoType = "concept"; + } + + return { data: infoData, type: infoType }; + }, [infos, activeTab]); + + console.log(data); + + return ( + + + {data && type === "money" && ( + + )} + {data && type === "concept" &&
Concept
} +
+ ); +}; From a226d86b76ed456e9ebaa0cd32a5cdfd2a5a32a8 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 27 Jun 2023 17:34:49 +0200 Subject: [PATCH 80/93] Iterate further towards concept charts --- .../src/js/entity-history/ConceptBubble.ts | 10 ++ .../TabbableTimeStratifiedInfos.tsx | 7 +- .../TimeStratifiedConceptChart.tsx | 91 +++++++++++++++++++ .../js/entity-history/timeline/YearHead.tsx | 10 +- 4 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 frontend/src/js/entity-history/ConceptBubble.ts create mode 100644 frontend/src/js/entity-history/TimeStratifiedConceptChart.tsx diff --git a/frontend/src/js/entity-history/ConceptBubble.ts b/frontend/src/js/entity-history/ConceptBubble.ts new file mode 100644 index 0000000000..9c1ff3539a --- /dev/null +++ b/frontend/src/js/entity-history/ConceptBubble.ts @@ -0,0 +1,10 @@ +import styled from "@emotion/styled"; + +export const ConceptBubble = styled("span")` + padding: 0 3px; + border-radius: ${({ theme }) => theme.borderRadius}; + color: ${({ theme }) => theme.col.black}; + border: 1px solid ${({ theme }) => theme.col.gray}; + background-color: white; + font-size: ${({ theme }) => theme.font.sm}; +`; diff --git a/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx b/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx index bdbe9ed57f..a6ccde9de1 100644 --- a/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx +++ b/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx @@ -5,6 +5,7 @@ import { TimeStratifiedInfo } from "../api/types"; import SmallTabNavigation from "../small-tab-navigation/SmallTabNavigation"; import { TimeStratifiedChart } from "./TimeStratifiedChart"; +import { TimeStratifiedConceptChart } from "./TimeStratifiedConceptChart"; import { isConceptColumn, isMoneyColumn } from "./timeline/util"; const Container = styled("div")` @@ -59,10 +60,10 @@ export const TabbableTimeStratifiedInfos = ({ selectedTab={activeTab} onSelectTab={setActiveTab} /> - {data && type === "money" && ( + {/* {data && type === "money" && ( - )} - {data && type === "concept" &&
Concept
} + )} */} + {data && } ); }; diff --git a/frontend/src/js/entity-history/TimeStratifiedConceptChart.tsx b/frontend/src/js/entity-history/TimeStratifiedConceptChart.tsx new file mode 100644 index 0000000000..d44b5bd1f0 --- /dev/null +++ b/frontend/src/js/entity-history/TimeStratifiedConceptChart.tsx @@ -0,0 +1,91 @@ +import styled from "@emotion/styled"; + +import { + ColumnDescriptionSemanticConceptColumn, + TimeStratifiedInfo, +} from "../api/types"; +import { getConceptById } from "../concept-trees/globalTreeStoreHelper"; +import WithTooltip from "../tooltip/WithTooltip"; + +import { ConceptBubble } from "./ConceptBubble"; + +const Container = styled("div")` + display: grid; + place-items: center; + gap: 0 3px; + padding: 10px; +`; + +const BubbleYes = styled("div")` + width: 10px; + height: 10px; + border-radius: ${({ theme }) => theme.borderRadius}; + background-color: ${({ theme }) => theme.col.blueGray}; +`; +const BubbleNo = styled("div")` + width: 10px; + height: 10px; + border-radius: ${({ theme }) => theme.borderRadius}; + background-color: ${({ theme }) => theme.col.grayLight}; +`; + +export const TimeStratifiedConceptChart = ({ + timeStratifiedInfo, +}: { + timeStratifiedInfo: TimeStratifiedInfo; +}) => { + const conceptColumn = timeStratifiedInfo.columns.at(-1); + + if (!conceptColumn) return null; + + const conceptSemantic = conceptColumn.semantics.find( + (s): s is ColumnDescriptionSemanticConceptColumn => + s.type === "CONCEPT_COLUMN", + ); + + if (!conceptSemantic) return null; + + const years = timeStratifiedInfo.years.map((y) => y.year); + console.log(timeStratifiedInfo.years, conceptColumn); + const valuesPerYear = timeStratifiedInfo.years.map((y) => + ((y.values[Object.keys(y.values)[0]] as string[]) || []).map( + (conceptId) => getConceptById(conceptId, conceptSemantic?.concept)!, + ), + ); + + const allValues = [ + ...new Set( + valuesPerYear + .flatMap((v) => v) + .sort((a, b) => { + const nA = Number(a?.label); + const nB = Number(b?.label); + if (!isNaN(nA) && !isNaN(nB)) return nA - nB; + return a?.label.localeCompare(b?.label!); + }), + ), + ]; + + return ( + +
+ {allValues.map((val) => ( + + {val.label} + + ))} + {years.map((y, i) => ( + <> +
{y}
+ {allValues.map((val) => + valuesPerYear[i].includes(val) ? : , + )} + + ))} + + ); +}; diff --git a/frontend/src/js/entity-history/timeline/YearHead.tsx b/frontend/src/js/entity-history/timeline/YearHead.tsx index 1d35d4660c..6b6871628f 100644 --- a/frontend/src/js/entity-history/timeline/YearHead.tsx +++ b/frontend/src/js/entity-history/timeline/YearHead.tsx @@ -11,6 +11,7 @@ import { exists } from "../../common/helpers/exists"; import { getConceptById } from "../../concept-trees/globalTreeStoreHelper"; import FaIcon from "../../icon/FaIcon"; import WithTooltip from "../../tooltip/WithTooltip"; +import { ConceptBubble } from "../ConceptBubble"; import { SmallHeading } from "./SmallHeading"; import { formatCurrency, isConceptColumn, isMoneyColumn } from "./util"; @@ -55,15 +56,6 @@ const ConceptRow = styled("div")` gap: 4px; `; -const ConceptBubble = styled("span")` - padding: 0 3px; - border-radius: ${({ theme }) => theme.borderRadius}; - color: ${({ theme }) => theme.col.black}; - border: 1px solid ${({ theme }) => theme.col.gray}; - background-color: white; - font-size: ${({ theme }) => theme.font.sm}; -`; - const Value = styled("div")` font-size: ${({ theme }) => theme.font.sm}; font-weight: 400; From 963d19d83f76a9e3b5435d8b95b020cd35711821 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 27 Jun 2023 18:32:39 +0200 Subject: [PATCH 81/93] Fix empty timeline --- frontend/src/js/entity-history/Timeline.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/js/entity-history/Timeline.tsx b/frontend/src/js/entity-history/Timeline.tsx index 518180033e..89e62f0aaf 100644 --- a/frontend/src/js/entity-history/Timeline.tsx +++ b/frontend/src/js/entity-history/Timeline.tsx @@ -86,10 +86,6 @@ const Timeline = ({ secondaryIds: columnBuckets.secondaryIds, }); - if (eventsByQuarterWithGroups.length === 0) { - return ; - } - return ( + {eventsByQuarterWithGroups.length === 0 && } {eventsByQuarterWithGroups.map(({ year, quarterwiseData }, i) => ( Date: Wed, 28 Jun 2023 10:51:44 +0200 Subject: [PATCH 82/93] Re-add charts --- .../src/js/entity-history/TabbableTimeStratifiedInfos.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx b/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx index a6ccde9de1..c3e2914132 100644 --- a/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx +++ b/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx @@ -60,9 +60,9 @@ export const TabbableTimeStratifiedInfos = ({ selectedTab={activeTab} onSelectTab={setActiveTab} /> - {/* {data && type === "money" && ( + {data && type === "money" && ( - )} */} + )} {data && } ); From 5a5d70f1fecfb3cb8ada08a28771e021fc3f8a2a Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Wed, 28 Jun 2023 11:13:03 +0200 Subject: [PATCH 83/93] Fix layout --- frontend/src/js/entity-history/EntityCard.tsx | 4 +++- frontend/src/js/entity-history/Timeline.tsx | 9 +++++++-- .../entity-history/timeline/TimelineEmptyPlaceholder.tsx | 8 ++++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/frontend/src/js/entity-history/EntityCard.tsx b/frontend/src/js/entity-history/EntityCard.tsx index d20e7ee7fc..6026552573 100644 --- a/frontend/src/js/entity-history/EntityCard.tsx +++ b/frontend/src/js/entity-history/EntityCard.tsx @@ -39,7 +39,9 @@ export const EntityCard = ({ - + {timeStratifiedInfos.length === 0 && ( + + )} ); }; diff --git a/frontend/src/js/entity-history/Timeline.tsx b/frontend/src/js/entity-history/Timeline.tsx index 858e383b3e..e8eb61319d 100644 --- a/frontend/src/js/entity-history/Timeline.tsx +++ b/frontend/src/js/entity-history/Timeline.tsx @@ -33,7 +33,7 @@ const Root = styled("div")` padding: 0 20px 20px 10px; display: inline-grid; grid-template-columns: 280px auto; - grid-auto-rows: minmax(min-content, max-content); + grid-auto-rows: minmax(min-content, max-content) 1fr; gap: 20px 4px; width: 100%; `; @@ -48,6 +48,11 @@ const SxEntityCard = styled(EntityCard)` grid-column: span 2; `; +const SxTimelineEmptyPlaceholder = styled(TimelineEmptyPlaceholder)` + grid-column: span 2; + height: 100%; +`; + const Timeline = ({ className, currentEntityInfos, @@ -93,7 +98,7 @@ const Timeline = ({ infos={currentEntityInfos} timeStratifiedInfos={currentEntityTimeStratifiedInfos} /> - {eventsByQuarterWithGroups.length === 0 && } + {eventsByQuarterWithGroups.length === 0 && } {eventsByQuarterWithGroups.map(({ year, quarterwiseData }, i) => ( { +export const TimelineEmptyPlaceholder = ({ + className, +}: { + className?: string; +}) => { const { t } = useTranslation(); const ids = useSelector( @@ -58,7 +62,7 @@ export const TimelineEmptyPlaceholder = () => { ); return ( - +
From 8e7384744ccc69e39f0e5f20a845b74cb7ba47cc Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Wed, 28 Jun 2023 11:57:34 +0200 Subject: [PATCH 84/93] always list ValdityDates even if there is no choice for the users --- .../datasets/concepts/FrontEndConceptBuilder.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/FrontEndConceptBuilder.java b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/FrontEndConceptBuilder.java index c88586659b..fee4d75bda 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/FrontEndConceptBuilder.java +++ b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/FrontEndConceptBuilder.java @@ -185,12 +185,11 @@ public static FrontendTable createTable(Connector con) { .collect(Collectors.toSet())) .build(); - if (con.getValidityDates().size() > 1) { - result.setDateColumn(new FrontendValidityDate(con.getValidityDatesDescription(), null, con.getValidityDates() - .stream() - .map(vd -> new FrontendValue(vd.getId() - .toString(), vd.getLabel())) - .collect(Collectors.toList()))); + if (!con.getValidityDates().isEmpty()) { + result.setDateColumn(new FrontendValidityDate(con.getValidityDatesDescription(), null, + con.getValidityDates().stream() + .map(vd -> new FrontendValue(vd.getId().toString(), vd.getLabel())) + .collect(Collectors.toList()))); if (!result.getDateColumn().getOptions().isEmpty()) { result.getDateColumn().setDefaultValue(result.getDateColumn().getOptions().get(0).getValue()); From be6e2fab073f03b40b49f54bb03b0ce69b9fc852 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Wed, 28 Jun 2023 11:59:22 +0200 Subject: [PATCH 85/93] Fix yearhead and charts rendering --- frontend/src/js/entity-history/EntityCard.tsx | 2 +- frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx | 2 -- frontend/src/js/entity-history/timeline/YearHead.tsx | 3 --- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/frontend/src/js/entity-history/EntityCard.tsx b/frontend/src/js/entity-history/EntityCard.tsx index 6026552573..b9d4f86bab 100644 --- a/frontend/src/js/entity-history/EntityCard.tsx +++ b/frontend/src/js/entity-history/EntityCard.tsx @@ -39,7 +39,7 @@ export const EntityCard = ({ - {timeStratifiedInfos.length === 0 && ( + {timeStratifiedInfos.length > 0 && ( )} diff --git a/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx b/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx index c3e2914132..d23f424625 100644 --- a/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx +++ b/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx @@ -51,8 +51,6 @@ export const TabbableTimeStratifiedInfos = ({ return { data: infoData, type: infoType }; }, [infos, activeTab]); - console.log(data); - return ( theme.font.sm}; font-weight: 400; justify-self: end; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; width: 100%; text-align: right; `; From af2f5cd59a95b1e9bb8d312cb24cebdc5beb48ea Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Wed, 28 Jun 2023 15:45:11 +0200 Subject: [PATCH 86/93] validate uniqueness constraint for timeStratifiedInfos globally --- .../models/datasets/PreviewConfig.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/backend/src/main/java/com/bakdata/conquery/models/datasets/PreviewConfig.java b/backend/src/main/java/com/bakdata/conquery/models/datasets/PreviewConfig.java index 9deccb3249..4d84e6150e 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/datasets/PreviewConfig.java +++ b/backend/src/main/java/com/bakdata/conquery/models/datasets/PreviewConfig.java @@ -1,5 +1,6 @@ package com.bakdata.conquery.models.datasets; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -114,17 +115,18 @@ public record InfoCardSelect(@NotNull String label, SelectId select, String desc * Defines a group of selects that will be evaluated per quarter and year in the requested period of the entity-preview. */ public record TimeStratifiedSelects(@NotNull String label, String description, @NotEmpty List selects){ - @ValidationMethod(message = "Selects may be referenced only once.") - @JsonIgnore - public boolean isSelectsUnique() { - return selects().stream().map(InfoCardSelect::select).distinct().count() == selects().size(); - } + } - @ValidationMethod(message = "Labels must be unique.") - @JsonIgnore - public boolean isLabelsUnique() { - return selects().stream().map(InfoCardSelect::label).distinct().count() == selects().size(); - } + @ValidationMethod(message = "Selects may be referenced only once.") + @JsonIgnore + public boolean isSelectsUnique() { + return timeStratifiedSelects.stream().map(TimeStratifiedSelects::selects).flatMap(Collection::stream).map(InfoCardSelect::select).distinct().count() == timeStratifiedSelects.stream().map(TimeStratifiedSelects::selects).flatMap(Collection::stream).count(); + } + + @ValidationMethod(message = "Labels must be unique.") + @JsonIgnore + public boolean isLabelsUnique() { + return timeStratifiedSelects.stream().map(TimeStratifiedSelects::selects).flatMap(Collection::stream).map(InfoCardSelect::label).distinct().count() == timeStratifiedSelects.stream().map(TimeStratifiedSelects::selects).flatMap(Collection::stream).count(); } @JsonIgnore From e976aa990619199f15a90adccb229c9142b1a4e7 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Wed, 28 Jun 2023 16:39:20 +0200 Subject: [PATCH 87/93] Iterate editor v2 time condition --- frontend/src/js/api/apiHelper.ts | 11 +++- frontend/src/js/editor-v2/EditorLayout.ts | 1 + frontend/src/js/editor-v2/EditorV2.tsx | 61 ++++++++++--------- frontend/src/js/editor-v2/TreeNode.tsx | 50 ++++++++++++--- .../connector-update/useConnectorRotation.ts | 2 +- .../time-connection/TimeConnection.tsx | 13 +++- .../time-connection/TimeConnectionModal.tsx | 25 +++++--- frontend/src/js/editor-v2/util.ts | 10 +-- .../src/js/external-forms/FormsNavigation.tsx | 4 +- .../upload/CSVColumnPicker.tsx | 4 +- .../src/js/query-node-editor/ConceptEntry.tsx | 4 +- .../QueryClearButton.tsx | 4 +- .../TimebasedQueryClearButton.tsx | 4 +- frontend/src/localization/de.json | 3 +- frontend/src/localization/en.json | 3 +- 15 files changed, 132 insertions(+), 67 deletions(-) diff --git a/frontend/src/js/api/apiHelper.ts b/frontend/src/js/api/apiHelper.ts index 5320441c71..a20af07de0 100644 --- a/frontend/src/js/api/apiHelper.ts +++ b/frontend/src/js/api/apiHelper.ts @@ -242,7 +242,16 @@ const transformTreeToApi = (tree: Tree): unknown => { node = { type: "BEFORE", // SHOULD BE: tree.children.operator, days: { - ...(tree.children.interval || {}), + min: tree.children.interval + ? tree.children.interval.min === null + ? 1 + : tree.children.interval.max + : undefined, + max: tree.children.interval + ? tree.children.interval.max === null + ? undefined + : tree.children.interval.max + : undefined, }, // TODO: improve this to be more flexible with the "preceding" and "index" keys // based on the operator, which would be "before" | "after" | "while" diff --git a/frontend/src/js/editor-v2/EditorLayout.ts b/frontend/src/js/editor-v2/EditorLayout.ts index c6f993e92d..3cb8d5902e 100644 --- a/frontend/src/js/editor-v2/EditorLayout.ts +++ b/frontend/src/js/editor-v2/EditorLayout.ts @@ -14,6 +14,7 @@ export const Connector = styled("span")` text-transform: uppercase; font-size: ${({ theme }) => theme.font.sm}; color: black; + user-select: none; border-radius: ${({ theme }) => theme.borderRadius}; padding: 0px 5px; diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 3e4101d620..67765fe649 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -21,6 +21,8 @@ import { DragItemConceptTreeNode, DragItemQuery, } from "../standard-query-editor/types"; +import { ConfirmableTooltip } from "../tooltip/ConfirmableTooltip"; +import WithTooltip from "../tooltip/WithTooltip"; import Dropzone from "../ui-components/Dropzone"; import { Connector, Grid } from "./EditorLayout"; @@ -350,20 +352,6 @@ export function EditorV2({ )} - {selectedNode?.children && ( - - { - e.stopPropagation(); - onFlip(); - }} - > - {t("editorV2.flip")} - - - )} {featureConnectorRotate && selectedNode?.children && ( )} + + + {selectedNode?.children && ( + + { + e.stopPropagation(); + onFlip(); + }} + > + {t("editorV2.flip")} + + + )} {selectedNode && ( )} + + + + + - - - {t("editorV2.clear")} - - )} {tree ? ( { - e.stopPropagation(); - if (!selectedNode) return; - if ( - selectedNode?.data && - nodeIsConceptQueryNode(selectedNode.data) - ) { - onOpenQueryNodeEditor(); - } - }} tree={tree} updateTreeNode={updateTreeNode} selectedNode={selectedNode} setSelectedNodeId={setSelectedNodeId} droppable={{ h: true, v: true }} featureContentInfos={featureContentInfos} + onOpenQueryNodeEditor={onOpenQueryNodeEditor} + onOpenTimeModal={onOpenTimeModal} + onRotateConnector={onRotateConnector} /> ) : ( void; - onDoubleClick?: DOMAttributes["onDoubleClick"]; featureContentInfos?: boolean; + onOpenQueryNodeEditor?: () => void; + onOpenTimeModal?: () => void; + onRotateConnector?: () => void; }) { const gridStyles = getGridStyles(tree); @@ -256,14 +260,25 @@ export function TreeNode({ negated={tree.negation} leaf={!tree.children} selected={selectedNode?.id === tree.id} - onDoubleClick={onDoubleClick} + onDoubleClick={(e) => { + if (tree.data && nodeIsConceptQueryNode(tree.data)) { + e.stopPropagation(); + onOpenQueryNodeEditor?.(); + } + }} onClick={(e) => { e.stopPropagation(); setSelectedNodeId(tree.id); }} > {tree.children && tree.children.connection === "time" && ( - + { + e.stopPropagation(); + onOpenTimeModal?.(); + }} + /> )} {tree.dates?.restriction && ( @@ -315,6 +330,9 @@ export function TreeNode({ updateTreeNode={updateTreeNode} selectedNode={selectedNode} setSelectedNodeId={setSelectedNodeId} + onOpenQueryNodeEditor={onOpenQueryNodeEditor} + onOpenTimeModal={onOpenTimeModal} + onRotateConnector={onRotateConnector} droppable={{ h: !item.children && @@ -337,6 +355,10 @@ export function TreeNode({ > {() => ( { + e.stopPropagation(); + onRotateConnector?.(); + }} connection={tree.children?.connection} /> )} @@ -384,8 +406,20 @@ export function TreeNode({ ); } -const Connection = memo(({ connection }: { connection?: ConnectionKind }) => { - const getTranslatedConnection = useGetTranslatedConnection(); +const Connection = memo( + ({ + connection, + onDoubleClick, + }: { + connection?: ConnectionKind; + onDoubleClick?: DOMAttributes["onDoubleClick"]; + }) => { + const getTranslatedConnection = useGetTranslatedConnection(); - return {getTranslatedConnection(connection)}; -}); + return ( + + {getTranslatedConnection(connection)} + + ); + }, +); diff --git a/frontend/src/js/editor-v2/connector-update/useConnectorRotation.ts b/frontend/src/js/editor-v2/connector-update/useConnectorRotation.ts index d74490249d..cc44406fc6 100644 --- a/frontend/src/js/editor-v2/connector-update/useConnectorRotation.ts +++ b/frontend/src/js/editor-v2/connector-update/useConnectorRotation.ts @@ -29,7 +29,7 @@ const getNextConnector = ( items: children.items, direction: children.direction, connection: "time" as const, - timestamps: children.items.map(() => "every" as const), + timestamps: children.items.map(() => "some" as const), operator: "before" as const, }; } diff --git a/frontend/src/js/editor-v2/time-connection/TimeConnection.tsx b/frontend/src/js/editor-v2/time-connection/TimeConnection.tsx index 2e6e18176a..f1f3f0e3ed 100644 --- a/frontend/src/js/editor-v2/time-connection/TimeConnection.tsx +++ b/frontend/src/js/editor-v2/time-connection/TimeConnection.tsx @@ -1,5 +1,5 @@ import styled from "@emotion/styled"; -import { memo } from "react"; +import { DOMAttributes, memo } from "react"; import { useTranslation } from "react-i18next"; import { TreeChildrenTime } from "../types"; @@ -14,6 +14,7 @@ const Container = styled("div")` margin: 0 auto; display: inline-flex; flex-direction: column; + user-select: none; `; const Row = styled("div")` @@ -41,7 +42,13 @@ const Operator = styled("span")` `; export const TimeConnection = memo( - ({ conditions }: { conditions: TreeChildrenTime }) => { + ({ + conditions, + onDoubleClick, + }: { + conditions: TreeChildrenTime; + onDoubleClick: DOMAttributes["onDoubleClick"]; + }) => { const { t } = useTranslation(); const getNodeLabel = useGetNodeLabel(); const getTranslatedTimestamp = useGetTranslatedTimestamp(); @@ -54,7 +61,7 @@ export const TimeConnection = memo( const interval = useTranslatedInterval(conditions.interval); return ( - + {aTimestamp} {t("editorV2.dateRangeFrom")} diff --git a/frontend/src/js/editor-v2/time-connection/TimeConnectionModal.tsx b/frontend/src/js/editor-v2/time-connection/TimeConnectionModal.tsx index d3a7a0f6d3..65cf034484 100644 --- a/frontend/src/js/editor-v2/time-connection/TimeConnectionModal.tsx +++ b/frontend/src/js/editor-v2/time-connection/TimeConnectionModal.tsx @@ -62,10 +62,10 @@ export const TimeConnectionModal = memo( const { t } = useTranslation(); const TIMESTAMP_OPTIONS = useMemo( () => [ - { value: "every", label: t("editorV2.every") }, { value: "some", label: t("editorV2.some") }, { value: "latest", label: t("editorV2.latest") }, { value: "earliest", label: t("editorV2.earliest") }, + { value: "every", label: t("editorV2.every") }, ], [t], ); @@ -89,7 +89,7 @@ export const TimeConnectionModal = memo( const [aTimestamp, setATimestamp] = useState(conditions.timestamps[0]); const [bTimestamp, setBTimestamp] = useState(conditions.timestamps[1]); const [operator, setOperator] = useState(conditions.operator); - const [interval, setInterval] = useState(conditions.interval); + const [interval, setTheInterval] = useState(conditions.interval); const getNodeLabel = useGetNodeLabel(); const a = getNodeLabel(conditions.items[0]); @@ -135,27 +135,33 @@ export const TimeConnectionModal = memo( { - setInterval({ min: val as number, max: interval?.max || null }); + setTheInterval({ + min: val as number, + max: interval ? interval.max : null, + }); }} /> { - setInterval({ max: val as number, min: interval?.min || null }); + setTheInterval({ + max: val as number | null, + min: interval ? interval.min : null, + }); }} /> { if (opt?.value === "some") { - setInterval(undefined); + setTheInterval(undefined); } else { - setInterval({ min: 0, max: 0 }); + setTheInterval({ min: 1, max: null }); } }} /> @@ -177,7 +183,8 @@ export const TimeConnectionModal = memo( if (opt) { setOperator(opt.value as TimeOperator); if (opt.value === "while") { - setInterval(undefined); + // Timeout to avoid race condition on effect update above + setTimeout(() => setTheInterval(undefined), 10); } } }} diff --git a/frontend/src/js/editor-v2/util.ts b/frontend/src/js/editor-v2/util.ts index 209b6f0cb8..df43e0ecdc 100644 --- a/frontend/src/js/editor-v2/util.ts +++ b/frontend/src/js/editor-v2/util.ts @@ -105,10 +105,12 @@ export const useTranslatedInterval = ( const { min, max } = interval; - if (!min && !max) return t("editorV2.intervalSome"); - if (min && !max) return t("editorV2.intervalMinDays", { days: min }); - if (!min && max) return t("editorV2.intervalMaxDays", { days: max }); - if (min && max) + if (min === null && max === null) return t("editorV2.intervalSome"); + if (min !== null && max === null) + return t("editorV2.intervalMinDays", { days: min }); + if (min === null && max !== null) + return t("editorV2.intervalMaxDays", { days: max }); + if (min !== null && max !== null) return t("editorV2.intervalMinMaxDays", { minDays: min, maxDays: max }); return t("editorV2.intervalSome"); diff --git a/frontend/src/js/external-forms/FormsNavigation.tsx b/frontend/src/js/external-forms/FormsNavigation.tsx index 0e9832423b..6da71d6a0a 100644 --- a/frontend/src/js/external-forms/FormsNavigation.tsx +++ b/frontend/src/js/external-forms/FormsNavigation.tsx @@ -1,5 +1,5 @@ import styled from "@emotion/styled"; -import { faTrashAlt } from "@fortawesome/free-regular-svg-icons"; +import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { useTranslation } from "react-i18next"; import { useSelector, useDispatch } from "react-redux"; @@ -89,7 +89,7 @@ const FormsNavigation = ({ onReset }: { onReset: () => void }) => { confirmationText={t("externalForms.common.clearConfirm")} > - + diff --git a/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx b/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx index a330e6220f..f53f1f6cb9 100644 --- a/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx +++ b/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx @@ -1,5 +1,5 @@ import styled from "@emotion/styled"; -import { faTrashAlt } from "@fortawesome/free-regular-svg-icons"; +import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { faCheckCircle, faDownload, @@ -282,7 +282,7 @@ const CSVColumnPicker: FC = ({ {csv.length} Zeilen
- + {csv.length > 0 && ( diff --git a/frontend/src/js/query-node-editor/ConceptEntry.tsx b/frontend/src/js/query-node-editor/ConceptEntry.tsx index 310881ebff..07a41c396b 100644 --- a/frontend/src/js/query-node-editor/ConceptEntry.tsx +++ b/frontend/src/js/query-node-editor/ConceptEntry.tsx @@ -1,5 +1,5 @@ import styled from "@emotion/styled"; -import { faTrashAlt } from "@fortawesome/free-regular-svg-icons"; +import { faTrashCan } from "@fortawesome/free-regular-svg-icons"; import { useTranslation } from "react-i18next"; import type { ConceptIdT, ConceptT } from "../api/types"; @@ -77,7 +77,7 @@ const ConceptEntry = ({ onRemoveConcept(conceptId)} tiny - icon={faTrashAlt} + icon={faTrashCan} /> )} diff --git a/frontend/src/js/standard-query-editor/QueryClearButton.tsx b/frontend/src/js/standard-query-editor/QueryClearButton.tsx index 97aa4c7575..9cf8ef0ef2 100644 --- a/frontend/src/js/standard-query-editor/QueryClearButton.tsx +++ b/frontend/src/js/standard-query-editor/QueryClearButton.tsx @@ -1,4 +1,4 @@ -import { faTrashAlt } from "@fortawesome/free-regular-svg-icons"; +import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { FC } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; @@ -25,7 +25,7 @@ const QueryClearButton: FC = ({ className }) => { onConfirm={onClearQuery} > - +
diff --git a/frontend/src/js/timebased-query-editor/TimebasedQueryClearButton.tsx b/frontend/src/js/timebased-query-editor/TimebasedQueryClearButton.tsx index af1e4e7577..e1b4be4db7 100644 --- a/frontend/src/js/timebased-query-editor/TimebasedQueryClearButton.tsx +++ b/frontend/src/js/timebased-query-editor/TimebasedQueryClearButton.tsx @@ -1,5 +1,5 @@ import styled from "@emotion/styled"; -import { faTrashAlt } from "@fortawesome/free-regular-svg-icons"; +import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; @@ -30,7 +30,7 @@ const TimebasedQueryClearButton = () => { {t("common.clear")} diff --git a/frontend/src/localization/de.json b/frontend/src/localization/de.json index da050656ef..4d429dc251 100644 --- a/frontend/src/localization/de.json +++ b/frontend/src/localization/de.json @@ -524,7 +524,8 @@ "time": "ZEIT", "and": "UND", "or": "ODER", - "clear": "Leeren", + "clear": "Editor vollständig zurücksetzen", + "clearConfirm": "Jetzt zurücksetzen", "flip": "Drehen", "dates": "Datum", "negate": "Nicht", diff --git a/frontend/src/localization/en.json b/frontend/src/localization/en.json index 007499910f..69aed6ea75 100644 --- a/frontend/src/localization/en.json +++ b/frontend/src/localization/en.json @@ -524,7 +524,8 @@ "time": "TIME", "and": "AND", "or": "OR", - "clear": "Clear", + "clear": "Reset editor completely", + "clearConfirm": "Reset now", "flip": "Flip", "dates": "Dates", "negate": "Negate", From fda70d5ab3b35344e9e675f79968ca613de5c8ca Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Wed, 28 Jun 2023 17:02:08 +0200 Subject: [PATCH 88/93] Update palette --- frontend/src/app-theme.ts | 8 ++++---- .../src/js/editor-v2/time-connection/TimeConnection.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/app-theme.ts b/frontend/src/app-theme.ts index fb8cfc19c4..4435b90328 100644 --- a/frontend/src/app-theme.ts +++ b/frontend/src/app-theme.ts @@ -16,13 +16,13 @@ export const theme: Theme = { green: "#36971C", orange: "#E9711C", palette: [ - "#f9c74f", - "#f8961e", "#277da1", - "#90be6d", "#43aa8b", - "#f94144", "#5e60ce", + "#f9c74f", + "#90be6d", + "#f8961e", + "#f94144", "#aaa", "#777", "#fff", diff --git a/frontend/src/js/editor-v2/time-connection/TimeConnection.tsx b/frontend/src/js/editor-v2/time-connection/TimeConnection.tsx index f1f3f0e3ed..c57748319c 100644 --- a/frontend/src/js/editor-v2/time-connection/TimeConnection.tsx +++ b/frontend/src/js/editor-v2/time-connection/TimeConnection.tsx @@ -21,7 +21,7 @@ const Row = styled("div")` display: flex; align-items: center; gap: 5px; - font-size: ${({ theme }) => theme.font.xs}; + font-size: ${({ theme }) => theme.font.sm}; `; const ConceptName = styled("span")` @@ -30,15 +30,15 @@ const ConceptName = styled("span")` `; const Timestamp = styled("span")` font-weight: bold; - color: ${({ theme }) => theme.col.palette[6]}; + color: ${({ theme }) => theme.col.palette[0]}; `; const Interval = styled("span")` font-weight: bold; - color: ${({ theme }) => theme.col.orange}; + color: ${({ theme }) => theme.col.palette[1]}; `; const Operator = styled("span")` font-weight: bold; - color: ${({ theme }) => theme.col.green}; + color: ${({ theme }) => theme.col.palette[2]}; `; export const TimeConnection = memo( From d6932776db30e58597a89a002042b0f1c10032a2 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Wed, 28 Jun 2023 17:33:58 +0200 Subject: [PATCH 89/93] Fix time stratified concepts chart --- frontend/src/js/editor-v2/EditorV2.tsx | 2 +- .../src/js/entity-history/TabbableTimeStratifiedInfos.tsx | 6 ++++-- .../src/js/entity-history/TimeStratifiedConceptChart.tsx | 7 +++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 67765fe649..4eff775dd8 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -432,7 +432,7 @@ export function EditorV2({ )} info.label === activeTab); - if (infoData?.columns.some((c) => !isMoneyColumn(c))) { + if (infoData?.columns.some((c) => isMoneyColumn(c))) { const columns = infoData?.columns.filter(isMoneyColumn); infoData = { @@ -61,7 +61,9 @@ export const TabbableTimeStratifiedInfos = ({ {data && type === "money" && ( )} - {data && } + {data && type === "concept" && ( + + )}
); }; diff --git a/frontend/src/js/entity-history/TimeStratifiedConceptChart.tsx b/frontend/src/js/entity-history/TimeStratifiedConceptChart.tsx index d44b5bd1f0..8fb7a71cd8 100644 --- a/frontend/src/js/entity-history/TimeStratifiedConceptChart.tsx +++ b/frontend/src/js/entity-history/TimeStratifiedConceptChart.tsx @@ -29,6 +29,10 @@ const BubbleNo = styled("div")` background-color: ${({ theme }) => theme.col.grayLight}; `; +const Year = styled("div")` + font-size: ${({ theme }) => theme.font.sm}; +`; + export const TimeStratifiedConceptChart = ({ timeStratifiedInfo, }: { @@ -46,7 +50,6 @@ export const TimeStratifiedConceptChart = ({ if (!conceptSemantic) return null; const years = timeStratifiedInfo.years.map((y) => y.year); - console.log(timeStratifiedInfo.years, conceptColumn); const valuesPerYear = timeStratifiedInfo.years.map((y) => ((y.values[Object.keys(y.values)[0]] as string[]) || []).map( (conceptId) => getConceptById(conceptId, conceptSemantic?.concept)!, @@ -80,7 +83,7 @@ export const TimeStratifiedConceptChart = ({ ))} {years.map((y, i) => ( <> -
{y}
+ {y} {allValues.map((val) => valuesPerYear[i].includes(val) ? : , )} From 1e1d54a9f6cc1f00ef344d7b080063353e9ac5ee Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Thu, 29 Jun 2023 13:34:56 +0200 Subject: [PATCH 90/93] log ConqueryError at WARN level --- .../models/query/ExecutionManager.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/backend/src/main/java/com/bakdata/conquery/models/query/ExecutionManager.java b/backend/src/main/java/com/bakdata/conquery/models/query/ExecutionManager.java index ea2e844612..933fe3daab 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/query/ExecutionManager.java +++ b/backend/src/main/java/com/bakdata/conquery/models/query/ExecutionManager.java @@ -14,6 +14,7 @@ import com.bakdata.conquery.models.auth.entities.User; import com.bakdata.conquery.models.config.ConqueryConfig; import com.bakdata.conquery.models.datasets.Dataset; +import com.bakdata.conquery.models.error.ConqueryError; import com.bakdata.conquery.models.execution.ExecutionState; import com.bakdata.conquery.models.execution.InternalExecution; import com.bakdata.conquery.models.execution.ManagedExecution; @@ -34,13 +35,6 @@ public class ExecutionManager { private final MetaStorage storage; - private final Cache>> executionResults = - CacheBuilder.newBuilder() - .softValues() - .removalListener(this::executionRemoved) - .build(); - - /** * Manage state of evicted Queries, setting them to NEW. */ @@ -56,7 +50,11 @@ private void executionRemoved(RemovalNotification> r log.warn("Evicted Results for Query[{}] (Reason: {})", executionId, removalNotification.getCause()); storage.getExecution(executionId).reset(); - } + } private final Cache>> executionResults = + CacheBuilder.newBuilder() + .softValues() + .removalListener(this::executionRemoved) + .build(); public ManagedExecution runQuery(Namespace namespace, QueryDescription query, User user, Dataset submittedDataset, ConqueryConfig config, boolean system) { final ManagedExecution execution = createExecution(query, user, submittedDataset, system); @@ -70,14 +68,18 @@ public ManagedExecution createExecution(QueryDescription query, User user, Datas } public void execute(Namespace namespace, ManagedExecution execution, ConqueryConfig config) { - // Initialize the query / create subqueries try { execution.initExecutable(namespace, config); } catch (Exception e) { - log.error("Failed to initialize Query[{}]", execution.getId(), e); + // ConqueryErrors are usually user input errors so no need to log them at level=ERROR + if (e instanceof ConqueryError) { + log.warn("Failed to initialize Query[{}]", execution.getId(), e); + } + else { + log.error("Failed to initialize Query[{}]", execution.getId(), e); + } - //TODO we don't want to store completely faulty queries but is that right like this? storage.removeExecution(execution.getId()); throw e; } @@ -96,7 +98,6 @@ public void execute(Namespace namespace, ManagedExecution execution, ConqueryCon } } - public ManagedExecution createQuery(QueryDescription query, UUID queryId, User user, Dataset submittedDataset, boolean system) { // Transform the submitted query into an initialized execution ManagedExecution managed = query.toManagedExecution(user, submittedDataset, storage); From 026a115b0b462a92e41117fcaf6e6c76b33e2011 Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Thu, 29 Jun 2023 13:39:41 +0200 Subject: [PATCH 91/93] adds descriptions for optional CQYes populations --- .../conquery/frontend/forms/export_form.frontend_conf.json | 4 ++-- .../frontend/forms/table_export_form.frontend_conf.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/main/resources/com/bakdata/conquery/frontend/forms/export_form.frontend_conf.json b/backend/src/main/resources/com/bakdata/conquery/frontend/forms/export_form.frontend_conf.json index 8516063810..cf71a70858 100644 --- a/backend/src/main/resources/com/bakdata/conquery/frontend/forms/export_form.frontend_conf.json +++ b/backend/src/main/resources/com/bakdata/conquery/frontend/forms/export_form.frontend_conf.json @@ -27,8 +27,8 @@ "en": "Cohort (Previous Query)" }, "dropzoneLabel": { - "de": "Füge eine Versichertengruppe aus einer bestehenden Anfrage hinzu", - "en": "Add a cohort from a previous query" + "de": "Füge eine Versichertengruppe aus einer bestehenden Anfrage hinzu. Ist das Feld leer wird die Gesamtpopulation verwendet.", + "en": "Add a cohort from a previous query. When no population is provided, the entire dataset's population is used." }, "validations": [ ], diff --git a/backend/src/main/resources/com/bakdata/conquery/frontend/forms/table_export_form.frontend_conf.json b/backend/src/main/resources/com/bakdata/conquery/frontend/forms/table_export_form.frontend_conf.json index e736367a43..cb51887547 100644 --- a/backend/src/main/resources/com/bakdata/conquery/frontend/forms/table_export_form.frontend_conf.json +++ b/backend/src/main/resources/com/bakdata/conquery/frontend/forms/table_export_form.frontend_conf.json @@ -27,8 +27,8 @@ "en": "Cohort (Previous Query)" }, "dropzoneLabel": { - "de": "Füge eine Versichertengruppe aus einer bestehenden Anfrage hinzu.", - "en": "Add a cohort from a previous query" + "de": "Füge eine Versichertengruppe aus einer bestehenden Anfrage hinzu. Ist das Feld leer wird die Gesamtpopulation verwendet.", + "en": "Add a cohort from a previous query. When no population is provided, the entire dataset's population is used." }, "validations": [ ], From 8c1ad749a8f6b7c04655e045dc8e45f940c715a2 Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Thu, 29 Jun 2023 14:13:33 +0200 Subject: [PATCH 92/93] use years as observationPeriod --- .../bakdata/conquery/models/config/FrontendConfig.java | 9 ++++----- .../bakdata/conquery/resources/api/ConfigResource.java | 4 +++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/com/bakdata/conquery/models/config/FrontendConfig.java b/backend/src/main/java/com/bakdata/conquery/models/config/FrontendConfig.java index 599b78a222..1bbfcddd56 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/config/FrontendConfig.java +++ b/backend/src/main/java/com/bakdata/conquery/models/config/FrontendConfig.java @@ -2,12 +2,11 @@ import java.net.URI; import java.net.URL; -import java.time.LocalDate; -import java.time.temporal.ChronoUnit; import javax.annotation.Nullable; import javax.validation.Valid; import javax.validation.constraints.Email; +import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; import com.bakdata.conquery.models.forms.frontendconfiguration.FormScanner; @@ -29,10 +28,10 @@ public class FrontendConfig { private CurrencyConfig currency = new CurrencyConfig(); /** - * Default start-date for EntityPreview and DatePicker. + * Years to include in entity preview. */ - @NotNull - private LocalDate observationStart = LocalDate.now().minus(10, ChronoUnit.YEARS); + @Min(0) + private int observationPeriodYears = 6; /** * The url that points a manual. This is also used by the {@link FormScanner} diff --git a/backend/src/main/java/com/bakdata/conquery/resources/api/ConfigResource.java b/backend/src/main/java/com/bakdata/conquery/resources/api/ConfigResource.java index 0c2432c762..deeb7e5d21 100644 --- a/backend/src/main/java/com/bakdata/conquery/resources/api/ConfigResource.java +++ b/backend/src/main/java/com/bakdata/conquery/resources/api/ConfigResource.java @@ -1,5 +1,7 @@ package com.bakdata.conquery.resources.api; +import java.time.Year; + import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -38,7 +40,7 @@ public FrontendConfiguration getFrontendConfig() { idColumns, frontendConfig.getManualUrl(), frontendConfig.getContactEmail(), - frontendConfig.getObservationStart() + Year.now().minusYears(frontendConfig.getObservationPeriodYears()).atDay(1) ); } From eb162241d4d285991e40bb331f2e2890c3cdd8ac Mon Sep 17 00:00:00 2001 From: awildturtok <1553491+awildturtok@users.noreply.github.com> Date: Mon, 3 Jul 2023 14:56:11 +0200 Subject: [PATCH 93/93] limit SELECT FilterValue size to < 20000 as big values cause memory issues --- .../query/concept/filter/FilterValue.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/query/concept/filter/FilterValue.java b/backend/src/main/java/com/bakdata/conquery/apiv1/query/concept/filter/FilterValue.java index 681899eeed..f3d2a1de35 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/query/concept/filter/FilterValue.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/query/concept/filter/FilterValue.java @@ -19,6 +19,7 @@ import com.bakdata.conquery.models.identifiable.ids.specific.FilterId; import com.bakdata.conquery.models.query.QueryResolveContext; import com.bakdata.conquery.models.query.queryplan.filter.FilterNode; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; @@ -27,6 +28,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; +import io.dropwizard.validation.ValidationMethod; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -44,6 +46,10 @@ @EqualsAndHashCode @ToString(of = "value") public abstract class FilterValue { + /** + * Very large SELECT FilterValues can cause issues, so we just limit it to large but not gigantic quantities. + */ + private static final int MAX_NUMBER_FILTER_VALUES = 20_000; @NotNull @Nonnull @NsIdRef @@ -68,6 +74,12 @@ public static class CQMultiSelectFilter extends FilterValue { public CQMultiSelectFilter(@NsIdRef Filter filter, String[] value) { super(filter, value); } + + @ValidationMethod(message = "Too many values selected.") + @JsonIgnore + public boolean isSaneAmountOfFilterValues() { + return getValue().length < MAX_NUMBER_FILTER_VALUES; + } } @NoArgsConstructor @@ -77,6 +89,12 @@ public static class CQBigMultiSelectFilter extends FilterValue { public CQBigMultiSelectFilter(@NsIdRef Filter filter, String[] value) { super(filter, value); } + + @ValidationMethod(message = "Too many values selected.") + @JsonIgnore + public boolean isSaneAmountOfFilterValues() { + return getValue().length < MAX_NUMBER_FILTER_VALUES; + } } @NoArgsConstructor