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

Commit

Permalink
Merge pull request #255 from statelyai/farskid/sta-513-implement-embe…
Browse files Browse the repository at this point in the history
…dded-mode-iframe

Embed preview
  • Loading branch information
mattpocock authored Sep 24, 2021
2 parents 56e5358 + bbec808 commit b70bd21
Show file tree
Hide file tree
Showing 18 changed files with 800 additions and 101 deletions.
7 changes: 7 additions & 0 deletions .changeset/gorgeous-hotels-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'xstate-viz-app': minor
---

Adds the embed preview frame that lets users to configure how they want the embedded visualizer to look like

<img width="1214" alt="image" src="https://user-images.githubusercontent.com/8332043/134560683-5c654c59-799f-4f18-a927-bea0fd61ec34.png">
47 changes: 47 additions & 0 deletions cypress/integration/embed-preview.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { GetSourceFileSsrQuery } from '../../src/graphql/GetSourceFileSSR.generated';

const SOURCE_ID = 'source-file-id';

const getSSRParam = (
data: Partial<GetSourceFileSsrQuery['getSourceFile']> & { id: string },
) => {
return encodeURIComponent(JSON.stringify({ data, id: data.id }));
};

describe('Embed Preview', () => {
beforeEach(() => {
cy.interceptGraphQL({
getSourceFile: {
id: SOURCE_ID,
text: `
import { createModel } from "xstate/lib/model";
import { createMachine } from "xstate";
createMachine({
id: "simple",
states: {
a: {},
b: {},
},
});
`,
},
});
cy.visit(
`/viz/${SOURCE_ID}?ssr=${getSSRParam({
id: SOURCE_ID,
})}`,
);
});

it('Shows Embed button under share menu', () => {
cy.findByRole('button', { name: /share/i }).click();
cy.findByRole('menuitem', { name: /embed/i }).should('be.visible');
});

it('Clicking on the Embed button opens the Embed preview', () => {
cy.findByRole('button', { name: /share/i }).click();
cy.findByRole('menuitem', { name: /embed/i }).click();
cy.getEmbedPreview().should('be.visible');
});
});
3 changes: 3 additions & 0 deletions cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ const getControlButtons = () => {
return cy.findByTestId('controls');
};

const getEmbedPreview = () => cy.findByTestId('embed-preview');
const getResizeHandle = () => {
return cy.findByTestId('resize-handle');
};
Expand Down Expand Up @@ -270,6 +271,7 @@ declare global {

getControlButtons: typeof getControlButtons;

getEmbedPreview: typeof getEmbedPreview;
getResizeHandle: typeof getResizeHandle;
}
}
Expand All @@ -291,4 +293,5 @@ Cypress.Commands.add('getCanvasHeader', getCanvasHeader);
Cypress.Commands.add('getStatePanel', getStatePanel);
Cypress.Commands.add('getCanvasGraph', getCanvasGraph);
Cypress.Commands.add('getControlButtons', getControlButtons);
Cypress.Commands.add('getEmbedPreview', getEmbedPreview);
Cypress.Commands.add('getResizeHandle', getResizeHandle);
27 changes: 24 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { theme } from './theme';
import { EditorThemeProvider } from './themeContext';
import { EmbedContext, EmbedMode } from './types';
import { useInterpretCanvas } from './useInterpretCanvas';
import { useRouter } from 'next/router';
import router, { useRouter } from 'next/router';
import { parseEmbedQuery, withoutEmbedQueryParams } from './utils';
import { registryLinks } from './registryLinks';

Expand Down Expand Up @@ -49,6 +49,17 @@ const VizHead = () => {
);
};

const useReceiveMessage = (
eventHandlers?: Record<string, (data: any) => void>,
) => {
useEffect(() => {
window.onmessage = async (message) => {
const { data } = message;
eventHandlers && eventHandlers[data.type]?.(data);
};
}, []);
};

const getGridArea = (embed?: EmbedContext) => {
if (embed?.isEmbedded && embed.mode === EmbedMode.Viz) {
return 'canvas';
Expand All @@ -62,15 +73,16 @@ const getGridArea = (embed?: EmbedContext) => {
};

function App({ isEmbedded = false }: { isEmbedded?: boolean }) {
const { query } = useRouter();
const { query, asPath } = useRouter();
const embed = useMemo(
() => ({
...parseEmbedQuery(query),
isEmbedded,
originalUrl: withoutEmbedQueryParams(query),
}),
[query],
[query, asPath],
);

const paletteService = useInterpret(paletteMachine);
// don't use `devTools: true` here as it would freeze your browser
const simService = useInterpret(simulationMachine);
Expand All @@ -83,13 +95,22 @@ function App({ isEmbedded = false }: { isEmbedded?: boolean }) {
const sourceService = useSelector(useAuth(), getSourceActor);
const [sourceState, sendToSourceService] = useActor(sourceService!);

useReceiveMessage({
// used to receive messages from the iframe in embed preview
EMBED_PARAMS_CHANGED: (data) => {
router.replace(data.url, data.url);
},
});

useEffect(() => {
sendToSourceService({
type: 'MACHINE_ID_CHANGED',
id: machine?.id || '',
});
}, [machine?.id, sendToSourceService]);

// TODO: Subject to refactor into embedActor

const sourceID = sourceState!.context.sourceID;

const canvasService = useInterpretCanvas({
Expand Down
42 changes: 19 additions & 23 deletions src/CanvasContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,8 @@ const dragMachine = dragModel.createMachine(

const getCursorByState = (state: AnyState) =>
(
Object.values(state.meta).find(
(m) => !!(m as { cursor?: CSSProperties['cursor'] }).cursor,
Object.values(state.meta).find((m) =>
Boolean((m as { cursor?: CSSProperties['cursor'] }).cursor),
) as { cursor?: CSSProperties['cursor'] }
)?.cursor;

Expand All @@ -379,27 +379,23 @@ export const CanvasContainer: React.FC<{ panModeEnabled: boolean }> = ({
const canvasService = useCanvas();
const embed = useEmbed();
const canvasRef = useRef<HTMLDivElement>(null!);
const [state, send] = useMachine(
dragMachine.withConfig(
{
actions: {
sendPanChange: actions.send(
(ctx, ev: any) => {
return canvasModel.events.PAN(ev.delta.x, ev.delta.y);
},
{ to: canvasService as any },
),
},
guards: {
isPanDisabled: () => !!embed?.isEmbedded && !embed.pan,
const [state, send] = useMachine(dragMachine, {
actions: {
sendPanChange: actions.send(
(_, ev: any) => {
return canvasModel.events.PAN(ev.delta.x, ev.delta.y);
},
},
{
...dragModel.initialContext,
ref: canvasRef,
},
),
);
{ to: canvasService as any },
),
},
guards: {
isPanDisabled: () => !!embed?.isEmbedded && !embed.pan,
},
context: {
...dragModel.initialContext,
ref: canvasRef,
},
});

React.useEffect(() => {
if (panModeEnabled) {
Expand Down Expand Up @@ -472,7 +468,7 @@ export const CanvasContainer: React.FC<{ panModeEnabled: boolean }> = ({
return () => {
canvasEl.removeEventListener('wheel', onCanvasWheel);
};
}, [canvasService]);
}, [canvasService, embed]);

return (
<div
Expand Down
4 changes: 2 additions & 2 deletions src/CanvasView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ export const CanvasView: React.FC = () => {

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

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

const simulationMode = useSimulationMode();
Expand Down
34 changes: 15 additions & 19 deletions src/EditorPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,12 @@ const editorPanelMachine = editorPanelModel.createMachine(
src: async (ctx) => {
const monaco = ctx.monacoRef!;
const uri = monaco.Uri.parse(ctx.mainFile);
const getWorker =
await monaco.languages.typescript.getTypeScriptWorker();
const getWorker = await monaco.languages.typescript.getTypeScriptWorker();
const tsWorker = await getWorker(uri);

const usedXStateGistIdentifiers: string[] = await (
tsWorker as any
).queryXStateGistIdentifiers(uri.toString());
const usedXStateGistIdentifiers: string[] = await (tsWorker as any).queryXStateGistIdentifiers(
uri.toString(),
);

if (usedXStateGistIdentifiers.length > 0) {
const fixupImportsText = buildGistFixupImportsText(
Expand Down Expand Up @@ -396,24 +395,21 @@ export const EditorPanel: React.FC<{

const value = getEditorValue(sourceState);

const [current, send] = useMachine(
// TODO: had to shut up TS by extending model.initialContext
editorPanelMachine.withContext({
const [current, send] = useMachine(editorPanelMachine, {
actions: {
onChange: (ctx) => {
onChange(ctx.machines!);
},
onChangedCodeValue: (ctx) => {
onChangedCodeValue(ctx.code);
},
},
context: {
...editorPanelModel.initialContext,
code: value,
sourceRef: sourceService,
}),
{
actions: {
onChange: (ctx) => {
onChange(ctx.machines!);
},
onChangedCodeValue: (ctx) => {
onChangedCodeValue(ctx.code);
},
},
},
);
});
const isVisualizing = current.hasTag('visualizing');

return (
Expand Down
Loading

0 comments on commit b70bd21

Please sign in to comment.