Skip to content

Commit 52f03db

Browse files
fix(site): fix build logs scrolling on safari (#14884)
Fix #9687
1 parent bcdb8a4 commit 52f03db

File tree

5 files changed

+97
-38
lines changed

5 files changed

+97
-38
lines changed

site/jest.setup.ts

+36-34
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,30 @@ import { server } from "testHelpers/server";
1111
// This would fail unit testing, or at least make it very slow with
1212
// actual network requests. So just globally mock this hook.
1313
jest.mock("contexts/useProxyLatency", () => ({
14-
useProxyLatency: (proxies?: Region[]) => {
15-
// Must use `useMemo` here to avoid infinite loop.
16-
// Mocking the hook with a hook.
17-
const proxyLatencies = useMemo(() => {
18-
if (!proxies) {
19-
return {} as Record<string, ProxyLatencyReport>;
20-
}
21-
return proxies.reduce(
22-
(acc, proxy) => {
23-
acc[proxy.id] = {
24-
accurate: true,
25-
// Return a constant latency of 8ms.
26-
// If you make this random it could break stories.
27-
latencyMS: 8,
28-
at: new Date(),
29-
};
30-
return acc;
31-
},
32-
{} as Record<string, ProxyLatencyReport>,
33-
);
34-
}, [proxies]);
14+
useProxyLatency: (proxies?: Region[]) => {
15+
// Must use `useMemo` here to avoid infinite loop.
16+
// Mocking the hook with a hook.
17+
const proxyLatencies = useMemo(() => {
18+
if (!proxies) {
19+
return {} as Record<string, ProxyLatencyReport>;
20+
}
21+
return proxies.reduce(
22+
(acc, proxy) => {
23+
acc[proxy.id] = {
24+
accurate: true,
25+
// Return a constant latency of 8ms.
26+
// If you make this random it could break stories.
27+
latencyMS: 8,
28+
at: new Date(),
29+
};
30+
return acc;
31+
},
32+
{} as Record<string, ProxyLatencyReport>,
33+
);
34+
}, [proxies]);
3535

36-
return { proxyLatencies, refetch: jest.fn() };
37-
},
36+
return { proxyLatencies, refetch: jest.fn() };
37+
},
3838
}));
3939

4040
global.scrollTo = jest.fn();
@@ -43,28 +43,30 @@ window.HTMLElement.prototype.scrollIntoView = jest.fn();
4343
window.open = jest.fn();
4444
navigator.sendBeacon = jest.fn();
4545

46+
global.ResizeObserver = require("resize-observer-polyfill");
47+
4648
// Polyfill the getRandomValues that is used on utils/random.ts
4749
Object.defineProperty(global.self, "crypto", {
48-
value: {
49-
getRandomValues: function (buffer: Buffer) {
50-
return crypto.randomFillSync(buffer);
51-
},
52-
},
50+
value: {
51+
getRandomValues: function (buffer: Buffer) {
52+
return crypto.randomFillSync(buffer);
53+
},
54+
},
5355
});
5456

5557
// Establish API mocking before all tests through MSW.
5658
beforeAll(() =>
57-
server.listen({
58-
onUnhandledRequest: "warn",
59-
}),
59+
server.listen({
60+
onUnhandledRequest: "warn",
61+
}),
6062
);
6163

6264
// Reset any request handlers that we may add during the tests,
6365
// so they don't affect other tests.
6466
afterEach(() => {
65-
cleanup();
66-
server.resetHandlers();
67-
jest.resetAllMocks();
67+
cleanup();
68+
server.resetHandlers();
69+
jest.resetAllMocks();
6870
});
6971

7072
// Clean up after the tests are finished.

site/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"react-virtualized-auto-sizer": "1.0.24",
9191
"react-window": "1.8.10",
9292
"remark-gfm": "4.0.0",
93+
"resize-observer-polyfill": "1.5.1",
9394
"rollup-plugin-visualizer": "5.12.0",
9495
"semver": "7.6.2",
9596
"tzdata": "1.0.40",

site/pnpm-lock.yaml

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ describe("WorkspaceBuildPage", () => {
6565

6666
test("shows selected agent logs", async () => {
6767
const server = new WS(
68-
`ws://localhost/api/v2/workspaceagents/${MockWorkspaceAgent.id}/logs?follow&after=0`,
68+
`ws://localhost/api/v2/workspaceagents/${
69+
MockWorkspaceAgent.id
70+
}/logs?follow&after=0`,
6971
);
7072
renderWithAuth(<WorkspaceBuildPage />, {
7173
route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}/builds/${MockWorkspace.latest_build.build_number}?${LOGS_TAB_KEY}=${MockWorkspaceAgent.id}`,

site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx

+49-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,14 @@ import {
2424
WorkspaceBuildDataSkeleton,
2525
} from "modules/workspaces/WorkspaceBuildData/WorkspaceBuildData";
2626
import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs";
27-
import type { FC } from "react";
27+
import {
28+
type CSSProperties,
29+
type FC,
30+
type HTMLProps,
31+
useLayoutEffect,
32+
useRef,
33+
useState,
34+
} from "react";
2835
import { Link } from "react-router-dom";
2936
import { displayWorkspaceBuildDuration } from "utils/workspace";
3037
import { Sidebar, SidebarCaption, SidebarItem } from "./Sidebar";
@@ -144,7 +151,7 @@ export const WorkspaceBuildPageView: FC<WorkspaceBuildPageViewProps> = ({
144151
))}
145152
</Sidebar>
146153

147-
<div css={{ height: "100%", overflowY: "auto", width: "100%" }}>
154+
<ScrollArea>
148155
<Tabs active={tabState.value}>
149156
<TabsList>
150157
<TabLink to={`?${LOGS_TAB_KEY}=build`} value="build">
@@ -197,12 +204,51 @@ export const WorkspaceBuildPageView: FC<WorkspaceBuildPageViewProps> = ({
197204
agent={selectedAgent!}
198205
/>
199206
)}
200-
</div>
207+
</ScrollArea>
201208
</div>
202209
</DashboardFullPage>
203210
);
204211
};
205212

213+
const ScrollArea: FC<HTMLProps<HTMLDivElement>> = (props) => {
214+
// TODO: Use only CSS to set the height of the content.
215+
// Note: On Safari, when content is rendered inside a flex container and needs
216+
// to scroll, the parent container must have a height set. Achieving this may
217+
// require significant refactoring of the layout components where we currently
218+
// use height and min-height set to 100%.
219+
// Issue: https://github.com/coder/coder/issues/9687
220+
// Reference: https://stackoverflow.com/questions/43381836/height100-works-in-chrome-but-not-in-safari
221+
const contentRef = useRef<HTMLDivElement>(null);
222+
const [height, setHeight] = useState<CSSProperties["height"]>("100%");
223+
useLayoutEffect(() => {
224+
const contentEl = contentRef.current;
225+
if (!contentEl) {
226+
return;
227+
}
228+
229+
const resizeObserver = new ResizeObserver(() => {
230+
const parentEl = contentEl.parentElement;
231+
if (!parentEl) {
232+
return;
233+
}
234+
setHeight(parentEl.clientHeight);
235+
});
236+
resizeObserver.observe(document.body);
237+
238+
return () => {
239+
resizeObserver.disconnect();
240+
};
241+
}, []);
242+
243+
return (
244+
<div
245+
ref={contentRef}
246+
css={{ height, overflowY: "auto", width: "100%" }}
247+
{...props}
248+
/>
249+
);
250+
};
251+
206252
const BuildLogsContent: FC<{ logs?: ProvisionerJobLog[] }> = ({ logs }) => {
207253
if (!logs) {
208254
return <Loader />;

0 commit comments

Comments
 (0)