Skip to content

Commit

Permalink
Incremental proggress
Browse files Browse the repository at this point in the history
  • Loading branch information
bvaughn committed May 29, 2022
1 parent 1fb3c2d commit d7e8e62
Show file tree
Hide file tree
Showing 28 changed files with 1,172 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.Error {
background-color: var(--color-red-light);
color: var(--color-red-dark);
padding: 1rem;
}
28 changes: 28 additions & 0 deletions packages/bvaughn-architecture-demo/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { Component, PropsWithChildren } from "react";

import styles from "./ErrorBoundary.module.css";

type ErrorBoundaryState = { error: Error | null };

export default class ErrorBoundary extends Component<PropsWithChildren<{}>, ErrorBoundaryState> {
state: ErrorBoundaryState = { error: null };

static getDerivedStateFromError(error): ErrorBoundaryState {
return { error };
}

render() {
const { error } = this.state;

if (error !== null) {
return (
<div className={styles.Error}>
<div>{error.message}</div>
<pre>{error.stack}</pre>
</div>
);
}

return this.props.children;
}
}
66 changes: 66 additions & 0 deletions packages/bvaughn-architecture-demo/components/Initializer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { number, string } from "prop-types";
import { client, initSocket } from "protocol/socket";
import { ReactNode, useEffect, useRef, useState } from "react";

import { SessionContext } from "../src/contexts";

const DISPATCH_URL = "wss://dispatch.replay.io";

// HACK Hack around the fact that the initSocket() function is side effectful
// and writes to an "app" global on the window object.
if (typeof window !== "undefined") {
(window as any).app = {};
}

type ContextType = { duration: number; endPoint: string; recordingId: string; sessionId: string };

export default function Initializer({ children }: { children: ReactNode }) {
const [context, setContext] = useState<ContextType | null>(null);
const didInitializeRef = useRef<boolean>(false);

useEffect(() => {
// The WebSocket and session/authentication are global.
// We only need to initialize them once.
if (!didInitializeRef.current) {
const asyncInitialize = async () => {
initSocket(DISPATCH_URL);

// Read some of the hard-coded values from query params.
// (This is just a prototype; no sense building a full authentication flow.)
const url = new URL(window.location.href);
const accessToken = url.searchParams.get("accessToken");
if (!accessToken) {
throw Error(`Must specify "accessToken" parameter.`);
}
const recordingId = url.searchParams.get("recordingId");
if (!recordingId) {
throw Error(`Must specify "recordingId" parameter.`);
}

// Authenticate
await client.Authentication.setAccessToken({ accessToken });

// Create session
const { sessionId } = await client.Recording.createSession({ recordingId });
const { endpoint } = await client.Session.getEndpoint({}, sessionId);

setContext({
duration: endpoint.time,
endPoint: endpoint.point,
recordingId,
sessionId,
});
};

asyncInitialize();
}

didInitializeRef.current = true;
}, []);

if (context === null) {
return null;
}

return <SessionContext.Provider value={context}>{children}</SessionContext.Provider>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.Loader {
padding: 1rem;
}
5 changes: 5 additions & 0 deletions packages/bvaughn-architecture-demo/components/Loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import styles from "./Loader.module.css";

export default function Loader() {
return <div className={styles.Loader}>Loading...</div>;
}
52 changes: 52 additions & 0 deletions packages/bvaughn-architecture-demo/components/Messages.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.Container,
.ContainerPending {
position: relative;
overflow: auto;
height: 100%;
}
.ContainerPending {
opacity: 0.5;
}

.CountRow,
.MessageRow,
.NoMessagesRow,
.OverflowRow {
padding: 0.25rem;
}

.OverflowRow {
background-color: var(--color-red-dark);
color: var(--color-white);
}

.CountRow {
background-color: var(--color-gray-1);
}

.MessageRow,
.MessageRowError,
.MessageRowWarning {
display: flex;
flex-direction: row;
gap: 1ch;
padding: 0.25rem;
font-family: monospace;
font-size: 10px;
border-bottom: 1px solid var(--color-gray-1);
}
.MessageRow {
background-color: var(--color-white);
}
.MessageRowError {
background-color: var(--color-red-light);
color: 1px solid var(--color-red-dark);
}
.MessageRowWarning {
background-color: var(--color-yellow-light);
color: var(--color-yellow-dark);
}

.Time {
color: var(--color-blue);
}
101 changes: 101 additions & 0 deletions packages/bvaughn-architecture-demo/components/Messages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { memo, useContext } from "react";
import { Message } from "@replayio/protocol";

import { SessionContext } from "../src/contexts";
import { useFilteredMessages } from "../src/hooks";
import { getMessages } from "../src/MessagesCache";
import { getClosestPointForTime } from "../src/PointsCache";
import { formatTimestamp } from "../src/utils/time";

import styles from "./Messages.module.css";

export default function Messages({
focusStartTime,
focusEndTime,
isPending,
showErrors,
showLogs,
showWarnings,
}: {
focusStartTime: number | null;
focusEndTime: number | null;
isPending: boolean;
showErrors: boolean;
showLogs: boolean;
showWarnings: boolean;
}) {
const { sessionId } = useContext(SessionContext);

let focusMode = null;
if (focusStartTime !== null && focusEndTime !== null) {
const startPoint = getClosestPointForTime(focusStartTime, sessionId);
const endPoint = getClosestPointForTime(focusEndTime, sessionId);

focusMode = {
begin: {
point: startPoint,
time: focusStartTime,
},
end: {
point: endPoint,
time: focusEndTime,
},
};
}

const { countAfter, countBefore, didOverflow, messages } = getMessages(sessionId, focusMode);

// TODO Derived memoized selector that joins log points and messages and filters by criteria (e.g. type)
const filteredMessages = useFilteredMessages(messages, { showErrors, showLogs, showWarnings });

return (
<div className={isPending ? styles.ContainerPending : styles.Container}>
{didOverflow && (
<div className={styles.OverflowRow}>There were too many messages to fetch them all</div>
)}
{countBefore > 0 && (
<div className={styles.CountRow}>
{countBefore} messages filtered before the focus range
</div>
)}
{filteredMessages.length === 0 && (
<div className={styles.NoMessagesRow}>No messages found.</div>
)}
{filteredMessages.map((message: Message, index: number) => (
<Row key={index} message={message} />
))}
{countAfter > 0 && (
<div className={styles.CountRow}>{countAfter} messages filtered after the focus range</div>
)}
</div>
);
}

// This is a really crappy approximation of the console renderer.
// It isn't meant to be the focus of this branch.
const Row = memo(function Row({ message }: { message: Message }) {
const { argumentValues, point, text } = message;

let className = styles.MessageRow;
switch (message.level) {
case "warning": {
className = styles.MessageRowWarning;
break;
}
case "error": {
className = styles.MessageRowError;
break;
}
}

return (
<div className={className}>
<div className={styles.Time}>{formatTimestamp(point.time)}</div>
{text}
{argumentValues != null &&
argumentValues.map((argumentValue: any, index: number) => (
<span key={index}>{argumentValue.value || argumentValue.object}</span>
))}
</div>
);
});
18 changes: 17 additions & 1 deletion packages/bvaughn-architecture-demo/package-lock.json

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

56 changes: 56 additions & 0 deletions packages/bvaughn-architecture-demo/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Head from "next/head";
import type { AppContext, AppProps } from "next/app";
import NextApp from "next/app";
import React from "react";

import ErrorBoundary from "../components/ErrorBoundary";
import Initializer from "../components/Initializer";
import Loader from "../components/Loader";

import "./global.css";

interface AuthProps {
apiKey?: string;
}

function Routing({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<meta httpEquiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<title>Replay</title>
</Head>
<ErrorBoundary>
<Initializer>
<React.Suspense fallback={<Loader />}>
<Component {...pageProps} />
</React.Suspense>
</Initializer>
</ErrorBoundary>
</>
);
}

const App = ({ apiKey, ...props }: AppProps & AuthProps) => {
return <Routing {...props} />;
};

App.getInitialProps = (appContext: AppContext) => {
const props = NextApp.getInitialProps(appContext);
const authHeader = appContext.ctx.req?.headers.authorization;
const authProps: AuthProps = { apiKey: undefined };

if (authHeader) {
const [scheme, token] = authHeader.split(" ", 2);
if (!token || !/^Bearer$/i.test(scheme)) {
console.error("Format is Authorization: Bearer [token]");
} else {
authProps.apiKey = token;
}
}

return { ...props, ...authProps };
};

export default App;
23 changes: 23 additions & 0 deletions packages/bvaughn-architecture-demo/pages/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
html,
body {
padding: 0;
margin: 0;
font-family: sans-serif;
font-size: 14px;
}

* {
box-sizing: content-box;
}

:root {
--color-blue: #0a84ff;
--color-gray-1: #d0d5dc;
--color-gray-2: #9ca3af;
--color-gray-3: #234;
--color-red-dark: #ea2330;
--color-red-light: #ffb3d2;
--color-yellow-dark: #6c5914;
--color-yellow-light: #fffac8;
--color-white: #fff;
}
Loading

0 comments on commit d7e8e62

Please sign in to comment.