-
Notifications
You must be signed in to change notification settings - Fork 136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Architecture proof of concept #6992
Changes from 4 commits
8d81ddb
ad91149
8d9e7a7
594d524
560b1ec
0e2ee80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
.Error { | ||
background-color: var(--color-red-light); | ||
color: var(--color-red-dark); | ||
padding: 1rem; | ||
margin: 0; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
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: Error): ErrorBoundaryState { | ||
return { error }; | ||
} | ||
|
||
render() { | ||
const { error } = this.state; | ||
|
||
if (error !== null) { | ||
return <pre className={styles.Error}>{error.stack}</pre>; | ||
} | ||
|
||
return this.props.children; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
// This file is not really part of the architectural demo. | ||
// It's just a bootstrap for things like auth that I didn't want to spend time actually implementing. | ||
|
||
import { ReactNode, useContext, useEffect, useRef, useState } from "react"; | ||
|
||
import { ReplayClientContext, SessionContext } from "../src/contexts"; | ||
|
||
// 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 = { | ||
prefs: {}, | ||
}; | ||
} | ||
|
||
type ContextType = { duration: number; endPoint: string; recordingId: string; sessionId: string }; | ||
|
||
export default function Initializer({ children }: { children: ReactNode }) { | ||
const client = useContext(ReplayClientContext); | ||
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 () => { | ||
// 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"); | ||
const recordingId = url.searchParams.get("recordingId"); | ||
if (!recordingId) { | ||
throw Error(`Must specify "recordingId" parameter.`); | ||
} | ||
|
||
const sessionId = await client.initialize(recordingId, accessToken); | ||
const endpoint = await client.getSessionEndpoint(sessionId); | ||
|
||
// The demo doesn't use these directly, but the client throws if they aren't loaded. | ||
await client.findSources(); | ||
|
||
setContext({ | ||
duration: endpoint.time, | ||
endPoint: endpoint.point, | ||
recordingId, | ||
sessionId, | ||
}); | ||
}; | ||
|
||
asyncInitialize(); | ||
} | ||
|
||
didInitializeRef.current = true; | ||
}, [client]); | ||
|
||
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: 0.25rem; | ||
} |
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>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
.FilterToggles { | ||
padding: 0.25rem 1ch; | ||
flex: 0 0 auto; | ||
display: flex; | ||
gap: 1ch; | ||
border-right: 1px solid var(--color-gray-2); | ||
border-bottom: 1px solid var(--color-gray-2); | ||
} | ||
|
||
.FilterInput { | ||
flex: 1; | ||
height: 100%; | ||
background-color: var(--color-white); | ||
padding: 0 1ch; | ||
border: none; | ||
outline: none; | ||
border-bottom: 1px solid var(--color-gray-2); | ||
} | ||
|
||
.FilterLabel { | ||
display: flex; | ||
align-items: center; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import React, { useContext } from "react"; | ||
import { ConsoleFiltersContext } from "../../src/contexts"; | ||
|
||
import styles from "./Filters.module.css"; | ||
|
||
export default function Filters() { | ||
const { filterByDisplayText, levelFlags, update } = useContext(ConsoleFiltersContext); | ||
|
||
return ( | ||
<> | ||
<div className={styles.FilterToggles}> | ||
<label className={styles.FilterLabel}> | ||
<input | ||
type="checkbox" | ||
checked={levelFlags.showLogs} | ||
onChange={event => | ||
update(filterByDisplayText, { ...levelFlags, showLogs: event.currentTarget.checked }) | ||
} | ||
/> | ||
Logs? | ||
</label> | ||
<label className={styles.FilterLabel}> | ||
<input | ||
type="checkbox" | ||
checked={levelFlags.showWarnings} | ||
onChange={event => | ||
update(filterByDisplayText, { | ||
...levelFlags, | ||
showWarnings: event.currentTarget.checked, | ||
}) | ||
} | ||
/> | ||
Warnings? | ||
</label> | ||
<label className={styles.FilterLabel}> | ||
<input | ||
type="checkbox" | ||
checked={levelFlags.showErrors} | ||
onChange={event => | ||
update(filterByDisplayText, { | ||
...levelFlags, | ||
showErrors: event.currentTarget.checked, | ||
}) | ||
} | ||
/> | ||
Errors? | ||
</label> | ||
</div> | ||
<input | ||
className={styles.FilterInput} | ||
name="filter messages" | ||
value={filterByDisplayText} | ||
onChange={event => update(event.currentTarget.value, levelFlags)} | ||
placeholder="Filter output" | ||
/> | ||
</> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
.FocusRegionRowOff, | ||
.FocusRegionRowOn { | ||
padding: 1ch; | ||
display: flex; | ||
flex-direction: row; | ||
flex: 1; | ||
gap: 1ch; | ||
align-items: center; | ||
} | ||
.FocusRegionRowOn { | ||
background-color: var(--color-blue); | ||
} | ||
|
||
.FocusToggleButton { | ||
width: 10ch; | ||
cursor: pointer; | ||
border-radius: 0.25rem; | ||
border: none; | ||
padding: 0.25rem; | ||
} | ||
|
||
.RangeSlider { | ||
flex: 1; | ||
position: relative; | ||
display: flex; | ||
flex-direction: row; | ||
align-items: center; | ||
} | ||
|
||
.RangeTrackFocused { | ||
background-color: rgba(0, 0, 0, 0.25); | ||
height: 4px; | ||
} | ||
.RangeTrackUnfocused { | ||
height: 2px; | ||
background-color: rgba(0, 0, 0, 0.1); | ||
} | ||
|
||
.FocusedRange { | ||
background-color: red; | ||
} | ||
|
||
.RangeStartThumb, | ||
.RangeEndThumb { | ||
position: absolute; | ||
cursor: grab; | ||
width: 1px; | ||
height: 20px; | ||
width: 6px; | ||
} | ||
.RangeStartThumb { | ||
background-color: var(--color-gray-3); | ||
border-radius: 4px 0px 0px 4px; | ||
} | ||
.RangeEndThumb { | ||
background-color: var(--color-gray-3); | ||
border-radius: 0px 4px 4px 0px; | ||
} | ||
|
||
.FocusTimeStamps { | ||
white-space: nowrap; | ||
font-size: 12px; | ||
background-color: rgba(255, 255, 255, 0.5); | ||
border-radius: 4px; | ||
padding: 2px 4px; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { fireEvent, screen } from "@testing-library/react"; | ||
|
||
import { renderFocused } from "../../src/utils/testing"; | ||
|
||
import Focuser from "./Focuser"; | ||
|
||
describe("Focuser", () => { | ||
it("should render the with no focus region", async () => { | ||
await renderFocused(<Focuser />, { | ||
focusContext: { | ||
range: null, | ||
rangeForDisplay: null, | ||
}, | ||
sessionContext: { | ||
duration: 60_000, | ||
}, | ||
}); | ||
|
||
expect(await screen.queryByText("Focus off")).toBeInTheDocument(); | ||
expect(await screen.queryByText("0:00 – 1:00")).toBeInTheDocument(); | ||
}); | ||
Comment on lines
+8
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an example of the approach we might use to render a small part of our app to test in isolation. The |
||
|
||
it("should render the current focus region", async () => { | ||
await renderFocused(<Focuser />, { | ||
focusContext: { | ||
range: [0, 30_000], | ||
rangeForDisplay: [0, 30_000], | ||
}, | ||
sessionContext: { | ||
duration: 60_000, | ||
}, | ||
}); | ||
|
||
expect(await screen.queryByText("Focus on")).toBeInTheDocument(); | ||
expect(await screen.queryByText("0:00 – 0:30")).toBeInTheDocument(); | ||
}); | ||
|
||
it("should allow the focus region to be toggled on and off", async () => { | ||
const { | ||
focusContext: { update }, | ||
} = await renderFocused(<Focuser />, { | ||
focusContext: { | ||
range: null, | ||
rangeForDisplay: null, | ||
}, | ||
sessionContext: { | ||
duration: 60_000, | ||
}, | ||
}); | ||
|
||
expect(update).not.toHaveBeenCalled(); | ||
|
||
fireEvent.click(screen.getByText("Focus off")); | ||
|
||
expect(update).toHaveBeenCalledWith([0, 60_000], false); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Upgrading this fixed the
createRoot
vsrender
warning and our tests all still passed so...