mirror of
https://github.com/sartography/spiff-arena.git
synced 2025-02-23 22:58:09 +00:00
new process model tree page w/ burnettk
This commit is contained in:
parent
4ec0884b6c
commit
e6a46296bf
@ -65,12 +65,14 @@ function SideNav({
|
|||||||
let selectedTab = 0;
|
let selectedTab = 0;
|
||||||
if (location.pathname === '/newui/startprocess') {
|
if (location.pathname === '/newui/startprocess') {
|
||||||
selectedTab = 1;
|
selectedTab = 1;
|
||||||
} else if (location.pathname === '/newui/data-stores') {
|
} else if (location.pathname.startsWith('/newui/processes')) {
|
||||||
selectedTab = 2;
|
selectedTab = 2;
|
||||||
} else if (location.pathname === '/newui/messages') {
|
} else if (location.pathname === '/newui/data-stores') {
|
||||||
selectedTab = 3;
|
selectedTab = 3;
|
||||||
} else if (location.pathname.startsWith('/newui/configuration')) {
|
} else if (location.pathname === '/newui/messages') {
|
||||||
selectedTab = 4;
|
selectedTab = 4;
|
||||||
|
} else if (location.pathname.startsWith('/newui/configuration')) {
|
||||||
|
selectedTab = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionInfo = appVersionInfo();
|
const versionInfo = appVersionInfo();
|
||||||
@ -168,6 +170,7 @@ function SideNav({
|
|||||||
{[
|
{[
|
||||||
{ text: 'HOME', icon: <Home /> },
|
{ text: 'HOME', icon: <Home /> },
|
||||||
{ text: 'START NEW PROCESS', icon: <Add /> },
|
{ text: 'START NEW PROCESS', icon: <Add /> },
|
||||||
|
{ text: 'PROCESSES' },
|
||||||
{ text: 'DATA STORES' },
|
{ text: 'DATA STORES' },
|
||||||
{ text: 'MESSAGES' },
|
{ text: 'MESSAGES' },
|
||||||
{ text: 'CONFIGURATION' },
|
{ text: 'CONFIGURATION' },
|
||||||
@ -181,6 +184,8 @@ function SideNav({
|
|||||||
navigate('/newui');
|
navigate('/newui');
|
||||||
} else if (index === 1) {
|
} else if (index === 1) {
|
||||||
navigate('/newui/startprocess');
|
navigate('/newui/startprocess');
|
||||||
|
} else if (item.text === 'PROCESSES') {
|
||||||
|
navigate('/newui/processes');
|
||||||
} else if (item.text === 'DATA STORES') {
|
} else if (item.text === 'DATA STORES') {
|
||||||
navigate('/newui/data-stores');
|
navigate('/newui/data-stores');
|
||||||
} else if (item.text === 'MESSAGES') {
|
} else if (item.text === 'MESSAGES') {
|
||||||
|
@ -28,7 +28,7 @@ export default function useProcessGroups({
|
|||||||
let path = '/process-groups';
|
let path = '/process-groups';
|
||||||
if (getRunnableProcessModels) {
|
if (getRunnableProcessModels) {
|
||||||
path =
|
path =
|
||||||
'/process-models?filter_runnable_by_user=false&recursive=true&group_by_process_group=True&per_page=1000';
|
'/process-models?filter_runnable_by_user=true&recursive=true&group_by_process_group=True&per_page=1000';
|
||||||
}
|
}
|
||||||
const getProcessGroups = async () => {
|
const getProcessGroups = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { ReactElement } from 'react';
|
import { ReactElement } from 'react';
|
||||||
|
|
||||||
|
export enum ProcessModelAction {
|
||||||
|
Open = 'Open',
|
||||||
|
StartProcess = 'StartProcess',
|
||||||
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
|
@ -4,6 +4,7 @@ import { PointerEvent, useEffect, useState } from 'react';
|
|||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
import { modifyProcessIdentifierForPathParam } from '../../../helpers';
|
import { modifyProcessIdentifierForPathParam } from '../../../helpers';
|
||||||
import { getStorageValue } from '../../services/LocalStorageService';
|
import { getStorageValue } from '../../services/LocalStorageService';
|
||||||
|
import { ProcessModelAction } from '../../interfaces';
|
||||||
|
|
||||||
const defaultStyle = {
|
const defaultStyle = {
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
@ -31,8 +32,10 @@ export default function ProcessModelCard({
|
|||||||
stream,
|
stream,
|
||||||
lastSelected,
|
lastSelected,
|
||||||
onStartProcess,
|
onStartProcess,
|
||||||
|
processModelAction,
|
||||||
}: {
|
}: {
|
||||||
model: Record<string, any>;
|
model: Record<string, any>;
|
||||||
|
processModelAction: ProcessModelAction;
|
||||||
stream?: Subject<Record<string, any>>;
|
stream?: Subject<Record<string, any>>;
|
||||||
lastSelected?: Record<string, any>;
|
lastSelected?: Record<string, any>;
|
||||||
onStartProcess?: () => void;
|
onStartProcess?: () => void;
|
||||||
@ -59,6 +62,15 @@ export default function ProcessModelCard({
|
|||||||
navigate(`/newui/${modifiedProcessModelId}/start`);
|
navigate(`/newui/${modifiedProcessModelId}/start`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleViewProcess = (e: PointerEvent) => {
|
||||||
|
stopEventBubble(e);
|
||||||
|
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
|
||||||
|
model.id,
|
||||||
|
);
|
||||||
|
// navigate(`/newui/process-models/${modifiedProcessModelId}`);
|
||||||
|
navigate(`/process-models/${modifiedProcessModelId}`);
|
||||||
|
};
|
||||||
|
|
||||||
const handleClickStream = (item: Record<string, any>) => {
|
const handleClickStream = (item: Record<string, any>) => {
|
||||||
if (model.id === item.id) {
|
if (model.id === item.id) {
|
||||||
setSelectedStyle((prev) => ({
|
setSelectedStyle((prev) => ({
|
||||||
@ -169,14 +181,25 @@ export default function ProcessModelCard({
|
|||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
{processModelAction === ProcessModelAction.StartProcess ? (
|
||||||
variant="contained"
|
<Button
|
||||||
color="primary"
|
variant="contained"
|
||||||
size="small"
|
color="primary"
|
||||||
onClick={(e) => handleStartProcess(e as unknown as PointerEvent)}
|
size="small"
|
||||||
>
|
onClick={(e) => handleStartProcess(e as unknown as PointerEvent)}
|
||||||
Start this process
|
>
|
||||||
</Button>
|
Start this process
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => handleViewProcess(e as unknown as PointerEvent)}
|
||||||
|
>
|
||||||
|
View process
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
@ -0,0 +1,418 @@
|
|||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Stack,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { Subject, Subscription } from 'rxjs';
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import StarRateIcon from '@mui/icons-material/StarRate';
|
||||||
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
import useProcessGroups from '../../hooks/useProcessGroups';
|
||||||
|
import TreePanel, { TreeRef, SHOW_FAVORITES } from './TreePanel';
|
||||||
|
import SearchBar from './SearchBar';
|
||||||
|
import ProcessGroupCard from './ProcessGroupCard';
|
||||||
|
import ProcessModelCard from './ProcessModelCard';
|
||||||
|
import {
|
||||||
|
SPIFF_FAVORITES,
|
||||||
|
getStorageValue,
|
||||||
|
} from '../../services/LocalStorageService';
|
||||||
|
import SpiffBreadCrumbs, { Crumb, SPIFF_ID } from './SpiffBreadCrumbs';
|
||||||
|
import { modifyProcessIdentifierForPathParam } from '../../../helpers';
|
||||||
|
import {
|
||||||
|
ProcessGroup,
|
||||||
|
ProcessGroupLite,
|
||||||
|
ProcessModelAction,
|
||||||
|
} from '../../interfaces';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
setNavElementCallback?: Function;
|
||||||
|
processModelAction: ProcessModelAction;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Top level layout and control container for this view,
|
||||||
|
* feeds various streams, data and callbacks to children.
|
||||||
|
*/
|
||||||
|
export default function ProcessModelTreePage({
|
||||||
|
setNavElementCallback,
|
||||||
|
processModelAction,
|
||||||
|
}: OwnProps) {
|
||||||
|
const { processGroups } = useProcessGroups({
|
||||||
|
processInfo: {},
|
||||||
|
getRunnableProcessModels:
|
||||||
|
processModelAction === ProcessModelAction.StartProcess,
|
||||||
|
});
|
||||||
|
const [groups, setGroups] = useState<
|
||||||
|
ProcessGroup[] | ProcessGroupLite[] | null
|
||||||
|
>(null);
|
||||||
|
const [models, setModels] = useState<Record<string, any>[]>([]);
|
||||||
|
const [flatItems, setFlatItems] = useState<Record<string, any>>([]);
|
||||||
|
// On load, there are always groups and never models, expand accordingly.
|
||||||
|
const [groupsExpanded, setGroupsExpanded] = useState(true);
|
||||||
|
const [modelsExpanded, setModelsExpanded] = useState(false);
|
||||||
|
const [lastSelected, setLastSelected] = useState<Record<string, any>>({});
|
||||||
|
const [crumbs, setCrumbs] = useState<Crumb[]>([]);
|
||||||
|
// const [treeCollapsed, setTreeCollapsed] = useState(false);
|
||||||
|
const [treeCollapsed] = useState(false);
|
||||||
|
const treeRef = useRef<TreeRef>(null);
|
||||||
|
// Pass to anything that wants to broadcast to all subscribers
|
||||||
|
const clickStream = new Subject<Record<string, any>>();
|
||||||
|
const favoriteCrumb: Crumb = { id: 'favorites', displayName: 'Favorites' };
|
||||||
|
const gridProps = {
|
||||||
|
width: '100%',
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fill, 400px)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gridGap: 20,
|
||||||
|
};
|
||||||
|
|
||||||
|
const processCrumbs = (item: Record<string, any>): Crumb[] => {
|
||||||
|
/**
|
||||||
|
* Create the paths for the crumbs.
|
||||||
|
* This logic is repeated from TreePanel.
|
||||||
|
* TODO: Consider a service, or hook, or something.
|
||||||
|
*/
|
||||||
|
const pathIds: string[] = [];
|
||||||
|
const split: string[] = item.id.split('/');
|
||||||
|
split.forEach((id, i) => {
|
||||||
|
pathIds.push(i === 0 ? id : `${pathIds[i - 1]}/${id}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Look up the display names in the flattened items and return.
|
||||||
|
return pathIds.map((id) => {
|
||||||
|
const found = flatItems.find((flatItem: any) => flatItem.id === id);
|
||||||
|
return { id, displayName: found?.display_name || id };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickStream = (item: Record<string, any>) => {
|
||||||
|
setCrumbs(processCrumbs(item));
|
||||||
|
setLastSelected(item);
|
||||||
|
let itemToUse: any = { ...item };
|
||||||
|
// Duck type to find out if this is a model ore a group.
|
||||||
|
// If model, we want its parent group, which can be found in the id.
|
||||||
|
if (!('process_groups' in item)) {
|
||||||
|
// recursively find the parent group.
|
||||||
|
const findParent = (
|
||||||
|
searchGroups: Record<string, any>[],
|
||||||
|
id: string,
|
||||||
|
): Record<string, any> | undefined => {
|
||||||
|
return searchGroups.find((group) => {
|
||||||
|
if (group.id === id) {
|
||||||
|
itemToUse = group;
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
if (group.process_groups) {
|
||||||
|
return findParent(group.process_groups, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// Remove the last part of the id, which is not and expandable entity.
|
||||||
|
const parentId = item.id.split('/').slice(0, -1).join('/');
|
||||||
|
findParent(processGroups || [], parentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemToUse?.process_models) {
|
||||||
|
// If a user clicks a group, and it has models, expand them for the user.
|
||||||
|
if (itemToUse.process_models.length) {
|
||||||
|
setModelsExpanded(true);
|
||||||
|
}
|
||||||
|
setModels(itemToUse.process_models);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are groups in this group, set them into state for display
|
||||||
|
if (itemToUse?.process_groups) {
|
||||||
|
setGroups(itemToUse.process_groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.getElementById('scrollable-process-card-area');
|
||||||
|
const cardElement = document.getElementById(
|
||||||
|
`card-${modifyProcessIdentifierForPathParam(item.id)}`,
|
||||||
|
);
|
||||||
|
if (container && cardElement) {
|
||||||
|
setTimeout(() => {
|
||||||
|
container.scrollTo({
|
||||||
|
top: cardElement.offsetTop - container.offsetTop,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}, 100); // Slight delay before scrolling to ensure rendering
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Tree calls back here so we can imperatively rework groups etc. */
|
||||||
|
const handleFavorites = ({ text }: { text: string }) => {
|
||||||
|
if (text === SHOW_FAVORITES) {
|
||||||
|
const storage = JSON.parse(getStorageValue('spifffavorites'));
|
||||||
|
const favs = flatItems.filter((item: any) => storage.includes(item.id));
|
||||||
|
// If there's no favorites, the user is just left looking at nothing.
|
||||||
|
// Load the top level groups instead.
|
||||||
|
// Expand accordions accordingly (haha).
|
||||||
|
setGroups(favs.length ? [] : processGroups);
|
||||||
|
setModels(favs);
|
||||||
|
setModelsExpanded(!!favs.length);
|
||||||
|
setGroupsExpanded(!favs.length);
|
||||||
|
setCrumbs([favoriteCrumb]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For now, we're just pasting together some info fields that make sense.
|
||||||
|
* This is simple and works and is easily expanded,
|
||||||
|
* but eventually might need to be more robust.
|
||||||
|
*/
|
||||||
|
const handleSearch = useDebouncedCallback((search: string) => {
|
||||||
|
// Indicate to user this is a search result.
|
||||||
|
setCrumbs([
|
||||||
|
{ id: search, displayName: `Searching for: ${search || '(all)'}` },
|
||||||
|
]);
|
||||||
|
// Search the flattened items for the search term.
|
||||||
|
const foundGroups = flatItems.filter((item: any) => {
|
||||||
|
return (
|
||||||
|
item.id.toLowerCase().includes(search.toLowerCase()) &&
|
||||||
|
item?.process_groups
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const foundModels = flatItems.filter((item: any) => {
|
||||||
|
return (
|
||||||
|
(item.id + item.display_name + item.description)
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(search.toLowerCase()) && !item?.process_groups
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
setGroups(foundGroups);
|
||||||
|
setGroupsExpanded(!!foundGroups.length);
|
||||||
|
setModels(foundModels);
|
||||||
|
setModelsExpanded(!!foundModels.length);
|
||||||
|
// The tree isn't connected to these results, so imperatively wipe the expanded nodes.
|
||||||
|
treeRef.current?.clearExpanded();
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a crumb is clicked, we need to find the item in the flatItems list
|
||||||
|
* and feed it to the clickstream.
|
||||||
|
*/
|
||||||
|
const handleCrumbClick = (crumb: Crumb) => {
|
||||||
|
// If this is s top-level crumb, just reset the view.
|
||||||
|
if (crumb.id === SPIFF_ID) {
|
||||||
|
setGroups(processGroups);
|
||||||
|
setModels([]);
|
||||||
|
setModelsExpanded(false);
|
||||||
|
setGroupsExpanded(true);
|
||||||
|
setCrumbs([]);
|
||||||
|
treeRef.current?.clearExpanded();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Otherwise, find the item in the flatItems list and feed it to the clickstream.
|
||||||
|
const found = flatItems.find((item: any) => item.id === crumb.id);
|
||||||
|
if (found) {
|
||||||
|
clickStream.next(found);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Recursively flatten the entire hierarchy of process groups and models */
|
||||||
|
const flattenAllItems = (items: ProcessGroup[], flat: ProcessGroup[]) => {
|
||||||
|
items.forEach((item) => {
|
||||||
|
flat.push(item);
|
||||||
|
// Duck type to see if it's a group, and if so, recurse.
|
||||||
|
if (item.process_groups) {
|
||||||
|
flattenAllItems(
|
||||||
|
[...item.process_groups, ...(item.process_models || [])],
|
||||||
|
flat,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return flat;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If no favorites, proceed with the normal process groups.
|
||||||
|
if (processGroups) {
|
||||||
|
/**
|
||||||
|
* Do this now and put it in state.
|
||||||
|
* You do not want to do this on every change to the search filter.
|
||||||
|
* The flattened map makes searching globally simple.
|
||||||
|
*/
|
||||||
|
const flattened = flattenAllItems(processGroups || [], []);
|
||||||
|
setFlatItems(flattened);
|
||||||
|
|
||||||
|
// If there are favorites, that's all we want to display, return.
|
||||||
|
const favorites = JSON.parse(getStorageValue(SPIFF_FAVORITES));
|
||||||
|
if (favorites.length) {
|
||||||
|
setModels(flattened.filter((item) => favorites.includes(item.id)));
|
||||||
|
setGroups([]);
|
||||||
|
setModelsExpanded(true);
|
||||||
|
setGroupsExpanded(false);
|
||||||
|
setCrumbs([favoriteCrumb]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setGroups(processGroups);
|
||||||
|
setGroupsExpanded(!!processGroups.length);
|
||||||
|
setCrumbs([]);
|
||||||
|
if (setNavElementCallback) {
|
||||||
|
setNavElementCallback(
|
||||||
|
<TreePanel
|
||||||
|
ref={treeRef}
|
||||||
|
processGroups={processGroups}
|
||||||
|
stream={clickStream}
|
||||||
|
// callback={() => handleFavorites({ text: SHOW_FAVORITES })}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [processGroups]);
|
||||||
|
|
||||||
|
let cardStreamSub: Subscription;
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cardStreamSub && clickStream) {
|
||||||
|
clickStream.subscribe(handleClickStream);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [clickStream]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography variant="h1" sx={{ mb: 2 }}>
|
||||||
|
{processModelAction === ProcessModelAction.StartProcess
|
||||||
|
? 'Start new process'
|
||||||
|
: 'Process Groups'}
|
||||||
|
</Typography>
|
||||||
|
<Container
|
||||||
|
maxWidth={false}
|
||||||
|
sx={{
|
||||||
|
padding: '0px !important',
|
||||||
|
overflow: 'hidden',
|
||||||
|
height: '100vh',
|
||||||
|
}}
|
||||||
|
id="list-container"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
gap={2}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
paddingTop: 2,
|
||||||
|
paddingLeft: 2,
|
||||||
|
paddingRight: 2,
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SearchBar callback={handleSearch} stream={clickStream} />
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
overflowY: 'auto',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
paddingLeft: 2,
|
||||||
|
paddingRight: 2,
|
||||||
|
}}
|
||||||
|
id="scrollable-process-card-area"
|
||||||
|
>
|
||||||
|
<Stack direction="row" gap={1} sx={{ paddingBottom: 0.5 }}>
|
||||||
|
{treeCollapsed && (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
gap={1}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'borders.primary',
|
||||||
|
borderRadius: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingRight: 2,
|
||||||
|
position: 'relative',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={() => handleFavorites({ text: SHOW_FAVORITES })}
|
||||||
|
>
|
||||||
|
<StarRateIcon
|
||||||
|
sx={{
|
||||||
|
transform: 'scale(.8)',
|
||||||
|
color: 'spotColors.goldStar',
|
||||||
|
position: 'relative',
|
||||||
|
top: -1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography variant="caption">Favorites</Typography>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
<SpiffBreadCrumbs crumbs={crumbs} callback={handleCrumbClick} />
|
||||||
|
</Stack>
|
||||||
|
<Accordion
|
||||||
|
expanded={modelsExpanded}
|
||||||
|
onChange={() => setModelsExpanded((prev) => !prev)}
|
||||||
|
>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon />}
|
||||||
|
aria-controls="Process Models Accordion"
|
||||||
|
>
|
||||||
|
({models.length}) Process Models
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Box sx={gridProps}>
|
||||||
|
{models.map((model: Record<string, any>) => (
|
||||||
|
<ProcessModelCard
|
||||||
|
key={model.id}
|
||||||
|
model={model}
|
||||||
|
stream={clickStream}
|
||||||
|
lastSelected={lastSelected}
|
||||||
|
processModelAction={processModelAction}
|
||||||
|
onStartProcess={() => {
|
||||||
|
if (setNavElementCallback) {
|
||||||
|
// remove the TreePanel from the SideNav when starting a process
|
||||||
|
setNavElementCallback(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
<Accordion
|
||||||
|
expanded={groupsExpanded}
|
||||||
|
onChange={() => setGroupsExpanded((prev) => !prev)}
|
||||||
|
>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon />}
|
||||||
|
aria-controls="Process Groups Accordion"
|
||||||
|
>
|
||||||
|
({groups?.length}) Process Groups
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Box sx={gridProps}>
|
||||||
|
{groups?.map((group: Record<string, any>) => (
|
||||||
|
<ProcessGroupCard
|
||||||
|
key={group.id}
|
||||||
|
group={group}
|
||||||
|
stream={clickStream}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
import ProcessModelTreePage from './ProcessModelTreePage';
|
||||||
|
import { ProcessModelAction } from '../../interfaces';
|
||||||
|
|
||||||
|
export default function Processes() {
|
||||||
|
return <ProcessModelTreePage processModelAction={ProcessModelAction.Open} />;
|
||||||
|
}
|
@ -1,29 +1,5 @@
|
|||||||
import {
|
import ProcessModelTreePage from './ProcessModelTreePage';
|
||||||
Accordion,
|
import { ProcessModelAction } from '../../interfaces';
|
||||||
AccordionDetails,
|
|
||||||
AccordionSummary,
|
|
||||||
Box,
|
|
||||||
Container,
|
|
||||||
Stack,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import { Subject, Subscription } from 'rxjs';
|
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
||||||
import StarRateIcon from '@mui/icons-material/StarRate';
|
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
|
||||||
import useProcessGroups from '../../hooks/useProcessGroups';
|
|
||||||
import TreePanel, { TreeRef, SHOW_FAVORITES } from './TreePanel';
|
|
||||||
import SearchBar from './SearchBar';
|
|
||||||
import ProcessGroupCard from './ProcessGroupCard';
|
|
||||||
import ProcessModelCard from './ProcessModelCard';
|
|
||||||
import {
|
|
||||||
SPIFF_FAVORITES,
|
|
||||||
getStorageValue,
|
|
||||||
} from '../../services/LocalStorageService';
|
|
||||||
import SpiffBreadCrumbs, { Crumb, SPIFF_ID } from './SpiffBreadCrumbs';
|
|
||||||
import { modifyProcessIdentifierForPathParam } from '../../../helpers';
|
|
||||||
import { ProcessGroup, ProcessGroupLite } from '../../../interfaces';
|
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
setNavElementCallback: Function;
|
setNavElementCallback: Function;
|
||||||
@ -33,372 +9,10 @@ type OwnProps = {
|
|||||||
* feeds various streams, data and callbacks to children.
|
* feeds various streams, data and callbacks to children.
|
||||||
*/
|
*/
|
||||||
export default function StartProcess({ setNavElementCallback }: OwnProps) {
|
export default function StartProcess({ setNavElementCallback }: OwnProps) {
|
||||||
const { processGroups } = useProcessGroups({
|
|
||||||
processInfo: {},
|
|
||||||
getRunnableProcessModels: true,
|
|
||||||
});
|
|
||||||
const [groups, setGroups] = useState<
|
|
||||||
ProcessGroup[] | ProcessGroupLite[] | null
|
|
||||||
>(null);
|
|
||||||
const [models, setModels] = useState<Record<string, any>[]>([]);
|
|
||||||
const [flatItems, setFlatItems] = useState<Record<string, any>>([]);
|
|
||||||
// On load, there are always groups and never models, expand accordingly.
|
|
||||||
const [groupsExpanded, setGroupsExpanded] = useState(true);
|
|
||||||
const [modelsExpanded, setModelsExpanded] = useState(false);
|
|
||||||
const [lastSelected, setLastSelected] = useState<Record<string, any>>({});
|
|
||||||
const [crumbs, setCrumbs] = useState<Crumb[]>([]);
|
|
||||||
// const [treeCollapsed, setTreeCollapsed] = useState(false);
|
|
||||||
const [treeCollapsed] = useState(false);
|
|
||||||
const treeRef = useRef<TreeRef>(null);
|
|
||||||
// Pass to anything that wants to broadcast to all subscribers
|
|
||||||
const clickStream = new Subject<Record<string, any>>();
|
|
||||||
const favoriteCrumb: Crumb = { id: 'favorites', displayName: 'Favorites' };
|
|
||||||
const gridProps = {
|
|
||||||
width: '100%',
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: 'repeat(auto-fill, 400px)',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gridGap: 20,
|
|
||||||
};
|
|
||||||
|
|
||||||
const processCrumbs = (item: Record<string, any>): Crumb[] => {
|
|
||||||
/**
|
|
||||||
* Create the paths for the crumbs.
|
|
||||||
* This logic is repeated from TreePanel.
|
|
||||||
* TODO: Consider a service, or hook, or something.
|
|
||||||
*/
|
|
||||||
const pathIds: string[] = [];
|
|
||||||
const split: string[] = item.id.split('/');
|
|
||||||
split.forEach((id, i) => {
|
|
||||||
pathIds.push(i === 0 ? id : `${pathIds[i - 1]}/${id}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Look up the display names in the flattened items and return.
|
|
||||||
return pathIds.map((id) => {
|
|
||||||
const found = flatItems.find((flatItem: any) => flatItem.id === id);
|
|
||||||
return { id, displayName: found?.display_name || id };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClickStream = (item: Record<string, any>) => {
|
|
||||||
setCrumbs(processCrumbs(item));
|
|
||||||
setLastSelected(item);
|
|
||||||
let itemToUse: any = { ...item };
|
|
||||||
// Duck type to find out if this is a model ore a group.
|
|
||||||
// If model, we want its parent group, which can be found in the id.
|
|
||||||
if (!('process_groups' in item)) {
|
|
||||||
// recursively find the parent group.
|
|
||||||
const findParent = (
|
|
||||||
searchGroups: Record<string, any>[],
|
|
||||||
id: string,
|
|
||||||
): Record<string, any> | undefined => {
|
|
||||||
return searchGroups.find((group) => {
|
|
||||||
if (group.id === id) {
|
|
||||||
itemToUse = group;
|
|
||||||
return group;
|
|
||||||
}
|
|
||||||
if (group.process_groups) {
|
|
||||||
return findParent(group.process_groups, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
// Remove the last part of the id, which is not and expandable entity.
|
|
||||||
const parentId = item.id.split('/').slice(0, -1).join('/');
|
|
||||||
findParent(processGroups || [], parentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemToUse?.process_models) {
|
|
||||||
// If a user clicks a group, and it has models, expand them for the user.
|
|
||||||
if (itemToUse.process_models.length) {
|
|
||||||
setModelsExpanded(true);
|
|
||||||
}
|
|
||||||
setModels(itemToUse.process_models);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are groups in this group, set them into state for display
|
|
||||||
if (itemToUse?.process_groups) {
|
|
||||||
setGroups(itemToUse.process_groups);
|
|
||||||
}
|
|
||||||
|
|
||||||
const container = document.getElementById('scrollable-process-card-area');
|
|
||||||
const cardElement = document.getElementById(
|
|
||||||
`card-${modifyProcessIdentifierForPathParam(item.id)}`,
|
|
||||||
);
|
|
||||||
if (container && cardElement) {
|
|
||||||
setTimeout(() => {
|
|
||||||
container.scrollTo({
|
|
||||||
top: cardElement.offsetTop - container.offsetTop,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
}, 100); // Slight delay before scrolling to ensure rendering
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Tree calls back here so we can imperatively rework groups etc. */
|
|
||||||
const handleFavorites = ({ text }: { text: string }) => {
|
|
||||||
if (text === SHOW_FAVORITES) {
|
|
||||||
const storage = JSON.parse(getStorageValue('spifffavorites'));
|
|
||||||
const favs = flatItems.filter((item: any) => storage.includes(item.id));
|
|
||||||
// If there's no favorites, the user is just left looking at nothing.
|
|
||||||
// Load the top level groups instead.
|
|
||||||
// Expand accordions accordingly (haha).
|
|
||||||
setGroups(favs.length ? [] : processGroups);
|
|
||||||
setModels(favs);
|
|
||||||
setModelsExpanded(!!favs.length);
|
|
||||||
setGroupsExpanded(!favs.length);
|
|
||||||
setCrumbs([favoriteCrumb]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For now, we're just pasting together some info fields that make sense.
|
|
||||||
* This is simple and works and is easily expanded,
|
|
||||||
* but eventually might need to be more robust.
|
|
||||||
*/
|
|
||||||
const handleSearch = useDebouncedCallback((search: string) => {
|
|
||||||
// Indicate to user this is a search result.
|
|
||||||
setCrumbs([
|
|
||||||
{ id: search, displayName: `Searching for: ${search || '(all)'}` },
|
|
||||||
]);
|
|
||||||
// Search the flattened items for the search term.
|
|
||||||
const foundGroups = flatItems.filter((item: any) => {
|
|
||||||
return (
|
|
||||||
item.id.toLowerCase().includes(search.toLowerCase()) &&
|
|
||||||
item?.process_groups
|
|
||||||
);
|
|
||||||
});
|
|
||||||
const foundModels = flatItems.filter((item: any) => {
|
|
||||||
return (
|
|
||||||
(item.id + item.display_name + item.description)
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(search.toLowerCase()) && !item?.process_groups
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
setGroups(foundGroups);
|
|
||||||
setGroupsExpanded(!!foundGroups.length);
|
|
||||||
setModels(foundModels);
|
|
||||||
setModelsExpanded(!!foundModels.length);
|
|
||||||
// The tree isn't connected to these results, so imperatively wipe the expanded nodes.
|
|
||||||
treeRef.current?.clearExpanded();
|
|
||||||
}, 250);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When a crumb is clicked, we need to find the item in the flatItems list
|
|
||||||
* and feed it to the clickstream.
|
|
||||||
*/
|
|
||||||
const handleCrumbClick = (crumb: Crumb) => {
|
|
||||||
// If this is s top-level crumb, just reset the view.
|
|
||||||
if (crumb.id === SPIFF_ID) {
|
|
||||||
setGroups(processGroups);
|
|
||||||
setModels([]);
|
|
||||||
setModelsExpanded(false);
|
|
||||||
setGroupsExpanded(true);
|
|
||||||
setCrumbs([]);
|
|
||||||
treeRef.current?.clearExpanded();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Otherwise, find the item in the flatItems list and feed it to the clickstream.
|
|
||||||
const found = flatItems.find((item: any) => item.id === crumb.id);
|
|
||||||
if (found) {
|
|
||||||
clickStream.next(found);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Recursively flatten the entire hierarchy of process groups and models */
|
|
||||||
const flattenAllItems = (items: ProcessGroup[], flat: ProcessGroup[]) => {
|
|
||||||
items.forEach((item) => {
|
|
||||||
flat.push(item);
|
|
||||||
// Duck type to see if it's a group, and if so, recurse.
|
|
||||||
if (item.process_groups) {
|
|
||||||
flattenAllItems(
|
|
||||||
[...item.process_groups, ...(item.process_models || [])],
|
|
||||||
flat,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return flat;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// If no favorites, proceed with the normal process groups.
|
|
||||||
if (processGroups) {
|
|
||||||
/**
|
|
||||||
* Do this now and put it in state.
|
|
||||||
* You do not want to do this on every change to the search filter.
|
|
||||||
* The flattened map makes searching globally simple.
|
|
||||||
*/
|
|
||||||
const flattened = flattenAllItems(processGroups || [], []);
|
|
||||||
setFlatItems(flattened);
|
|
||||||
|
|
||||||
// If there are favorites, that's all we want to display, return.
|
|
||||||
const favorites = JSON.parse(getStorageValue(SPIFF_FAVORITES));
|
|
||||||
if (favorites.length) {
|
|
||||||
setModels(flattened.filter((item) => favorites.includes(item.id)));
|
|
||||||
setGroups([]);
|
|
||||||
setModelsExpanded(true);
|
|
||||||
setGroupsExpanded(false);
|
|
||||||
setCrumbs([favoriteCrumb]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setGroups(processGroups);
|
|
||||||
setGroupsExpanded(!!processGroups.length);
|
|
||||||
setCrumbs([]);
|
|
||||||
if (setNavElementCallback) {
|
|
||||||
setNavElementCallback(
|
|
||||||
<TreePanel
|
|
||||||
ref={treeRef}
|
|
||||||
processGroups={processGroups}
|
|
||||||
stream={clickStream}
|
|
||||||
// callback={() => handleFavorites({ text: SHOW_FAVORITES })}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [processGroups]);
|
|
||||||
|
|
||||||
let cardStreamSub: Subscription;
|
|
||||||
useEffect(() => {
|
|
||||||
if (!cardStreamSub && clickStream) {
|
|
||||||
clickStream.subscribe(handleClickStream);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [clickStream]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ProcessModelTreePage
|
||||||
<Typography variant="h1" sx={{ mb: 2 }}>
|
setNavElementCallback={setNavElementCallback}
|
||||||
Start new process
|
processModelAction={ProcessModelAction.StartProcess}
|
||||||
</Typography>
|
/>
|
||||||
<Container
|
|
||||||
maxWidth={false}
|
|
||||||
sx={{
|
|
||||||
padding: '0px !important',
|
|
||||||
overflow: 'hidden',
|
|
||||||
height: '100vh',
|
|
||||||
}}
|
|
||||||
id="list-container"
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
gap={2}
|
|
||||||
sx={{
|
|
||||||
width: '100%',
|
|
||||||
height: '100vh',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
sx={{
|
|
||||||
width: '100%',
|
|
||||||
paddingTop: 2,
|
|
||||||
paddingLeft: 2,
|
|
||||||
paddingRight: 2,
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SearchBar callback={handleSearch} stream={clickStream} />
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Stack
|
|
||||||
sx={{
|
|
||||||
width: '100%',
|
|
||||||
overflowY: 'auto',
|
|
||||||
overflowX: 'hidden',
|
|
||||||
paddingLeft: 2,
|
|
||||||
paddingRight: 2,
|
|
||||||
}}
|
|
||||||
id="scrollable-process-card-area"
|
|
||||||
>
|
|
||||||
<Stack direction="row" gap={1} sx={{ paddingBottom: 0.5 }}>
|
|
||||||
{treeCollapsed && (
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
gap={1}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: 'background.paper',
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: 'borders.primary',
|
|
||||||
borderRadius: 1,
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingRight: 2,
|
|
||||||
position: 'relative',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
onClick={() => handleFavorites({ text: SHOW_FAVORITES })}
|
|
||||||
>
|
|
||||||
<StarRateIcon
|
|
||||||
sx={{
|
|
||||||
transform: 'scale(.8)',
|
|
||||||
color: 'spotColors.goldStar',
|
|
||||||
position: 'relative',
|
|
||||||
top: -1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Typography variant="caption">Favorites</Typography>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
<SpiffBreadCrumbs crumbs={crumbs} callback={handleCrumbClick} />
|
|
||||||
</Stack>
|
|
||||||
<Accordion
|
|
||||||
expanded={modelsExpanded}
|
|
||||||
onChange={() => setModelsExpanded((prev) => !prev)}
|
|
||||||
>
|
|
||||||
<AccordionSummary
|
|
||||||
expandIcon={<ExpandMoreIcon />}
|
|
||||||
aria-controls="Process Models Accordion"
|
|
||||||
>
|
|
||||||
({models.length}) Process Models
|
|
||||||
</AccordionSummary>
|
|
||||||
<AccordionDetails>
|
|
||||||
<Box sx={gridProps}>
|
|
||||||
{models.map((model: Record<string, any>) => (
|
|
||||||
<ProcessModelCard
|
|
||||||
key={model.id}
|
|
||||||
model={model}
|
|
||||||
stream={clickStream}
|
|
||||||
lastSelected={lastSelected}
|
|
||||||
onStartProcess={() => {
|
|
||||||
// remove the TreePanel from the SideNav when starting a process
|
|
||||||
setNavElementCallback(null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
<Accordion
|
|
||||||
expanded={groupsExpanded}
|
|
||||||
onChange={() => setGroupsExpanded((prev) => !prev)}
|
|
||||||
>
|
|
||||||
<AccordionSummary
|
|
||||||
expandIcon={<ExpandMoreIcon />}
|
|
||||||
aria-controls="Process Groups Accordion"
|
|
||||||
>
|
|
||||||
({groups?.length}) Process Groups
|
|
||||||
</AccordionSummary>
|
|
||||||
<AccordionDetails>
|
|
||||||
<Box sx={gridProps}>
|
|
||||||
{groups?.map((group: Record<string, any>) => (
|
|
||||||
<ProcessGroupCard
|
|
||||||
key={group.id}
|
|
||||||
group={group}
|
|
||||||
stream={clickStream}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import { createSpiffTheme } from '../a-spiffui-v3/assets/theme/SpiffTheme';
|
|||||||
import Homepage from '../a-spiffui-v3/views/Homepage';
|
import Homepage from '../a-spiffui-v3/views/Homepage';
|
||||||
import '../a-spiffui-v3/assets/styles/transitions.css';
|
import '../a-spiffui-v3/assets/styles/transitions.css';
|
||||||
import StartProcess from '../a-spiffui-v3/views/StartProcess/StartProcess';
|
import StartProcess from '../a-spiffui-v3/views/StartProcess/StartProcess';
|
||||||
|
import Processes from '../a-spiffui-v3/views/StartProcess/Processes';
|
||||||
import StartProcessInstance from '../a-spiffui-v3/views/StartProcess/StartProcessInstance';
|
import StartProcessInstance from '../a-spiffui-v3/views/StartProcess/StartProcessInstance';
|
||||||
import SideNav from '../a-spiffui-v3/components/SideNav';
|
import SideNav from '../a-spiffui-v3/components/SideNav';
|
||||||
import LoginHandler from '../components/LoginHandler';
|
import LoginHandler from '../components/LoginHandler';
|
||||||
@ -229,6 +230,7 @@ export default function SpiffUIV3() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route path="/processes" element={<Processes />} />
|
||||||
<Route
|
<Route
|
||||||
path="/:modifiedProcessModelId/start"
|
path="/:modifiedProcessModelId/start"
|
||||||
element={<StartProcessInstance />}
|
element={<StartProcessInstance />}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user