Skip to content
This repository has been archived by the owner on Feb 25, 2024. It is now read-only.

Added an embed mode which works via the URL, not any data in our db #323

Open
wants to merge 10 commits into
base: dev
Choose a base branch
from
41 changes: 39 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { MachineNameChooserModal } from './MachineNameChooserModal';
import { PaletteProvider } from './PaletteContext';
import { paletteMachine } from './paletteMachine';
import { PanelsView } from './PanelsView';
import { SimulationProvider } from './SimulationContext';
import { SimulationProvider, useSimulationMode } from './SimulationContext';
import { simulationMachine } from './simulationMachine';
import { getSourceActor, useSourceRegistryData } from './sourceMachine';
import { theme } from './theme';
Expand All @@ -21,6 +21,7 @@ import { useInterpretCanvas } from './useInterpretCanvas';
import router, { useRouter } from 'next/router';
import { parseEmbedQuery, withoutEmbedQueryParams } from './utils';
import { registryLinks } from './registryLinks';
import { canZoom, canZoomIn, canZoomOut } from './canvasMachine';

const defaultHeadProps = {
title: 'XState Visualizer',
Expand Down Expand Up @@ -88,6 +89,7 @@ function App({ isEmbedded = false }: { isEmbedded?: boolean }) {
const paletteService = useInterpret(paletteMachine);
// don't use `devTools: true` here as it would freeze your browser
const simService = useInterpret(simulationMachine);

const machine = useSelector(simService, (state) => {
return state.context.currentSessionId
? state.context.serviceDataMap[state.context.currentSessionId!]?.machine
Expand Down Expand Up @@ -120,6 +122,32 @@ function App({ isEmbedded = false }: { isEmbedded?: boolean }) {
embed,
});

const shouldEnableZoomOutButton = useSelector(
canvasService,
(state) => canZoom(embed) && canZoomOut(state.context),
);

const shouldEnableZoomInButton = useSelector(
canvasService,
(state) => canZoom(embed) && canZoomIn(state.context),
);

const canShowWelcomeMessage = sourceState.hasTag('canShowWelcomeMessage');

const showControls = useMemo(
() => !embed?.isEmbedded || embed.controls,
[embed],
);

const showZoomButtonsInEmbed = useMemo(
() => !embed?.isEmbedded || (embed.controls && embed.zoom),
[embed],
);
const showPanButtonInEmbed = useMemo(
() => !embed?.isEmbedded || (embed.controls && embed.pan),
[embed],
);
mattpocock marked this conversation as resolved.
Show resolved Hide resolved

// This is because we're doing loads of things on client side anyway
if (!isOnClientSide()) return <VizHead />;

Expand All @@ -142,7 +170,16 @@ function App({ isEmbedded = false }: { isEmbedded?: boolean }) {
>
{!(embed?.isEmbedded && embed.mode === EmbedMode.Panels) && (
<CanvasProvider value={canvasService}>
<CanvasView />
<CanvasView
shouldEnableZoomOutButton={shouldEnableZoomOutButton}
shouldEnableZoomInButton={shouldEnableZoomInButton}
canShowWelcomeMessage={canShowWelcomeMessage}
showControls={showControls}
showZoomButtonsInEmbed={showZoomButtonsInEmbed}
showPanButtonInEmbed={showPanButtonInEmbed}
isEmbedded={embed?.isEmbedded}
mattpocock marked this conversation as resolved.
Show resolved Hide resolved
hideHeader={false}
/>
</CanvasProvider>
)}
<PanelsView />
Expand Down
86 changes: 34 additions & 52 deletions src/CanvasView.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {
AddIcon,
MinusIcon,
RepeatIcon,
QuestionOutlineIcon,
RepeatIcon,
} from '@chakra-ui/icons';
import {
Box,
Expand All @@ -19,78 +19,60 @@ import {
VStack,
} from '@chakra-ui/react';
import { useSelector } from '@xstate/react';
import xstatePkgJson from 'xstate/package.json';
import React, { useMemo } from 'react';
import xstatePkgJson from 'xstate/package.json';
import { CanvasContainer } from './CanvasContainer';
import { useCanvas } from './CanvasContext';
import { canZoom, canZoomIn, canZoomOut } from './canvasMachine';
import { CanvasHeader } from './CanvasHeader';
import { toDirectedGraph } from './directedGraph';
import { Graph } from './Graph';
import { useSimulation, useSimulationMode } from './SimulationContext';
import { CanvasHeader } from './CanvasHeader';
import { Overlay } from './Overlay';
import { useEmbed } from './embedContext';
import { CompressIcon, HandIcon } from './Icons';
import { useSourceActor } from './sourceMachine';
import { Overlay } from './Overlay';
import { useSimulation, useSimulationMode } from './SimulationContext';
import { WelcomeArea } from './WelcomeArea';

export const CanvasView: React.FC = () => {
export const CanvasView = (props: {
shouldEnableZoomOutButton?: boolean;
shouldEnableZoomInButton?: boolean;
canShowWelcomeMessage?: boolean;
showControls?: boolean;
showZoomButtonsInEmbed?: boolean;
showPanButtonInEmbed?: boolean;
isEmbedded?: boolean;
hideHeader: boolean;
}) => {
// TODO: refactor this so an event can be explicitly sent to a machine
// it isn't straightforward to do at the moment cause the target machine lives in a child component
const [panModeEnabled, setPanModeEnabled] = React.useState(false);
const embed = useEmbed();
const simService = useSimulation();
const canvasService = useCanvas();
const [sourceState] = useSourceActor();

const simService = useSimulation();

const machine = useSelector(simService, (state) => {
return state.context.currentSessionId
? state.context.serviceDataMap[state.context.currentSessionId!]?.machine
: undefined;
});
const isLayoutPending = useSelector(simService, (state) =>
state.hasTag('layoutPending'),
);
const isEmpty = useSelector(simService, (state) => state.hasTag('empty'));
const digraph = useMemo(
() => (machine ? toDirectedGraph(machine) : undefined),
[machine],
);

const shouldEnableZoomOutButton = useSelector(
canvasService,
(state) => canZoom(embed) && canZoomOut(state.context),
);

const shouldEnableZoomInButton = useSelector(
canvasService,
(state) => canZoom(embed) && canZoomIn(state.context),
);

const simulationMode = useSimulationMode();

const canShowWelcomeMessage = sourceState.hasTag('canShowWelcomeMessage');

const showControls = useMemo(
() => !embed?.isEmbedded || embed.controls,
[embed],
const digraph = useMemo(
() => (machine ? toDirectedGraph(machine) : undefined),
[machine],
);

const showZoomButtonsInEmbed = useMemo(
() => !embed?.isEmbedded || (embed.controls && embed.zoom),
[embed],
);
const showPanButtonInEmbed = useMemo(
() => !embed?.isEmbedded || (embed.controls && embed.pan),
[embed],
const isLayoutPending = useSelector(simService, (state) =>
state.hasTag('layoutPending'),
);
const isEmpty = useSelector(simService, (state) => state.hasTag('empty'));

return (
<Box
display="grid"
height="100%"
{...(!embed?.isEmbedded && { gridTemplateRows: '3rem 1fr auto' })}
{...(!props.hideHeader && { gridTemplateRows: '3rem 1fr auto' })}
>
{!embed?.isEmbedded && (
{!props.hideHeader && (
<Box data-testid="canvas-header" bg="gray.800" zIndex={1} padding="0">
<CanvasHeader />
</Box>
Expand All @@ -107,10 +89,10 @@ export const CanvasView: React.FC = () => {
</Box>
</Overlay>
)}
{isEmpty && canShowWelcomeMessage && <WelcomeArea />}
{isEmpty && props.canShowWelcomeMessage && <WelcomeArea />}
</CanvasContainer>

{showControls && (
{props.showControls && (
<Box
display="flex"
flexDirection="row"
Expand All @@ -126,21 +108,21 @@ export const CanvasView: React.FC = () => {
data-testid="controls"
>
<ButtonGroup size="sm" spacing={2} isAttached>
{showZoomButtonsInEmbed && (
{props.showZoomButtonsInEmbed && (
<>
<IconButton
aria-label="Zoom out"
title="Zoom out"
icon={<MinusIcon />}
disabled={!shouldEnableZoomOutButton}
disabled={!props.shouldEnableZoomOutButton}
onClick={() => canvasService.send('ZOOM.OUT')}
variant="secondary"
/>
<IconButton
aria-label="Zoom in"
title="Zoom in"
icon={<AddIcon />}
disabled={!shouldEnableZoomInButton}
disabled={!props.shouldEnableZoomInButton}
onClick={() => canvasService.send('ZOOM.IN')}
variant="secondary"
/>
Expand All @@ -153,7 +135,7 @@ export const CanvasView: React.FC = () => {
onClick={() => canvasService.send('FIT_TO_CONTENT')}
variant="secondary"
/>
{!embed?.isEmbedded && (
{!props.isEmbedded && (
<IconButton
aria-label="Reset canvas"
title="Reset canvas"
Expand All @@ -163,7 +145,7 @@ export const CanvasView: React.FC = () => {
/>
)}
</ButtonGroup>
{showPanButtonInEmbed && (
{props.showPanButtonInEmbed && (
<IconButton
aria-label="Pan mode"
icon={<HandIcon />}
Expand All @@ -184,7 +166,7 @@ export const CanvasView: React.FC = () => {
RESET
</Button>
)}
{!embed?.isEmbedded && (
{!props.isEmbedded && (
<Menu closeOnSelect={true} placement="top-end">
<MenuButton
as={IconButton}
Expand Down
10 changes: 6 additions & 4 deletions src/embedContext.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createContext, useContext } from 'react';
import { EmbedContext } from './types';
import { createRequiredContext } from './utils';

export const [EmbedProvider, useEmbed] = createRequiredContext<
EmbedContext | undefined
>('Embed');
const EmbedReactContext = createContext(null as EmbedContext);

export const EmbedProvider = EmbedReactContext.Provider;

export const useEmbed = () => useContext(EmbedReactContext);
15 changes: 14 additions & 1 deletion src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import '../InvokeViz.scss';
import '../monacoPatch';
import '../StateNodeViz.scss';
import '../TransitionViz.scss';
import { NextComponentWithMeta } from '../types';

// import { isOnClientSide } from '../isOnClientSide';

Expand All @@ -32,7 +33,7 @@ if (
});
}

const MyApp = ({ pageProps, Component }: AppProps) => {
const AuthWrapper = ({ pageProps, Component }: AppProps) => {
const router = useRouter();

const authService = useInterpret(
Expand All @@ -50,4 +51,16 @@ const MyApp = ({ pageProps, Component }: AppProps) => {
);
};

const MyApp = (
props: AppProps & {
Component: NextComponentWithMeta;
},
) => {
if (props.Component.preventAuth) {
return <props.Component {...props.pageProps}></props.Component>;
}

return <AuthWrapper {...props}></AuthWrapper>;
};

export default MyApp;
91 changes: 91 additions & 0 deletions src/pages/view-only.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Box, ChakraProvider } from '@chakra-ui/react';
import { useInterpret } from '@xstate/react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import React, { useEffect, useMemo } from 'react';
import { createMachine } from 'xstate';
import { CanvasProvider } from '../CanvasContext';
import { CanvasView } from '../CanvasView';
import { EmbedProvider } from '../embedContext';
import { SimulationProvider } from '../SimulationContext';
import { simulationMachine } from '../simulationMachine';
import { theme } from '../theme';
import { NextComponentWithMeta } from '../types';
import { useInterpretCanvas } from '../useInterpretCanvas';
import { parseEmbedQuery, withoutEmbedQueryParams } from '../utils';

const machine = createMachine({
initial: 'wow',
states: {
wow: {
on: {
NEXT: {
target: 'new',
},
},
},
new: {},
},
});

const ViewOnlyPage: NextComponentWithMeta = () => {
const canvasService = useInterpretCanvas({
sourceID: null,
});
const simulationService = useInterpret(simulationMachine);
const router = useRouter();

useEffect(() => {
simulationService.send({
type: 'MACHINES.REGISTER',
machines: [machine],
});
}, []);

const embed = useMemo(
() => ({
...parseEmbedQuery(router.query),
isEmbedded: true,
originalUrl: withoutEmbedQueryParams(router.query),
}),
[router.query],
);
return (
<>
mattpocock marked this conversation as resolved.
Show resolved Hide resolved
<Head>
<script src="https://unpkg.com/elkjs@0.7.1/lib/elk.bundled.js"></script>
</Head>
mattpocock marked this conversation as resolved.
Show resolved Hide resolved
<EmbedProvider value={embed}>
<ChakraProvider theme={theme}>
<CanvasProvider value={canvasService}>
<SimulationProvider value={simulationService}>
<Box
data-testid="app"
data-viz-theme="dark"
as="main"
display="grid"
gridTemplateColumns="1fr auto"
mattpocock marked this conversation as resolved.
Show resolved Hide resolved
gridTemplateAreas={`"canvas"`}
height="100vh"
>
<CanvasView
hideHeader
showControls
isEmbedded
shouldEnableZoomInButton
shouldEnableZoomOutButton
showPanButtonInEmbed
showZoomButtonsInEmbed
/>
</Box>
</SimulationProvider>
</CanvasProvider>
</ChakraProvider>
</EmbedProvider>
</>
);
};

ViewOnlyPage.preventAuth = true;

export default ViewOnlyPage;
Loading