diff --git a/dqops/src/main/frontend/src/components/DefinitionLayout/DefinitionTree.tsx b/dqops/src/main/frontend/src/components/DefinitionLayout/DefinitionTree.tsx index 3ad2a04b8e..d7ba1ef0ab 100644 --- a/dqops/src/main/frontend/src/components/DefinitionLayout/DefinitionTree.tsx +++ b/dqops/src/main/frontend/src/components/DefinitionLayout/DefinitionTree.tsx @@ -2,36 +2,26 @@ import React from 'react'; import { useEffect } from 'react'; import { useSelector } from 'react-redux'; import { - addFirstLevelTab, getSensorFolderTree, - toggleSensorFolderTree, - openRuleFolderTree, getRuleFolderTree, - toggleRuleFolderTree, toggleFirstLevelFolder, - openSensorFolderTree, - getdataQualityChecksFolderTree, - toggledataQualityChecksFolderTree, - opendataQualityChecksFolderTree + getdataQualityChecksFolderTree } from '../../redux/actions/definition.actions'; import { useActionDispatch } from '../../hooks/useActionDispatch'; import { IRootState } from '../../redux/reducers'; import { CheckDefinitionFolderModel, RuleFolderModel, - RuleListModel, - SensorFolderModel, - SensorListModel, - CheckDefinitionListModel + SensorFolderModel } from '../../api'; import SvgIcon from '../SvgIcon'; import clsx from 'clsx'; -import { ROUTES } from '../../shared/routes'; import SensorContextMenu from './SensorContextMenu'; import RuleContextMenu from './RuleContextMenu'; import DataQualityContextMenu from './DataQualityContextMenu'; import { urlencodeEncoder } from '../../utils'; import { Tooltip } from '@material-tailwind/react'; +import { useDefinition } from '../../contexts/definitionContext'; const defaultChecks = [ 'Profiling checks', @@ -56,6 +46,18 @@ export const DefinitionTree = () => { refreshSensorsTreeIndicator } = useSelector((state: IRootState) => state.definition); + const { + openCheckDefaultFirstLevelTab, + openCheckFirstLevelTab, + openRuleFirstLevelTab, + openSensorFirstLevelTab, + toggleTree, + nodes, + toggleSensorFolder, + toggleRuleFolder, + toggleDataQualityChecksFolder + } = useDefinition(); + useEffect(() => { dispatch(getSensorFolderTree()); }, [refreshSensorsTreeIndicator]); @@ -68,209 +70,13 @@ export const DefinitionTree = () => { dispatch(getdataQualityChecksFolderTree()); }, [refreshChecksTreeIndicator]); - const toggleSensorFolder = (key: string) => { - dispatch(toggleSensorFolderTree(key)); - }; - - const openSensorFolder = (key: string) => { - dispatch(openSensorFolderTree(key)); - }; - - const toggleRuleFolder = (key: string) => { - dispatch(toggleRuleFolderTree(key)); - }; - - const openRuleFolder = (key: string) => { - dispatch(openRuleFolderTree(key)); - }; - - const toggleDataQualityChecksFolder = (fullPath: string) => { - dispatch(toggledataQualityChecksFolderTree(fullPath)); - }; - const openDataQualityChecksFolder = (fullPath: string) => { - dispatch(opendataQualityChecksFolderTree(fullPath)); - }; - - const openSensorFirstLevelTab = (sensor: SensorListModel) => { - dispatch( - addFirstLevelTab({ - url: ROUTES.SENSOR_DETAIL(urlencodeEncoder(sensor.sensor_name) ?? ''), - value: ROUTES.SENSOR_DETAIL_VALUE( - urlencodeEncoder(sensor.sensor_name) ?? '' - ), - state: { - full_sensor_name: urlencodeEncoder(sensor.full_sensor_name) - }, - label: urlencodeEncoder(sensor.sensor_name) - }) - ); - }; - - const openRuleFirstLevelTab = (rule: RuleListModel) => { - dispatch( - addFirstLevelTab({ - url: ROUTES.RULE_DETAIL(urlencodeEncoder(rule.rule_name) ?? ''), - value: ROUTES.RULE_DETAIL_VALUE(urlencodeEncoder(rule.rule_name) ?? ''), - state: { - full_rule_name: urlencodeEncoder(rule.full_rule_name) - }, - label: urlencodeEncoder(rule.rule_name) - }) - ); - }; - - const openCheckFirstLevelTab = (check: CheckDefinitionListModel) => { - dispatch( - addFirstLevelTab({ - url: ROUTES.CHECK_DETAIL(urlencodeEncoder(check.check_name) ?? ''), - value: ROUTES.CHECK_DETAIL_VALUE( - urlencodeEncoder(check.check_name) ?? '' - ), - state: { - full_check_name: urlencodeEncoder(check.full_check_name), - custom: check.custom - }, - label: urlencodeEncoder(check.check_name) - }) - ); - }; - - const openCheckDefaultFirstLevelTab = (defaultCheck: string) => { - dispatch( - addFirstLevelTab({ - url: ROUTES.CHECK_DEFAULT_DETAIL(defaultCheck.replace(/\s/g, '_')), - value: ROUTES.CHECK_DEFAULT_DETAIL_VALUE( - defaultCheck.replace(/\s/g, '_') - ), - state: { - type: defaultCheck - }, - label: defaultCheck - }) - ); - }; - - const openAllUsersFirstLevelTab = () => { - dispatch( - addFirstLevelTab({ - url: ROUTES.USERS_LIST_DETAIL(), - value: ROUTES.USERS_LIST_DETAIL_VALUE(), - label: 'All users' - }) - ); - }; - - const openDefaultSchedulesFirstLevelTab = () => { - dispatch( - addFirstLevelTab({ - url: ROUTES.SCHEDULES_DEFAULT_DETAIL(), - value: ROUTES.SCHEDULES_DEFAULT_DETAIL_VALUE(), - label: 'Default schedules' - }) - ); - }; - - const openDefaultWebhooksFirstLevelTab = () => { - dispatch( - addFirstLevelTab({ - url: ROUTES.WEBHOOKS_DEFAULT_DETAIL(), - value: ROUTES.WEBHOOKS_DEFAULT_DETAIL_VALUE(), - label: 'Default webhooks' - }) - ); - }; - - const openSharedCredentialsFirstLevelTab = () => { - dispatch( - addFirstLevelTab({ - url: ROUTES.SHARED_CREDENTIALS_LIST_DETAIL(), - value: ROUTES.SHARED_CREDENTIALS_LIST_DETAIL_VALUE(), - label: 'Shared credentials' - }) - ); - }; - - const openDataDictionaryFirstLevelTab = () => { - dispatch( - addFirstLevelTab({ - url: ROUTES.DATA_DICTIONARY_LIST_DETAIL(), - value: ROUTES.DATA_DICTIONARY_LIST_VALUE(), - label: 'Data dictionaries' - }) - ); - }; - - const toggleFolderRecursively = ( - elements: string[], - index = 0, - type: string - ) => { - if (index >= elements.length - 1) { - return; - } - const path = elements.slice(0, index + 1).join('/'); - if (index === 0) { - if (type === 'checks') { - openDataQualityChecksFolder('undefined/' + path); - } else if (type === 'rules') { - openRuleFolder('undefined/' + path); - } else { - openSensorFolder('undefined/' + path); - } - } else { - if (type === 'checks') { - openDataQualityChecksFolder(path); - } else if (type === 'rules') { - openRuleFolder(path); - } else { - openSensorFolder(path); - } - } - toggleFolderRecursively(elements, index + 1, type); - }; - useEffect(() => { - const configuration = [ - { category: 'Sensors', isOpen: false }, - { category: 'Rules', isOpen: false }, - { category: 'Data quality checks', isOpen: false }, - { category: 'Default checks configuration', isOpen: false } - ]; - if (tabs && tabs.length !== 0) { - for (let i = 0; i < tabs.length; i++) { - if (tabs[i].url?.includes('default_checks')) { - configuration[3].isOpen = true; - } else if (tabs[i]?.url?.includes('sensors')) { - configuration[0].isOpen = true; - const arrayOfElemsToToggle = ( - tabs[i].state.full_sensor_name as string - )?.split('/'); - if (arrayOfElemsToToggle) { - toggleFolderRecursively(arrayOfElemsToToggle, 0, 'sensors'); - } - } else if (tabs[i]?.url?.includes('checks')) { - configuration[2].isOpen = true; - const arrayOfElemsToToggle = ( - tabs[i].state.fullCheckName as string - )?.split('/'); - if (arrayOfElemsToToggle) { - toggleFolderRecursively(arrayOfElemsToToggle, 0, 'checks'); - } - } else if (tabs[i]?.url?.includes('rules')) { - configuration[1].isOpen = true; - const arrayOfElemsToToggle = ( - tabs[i].state.full_rule_name as string - )?.split('/'); - if (arrayOfElemsToToggle) { - toggleFolderRecursively(arrayOfElemsToToggle, 0, 'rules'); - } - } - dispatch(toggleFirstLevelFolder(configuration)); - } - } else { - dispatch(toggleFirstLevelFolder(configuration)); - } - }, []); + toggleTree(tabs); + }, [activeTab]); + + const highlightedNode = activeTab + ?.split('/') + .at(activeTab?.split('/').length - 1); const renderSensorFolderTree = ( folder?: SensorFolderModel, @@ -599,15 +405,36 @@ export const DefinitionTree = () => { ); }; + const NodeComponent = ({ + onClick, + icon, + text + }: { + onClick: () => void; + icon: string; + text: string; + }) => ( +
+ +
+ {text} +
+
+ ); + return ( -
+
{definitionFirstLevelFolder?.map((x, index) => ( -
+
{ const updatedRootTree = [...definitionFirstLevelFolder]; updatedRootTree[index].isOpen = !updatedRootTree[index].isOpen; @@ -670,51 +497,9 @@ export const DefinitionTree = () => { )}
))} -
- -
- Manage users -
-
-
- -
- Default schedules -
-
-
- -
- Default webhooks -
-
-
- -
- Shared credentials -
-
-
- -
- Data dictionaries -
-
+ {(nodes as any[]).map((tab, index) => ( + + ))}
); }; diff --git a/dqops/src/main/frontend/src/components/DefinitionLayout/LeftView.tsx b/dqops/src/main/frontend/src/components/DefinitionLayout/LeftView.tsx new file mode 100644 index 0000000000..6010073fdc --- /dev/null +++ b/dqops/src/main/frontend/src/components/DefinitionLayout/LeftView.tsx @@ -0,0 +1,59 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react' +import DefinitionTree from './DefinitionTree'; +import { useDefinition } from '../../contexts/definitionContext'; + +export default function LeftView() { + const { setSidebarWidth, sidebarWidth } = useDefinition() + const sidebarRef = useRef(null); + const [isResizing, setIsResizing] = useState(false); + + const stopResizing = useCallback(() => { + setIsResizing(false); + }, []); + + const startResizing = useCallback((mouseDownEvent: MouseEvent) => { + setIsResizing(true); + mouseDownEvent.preventDefault(); + mouseDownEvent.stopPropagation(); + }, []); + + const resize = useCallback( + (mouseMoveEvent: MouseEvent) => { + if (isResizing) { + const newWidth = + mouseMoveEvent.clientX - + (sidebarRef.current as HTMLDivElement).getBoundingClientRect().left; + if (newWidth < 240 || newWidth > 700) return; + + setSidebarWidth(newWidth); + mouseMoveEvent.preventDefault(); + mouseMoveEvent.stopPropagation(); + } + }, + [isResizing] + ); + + useEffect(() => { + window.addEventListener('mousemove', resize); + window.addEventListener('mouseup', stopResizing); + return () => { + window.removeEventListener('mousemove', resize); + window.removeEventListener('mouseup', stopResizing); + }; + }, [resize, stopResizing]); + + return ( +
+ +
startResizing(event as any)} + style={{ left: sidebarWidth, userSelect: 'none' }} + /> +
+ ) +} diff --git a/dqops/src/main/frontend/src/components/DefinitionLayout/index.tsx b/dqops/src/main/frontend/src/components/DefinitionLayout/index.tsx index 1ecfcb8531..01dc8915c6 100644 --- a/dqops/src/main/frontend/src/components/DefinitionLayout/index.tsx +++ b/dqops/src/main/frontend/src/components/DefinitionLayout/index.tsx @@ -1,7 +1,6 @@ import React, { ReactNode, useEffect, useMemo } from 'react'; import Header from '../Header'; -import DefinitionTree from './DefinitionTree'; import { useDispatch, useSelector } from 'react-redux'; import { IRootState } from '../../redux/reducers'; import PageTabs from '../PageTabs'; @@ -26,6 +25,8 @@ import UserListDetail from '../../pages/UserListDetail'; import UserDetail from '../../pages/UserListDetail/UserDetail'; import DefaultCheckDetail from '../../pages/DefaultChecksDetail' import DefaultSchedules from '../../pages/DefaultSchedulesDetail' +import LeftView from './LeftView'; +import { useDefinition } from '../../contexts/definitionContext'; interface LayoutProps { route: string @@ -101,17 +102,17 @@ const DefinitionLayout = ({ route }: LayoutProps) => { }; const renderComponent: ReactNode = getComponent(); - + const { sidebarWidth } = useDefinition(); return ( -
+
- +
diff --git a/dqops/src/main/frontend/src/contexts/AppProvider.tsx b/dqops/src/main/frontend/src/contexts/AppProvider.tsx index db21980219..34320e7404 100644 --- a/dqops/src/main/frontend/src/contexts/AppProvider.tsx +++ b/dqops/src/main/frontend/src/contexts/AppProvider.tsx @@ -3,13 +3,16 @@ import React from 'react'; import { TreeProvider } from './treeContext'; import { ErrorProvider } from './errrorContext'; import { DashboardProvider } from "./dashboardContext"; +import { DefinitionProvider } from './definitionContext'; function AppProvider({ children }: { children: any }) { return ( - {children} + + {children} + diff --git a/dqops/src/main/frontend/src/contexts/definitionContext.tsx b/dqops/src/main/frontend/src/contexts/definitionContext.tsx new file mode 100644 index 0000000000..c92179e1cd --- /dev/null +++ b/dqops/src/main/frontend/src/contexts/definitionContext.tsx @@ -0,0 +1,288 @@ +import React, { useState } from 'react'; +import { useActionDispatch } from '../hooks/useActionDispatch'; +import { + addFirstLevelTab, + openRuleFolderTree, + openSensorFolderTree, + opendataQualityChecksFolderTree, + toggleFirstLevelFolder, + toggleRuleFolderTree, + toggleSensorFolderTree, + toggledataQualityChecksFolderTree +} from '../redux/actions/definition.actions'; +import { + SensorListModel, + RuleListModel, + CheckDefinitionListModel +} from '../api'; +import { ROUTES } from '../shared/routes'; +import { urlencodeEncoder } from '../utils'; +import { INestTab } from '../redux/reducers/source.reducer'; + +const DefinitionContext = React.createContext({} as any); + +function DefinitionProvider(props: any) { + const dispatch = useActionDispatch(); + const [sidebarWidth, setSidebarWidth] = useState(310); + const toggleSensorFolder = (key: string) => { + dispatch(toggleSensorFolderTree(key)); + }; + + const openSensorFolder = (key: string) => { + dispatch(openSensorFolderTree(key)); + }; + + const toggleRuleFolder = (key: string) => { + dispatch(toggleRuleFolderTree(key)); + }; + + const openRuleFolder = (key: string) => { + dispatch(openRuleFolderTree(key)); + }; + + const toggleDataQualityChecksFolder = (fullPath: string) => { + dispatch(toggledataQualityChecksFolderTree(fullPath)); + }; + const openDataQualityChecksFolder = (fullPath: string) => { + dispatch(opendataQualityChecksFolderTree(fullPath)); + }; + + const openSensorFirstLevelTab = (sensor: SensorListModel) => { + dispatch( + addFirstLevelTab({ + url: ROUTES.SENSOR_DETAIL(urlencodeEncoder(sensor.sensor_name) ?? ''), + value: ROUTES.SENSOR_DETAIL_VALUE( + urlencodeEncoder(sensor.sensor_name) ?? '' + ), + state: { + full_sensor_name: urlencodeEncoder(sensor.full_sensor_name) + }, + label: urlencodeEncoder(sensor.sensor_name) + }) + ); + }; + + const openRuleFirstLevelTab = (rule: RuleListModel) => { + dispatch( + addFirstLevelTab({ + url: ROUTES.RULE_DETAIL(urlencodeEncoder(rule.rule_name) ?? ''), + value: ROUTES.RULE_DETAIL_VALUE(urlencodeEncoder(rule.rule_name) ?? ''), + state: { + full_rule_name: urlencodeEncoder(rule.full_rule_name) + }, + label: urlencodeEncoder(rule.rule_name) + }) + ); + }; + + const openCheckFirstLevelTab = (check: CheckDefinitionListModel) => { + dispatch( + addFirstLevelTab({ + url: ROUTES.CHECK_DETAIL(urlencodeEncoder(check.check_name) ?? ''), + value: ROUTES.CHECK_DETAIL_VALUE( + urlencodeEncoder(check.check_name) ?? '' + ), + state: { + full_check_name: urlencodeEncoder(check.full_check_name), + custom: check.custom + }, + label: urlencodeEncoder(check.check_name) + }) + ); + }; + + const openCheckDefaultFirstLevelTab = (defaultCheck: string) => { + dispatch( + addFirstLevelTab({ + url: ROUTES.CHECK_DEFAULT_DETAIL(defaultCheck.replace(/\s/g, '_')), + value: ROUTES.CHECK_DEFAULT_DETAIL_VALUE( + defaultCheck.replace(/\s/g, '_') + ), + state: { + type: defaultCheck + }, + label: defaultCheck + }) + ); + }; + + const openAllUsersFirstLevelTab = () => { + dispatch( + addFirstLevelTab({ + url: ROUTES.USERS_LIST_DETAIL(), + value: ROUTES.USERS_LIST_DETAIL_VALUE(), + label: 'All users' + }) + ); + }; + + const openDefaultSchedulesFirstLevelTab = () => { + dispatch( + addFirstLevelTab({ + url: ROUTES.SCHEDULES_DEFAULT_DETAIL(), + value: ROUTES.SCHEDULES_DEFAULT_DETAIL_VALUE(), + label: 'Default schedules' + }) + ); + }; + + const openDefaultWebhooksFirstLevelTab = () => { + dispatch( + addFirstLevelTab({ + url: ROUTES.WEBHOOKS_DEFAULT_DETAIL(), + value: ROUTES.WEBHOOKS_DEFAULT_DETAIL_VALUE(), + label: 'Default webhooks' + }) + ); + }; + + const openSharedCredentialsFirstLevelTab = () => { + dispatch( + addFirstLevelTab({ + url: ROUTES.SHARED_CREDENTIALS_LIST_DETAIL(), + value: ROUTES.SHARED_CREDENTIALS_LIST_DETAIL_VALUE(), + label: 'Shared credentials' + }) + ); + }; + + const openDataDictionaryFirstLevelTab = () => { + dispatch( + addFirstLevelTab({ + url: ROUTES.DATA_DICTIONARY_LIST_DETAIL(), + value: ROUTES.DATA_DICTIONARY_LIST_VALUE(), + label: 'Data dictionaries' + }) + ); + }; + + const toggleFolderRecursively = ( + elements: string[], + index = 0, + type: string + ) => { + if (index >= elements.length - 1) { + return; + } + const path = elements.slice(0, index + 1).join('/'); + if (index === 0) { + if (type === 'checks') { + openDataQualityChecksFolder('undefined/' + path); + } else if (type === 'rules') { + openRuleFolder('undefined/' + path); + } else { + openSensorFolder('undefined/' + path); + } + } else { + if (type === 'checks') { + openDataQualityChecksFolder(path); + } else if (type === 'rules') { + openRuleFolder(path); + } else { + openSensorFolder(path); + } + } + toggleFolderRecursively(elements, index + 1, type); + }; + + const toggleTree = (tabs: INestTab[]) => { + const configuration = [ + { category: 'Sensors', isOpen: false }, + { category: 'Rules', isOpen: false }, + { category: 'Data quality checks', isOpen: false }, + { category: 'Default checks configuration', isOpen: false } + ]; + if (tabs && tabs.length !== 0) { + for (let i = 0; i < tabs.length; i++) { + if (tabs[i].url?.includes('default_checks')) { + configuration[3].isOpen = true; + } else if (tabs[i]?.url?.includes('sensors')) { + configuration[0].isOpen = true; + const arrayOfElemsToToggle = ( + tabs[i].state.full_sensor_name as string + )?.split('/'); + if (arrayOfElemsToToggle) { + toggleFolderRecursively(arrayOfElemsToToggle, 0, 'sensors'); + } + } else if (tabs[i]?.url?.includes('checks')) { + configuration[2].isOpen = true; + const arrayOfElemsToToggle = ( + tabs[i].state.full_check_name as string + )?.split('/'); + if (arrayOfElemsToToggle) { + toggleFolderRecursively(arrayOfElemsToToggle, 0, 'checks'); + } + } else if (tabs[i]?.url?.includes('rules')) { + configuration[1].isOpen = true; + const arrayOfElemsToToggle = ( + tabs[i].state.full_rule_name as string + )?.split('/'); + if (arrayOfElemsToToggle) { + toggleFolderRecursively(arrayOfElemsToToggle, 0, 'rules'); + } + } + dispatch(toggleFirstLevelFolder(configuration)); + } + } else { + dispatch(toggleFirstLevelFolder(configuration)); + } + }; + + const nodes = [ + { + onClick: openAllUsersFirstLevelTab, + icon: 'userprofile', + text: 'Manage users' + }, + { + onClick: openDefaultSchedulesFirstLevelTab, + icon: 'clock', + text: 'Default schedules' + }, + { + onClick: openDefaultWebhooksFirstLevelTab, + icon: 'webhooks', + text: 'Default webhooks' + }, + { + onClick: openSharedCredentialsFirstLevelTab, + icon: 'definitionsrules', + text: 'Shared credentials' + }, + { + onClick: openDataDictionaryFirstLevelTab, + icon: 'datadictionary', + text: 'Data Dictionary' + } + ]; + + return ( + + ); +} + +function useDefinition() { + const context = React.useContext(DefinitionContext); + + if (context === undefined) { + throw new Error('useDefinition must be used within a DefinitionProvider'); + } + return context; +} + +export { DefinitionProvider, useDefinition }; diff --git a/dqops/src/main/frontend/src/pages/CheckDetail/CheckEditor.tsx b/dqops/src/main/frontend/src/pages/CheckDetail/CheckEditor.tsx index 28c874fe84..4f25f1f3a4 100644 --- a/dqops/src/main/frontend/src/pages/CheckDetail/CheckEditor.tsx +++ b/dqops/src/main/frontend/src/pages/CheckDetail/CheckEditor.tsx @@ -136,6 +136,7 @@ const CheckEditor = ({ onChangeStandard(value); setIsUpdated(true); }} disabled={custom === false ? true : false} + className={custom === false ? 'cursor-default' : ''} label="Standard data quality check, always shown in the editor" />
@@ -159,6 +160,7 @@ const CheckEditor = ({ }} disableIcon={custom === false ? true : false} className="w-1/2" + triggerClassName={(custom === false || canEditDefinitions === false) ? "cursor-default" : ""} />