Skip to content

Commit

Permalink
Added a route for history cards view.
Browse files Browse the repository at this point in the history
  • Loading branch information
vagarenko authored and codablock committed Jun 22, 2023
1 parent ffb80e9 commit 791de20
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 76 deletions.
2 changes: 1 addition & 1 deletion pkg/webui/ui/src/components/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const Router = createHashRouter([
errorElement: <ErrorPage />,
children: [
{
path: "targets",
path: "targets/:targetKeyHash?",
element: <TargetsView/>,
errorElement: <ErrorPage/>,
},
Expand Down
7 changes: 4 additions & 3 deletions pkg/webui/ui/src/components/targets-view/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from "react";
import { Box, BoxProps, Paper, PaperProps } from "@mui/material"

export const cardWidth = 247;
Expand All @@ -21,9 +22,9 @@ export function CardPaper(props: PaperProps) {
/>
}

export function Card(props: BoxProps) {
return <Box display='flex' flexShrink={0} width={cardWidth} height={cardHeight} {...props} />
}
export const Card = React.forwardRef((props: BoxProps, ref) => {
return <Box display='flex' flexShrink={0} width={cardWidth} height={cardHeight} {...props} ref={ref} />
});

export function CardCol(props: BoxProps) {
return <Box display='flex' flexDirection='column' gap={`${cardGap}px`} {...props} />
Expand Down
99 changes: 54 additions & 45 deletions pkg/webui/ui/src/components/targets-view/HistoryCards.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import React, { useCallback, useContext, useEffect, useRef, useState } from "react";
import { Box, Divider, IconButton, SxProps, Tab, Tooltip, useTheme } from "@mui/material";
import { CommandResultSummary } from "../../models";
import { TargetSummary } from "../../project-summaries";
Expand Down Expand Up @@ -26,9 +26,8 @@ async function doGetRootNode(api: Api, rs: CommandResultSummary) {
}

export interface HistoryCardsProps {
rs: CommandResultSummary,
ts: TargetSummary,
initialCardRect: DOMRect,
targetSummary: TargetSummary,
initialCardRect?: DOMRect,
onClose: () => void
}

Expand All @@ -39,8 +38,6 @@ interface Rect {
height: number | string
}

type TransitionStatus = 'not-started' | 'running' | 'finished'

const CardContent = React.memo((props: { provider: SidePanelProvider }) => {
const { tabs, selectedTab, handleTabChange } = useSidePanelTabs(props.provider)

Expand Down Expand Up @@ -177,17 +174,34 @@ const HistoryCard = React.memo((props: {
</CardPaper>
});

type TransitionState =
{
type: 'initial'
} | {
type: 'started',
cardRect: Rect
} | {
type: 'running',
cardRect: Rect
} | {
type: 'finished'
}

export const HistoryCards = React.memo((props: HistoryCardsProps) => {
const theme = useTheme();
const containerElem = useRef<HTMLElement>();
const [cardRect, setCardRect] = useState<Rect | undefined>();
const [transitionStatus, setTransitionStatus] = useState<TransitionStatus>('not-started');
const [currentRS, setCurrentRS] = useState(props.rs);
const [transitionState, setTransitionState] = useState<TransitionState>({ type: 'initial' });
const [currentRSIndex, setCurrentRSIndex] = useState(0);

useEffect(() => {
const rect = containerElem.current?.getBoundingClientRect();
if (!rect) {
setCardRect(undefined);
setTransitionState({ type: 'initial' });
return;
}

if (!props.initialCardRect) {
setTransitionState({ type: 'finished' });
return;
}

Expand All @@ -198,11 +212,14 @@ export const HistoryCards = React.memo((props: HistoryCardsProps) => {
height: cardHeight
};

setCardRect(initialRect);
}, [props.initialCardRect]);
setTransitionState({
type: 'started',
cardRect: initialRect
});
}, [props.initialCardRect, theme.transitions.duration.enteringScreen]);

useEffect(() => {
if (!cardRect) {
if (transitionState.type !== 'started') {
return;
}

Expand All @@ -213,39 +230,28 @@ export const HistoryCards = React.memo((props: HistoryCardsProps) => {
height: '100%'
};

if (cardRect.left === targetRect.left
&& cardRect.top === targetRect.top
&& cardRect.width === targetRect.width
&& cardRect.height === targetRect.height
) {
return;
}

setTimeout(() => {
setCardRect(targetRect);
setTransitionStatus('running');
setTransitionState({
type: 'running',
cardRect: targetRect
});
setTimeout(() => {
setTransitionStatus('finished');
setTransitionState({ type: 'finished' });
}, theme.transitions.duration.enteringScreen);
}, 10);
}, [cardRect, theme.transitions.duration.enteringScreen]);

const currentRSIndex = useMemo(
() => props.ts.commandResults.indexOf(currentRS),
[currentRS, props.ts.commandResults]
)
}, [transitionState, theme.transitions.duration.enteringScreen]);

const onLeftArrowClick = useCallback(() => {
if (currentRSIndex > 0) {
setCurrentRS(props.ts.commandResults[currentRSIndex - 1]);
setCurrentRSIndex(i => i - 1);
}
}, [currentRSIndex, props.ts.commandResults]);
}, [currentRSIndex]);

const onRightArrowClick = useCallback(() => {
if (currentRSIndex < props.ts.commandResults.length - 1) {
setCurrentRS(props.ts.commandResults[currentRSIndex + 1]);
if (currentRSIndex < props.targetSummary.commandResults.length - 1) {
setCurrentRSIndex(i => i + 1);
}
}, [currentRSIndex, props.ts.commandResults]);
}, [currentRSIndex, props.targetSummary.commandResults.length]);

const paddingX = 40;
const gap = 2 * (paddingX + arrowButtonWidth);
Expand All @@ -261,30 +267,30 @@ export const HistoryCards = React.memo((props: HistoryCardsProps) => {
<ArrowButton
direction='left'
onClick={onLeftArrowClick}
hidden={currentRSIndex === 0 || transitionStatus !== 'finished'}
hidden={currentRSIndex === 0 || transitionState.type !== 'finished'}
/>
<Box
flex='0 0 auto'
width={`calc(100% - ${arrowButtonWidth}px * 2)`}
display='flex'
ref={containerElem}
>
{transitionStatus !== 'finished' && cardRect && <HistoryCard
{(transitionState.type === 'started' || transitionState.type === 'running') && <HistoryCard
sx={{
width: cardRect.width,
height: cardRect.height,
width: transitionState.cardRect.width,
height: transitionState.cardRect.height,
flex: '0 0 auto',
translate: `${cardRect.left}px ${cardRect.top}px`,
translate: `${transitionState.cardRect.left}px ${transitionState.cardRect.top}px`,
transition: theme.transitions.create(['translate', 'width', 'height'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
padding: '20px 0'
}}
rs={currentRS}
rs={props.targetSummary.commandResults[currentRSIndex]}
onClose={props.onClose}
/>}
{transitionStatus === 'finished' &&
{transitionState.type === 'finished' &&
<Box
flex='1 1 auto'
width='100%'
Expand All @@ -299,7 +305,7 @@ export const HistoryCards = React.memo((props: HistoryCardsProps) => {
})
}}
>
{props.ts.commandResults.map((rs) =>
{props.targetSummary.commandResults.map((rs) =>
<HistoryCard
sx={{
width: '100%',
Expand All @@ -309,7 +315,7 @@ export const HistoryCards = React.memo((props: HistoryCardsProps) => {
}}
rs={rs}
key={rs.id}
transitionFinished={transitionStatus === 'finished'}
transitionFinished={transitionState.type === 'finished'}
onClose={props.onClose}
/>
)}
Expand All @@ -319,7 +325,10 @@ export const HistoryCards = React.memo((props: HistoryCardsProps) => {
<ArrowButton
direction='right'
onClick={onRightArrowClick}
hidden={currentRSIndex === props.ts.commandResults.length - 1 || transitionStatus !== 'finished'}
hidden={
currentRSIndex === props.targetSummary.commandResults.length - 1
|| transitionState.type !== 'finished'
}
/>
</Box>;
});
81 changes: 54 additions & 27 deletions pkg/webui/ui/src/components/targets-view/TargetsView.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { CommandResultSummary } from "../../models";
import { TargetKey } from "../../models";
import { Box, Typography, useTheme } from "@mui/material";
import React, { useCallback, useContext, useState } from "react";
import React, { useCallback, useContext, useMemo, useRef, useState } from "react";
import { AppContext } from "../App";
import { ProjectItem } from "./Projects";
import { TargetItem } from "./Targets";
import Divider from "@mui/material/Divider";
import { CommandResultItem } from "./CommandResultItem";
import { TargetDetailsDrawer } from "./TargetDetailsDrawer";
import { Card, CardCol, cardGap, cardHeight, CardRow, cardWidth, projectCardHeight } from "./Card";
import { Card, CardCol, cardGap, cardHeight, CardPaper, CardRow, cardWidth, projectCardHeight } from "./Card";
import { TargetSummary } from "../../project-summaries";
import { buildListKey } from "../../utils/listKey";
import { HistoryCards } from "./HistoryCards";
import { useNavigate, useParams } from "react-router-dom";
import { sha256 } from "js-sha256";

const colWidth = 416;
const curveRadius = 12;
Expand Down Expand Up @@ -122,34 +124,55 @@ const RelationTree = React.memo(({ targetCount }: { targetCount: number }): JSX.
</svg>
});

function mkTargetHash(tk: TargetKey): string {
return sha256(JSON.stringify(tk));
}

export const TargetsView = () => {
const theme = useTheme();
const [selectedCommandResult, setSelectedCommandResult] = useState<{ rs: CommandResultSummary, ts: TargetSummary } | undefined>();
const [selectedTargetSummary, setSelectedTargetSummary] = useState<TargetSummary | undefined>();
const [selectedCardRect, setSelectedCardRect] = useState<DOMRect | undefined>();

const appContext = useContext(AppContext)
const projects = appContext.projects
const navigate = useNavigate();
const { targetKeyHash } = useParams();
const appContext = useContext(AppContext);
const projects = appContext.projects;

const targetsHashes = useMemo(() => {
const dict = new Map<string, TargetSummary>();
projects.forEach(ps =>
ps.targets.forEach(ts =>
dict.set(mkTargetHash(ts.target), ts)
)
);
return dict;
}, [projects]);

const historyCardsTarget = useMemo(() => {
return targetKeyHash ? targetsHashes.get(targetKeyHash) : undefined;
}, [targetKeyHash, targetsHashes]);

const onTargetDetailsDrawerClose = useCallback(() => {
setSelectedTargetSummary(undefined);
}, []);

const onSelectCommandResult = useCallback((o?: { rs: CommandResultSummary, ts: TargetSummary }) => {
const onSelectHistoryCardsTarget = useCallback((ts: TargetSummary) => {
onTargetDetailsDrawerClose();
setSelectedCommandResult(o);
}, [onTargetDetailsDrawerClose]);
navigate(`/targets/${mkTargetHash(ts.target)}`);
}, [navigate, onTargetDetailsDrawerClose]);

const onHistoryCardsClose = useCallback(() => {
setSelectedCommandResult(undefined);
setSelectedCardRect(undefined);
}, []);
navigate(`/targets/`);
}, [navigate]);

const cardRefs = useRef<Map<TargetSummary, HTMLElement>>(new Map());

if (historyCardsTarget) {
const cardElem = cardRefs.current.get(historyCardsTarget);
const cardRect = cardElem?.getBoundingClientRect();

if (selectedCommandResult && selectedCardRect) {
return <HistoryCards
rs={selectedCommandResult.rs}
ts={selectedCommandResult.ts}
initialCardRect={selectedCardRect}
targetSummary={historyCardsTarget}
initialCardRect={cardRect}
onClose={onHistoryCardsClose}
/>;
}
Expand Down Expand Up @@ -240,21 +263,25 @@ export const TargetsView = () => {
return <Card
key={rs.id}
sx={{
translate: i === 0 ? 'none' : `-${i * (cardWidth + cardGap / 2)}px`,
translate: `${-i * (cardWidth + cardGap / 2)}px`,
zIndex: -i,
display: i < 4 ? 'flex' : 'none'
}}
onClick={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
setSelectedCardRect(rect);
ref={(r: HTMLElement) => {
if (i === 0) {
cardRefs.current.set(ts, r);
}
}}
>
<CommandResultItem
ps={ps}
ts={ts}
rs={rs}
onSelectCommandResult={(rs) => onSelectCommandResult({ rs, ts })}
/>
{i === 0
? <CommandResultItem
ps={ps}
ts={ts}
rs={rs}
onSelectCommandResult={() => onSelectHistoryCardsTarget(ts)}
/>
: <CardPaper />
}
</Card>
})}
</CardRow>
Expand Down

0 comments on commit 791de20

Please sign in to comment.