diff --git a/src/components/BuildList.tsx b/src/components/BuildList.tsx index 2cdd8ecd..3281d4c6 100644 --- a/src/components/BuildList.tsx +++ b/src/components/BuildList.tsx @@ -21,6 +21,7 @@ import { selectBuild, } from "../contexts"; import { BuildStatusChip } from "./BuildStatusChip"; +import { SkeletonList } from "./SkeletonList"; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -38,67 +39,73 @@ const useStyles = makeStyles((theme: Theme) => const BuildList: FunctionComponent = () => { const classes = useStyles(); const history = useHistory(); - const { buildList, selectedBuildId } = useBuildState(); + const { buildList, selectedBuildId, loading } = useBuildState(); const buildDispatch = useBuildDispatch(); return ( - {buildList.map((build) => ( - { - selectBuild(buildDispatch, build.id); - history.push({ - search: "buildId=" + build.id, - }); - }} - classes={{ - container: classes.listItem, - }} - > - - - {`#${build.id}`} - - - } - secondary={ - - - - {build.createdAt} - + {loading ? ( + + ) : ( + buildList.map((build) => ( + { + selectBuild(buildDispatch, build.id); + history.push({ + search: "buildId=" + build.id, + }); + }} + classes={{ + container: classes.listItem, + }} + > + + + {`#${build.id}`} + - - - - - - - + } + secondary={ + + + + {build.createdAt} + + + + + + + + + + - - } - /> + } + /> - - { - deleteBuild(buildDispatch, build.id); - }} + - - - - - ))} + { + deleteBuild(buildDispatch, build.id); + }} + > + + + + + )) + )} ); }; diff --git a/src/components/DrawArea.tsx b/src/components/DrawArea.tsx index 26fcf512..c0d82ba6 100644 --- a/src/components/DrawArea.tsx +++ b/src/components/DrawArea.tsx @@ -4,9 +4,34 @@ import { RectConfig } from "konva/types/shapes/Rect"; import Rectangle, { MIN_RECT_SIDE_PIXEL } from "./Rectangle"; import { KonvaEventObject } from "konva/types/Node"; import { IgnoreArea } from "../types/ignoreArea"; +import { Grid, makeStyles, CircularProgress } from "@material-ui/core"; +import useImage from "use-image"; +import { staticService } from "../services"; +import { NoImagePlaceholder } from "./NoImageAvailable"; +import ImageDetails from "./ImageDetails"; + +const useStyles = makeStyles((theme) => ({ + imageContainer: { + overflow: "hidden", + }, + canvasBackground: { + width: "100%", + backgroundColor: "#f5f5f5", + }, + canvasContainer: { + overflow: "hidden", + backgroundColor: "white", + padding: theme.spacing(1), + margin: theme.spacing(0.5), + }, + progressContainer: { + minHeight: "300px", + }, +})); interface IDrawArea { - image: HTMLImageElement | undefined; + type: "Baseline" | "Image" | "Diff"; + imageName: string; ignoreAreas: IgnoreArea[]; setIgnoreAreas: (ignoreAreas: IgnoreArea[]) => void; selectedRectId: string | undefined; @@ -27,8 +52,9 @@ interface IDrawArea { stageScaleState: [number, React.Dispatch>]; drawModeState: [boolean, React.Dispatch>]; } -const DrawArea: FunctionComponent = ({ - image, +export const DrawArea: FunctionComponent = ({ + type, + imageName, ignoreAreas, setIgnoreAreas, selectedRectId, @@ -40,6 +66,7 @@ const DrawArea: FunctionComponent = ({ stagePosState, drawModeState, }) => { + const classes = useStyles(); const [stageInitPos, setStageInitPos] = stageInitPosState; const [stageOffset, setStageOffset] = stageOffsetState; const [stagePos, setStagePos] = stagePosState; @@ -48,6 +75,7 @@ const DrawArea: FunctionComponent = ({ const [isDrawMode, setIsDrawMode] = drawModeState; const [isDrawing, setIsDrawing] = React.useState(isDrawMode); + const [image, loading] = useImage(staticService.getImage(imageName)); const handleContentMousedown = (e: any) => { if (!isDrawMode) return; @@ -93,97 +121,131 @@ const DrawArea: FunctionComponent = ({ }; return ( -
{ - if (!isDrawMode && isDrag && !selectedRectId) { - event.preventDefault(); - setStagePos({ - x: event.clientX - stageInitPos.x, - y: event.clientY - stageInitPos.y, - }); - setStageOffset(stagePos); - } - }} - onMouseUp={(event) => { - setIsDrag(false); - setStageInitPos(stagePos); - }} - onMouseLeave={(event) => { - setIsDrag(false); - setStageInitPos(stagePos); - }} - onMouseDown={(event) => { - setIsDrag(true); - setStageInitPos({ - x: event.clientX - stageOffset.x, - y: event.clientY - stageOffset.y, - }); - }} - > - - - { - document.body.style.cursor = isDrawMode ? "crosshair" : "grab"; - }} - onMouseDown={(event) => { - document.body.style.cursor = "grabbing"; - }} - onMouseUp={(event) => { - document.body.style.cursor = "grab"; - }} - onMouseLeave={(event) => { - document.body.style.cursor = "default"; - }} - /> - {ignoreAreas.map((rect, i) => { - return ( - + + + + + {imageName ? ( + loading === "loading" ? ( + + + + + + ) : ( + +
setSelectedRectId(rect.id)} - onChange={(newAttrs: RectConfig) => { - const rects = ignoreAreas.slice(); + > +
{ + if (!isDrawMode && isDrag && !selectedRectId) { + event.preventDefault(); + setStagePos({ + x: event.clientX - stageInitPos.x, + y: event.clientY - stageInitPos.y, + }); + setStageOffset(stagePos); + } + }} + onMouseUp={(event) => { + setIsDrag(false); + setStageInitPos(stagePos); + }} + onMouseLeave={(event) => { + setIsDrag(false); + setStageInitPos(stagePos); + }} + onMouseDown={(event) => { + setIsDrag(true); + setStageInitPos({ + x: event.clientX - stageOffset.x, + y: event.clientY - stageOffset.y, + }); + }} + > + + + { + document.body.style.cursor = isDrawMode + ? "crosshair" + : "grab"; + }} + onMouseDown={(event) => { + document.body.style.cursor = "grabbing"; + }} + onMouseUp={(event) => { + document.body.style.cursor = "grab"; + }} + onMouseLeave={(event) => { + document.body.style.cursor = "default"; + }} + /> + {ignoreAreas.map((rect, i) => { + return ( + setSelectedRectId(rect.id)} + onChange={(newAttrs: RectConfig) => { + const rects = ignoreAreas.slice(); - rects[i].x = Math.round(newAttrs.x || 0); - rects[i].y = Math.round(newAttrs.y || 0); - rects[i].width = Math.round( - newAttrs.width || MIN_RECT_SIDE_PIXEL - ); - rects[i].height = Math.round( - newAttrs.height || MIN_RECT_SIDE_PIXEL - ); + rects[i].x = Math.round(newAttrs.x || 0); + rects[i].y = Math.round(newAttrs.y || 0); + rects[i].width = Math.round( + newAttrs.width || MIN_RECT_SIDE_PIXEL + ); + rects[i].height = Math.round( + newAttrs.height || MIN_RECT_SIDE_PIXEL + ); - setIgnoreAreas(rects); - }} - /> - ); - })} - - -
+ setIgnoreAreas(rects); + }} + /> + ); + })} + + +
+
+ + ) + ) : ( + + )} + + ); }; - -export default DrawArea; diff --git a/src/components/SkeletonList.tsx b/src/components/SkeletonList.tsx new file mode 100644 index 00000000..c1617df7 --- /dev/null +++ b/src/components/SkeletonList.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { Box } from "@material-ui/core"; +import { Skeleton } from "@material-ui/lab"; + +export const SkeletonList: React.FunctionComponent = () => { + const list = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + return ( + + {list.map((i) => ( + + + + ))} + + ); +}; diff --git a/src/components/TestDetailsModal.tsx b/src/components/TestDetailsModal.tsx index 904a329e..6ed53d6c 100644 --- a/src/components/TestDetailsModal.tsx +++ b/src/components/TestDetailsModal.tsx @@ -18,9 +18,8 @@ import { testVariationService, staticService, } from "../services"; -import DrawArea from "./DrawArea"; import { TestStatus } from "../types/testStatus"; -import { useHistory, Prompt, Link } from "react-router-dom"; +import { useHistory, Prompt } from "react-router-dom"; import { IgnoreArea } from "../types/ignoreArea"; import { KonvaEventObject } from "konva/types/Node"; import { @@ -33,27 +32,16 @@ import { Fullscreen, FullscreenExit, } from "@material-ui/icons"; -import ImageDetails from "./ImageDetails"; import { TestRunDetails } from "./TestRunDetails"; import useImage from "use-image"; import { routes } from "../constants"; -import { NoImagePlaceholder } from "./NoImageAvailable"; import { useTestRunDispatch, updateTestRun, selectTestRun } from "../contexts"; +import { DrawArea } from "./DrawArea"; const useStyles = makeStyles((theme) => ({ imageContainer: { overflow: "hidden", }, - canvasBackground: { - width: "100%", - backgroundColor: "#f5f5f5", - }, - canvasContainer: { - overflow: "hidden", - backgroundColor: "white", - padding: theme.spacing(1), - margin: theme.spacing(0.5), - }, })); const defaultStagePos = { @@ -76,15 +64,9 @@ const TestDetailsModal: React.FunctionComponent<{ const [stageInitPos, setStageInitPos] = React.useState(defaultStagePos); const [stageOffset, setStageOffset] = React.useState(defaultStagePos); - const [baseline] = useImage( - testRun.baselineName && staticService.getImage(testRun.baselineName) - ); const [image] = useImage( testRun.imageName && staticService.getImage(testRun.imageName) ); - const [diff] = useImage( - testRun.diffName && staticService.getImage(testRun.diffName) - ); const [isDrawMode, setIsDrawMode] = useState(false); const [isDiffShown, setIsDiffShown] = useState(false); @@ -202,12 +184,24 @@ const TestDetailsModal: React.FunctionComponent<{ - + + + + @@ -305,119 +299,52 @@ const TestDetailsModal: React.FunctionComponent<{ - - - - - - - - - - - - {testRun.baselineName ? ( - -
- -
-
- ) : ( - - )} -
+
{isDiffShown ? ( - - - - - {testRun.diffName ? ( - -
- -
-
- ) : ( - - )} -
+ ) : ( - - - - - {testRun.imageName ? ( - -
- -
-
- ) : ( - - )} -
+ )}
diff --git a/src/components/TestRunList.tsx b/src/components/TestRunList.tsx index 250c30c7..26f7177d 100644 --- a/src/components/TestRunList.tsx +++ b/src/components/TestRunList.tsx @@ -10,6 +10,7 @@ import { IconButton, Menu, MenuItem, + Box, } from "@material-ui/core"; import { MoreVert } from "@material-ui/icons"; import { useHistory } from "react-router-dom"; @@ -22,11 +23,13 @@ import { deleteTestRun, selectTestRun, } from "../contexts"; +import { Skeleton } from "@material-ui/lab"; +import { SkeletonList } from "./SkeletonList"; const TestRunList: React.FunctionComponent<{ items: TestRun[]; }> = ({ items }) => { - const { selectedTestRunId } = useTestRunState(); + const { selectedTestRunId, loading } = useTestRunState(); const testRunDispatch = useTestRunDispatch(); const history = useHistory(); const [anchorEl, setAnchorEl] = React.useState(null); @@ -49,59 +52,72 @@ const TestRunList: React.FunctionComponent<{ return ( - - - - - Name - OS - Device - Browser - Viewport - Status - - - - - {items.map((test) => ( - - { - history.push(buildTestRunLocation(test)); - selectTestRun(testRunDispatch, test.id) - }} - > - {test.name} - - - {test.os} - - - {test.device} - - - {test.browser} - - - {test.viewport} - - - - - - handleClick(event, test)}> - - - + {loading ? ( + + ) : ( + +
+ + + Name + OS + Device + Browser + Viewport + Status + - ))} - -
-
+ + {loading ? ( + [...Array(10)].map((i) => ( + + + + )) + ) : ( + + {items.map((test) => ( + + { + history.push(buildTestRunLocation(test)); + selectTestRun(testRunDispatch, test.id); + }} + > + {test.name} + + + {test.os} + + + {test.device} + + + {test.browser} + + + {test.viewport} + + + + + + handleClick(event, test)}> + + + + + ))} + + )} + + + )} + {selectedTestRun && ( void; type State = { selectedBuildId: string | undefined; buildList: Build[]; + loading: boolean; }; type BuildProviderProps = { children: React.ReactNode }; @@ -50,6 +51,7 @@ const BuildDispatchContext = React.createContext( const initialState: State = { selectedBuildId: undefined, buildList: [], + loading: false, }; function buildReducer(state: State, action: IAction): State { @@ -59,10 +61,16 @@ function buildReducer(state: State, action: IAction): State { ...state, selectedBuildId: action.payload, }; + case "request": + return { + ...state, + loading: true, + }; case "get": return { ...state, buildList: action.payload, + loading: false, }; case "delete": return { @@ -121,8 +129,6 @@ async function getBuildList(dispatch: Dispatch, id: string) { } async function deleteBuild(dispatch: Dispatch, id: string) { - dispatch({ type: "request" }); - buildsService .remove(id) .then((isDeleted) => { diff --git a/src/contexts/testRun.context.tsx b/src/contexts/testRun.context.tsx index 68915c9f..f2944c8e 100644 --- a/src/contexts/testRun.context.tsx +++ b/src/contexts/testRun.context.tsx @@ -63,6 +63,7 @@ type State = { selectedTestRunId: string | undefined; selectedTestRunIndex: number | undefined; testRuns: TestRun[]; + loading: boolean; }; type TestRunProviderProps = { children: React.ReactNode }; @@ -76,6 +77,7 @@ const initialState: State = { selectedTestRunId: undefined, selectedTestRunIndex: undefined, testRuns: [], + loading: false, }; function testRunReducer(state: State, action: IAction): State { @@ -92,10 +94,16 @@ function testRunReducer(state: State, action: IAction): State { (t) => t.id === action.payload ), }; + case "request": + return { + ...state, + loading: true, + }; case "get": return { ...state, testRuns: action.payload, + loading: false, }; case "delete": return { @@ -168,8 +176,6 @@ async function getTestRunList(dispatch: Dispatch, buildId: string) { } async function deleteTestRun(dispatch: Dispatch, id: string) { - dispatch({ type: "request" }); - testRunService .remove(id) .then((isDeleted) => {