Skip to content

Commit 94d9f86

Browse files
Merge pull request #1315 from topcoder-platform/PM-2179
Pm 2179
2 parents 38733b6 + e4be6d9 commit 94d9f86

File tree

6 files changed

+425
-14
lines changed

6 files changed

+425
-14
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
@import '@libs/ui/styles/includes';
2+
3+
.tableWrapper {
4+
min-height: calc($content-height - 250px);
5+
font-size: 14px;
6+
7+
&:global(.enhanced-table) {
8+
table {
9+
th,
10+
td {
11+
text-align: left;
12+
background-color: white;
13+
}
14+
15+
}
16+
}
17+
}
18+
19+
.tableCell {
20+
vertical-align: middle;
21+
}
22+
23+
.filenameCell {
24+
display: flex;
25+
align-items: center;
26+
gap: 6px;
27+
color: $link-blue-dark;
28+
cursor: pointer;
29+
opacity: 1;
30+
transition: opacity 0.15s ease;
31+
32+
&:hover {
33+
text-decoration: underline;
34+
}
35+
}
36+
37+
.downloading {
38+
cursor: wait;
39+
opacity: 0.6;
40+
}
41+
42+
.expired {
43+
cursor: not-allowed;
44+
color: #999;
45+
46+
&:hover {
47+
text-decoration: none;
48+
}
49+
}
50+
51+
.artifactType {
52+
display: flex;
53+
gap: 5px;
54+
align-items: center;
55+
56+
.artifactIcon {
57+
stroke: #00797A;
58+
}
59+
}
60+
61+
.noAttachmentText {
62+
text-align: center;
63+
}
64+
65+
.mobileRow {
66+
padding: 16px 8px;
67+
border-bottom: 1px solid #A8A8A8;
68+
}
69+
70+
.mobileHeader {
71+
display: flex;
72+
gap: 12px;
73+
}
74+
75+
.mobileExpanded {
76+
padding: 16px 20px 0px 32px;
77+
}
78+
79+
.rowItem {
80+
display: flex;
81+
justify-content: space-between;
82+
}
83+
84+
.rowItemHeading {
85+
font-weight: 700;
86+
color: #0a0a0a;
87+
}
Lines changed: 196 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,204 @@
1-
import { FC } from 'react'
1+
import { FC, useCallback, useMemo, useState } from 'react'
2+
import { noop } from 'lodash'
3+
import classNames from 'classnames'
4+
import moment from 'moment'
25

3-
// import styles from './ScorecardAttachments.module.scss'
6+
import { IconOutline, Table, TableColumn } from '~/libs/ui'
7+
import { useReviewsContext } from '~/apps/review/src/pages/reviews/ReviewsContext'
8+
import { useWindowSize, WindowSize } from '~/libs/shared'
9+
10+
import { AiWorkflowRunArtifact,
11+
AiWorkflowRunArtifactDownloadResponse,
12+
AiWorkflowRunAttachmentsResponse,
13+
useDownloadAiWorkflowsRunArtifact, useFetchAiWorkflowsRunAttachments } from '../../../hooks'
14+
import { TableWrapper } from '../../TableWrapper'
15+
import { TABLE_DATE_FORMAT } from '../../../constants'
16+
import { formatFileSize } from '../../common'
17+
import { ReviewsContextModel } from '../../../models'
18+
19+
import styles from './ScorecardAttachments.module.scss'
420

521
interface ScorecardAttachmentsProps {
622
className?: string
723
}
824

9-
const ScorecardAttachments: FC<ScorecardAttachmentsProps> = props => (
10-
<div className={props.className}>
11-
attachments
12-
</div>
13-
)
25+
const ScorecardAttachments: FC<ScorecardAttachmentsProps> = (props: ScorecardAttachmentsProps) => {
26+
const className = props.className
27+
const { width: screenWidth }: WindowSize = useWindowSize()
28+
const isTablet = useMemo(() => screenWidth <= 1000, [screenWidth])
29+
const { workflowId, workflowRun }: ReviewsContextModel = useReviewsContext()
30+
const { artifacts }: AiWorkflowRunAttachmentsResponse
31+
= useFetchAiWorkflowsRunAttachments(workflowId, workflowRun?.id)
32+
const { download, isDownloading }: AiWorkflowRunArtifactDownloadResponse = useDownloadAiWorkflowsRunArtifact(
33+
workflowId,
34+
workflowRun?.id,
35+
)
36+
37+
const handleDownload = useCallback(
38+
async (artifactId: number): Promise<void> => {
39+
await download(artifactId)
40+
},
41+
[download],
42+
)
43+
44+
const createDownloadHandler = useCallback(
45+
(id: number) => () => handleDownload(id),
46+
[handleDownload],
47+
)
48+
49+
const columns = useMemo<TableColumn<AiWorkflowRunArtifact>[]>(
50+
() => [
51+
{
52+
className: classNames(styles.tableCell),
53+
label: 'Filename',
54+
propertyName: 'name',
55+
renderer: (attachment: AiWorkflowRunArtifact) => {
56+
const isExpired = attachment.expired
57+
58+
return (
59+
<div
60+
className={classNames(
61+
styles.filenameCell,
62+
{
63+
[styles.expired]: isExpired,
64+
[styles.downloading]: isDownloading && !isExpired,
65+
},
66+
)}
67+
onClick={!isExpired ? createDownloadHandler(attachment.id) : undefined}
68+
>
69+
<span>{attachment.name}</span>
70+
{isExpired && <span>(Link Expired)</span>}
71+
</div>
72+
)
73+
},
74+
type: 'element',
75+
},
76+
{
77+
className: classNames(styles.tableCell),
78+
label: 'Type',
79+
renderer: () => (
80+
<div className={styles.artifactType}>
81+
<IconOutline.CubeIcon className={styles.artifactIcon} width={26} />
82+
<span>Artifact</span>
83+
</div>
84+
),
85+
type: 'element',
86+
},
87+
{
88+
className: classNames(styles.tableCell),
89+
label: 'Size',
90+
propertyName: 'sizeInBytes',
91+
renderer: (attachment: AiWorkflowRunArtifact) => (
92+
<div>{formatFileSize(attachment.size_in_bytes)}</div>
93+
),
94+
type: 'element',
95+
},
96+
{
97+
className: styles.tableCell,
98+
label: 'Attached Date',
99+
renderer: (attachment: AiWorkflowRunArtifact) => (
100+
<span className='last-element'>
101+
{moment(attachment.created_at)
102+
.local()
103+
.format(TABLE_DATE_FORMAT)}
104+
</span>
105+
),
106+
type: 'element',
107+
},
108+
],
109+
[createDownloadHandler, isDownloading],
110+
)
111+
112+
const [openRow, setOpenRow] = useState<number | undefined>(undefined)
113+
const toggleRow = useCallback(
114+
(id: number) => () => {
115+
setOpenRow(prev => (prev === id ? undefined : id))
116+
},
117+
[],
118+
)
119+
const renderMobileRow = (attachment: AiWorkflowRunArtifact): JSX.Element => {
120+
const isExpired = attachment.expired
121+
const downloading = isDownloading
122+
const isOpen = openRow === attachment.id
123+
124+
return (
125+
<div key={attachment.id} className={styles.mobileRow}>
126+
{/* Top collapsed row */}
127+
<div className={styles.mobileHeader}>
128+
<IconOutline.ChevronDownIcon
129+
onClick={toggleRow(attachment.id)}
130+
className={classNames(styles.chevron, {
131+
[styles.open]: isOpen,
132+
})}
133+
width={20}
134+
/>
135+
<div
136+
className={classNames(styles.filenameCell, {
137+
[styles.expired]: isExpired,
138+
[styles.downloading]: downloading,
139+
})}
140+
onClick={!isExpired ? createDownloadHandler(attachment.id) : undefined}
141+
>
142+
{attachment.name}
143+
</div>
144+
145+
</div>
146+
147+
{/* Expanded content */}
148+
{isOpen && (
149+
<div className={styles.mobileExpanded}>
150+
<div className={styles.rowItem}>
151+
<span className={styles.rowItemHeading}>Type:</span>
152+
<div className={styles.artifactType}>
153+
<IconOutline.CubeIcon className={styles.artifactIcon} width={20} />
154+
Artifact
155+
</div>
156+
</div>
157+
158+
<div className={styles.rowItem}>
159+
<span className={styles.rowItemHeading}>Size:</span>
160+
{formatFileSize(attachment.size_in_bytes)}
161+
</div>
162+
163+
<div className={styles.rowItem}>
164+
<span className={styles.rowItemHeading}>Date:</span>
165+
{moment(attachment.created_at)
166+
.local()
167+
.format(TABLE_DATE_FORMAT)}
168+
</div>
169+
</div>
170+
)}
171+
</div>
172+
)
173+
}
174+
175+
return (
176+
<TableWrapper
177+
className={classNames(
178+
styles.tableWrapper,
179+
className,
180+
'enhanced-table',
181+
)}
182+
>
183+
{!artifacts || artifacts.length === 0 ? (
184+
<div className={styles.noAttachmentText}>No attachments</div>
185+
) : isTablet ? (
186+
<div className={styles.mobileList}>
187+
{artifacts.map(renderMobileRow)}
188+
</div>
189+
) : (
190+
<Table
191+
columns={columns}
192+
data={artifacts}
193+
disableSorting
194+
onToggleSort={noop}
195+
removeDefaultSort
196+
/>
197+
)}
198+
199+
</TableWrapper>
200+
)
201+
202+
}
14203

15204
export default ScorecardAttachments

src/apps/review/src/lib/components/common/columnUtils.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,28 @@ export const getProfileUrl = (handle: string): string => {
6969

7070
return `${normalizedBase}/${encodeURIComponent(handle)}`
7171
}
72+
73+
/**
74+
* converts size_in_bytes into KB / MB / GB with correct formatting.
75+
*/
76+
export const formatFileSize = (bytes: number): string => {
77+
if (!bytes || bytes < 0) return '0 B'
78+
79+
const KB = 1024
80+
const MB = KB * 1024
81+
const GB = MB * 1024
82+
83+
if (bytes >= GB) {
84+
return `${(bytes / GB).toFixed(2)} GB`
85+
}
86+
87+
if (bytes >= MB) {
88+
return `${(bytes / MB).toFixed(2)} MB`
89+
}
90+
91+
if (bytes >= KB) {
92+
return `${(bytes / KB).toFixed(2)} KB`
93+
}
94+
95+
return `${bytes} B`
96+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export const SUBMISSION_TYPE_CONTEST = 'CONTEST_SUBMISSION'
22
export const SUBMISSION_TYPE_CHECKPOINT = 'CHECKPOINT_SUBMISSION'
3+
export const TABLE_DATE_FORMAT = 'MMM DD, HH:mm A'

0 commit comments

Comments
 (0)