Skip to content
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

Merged
merged 6 commits into from
May 31, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
341 changes: 153 additions & 188 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
"@storybook/react": "^6.4.21",
"@tailwindcss/forms": "^0.5.0",
"@testing-library/jest-dom": "^5.16.3",
"@testing-library/react": "^12.1.4",
"@testing-library/react": "^13.3.0",
Copy link
Contributor Author

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 vs render warning and our tests all still passed so...

"@testing-library/user-event": "^14.1.0",
"@trunkio/launcher": "^1.0.5",
"@types/classnames": "^2.2.11",
Expand Down
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;
}
23 changes: 23 additions & 0 deletions packages/bvaughn-architecture-demo/components/ErrorBoundary.tsx
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;
}
}
62 changes: 62 additions & 0 deletions packages/bvaughn-architecture-demo/components/Initializer.tsx
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;
}
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>;
}
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;
}
58 changes: 58 additions & 0 deletions packages/bvaughn-architecture-demo/components/console/Filters.tsx
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 renderFocused helper method could be shared between all tests. It accepts optional params which can be used to define a test scenario.


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);
});
});
Loading