Skip to content

Commit

Permalink
feat(poc): GraphQL subscription for operations
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr committed Apr 10, 2024
1 parent 41a41fd commit 282f2be
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 21 deletions.
1 change: 1 addition & 0 deletions editor.planx.uk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"rxjs": "^7.8.1",
"scroll-into-view-if-needed": "^3.1.0",
"sharedb": "^3.3.2",
"subscriptions-transport-ws": "^0.11.0",
"swr": "^2.2.4",
"tippy.js": "^6.3.7",
"uuid": "^9.0.1",
Expand Down
42 changes: 40 additions & 2 deletions editor.planx.uk/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 56 additions & 16 deletions editor.planx.uk/src/lib/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import {
from,
InMemoryCache,
Operation,
split,
} from "@apollo/client";
import { GraphQLErrors } from "@apollo/client/errors";
import { setContext } from "@apollo/client/link/context";
import { onError } from "@apollo/client/link/error";
import { RetryLink } from "@apollo/client/link/retry";
import { WebSocketLink } from "@apollo/client/link/ws";
import { getMainDefinition } from "@apollo/client/utilities";
import { logger } from "airbrake";
import { useStore } from "pages/FlowEditor/lib/store";
import { toast } from "react-toastify";
Expand All @@ -34,11 +37,62 @@ const customFetch = async (
return fetchResult;
};

const authHttpLink = createHttpLink({
const httpLink = createHttpLink({
uri: process.env.REACT_APP_HASURA_URL,
fetch: customFetch,
});

/**
* Set auth header in Apollo client
* Must be done post-authentication once we have a value for JWT
*/
export const authMiddleware = setContext(async () => {
const jwt = await getJWT();

return {
headers: {
authorization: jwt ? `Bearer ${jwt}` : undefined,
},
};
});

const authHttpLink = authMiddleware.concat(httpLink);

/**
* Authenticated web socket connection - used for GraphQL subscriptions
*/
const authWsLink = new WebSocketLink({
uri: `ws://localhost:7100/v1/graphql`,
options: {
reconnect: true,
connectionParams: async () => {
const jwt = await getJWT();
return {
headers: {
authorization: jwt ? `Bearer ${jwt}` : undefined,
},
};
},
},
});

/**
* Split requests between HTTPS and WS, based on operation types
* - Queries and mutations -> HTTPS
* - Subscriptions -> WS
*/
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
authWsLink,
authHttpLink,
);

const publicHttpLink = createHttpLink({
uri: process.env.REACT_APP_HASURA_URL,
fetch: customFetch,
Expand Down Expand Up @@ -127,20 +181,6 @@ const retryLink = new RetryLink({
},
});

/**
* Set auth header in Apollo client
* Must be done post-authentication once we have a value for JWT
*/
export const authMiddleware = setContext(async () => {
const jwt = await getJWT();

return {
headers: {
authorization: jwt ? `Bearer ${jwt}` : undefined,
},
};
});

/**
* Get the JWT from the store, and wait if not available
*/
Expand Down Expand Up @@ -168,7 +208,7 @@ const waitForAuthentication = async () =>
* Client used to make all requests by authorised users
*/
export const client = new ApolloClient({
link: from([retryLink, errorLink, authMiddleware, authHttpLink]),
link: from([retryLink, errorLink, splitLink]),
cache: new InMemoryCache(),
});

Expand Down
87 changes: 84 additions & 3 deletions editor.planx.uk/src/pages/FlowEditor/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import "./components/Settings";
import "./floweditor.scss";

import { gql, useSubscription } from "@apollo/client";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { format } from "date-fns";
import React, { useRef } from "react";

import { rootFlowPath } from "../../routes/utils";
Expand All @@ -10,16 +13,94 @@ import PreviewBrowser from "./components/PreviewBrowser";
import { useStore } from "./lib/store";
import useScrollControlsAndRememberPosition from "./lib/useScrollControlsAndRememberPosition";

interface Operation {
createdAt: string;
actor?: {
firstName: string;
lastName: string;
};
}

export const LastEdited = () => {
const [flowId] = useStore((state) => [state.id]);

const formattedDate = (dateString?: string) => {
if (!dateString) return "";
const date = new Date(dateString);
return format(date, "HH:mm:ss, dd LLL yy");
};

const { data, loading, error } = useSubscription<{ operations: Operation[] }>(
gql`
subscription GetMostRecentOperation($flow_id: uuid = "") {
operations(
limit: 1
where: { flow_id: { _eq: $flow_id } }
order_by: { updated_at: desc }
) {
createdAt: created_at
actor {
firstName: first_name
lastName: last_name
}
}
}
`,
{
variables: {
flow_id: flowId,
},
},
);

if (error) console.log(error.message);

// Handle missing operations (e.g. non-production data)
if (error || !data || !data.operations[0].actor) return null;

let message: string;

if (loading) {
message = "Loading...";
} else {
const {
operations: [operation],
} = data;
message = `Last edit by ${operation?.actor?.firstName} ${operation?.actor
?.lastName} ${formattedDate(operation?.createdAt)}`;
}

return (
<Box
sx={(theme) => ({
backgroundColor: theme.palette.background.paper,
borderBottom: `1px solid ${theme.palette.border.main}`,
padding: theme.spacing(1),
paddingLeft: theme.spacing(2),
[theme.breakpoints.up("md")]: {
paddingLeft: theme.spacing(3),
},
})}
>
<Typography variant="body2" fontSize="small">
{message}
</Typography>
</Box>
);
};

const FlowEditor: React.FC<any> = ({ flow, breadcrumbs }) => {
const scrollContainerRef = useRef<HTMLDivElement>(null);
useScrollControlsAndRememberPosition(scrollContainerRef);

const showPreview = useStore((state) => state.showPreview);

return (
<Box id="editor-container">
<Box id="editor" ref={scrollContainerRef} sx={{ position: "relative" }}>
<Flow flow={flow} breadcrumbs={breadcrumbs} />
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
<LastEdited />
<Box id="editor" ref={scrollContainerRef} sx={{ position: "relative" }}>
<Flow flow={flow} breadcrumbs={breadcrumbs} />
</Box>
</Box>
{showPreview && (
<PreviewBrowser
Expand Down

0 comments on commit 282f2be

Please sign in to comment.