Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/app/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { combineReducers, configureStore, Store} from '@reduxjs/toolkit'
import graphViewButtonReducer from '../util/ConfigExplorer/slice/LineageTab/Toolbar/GraphViewSlice';
import layoutReducer from '../util/ConfigExplorer/slice/LineageTab/Toolbar/LayoutSlice'
import graphExpansionReducer from '../util/ConfigExplorer/slice/LineageTab/Toolbar/GraphExpansionSlice'
import NodeAttributeFilterReducer from '../util/ConfigExplorer/slice/LineageTab/Toolbar/NodeAttributeFilterSlice'
import reactFlowReducer from '../util/ConfigExplorer/slice/LineageTab/Common/ReactFlowSlice'
import lineageTabReducer from '../util/ConfigExplorer/slice/LineageTab/Core/LineageTabCoreSlice'

Expand All @@ -16,6 +17,7 @@ const rootReducer = combineReducers({
graphViewSelector: graphViewButtonReducer,
layoutSelector: layoutReducer,
graphExpansion: graphExpansionReducer,
nodeAttributeFilter: NodeAttributeFilterReducer,
reactFlow: reactFlowReducer,
lineage: lineageTabReducer
})
Expand All @@ -28,4 +30,4 @@ const store: Store = configureStore({

export default store;
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export type AppDispatch = typeof store.dispatch
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ import { Chip, IconButton, Tooltip } from '@mui/joy';
import Box from '@mui/joy/Box';
import Typography from '@mui/joy/Typography';
import { Link } from "react-router-dom";
import { useAppSelector } from '../../../hooks/useRedux';

import { Position } from 'reactflow';
import { useFetchWorkflowRunsByElement } from '../../../hooks/useFetchData';
import { NodeType } from '../../../util/ConfigExplorer/Graphs';
import { flowProps, graphNodeProps, ReactFlowNodeProps } from '../../../util/ConfigExplorer/LineageTabUtils';
import { getIcon } from '../../../util/WorkflowsExplorer/StatusInfo';
import { getIcon, getPartitionStatus, getExecutionMode } from '../../../util/WorkflowsExplorer/StatusInfo';
import './LineageTab.css';
import { useWorkspace } from '../../../hooks/useWorkspace';
import { getSelectedNodeAttributes} from '../../../util/ConfigExplorer/slice/LineageTab/Toolbar/NodeAttributeFilterSlice';


/*
Styles to refactor (for the entire LineageTab folder)
Expand Down Expand Up @@ -148,6 +151,8 @@ export const CustomDataNode = ( {data} ) => {
const nodeTypeName: string = nodeType === NodeType.ActionNode ? "actions" :
nodeType === NodeType.DataNode ? "dataObjects" :
"";
const executionMode = jsonObject?.executionMode
const isPartioned = jsonObject?.partitions !== undefined && jsonObject?.partitions.length >= 1
const abbr = nodeSubTypeName.replace(/(?!^)[^A-Z\d]/g, ''); // take the capital letters and the first letter of the camelCase name
const { data: runs} = useFetchWorkflowRunsByElement(nodeTypeName, label);
const lastRun = runs?.at(-1); // this only shows the LAST run, but the times could be different for each object
Expand Down Expand Up @@ -198,6 +203,8 @@ export const CustomDataNode = ( {data} ) => {

function showObjectTitle(){
const objectType = nodeType === NodeType.ActionNode ? "Action Object" : "Data Object";
const selectedNodeAttributes = useAppSelector(state => getSelectedNodeAttributes(state));

return (
<Box sx={{display: 'flex', flexDirection: 'row', alignItems: 'center'}}>
<Tooltip title={objectType} arrow disableInteractive placement={layoutDirection=='TB' ? 'right' : 'bottom'}>
Expand All @@ -213,7 +220,9 @@ export const CustomDataNode = ( {data} ) => {
{/* <div>
{createConnectionChip(props.connection.id)} // need distinction on objects without conn.
</div> */}
<Box sx={{flex: 1}}/>
<Box sx={{flex: 1}}/>
{nodeType === NodeType.ActionNode && selectedNodeAttributes.includes("action-executionMode") ? getExecutionMode(executionMode?.type) : null }
{nodeType === NodeType.DataNode && selectedNodeAttributes.includes("data-partitionState") ? getPartitionStatus(isPartioned) : null}
{lastRun?.status !== undefined && (getIcon(lastRun?.status, '0px', {scale: '100%'}))}

{/* <div style={{justifyContent: 'flex-end'}}>
Expand Down
85 changes: 74 additions & 11 deletions src/components/ConfigExplorer/LineageTab/LineageGraphToolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Abc, AlignVerticalTop, Apps, ArrowDropDown, Clear, FitScreen, OpenInFull, Search, Send } from '@mui/icons-material';
import { Abc, AlignVerticalTop, Apps, ArrowDropDown, Clear, FitScreen, OpenInFull, Search, Send, FilterList } from '@mui/icons-material';
import AlignHorizontalLeft from '@mui/icons-material/AlignHorizontalLeft';
import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
import {CloudDownload, Close} from '@mui/icons-material';
Expand All @@ -11,7 +11,7 @@ import ToggleButtonGroup from '@mui/joy/ToggleButtonGroup';
import * as React from 'react';
import { ReactElement } from 'react';

import { Autocomplete, Button, Divider, Dropdown, IconButton, Input, ListItemDecorator, Menu, MenuButton, MenuItem, Tooltip } from '@mui/joy';
import { Autocomplete, Button, Divider, Dropdown, IconButton, Input, ListItemDecorator, Menu, MenuButton, MenuItem, Tooltip, Checkbox, Select, Option } from '@mui/joy';
// import Option from '@mui/joy/Option';
import Box from '@mui/material/Box';
import { toPng } from 'html-to-image';
Expand All @@ -29,6 +29,7 @@ import { getGraphView, setGraphView } from '../../../util/ConfigExplorer/slice/L
import { setGroupingState } from '../../../util/ConfigExplorer/slice/LineageTab/Toolbar/GroupingSlice';
import { getLayout, setLayout } from '../../../util/ConfigExplorer/slice/LineageTab/Toolbar/LayoutSlice';
import { nodeHeight, nodeWidth } from './LineageTabWithSeparateView';
import { nodeAttributes, getSelectedNodeAttributes, setSelectedNodeAttributes } from '../../../util/ConfigExplorer/slice/LineageTab/Toolbar/NodeAttributeFilterSlice';

/*
Styling
Expand Down Expand Up @@ -110,7 +111,7 @@ function LayoutButton() {
<IconButton color={'neutral'} onClick={() => dispatch(setLayout(layout === 'TB' ? 'LR' : 'TB'))}>
{layout === 'TB' ? <AlignVerticalTop /> : <AlignHorizontalLeft />}
</IconButton>
</Tooltip>
</Tooltip>
}

function GraphExpansionButton() {
Expand Down Expand Up @@ -204,7 +205,7 @@ function recomputeLayout(rfi: any, layoutDirection: any) {
const nonParentNodes = getNonParentNodesFromArray(rfNodes);
const parentNodes = getParentNodesFromArray(rfNodes);
const rfEdges = rfi.getEdges();

var layoutedNonParentNodes = dagreLayoutRf(nonParentNodes, rfEdges, layoutDirection, nodeWidth, nodeHeight);
var layoutedParentNodes = computeParentNodePositionFromArray(layoutedNonParentNodes, parentNodes);
layoutedNonParentNodes = computeNodePositionFromParent(layoutedNonParentNodes, layoutedParentNodes);
Expand Down Expand Up @@ -287,21 +288,21 @@ function GroupingButton() {
<Menu sx={{'--ListItemDecorator-size': '20px', overflow: 'visible' }}>
{/* this is a normal button to avoid closing the dropdown */}
<Button className='byName' onClick={() => setShowByNameSelector(true)} sx={{backgroundColor: (groupingOption=='byName' ? '#e6eef7' : 'white')}}>
<Tooltip arrow title='group by name' enterDelay={500} enterNextDelay={500} placement='right'>
<Tooltip arrow title='group by name' enterDelay={500} enterNextDelay={500} placement='right'>
<ListItemDecorator>
<Abc />
</ListItemDecorator>
</Tooltip>
</Button>
{/* this is an improvised "submenu" showing an input box for the name */}
{showByNameSelector &&
{showByNameSelector &&
<Box position="absolute" top={5} left={55} >
<form onSubmit={ev => handleApplyByName(ev)}>
<Input id="name" size="sm" sx={{width: 200}} autoFocus placeholder='Name substring...' endDecorator={<IconButton type="submit" size="sm"><Send/></IconButton>}/>
</form>
</Box>
}
<Tooltip arrow title='group by feed (only enabled if "action graph view" is selected)' enterDelay={500} enterNextDelay={500} placement='right'>
<Tooltip arrow title='group by feed (only enabled if "action graph view" is selected)' enterDelay={500} enterNextDelay={500} placement='right'>
<span>{/* <span> is used to show tooltip also if MenuItem is disabled */}
<MenuItem className='byFeed' selected={groupingOption === 'byFeed'} onClick={handleApplyByFeed} disabled={graphView !== 'action'} sx={{ outline: '0 !important' }}>
<ListItemDecorator>
Expand All @@ -322,6 +323,67 @@ function GroupingButton() {
)
}

function NodeAttributeSelector() {
const dispatch = useAppDispatch();

// Fetch selected items
const selected = useAppSelector(state => getSelectedNodeAttributes(state));

// Define Callbacks
const onChange = (selectedValues) => {
dispatch(setSelectedNodeAttributes(selectedValues))
}

const handleChange = (_, newValue) => {
dispatch(setSelectedNodeAttributes(newValue));
};

// Divide attributes into data and action node attributes
const dataNodeAttributes = nodeAttributes.filter(attr => attr.value.startsWith("data"))
const actionNodeAttributes = nodeAttributes.filter(attr => attr.value.startsWith("action"))

return (
<Tooltip
arrow
title={<>Select which attributes should be shown for the displayed nodes.</>}
enterDelay={500}
enterNextDelay={500}
placement='top'
>
<Select
multiple
value={selected}
onChange={handleChange}
startDecorator={<FilterList />}
variant="plain" // Do not show shadow box
placeholder=""
renderValue={() => null} // Do not display selected items
className = 'attribute-selection-dropdown-parent'
slotProps={{
// Set class on <ul> for CSS selector
listbox: {
className: 'attribute-selection-dropdown',
}
}}
>
{dataNodeAttributes.map(attr => (
<Option key={attr.value} value={attr.value} >
<Checkbox checked={selected.includes(attr.value)} />
{attr.label}
</Option>
))}
<Divider/>
{actionNodeAttributes.map(attr => (
<Option key={attr.value} value={attr.value} >
<Checkbox checked={selected.includes(attr.value)} />
{attr.label}
</Option>
))}
</Select>
</Tooltip>
);
}


export const NodeSearchButton = () => {
const rfi = useAppSelector((state) => getRFI(state));
Expand Down Expand Up @@ -396,7 +458,7 @@ export const NodeSearchButton = () => {
const rfNode = rfi.getNode(suggestion.id);
resetViewPortCentered(rfi, [rfNode]);
setOpen(false);
}
}
};

const [open, setOpen] = React.useState(false);
Expand All @@ -412,7 +474,7 @@ export const NodeSearchButton = () => {
</Tooltip>
</MenuButton>
<Menu placement="bottom-start" sx={{border: "none", padding: '0px', backgroundColor: 'transparent'}}>
<Autocomplete
<Autocomplete
sx={{ width: 390 }}
freeSolo
placeholder="Search node"
Expand All @@ -437,8 +499,8 @@ export default function LineageGraphToolbar() {
return (
<Draggable bounds="parent" nodeRef={nodeRef}>
<Box ref={nodeRef} sx={{ zIndex: componentZIndex, position: 'absolute', left: 0, top: 0, padding: 0.1, gap: 0.2, display: 'flex', flexDirection: 'row',
border: '1px solid', borderColor: 'divider', borderRadius: '10px', bgcolor: 'white',
}}
border: '1px solid', borderColor: 'divider', borderRadius: '10px', bgcolor: 'white',
}}
>
<ToggleButtonGroup variant="plain" spacing={0.1}>
<NodeSearchButton/>
Expand All @@ -448,6 +510,7 @@ export default function LineageGraphToolbar() {
{isPropsConfigDefined && <GraphExpansionButton />}
<GraphViewSelector />
<GroupingButton />
<NodeAttributeSelector />
</ToggleButtonGroup>
<Divider orientation="vertical" />
<ToggleButtonGroup variant="plain" spacing={0.1}>
Expand Down
49 changes: 47 additions & 2 deletions src/components/ConfigExplorer/LineageTab/LineageTab.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,60 @@
.react-flow__handle-top {
top: -15px;
}

.react-flow__handle-bottom {
bottom: -15px;
}

.react-flow__handle-left {
left: -15px;
}

.react-flow__handle-right {
right: -15px;
}

/* CSS variables for the attribute selection */
.node-attributes{
--letter-background: #17a2b8;
}

/* Node Attribution Selection */
.attribute-selection-dropdown-parent{
>.MuiSelect-indicator {
margin-left: -10px;
}
}

.attribute-selection-dropdown li{
background: transparent !important;

}

/* Letter styling for the ActionNode ExecutionMode 'icon'*/
.circle-letter-parent {
display: flex;
justify-content: center;
align-items: center;
height: 24px;
width: 24px;
}

.circle-letter {
display: flex;
justify-content: center;
align-items: center;
background-color: var(--letter-background);
width: 20px;
height: 20px;
color: white;
font-weight: bold;
border-radius: 50%;
}

/* Styling for the DataNode PartitionState icon */
svg.data-node-partition-state-icon {
color: var(--letter-background);
scale: 80%;
z-index: 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import type { RootState } from '../../../../../app/store'

export const nodeAttributes = [
{ label: "Action Execution Mode", value: "action-executionMode" },
{ label: "Data Partition State", value: "data-partitionState" },
];

interface nodeAttributeFilterState {
selectedAttributes: string[],
}

const initialState: nodeAttributeFilterState = {
selectedAttributes: nodeAttributes.map(attr => attr.value),
}

const nodeAttributeFilterSlice = createSlice({
name: 'nodeAttributeFilter',
initialState,
reducers: {
setSelectedNodeAttributes: (state, newState: PayloadAction<string[]>) => {
state.selectedAttributes = newState.payload;
}
}
})

export const {setSelectedNodeAttributes} = nodeAttributeFilterSlice.actions;
export const getSelectedNodeAttributes = (state: RootState) => state.nodeAttributeFilter.selectedAttributes;
export default nodeAttributeFilterSlice.reducer;
Loading