Skip to content

Commit c55ee7f

Browse files
committed
ui/jobs: add inline iframe viewer for execution detail files
Add an ExecutionDetailViewer modal component that renders viewable execution detail files (html, txt) inline in an iframe using blob URLs. This avoids the popup blocker issue entirely for the inline case and provides a better UX than opening a new tab. The View button is now shown only for viewable file types (html, txt). Non-viewable files (zip, binpb) show only the Download button. The viewer modal includes maximize and open-in-new-tab controls. Epic: None Release note: None
1 parent dcce97a commit c55ee7f

4 files changed

Lines changed: 265 additions & 121 deletions

File tree

pkg/ui/ui.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const (
3636
cspHeader = "default-src 'self'; " +
3737
"style-src 'self' 'unsafe-inline'; " +
3838
"font-src 'self' data:; " +
39+
"frame-src 'self' blob: https://cockroachdb.github.io; " +
3940
"img-src 'self' data:; " +
4041
"connect-src 'self' https://register.cockroachdb.com;"
4142
)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2026 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the CockroachDB Software License
4+
// included in the /LICENSE file.
5+
6+
import { Button, Icon } from "@cockroachlabs/ui-components";
7+
import classnames from "classnames/bind";
8+
import React, { useCallback } from "react";
9+
10+
import styles from "./jobProfilerView.module.scss";
11+
12+
const cx = classnames.bind(styles);
13+
14+
interface ExecutionDetailViewerProps {
15+
filename: string;
16+
blobUrl: string;
17+
onClose: () => void;
18+
}
19+
20+
export const ExecutionDetailViewer: React.FC<ExecutionDetailViewerProps> = ({
21+
filename,
22+
blobUrl,
23+
onClose,
24+
}) => {
25+
const openInNewTab = useCallback(() => {
26+
// Blob URL already exists so this is synchronous and won't
27+
// trigger popup blockers.
28+
window.open(blobUrl, "_blank");
29+
}, [blobUrl]);
30+
31+
return (
32+
<div className={cx("viewer-overlay")} onClick={onClose}>
33+
<div className={cx("viewer-panel")} onClick={e => e.stopPropagation()}>
34+
<div className={cx("viewer-header")}>
35+
<span className={cx("viewer-filename")}>{filename}</span>
36+
<span className={cx("viewer-actions")}>
37+
<Button
38+
as="a"
39+
size="small"
40+
intent="tertiary"
41+
onClick={openInNewTab}
42+
aria-label="Open in new tab"
43+
>
44+
<Icon iconName="Open" />
45+
</Button>
46+
<Button
47+
as="a"
48+
size="small"
49+
intent="tertiary"
50+
onClick={onClose}
51+
aria-label="Close"
52+
>
53+
<Icon iconName="Cancel" />
54+
</Button>
55+
</span>
56+
</div>
57+
<iframe
58+
src={blobUrl}
59+
className={cx("viewer-iframe")}
60+
sandbox="allow-same-origin allow-scripts"
61+
title={filename}
62+
/>
63+
</div>
64+
</div>
65+
);
66+
};

pkg/ui/workspaces/cluster-ui/src/jobs/jobDetailsPage/jobProfilerView.module.scss

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,96 @@
66
@import "src/core/index.module";
77

88
.crl-job-profiler-view {
9-
&__actions-column {
10-
display: flex;
11-
flex-direction: row;
12-
flex-wrap: nowrap;
13-
justify-content: flex-end;
14-
}
9+
&__actions-column {
10+
display: flex;
11+
flex-direction: row;
12+
flex-wrap: nowrap;
13+
justify-content: flex-end;
14+
}
1515
}
1616

1717
.column-size-medium {
18-
width: 230px;
18+
width: 230px;
1919
}
2020

2121
.view-execution-detail-button {
22-
white-space: nowrap;
22+
white-space: nowrap;
2323

24-
>svg {
25-
margin-right: $spacing-x-small;
26-
}
24+
> svg {
25+
margin-right: $spacing-x-small;
26+
}
2727
}
2828

2929
.download-execution-detail-button {
30-
white-space: nowrap;
30+
white-space: nowrap;
3131

32-
>svg {
33-
margin-right: $spacing-x-small;
34-
}
32+
> svg {
33+
margin-right: $spacing-x-small;
34+
}
3535
}
3636

3737
.full-width {
3838
width: 100%;
3939
}
40+
41+
// Execution detail viewer overlay and panel.
42+
.viewer-overlay {
43+
position: fixed;
44+
top: 0;
45+
left: 0;
46+
right: 0;
47+
bottom: 0;
48+
background: rgba(0, 0, 0, 0.15);
49+
z-index: 1000;
50+
display: flex;
51+
align-items: center;
52+
justify-content: center;
53+
}
54+
55+
.viewer-panel {
56+
background-color: white;
57+
border: 0.5px solid $colors--neutral-2;
58+
border-radius: 8px;
59+
box-shadow: 0 0 4px rgba(154, 161, 171, 0.33);
60+
width: 98vw;
61+
height: 98vh;
62+
display: flex;
63+
flex-direction: column;
64+
overflow: hidden;
65+
}
66+
67+
.viewer-header {
68+
display: flex;
69+
align-items: center;
70+
justify-content: space-between;
71+
padding: 12px 16px;
72+
border-bottom: 0.5px solid $colors--neutral-2;
73+
}
74+
75+
.viewer-filename {
76+
font-family: $font-family--base;
77+
font-weight: $font-weight--bold;
78+
font-size: $font-size--small;
79+
line-height: $line-height--small;
80+
letter-spacing: 0.3px;
81+
color: $colors--neutral-7;
82+
overflow: hidden;
83+
text-overflow: ellipsis;
84+
white-space: nowrap;
85+
min-width: 0;
86+
}
87+
88+
.viewer-actions {
89+
display: flex;
90+
gap: 8px;
91+
flex-shrink: 0;
92+
margin-left: 16px;
93+
}
94+
95+
.viewer-iframe {
96+
width: 100%;
97+
border: none;
98+
display: block;
99+
flex: 1;
100+
min-height: 0;
101+
}

0 commit comments

Comments
 (0)