openvsx-webui-test 0.19.0-dev.0 → 0.19.0-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/lib/components/error-dialog.d.ts.map +1 -1
  2. package/lib/components/error-dialog.js +3 -5
  3. package/lib/components/error-dialog.js.map +1 -1
  4. package/lib/components/scan-admin/scan-card/scan-card-content.d.ts +3 -3
  5. package/lib/components/scan-admin/scan-card/scan-card-content.d.ts.map +1 -1
  6. package/lib/components/scan-admin/scan-card/scan-card-content.js +146 -169
  7. package/lib/components/scan-admin/scan-card/scan-card-content.js.map +1 -1
  8. package/lib/components/scan-admin/scan-card/scan-card-header.d.ts.map +1 -1
  9. package/lib/components/scan-admin/scan-card/scan-card-header.js +17 -5
  10. package/lib/components/scan-admin/scan-card/scan-card-header.js.map +1 -1
  11. package/lib/components/scan-admin/scan-card/scan-card.js +1 -1
  12. package/lib/components/scan-admin/scan-card/scan-card.js.map +1 -1
  13. package/lib/components/timestamp.d.ts +1 -0
  14. package/lib/components/timestamp.d.ts.map +1 -1
  15. package/lib/components/timestamp.js +3 -2
  16. package/lib/components/timestamp.js.map +1 -1
  17. package/lib/default/menu-content.d.ts +1 -4
  18. package/lib/default/menu-content.d.ts.map +1 -1
  19. package/lib/default/menu-content.js +8 -16
  20. package/lib/default/menu-content.js.map +1 -1
  21. package/lib/extension-registry-service.js +1 -1
  22. package/lib/extension-registry-service.js.map +1 -1
  23. package/lib/extension-registry-types.d.ts +2 -0
  24. package/lib/extension-registry-types.d.ts.map +1 -1
  25. package/lib/extension-registry-types.js.map +1 -1
  26. package/lib/hooks/scan-admin/use-query-params-state.d.ts +15 -0
  27. package/lib/hooks/scan-admin/use-query-params-state.d.ts.map +1 -0
  28. package/lib/hooks/scan-admin/use-query-params-state.js +44 -0
  29. package/lib/hooks/scan-admin/use-query-params-state.js.map +1 -0
  30. package/lib/pages/admin-dashboard/admin-dashboard.d.ts.map +1 -1
  31. package/lib/pages/admin-dashboard/admin-dashboard.js +2 -2
  32. package/lib/pages/admin-dashboard/admin-dashboard.js.map +1 -1
  33. package/lib/pages/admin-dashboard/extension-admin.d.ts.map +1 -1
  34. package/lib/pages/admin-dashboard/extension-admin.js +4 -3
  35. package/lib/pages/admin-dashboard/extension-admin.js.map +1 -1
  36. package/lib/pages/admin-dashboard/namespace-input.d.ts +1 -0
  37. package/lib/pages/admin-dashboard/namespace-input.d.ts.map +1 -1
  38. package/lib/pages/admin-dashboard/namespace-input.js +2 -2
  39. package/lib/pages/admin-dashboard/namespace-input.js.map +1 -1
  40. package/lib/pages/extension-detail/extension-detail-overview.js +1 -1
  41. package/lib/pages/extension-detail/extension-detail-overview.js.map +1 -1
  42. package/lib/pages/extension-detail/extension-review-dialog.d.ts.map +1 -1
  43. package/lib/pages/extension-detail/extension-review-dialog.js +2 -11
  44. package/lib/pages/extension-detail/extension-review-dialog.js.map +1 -1
  45. package/lib/pages/user/avatar.d.ts.map +1 -1
  46. package/lib/pages/user/avatar.js +9 -9
  47. package/lib/pages/user/avatar.js.map +1 -1
  48. package/lib/pages/user/logout.d.ts +3 -2
  49. package/lib/pages/user/logout.d.ts.map +1 -1
  50. package/lib/pages/user/logout.js +5 -4
  51. package/lib/pages/user/logout.js.map +1 -1
  52. package/lib/pages/user/user-settings-extensions.js +1 -1
  53. package/lib/pages/user/user-settings-extensions.js.map +1 -1
  54. package/lib/pages/user/user-settings-tokens.d.ts.map +1 -1
  55. package/lib/pages/user/user-settings-tokens.js +1 -1
  56. package/lib/pages/user/user-settings-tokens.js.map +1 -1
  57. package/lib/server-request.d.ts.map +1 -1
  58. package/lib/server-request.js +5 -1
  59. package/lib/server-request.js.map +1 -1
  60. package/lib/utils.d.ts +1 -1
  61. package/lib/utils.d.ts.map +1 -1
  62. package/lib/utils.js +56 -22
  63. package/lib/utils.js.map +1 -1
  64. package/package.json +5 -4
  65. package/src/components/error-dialog.tsx +3 -5
  66. package/src/components/scan-admin/scan-card/scan-card-content.tsx +380 -408
  67. package/src/components/scan-admin/scan-card/scan-card-header.tsx +35 -6
  68. package/src/components/scan-admin/scan-card/scan-card.tsx +2 -2
  69. package/src/components/timestamp.tsx +3 -1
  70. package/src/default/menu-content.tsx +70 -96
  71. package/src/extension-registry-service.ts +1 -1
  72. package/src/extension-registry-types.ts +2 -0
  73. package/src/hooks/scan-admin/use-query-params-state.ts +55 -0
  74. package/src/pages/admin-dashboard/admin-dashboard.tsx +2 -1
  75. package/src/pages/admin-dashboard/extension-admin.tsx +4 -2
  76. package/src/pages/admin-dashboard/namespace-input.tsx +3 -1
  77. package/src/pages/extension-detail/extension-detail-overview.tsx +1 -1
  78. package/src/pages/extension-detail/extension-review-dialog.tsx +2 -12
  79. package/src/pages/user/avatar.tsx +30 -35
  80. package/src/pages/user/logout.tsx +6 -4
  81. package/src/pages/user/user-settings-extensions.tsx +1 -1
  82. package/src/pages/user/user-settings-tokens.tsx +1 -0
  83. package/src/server-request.ts +5 -1
  84. package/src/utils.ts +47 -19
@@ -11,447 +11,419 @@
11
11
  * SPDX-License-Identifier: EPL-2.0
12
12
  ********************************************************************************/
13
13
 
14
- import { FC, useState } from "react";
14
+ import { FunctionComponent, PropsWithChildren, useState } from 'react';
15
15
  import { Box, Typography, Link, IconButton, Tooltip } from '@mui/material';
16
+ import { styled, useTheme } from '@mui/material/styles';
16
17
  import {
17
18
  Check as CheckIcon,
18
19
  Warning as WarningAmberIcon,
19
20
  } from '@mui/icons-material';
20
21
  import { ScanResult } from '../../../context/scan-admin';
21
22
  import { ConditionalTooltip, formatDateTime, formatDuration } from '../common';
22
- import { useTheme } from '@mui/material/styles';
23
- import {
24
- isRunning,
25
- hasDownload,
26
- getFileName,
27
- } from './utils';
28
-
29
- interface ScanCardContentProps {
30
- scan: ScanResult;
31
- showCheckbox?: boolean;
32
- checked?: boolean;
33
- onCheckboxChange?: (id: string, checked: boolean) => void;
34
- liveDuration: string;
35
- }
23
+ import { isRunning, hasDownload, getFileName } from './utils';
36
24
 
37
25
  /**
38
- * Content section of the ScanCard containing:
39
- * - Publisher, Version, Download (Row 2)
40
- * - Scan Start, Scan End, Duration, Decision Status (Row 3)
41
- * - Checkbox for selection
26
+ * Grid cell positioned by row/column within the parent CSS Grid.
27
+ * Note: MUI's Grid/Grid2 components are flexbox-based, not CSS Grid.
42
28
  */
43
- export const ScanCardContent: FC<ScanCardContentProps> = ({
44
- scan,
45
- showCheckbox,
46
- checked,
47
- onCheckboxChange,
48
- liveDuration,
49
- }) => {
50
- const theme = useTheme();
51
- const [isCheckboxHovering, setIsCheckboxHovering] = useState(false);
29
+ const GridCell = styled(Box, {
30
+ shouldForwardProp: (prop) => prop !== 'row' && prop !== 'column' && prop !== 'columnSpan',
31
+ })<{ row: number; column: number; columnSpan?: number }>(({ row, column, columnSpan }) => ({
32
+ gridRow: String(row),
33
+ gridColumn: columnSpan ? `${column} / span ${columnSpan}` : String(column),
34
+ minWidth: 0,
35
+ }));
52
36
 
53
- return (
54
- <>
55
- {/* ROW 2 - Publisher, Version, Download, Checkbox */}
56
- {/* Column 1: Empty (below icon) */}
57
- <Box sx={{ gridRow: '2', gridColumn: '1' }} />
37
+ /** Typography with text-overflow ellipsis */
38
+ const EllipsisText = styled(Typography)({
39
+ display: 'block',
40
+ overflow: 'hidden',
41
+ textOverflow: 'ellipsis',
42
+ whiteSpace: 'nowrap',
43
+ });
58
44
 
59
- {/* Column 2: Publisher */}
60
- <Box sx={{ gridRow: '2', gridColumn: '2', minWidth: 0 }}>
61
- <Typography
62
- variant='caption'
63
- color='text.secondary'
64
- display='block'
65
- sx={{
66
- overflow: 'hidden',
67
- textOverflow: 'ellipsis',
68
- whiteSpace: 'nowrap',
69
- }}
70
- >
71
- Publisher
72
- </Typography>
73
- <ConditionalTooltip title={scan.publisher} arrow>
74
- <Box
75
- sx={{
76
- overflow: 'hidden',
77
- textOverflow: 'ellipsis',
78
- whiteSpace: 'nowrap',
79
- }}
80
- >
81
- <Link
82
- href={scan.publisherUrl || undefined}
83
- target='_blank'
84
- rel='noopener noreferrer'
85
- variant='body2'
86
- sx={{
87
- overflow: 'hidden',
88
- textOverflow: 'ellipsis',
89
- whiteSpace: 'nowrap',
90
- display: 'block',
91
- }}
92
- >
93
- {scan.publisher}
94
- </Link>
95
- </Box>
96
- </ConditionalTooltip>
97
- </Box>
45
+ /** Box with text-overflow ellipsis */
46
+ const EllipsisBox = styled(Box)({
47
+ overflow: 'hidden',
48
+ textOverflow: 'ellipsis',
49
+ whiteSpace: 'nowrap',
50
+ });
98
51
 
99
- {/* Column 3: Version */}
100
- <Box sx={{ gridRow: '2', gridColumn: '3', minWidth: 0 }}>
101
- <Typography
102
- variant='caption'
103
- color='text.secondary'
104
- display='block'
105
- sx={{
106
- overflow: 'hidden',
107
- textOverflow: 'ellipsis',
108
- whiteSpace: 'nowrap',
109
- }}
110
- >
111
- Version
112
- </Typography>
113
- <ConditionalTooltip title={scan.version} arrow>
114
- <Typography
115
- variant='body2'
116
- sx={{
117
- display: 'block',
118
- overflow: 'hidden',
119
- textOverflow: 'ellipsis',
120
- whiteSpace: 'nowrap',
121
- }}
122
- >
123
- {scan.version}
124
- </Typography>
125
- </ConditionalTooltip>
126
- </Box>
52
+ /** Link with text-overflow ellipsis */
53
+ const EllipsisLink = styled(Link)({
54
+ display: 'block',
55
+ overflow: 'hidden',
56
+ textOverflow: 'ellipsis',
57
+ whiteSpace: 'nowrap',
58
+ });
127
59
 
128
- {/* Column 4: Download */}
129
- <Box sx={{ gridRow: '2', gridColumn: '4', minWidth: 0 }}>
130
- <Typography
131
- variant='caption'
132
- color='text.secondary'
133
- display='block'
134
- sx={{
135
- overflow: 'hidden',
136
- textOverflow: 'ellipsis',
137
- whiteSpace: 'nowrap',
138
- }}
60
+ /** Shimmer-animated text for in-progress scan states */
61
+ const ShimmerText = styled(EllipsisText)(({ theme }) => ({
62
+ background: theme.palette.gray.gradient,
63
+ backgroundSize: '200% 100%',
64
+ backgroundClip: 'text',
65
+ WebkitBackgroundClip: 'text',
66
+ color: 'transparent',
67
+ animation: 'shimmer 2s infinite',
68
+ '@keyframes shimmer': {
69
+ '0%': { backgroundPosition: '200% 0' },
70
+ '100%': { backgroundPosition: '-200% 0' },
71
+ },
72
+ }));
73
+
74
+ /** Circular checkbox outline with checked/hover state transitions */
75
+ const CheckboxOutline = styled(Box, {
76
+ shouldForwardProp: (prop) => prop !== 'isChecked' && prop !== 'isHovering',
77
+ })<{ isChecked: boolean; isHovering: boolean }>(({ theme, isChecked, isHovering }) => {
78
+ const borderColor = isHovering ? theme.palette.selected.border : theme.palette.scanBackground.light;
79
+ const uncheckedBg = isHovering ? theme.palette.selected.background : 'transparent';
80
+
81
+ return {
82
+ position: 'absolute',
83
+ width: 36,
84
+ height: 36,
85
+ borderRadius: '50%',
86
+ border: isChecked ? 'none' : `2px solid ${borderColor}`,
87
+ backgroundColor: isChecked ? theme.palette.secondary.main : uncheckedBg,
88
+ transition: 'border-color 0.2s, background-color 0.2s',
89
+ };
90
+ });
91
+
92
+ /** Section caption label (e.g. "Publisher", "Version") */
93
+ const CaptionLabel: FunctionComponent<PropsWithChildren> = ({ children }) => (
94
+ <EllipsisText variant='caption' color='text.secondary'>
95
+ {children}
96
+ </EllipsisText>
97
+ );
98
+
99
+ /** N/A placeholder for unavailable data */
100
+ const NotAvailable: FunctionComponent = () => (
101
+ <Typography variant='body2' color='text.disabled'>N/A</Typography>
102
+ );
103
+
104
+ /** Publisher name with external link */
105
+ const PublisherCell: FunctionComponent<{ publisher: string; publisherUrl: string | null }> = ({
106
+ publisher,
107
+ publisherUrl,
108
+ }) => (
109
+ <>
110
+ <CaptionLabel>Publisher</CaptionLabel>
111
+ <ConditionalTooltip title={publisher} arrow>
112
+ <EllipsisBox>
113
+ <EllipsisLink
114
+ href={publisherUrl || undefined}
115
+ target='_blank'
116
+ rel='noopener noreferrer'
117
+ variant='body2'
139
118
  >
140
- Download
141
- </Typography>
142
- {isRunning(scan.status) ? (
143
- <Typography variant='body2' color='text.disabled'>
144
- N/A
145
- </Typography>
146
- ) : hasDownload(scan) && scan.downloadUrl ? (
147
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, minWidth: 0 }}>
148
- {scan.status === 'QUARANTINED' && (
149
- <Tooltip
150
- title='Potentially malicious'
151
- arrow
152
- disableInteractive
153
- PopperProps={{
154
- disablePortal: true,
155
- sx: { pointerEvents: 'none' },
156
- }}
157
- >
158
- <WarningAmberIcon
159
- sx={{
160
- fontSize: 16,
161
- color: theme.palette.quarantined.dark,
162
- flexShrink: 0,
163
- }}
164
- />
165
- </Tooltip>
166
- )}
167
- <ConditionalTooltip title={getFileName(scan.downloadUrl)} arrow>
168
- <Link
169
- href={scan.downloadUrl}
170
- variant='body2'
171
- sx={{
172
- overflow: 'hidden',
173
- textOverflow: 'ellipsis',
174
- whiteSpace: 'nowrap',
175
- minWidth: 0,
176
- display: 'block',
177
- fontSize: '0.875rem',
178
- }}
179
- >
180
- {getFileName(scan.downloadUrl)}
181
- </Link>
182
- </ConditionalTooltip>
183
- </Box>
184
- ) : (
185
- <Typography variant='body2' color='text.disabled'>
186
- N/A
187
- </Typography>
188
- )}
189
- </Box>
119
+ {publisher}
120
+ </EllipsisLink>
121
+ </EllipsisBox>
122
+ </ConditionalTooltip>
123
+ </>
124
+ );
125
+
126
+ /** Extension version display */
127
+ const VersionCell: FunctionComponent<{ version: string }> = ({ version }) => (
128
+ <>
129
+ <CaptionLabel>Version</CaptionLabel>
130
+ <ConditionalTooltip title={version} arrow>
131
+ <EllipsisText variant='body2'>{version}</EllipsisText>
132
+ </ConditionalTooltip>
133
+ </>
134
+ );
190
135
 
191
- {/* Column 5: Checkbox */}
192
- <Box sx={{ gridRow: '2', gridColumn: '5', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', minWidth: 0 }}>
193
- {showCheckbox && (
194
- <IconButton
195
- onClick={() => onCheckboxChange?.(scan.id, !checked)}
196
- onMouseEnter={() => setIsCheckboxHovering(true)}
197
- onMouseLeave={() => setIsCheckboxHovering(false)}
198
- disableRipple
199
- sx={{
200
- padding: 0,
201
- width: 36,
202
- height: 36,
203
- backgroundColor: 'transparent',
204
- }}
205
- >
206
- <Box
207
- className='checkbox-circle'
208
- sx={{
209
- position: 'relative',
210
- width: 36,
211
- height: 36,
212
- display: 'flex',
213
- alignItems: 'center',
214
- justifyContent: 'center',
215
- }}
136
+ /** Target platform display */
137
+ const PlatformCell: FunctionComponent<{ targetPlatform: string }> = ({ targetPlatform }) => (
138
+ <>
139
+ <CaptionLabel>Platform</CaptionLabel>
140
+ <ConditionalTooltip title={targetPlatform} arrow>
141
+ <EllipsisText variant='body2'>{targetPlatform}</EllipsisText>
142
+ </ConditionalTooltip>
143
+ </>
144
+ );
145
+
146
+ /** Download link with optional quarantine warning icon */
147
+ const DownloadCell: FunctionComponent<{ scan: ScanResult }> = ({ scan }) => {
148
+ const theme = useTheme();
149
+
150
+ if (isRunning(scan.status)) {
151
+ return (
152
+ <>
153
+ <CaptionLabel>Download</CaptionLabel>
154
+ <NotAvailable />
155
+ </>
156
+ );
157
+ }
158
+
159
+ if (hasDownload(scan) && scan.downloadUrl) {
160
+ return (
161
+ <>
162
+ <CaptionLabel>Download</CaptionLabel>
163
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, minWidth: 0 }}>
164
+ {scan.status === 'QUARANTINED' && (
165
+ <Tooltip
166
+ title='Potentially malicious'
167
+ arrow
168
+ disableInteractive
169
+ PopperProps={{ disablePortal: true, sx: { pointerEvents: 'none' } }}
216
170
  >
217
- <Box
218
- className='checkbox-circle-outline'
171
+ <WarningAmberIcon
219
172
  sx={{
220
- position: 'absolute',
221
- width: 36,
222
- height: 36,
223
- borderRadius: '50%',
224
- border: checked ? 'none' : `2px solid ${isCheckboxHovering ? theme.palette.selected.border : theme.palette.scanBackground.light}`,
225
- backgroundColor: checked
226
- ? 'secondary.main'
227
- : isCheckboxHovering
228
- ? theme.palette.selected.background
229
- : 'transparent',
230
- transition: 'border-color 0.2s, background-color 0.2s',
173
+ fontSize: 16,
174
+ color: theme.palette.quarantined.dark,
175
+ flexShrink: 0,
231
176
  }}
232
177
  />
233
- <CheckIcon
234
- className='checkbox-icon'
235
- sx={{
236
- fontSize: 24,
237
- color: checked
238
- ? 'white'
239
- : isCheckboxHovering
240
- ? theme.palette.selected.border
241
- : theme.palette.scanBackground.light,
242
- position: 'relative',
243
- zIndex: 1,
244
- transition: 'color 0.2s',
245
- }}
246
- />
247
- </Box>
248
- </IconButton>
249
- )}
250
- </Box>
178
+ </Tooltip>
179
+ )}
180
+ <ConditionalTooltip title={getFileName(scan.downloadUrl)} arrow>
181
+ <EllipsisLink href={scan.downloadUrl} variant='body2' sx={{ fontSize: '0.875rem', minWidth: 0 }}>
182
+ {getFileName(scan.downloadUrl)}
183
+ </EllipsisLink>
184
+ </ConditionalTooltip>
185
+ </Box>
186
+ </>
187
+ );
188
+ }
251
189
 
252
- {/* ROW 3 - Scan Start, Scan End, Scan Duration, Decision Status */}
253
- {/* Column 1: Empty (below icon) */}
254
- <Box sx={{ gridRow: '3', gridColumn: '1' }} />
190
+ return (
191
+ <>
192
+ <CaptionLabel>Download</CaptionLabel>
193
+ <NotAvailable />
194
+ </>
195
+ );
196
+ };
255
197
 
256
- {/* Column 2: Scan Start */}
257
- <Box sx={{ gridRow: '3', gridColumn: '2', minWidth: 0 }}>
258
- <Typography
259
- variant='caption'
260
- color='text.secondary'
261
- display='block'
262
- sx={{
263
- overflow: 'hidden',
264
- textOverflow: 'ellipsis',
265
- whiteSpace: 'nowrap',
266
- }}
267
- >
268
- Scan Start
269
- </Typography>
270
- <ConditionalTooltip title={formatDateTime(scan.dateScanStarted)} arrow>
271
- <Typography
272
- variant='body2'
273
- sx={{
274
- display: 'block',
275
- fontSize: '0.8rem',
276
- overflow: 'hidden',
277
- textOverflow: 'ellipsis',
278
- whiteSpace: 'nowrap',
279
- }}
280
- >
281
- {formatDateTime(scan.dateScanStarted)}
282
- </Typography>
283
- </ConditionalTooltip>
284
- </Box>
198
+ /** Circular selection checkbox with hover/check animations */
199
+ const SelectionCheckbox: FunctionComponent<{
200
+ checked?: boolean;
201
+ onChange?: (checked: boolean) => void;
202
+ }> = ({ checked = false, onChange }) => {
203
+ const theme = useTheme();
204
+ const [isHovering, setIsHovering] = useState(false);
205
+ const uncheckedIconColor = isHovering ? theme.palette.selected.border : theme.palette.scanBackground.light;
285
206
 
286
- {/* Column 3: Scan End */}
287
- <Box sx={{ gridRow: '3', gridColumn: '3', minWidth: 0 }}>
288
- <Typography
289
- variant='caption'
290
- color='text.secondary'
291
- display='block'
207
+ return (
208
+ <IconButton
209
+ onClick={() => onChange?.(!checked)}
210
+ onMouseEnter={() => setIsHovering(true)}
211
+ onMouseLeave={() => setIsHovering(false)}
212
+ disableRipple
213
+ sx={{ padding: 0, width: 36, height: 36, backgroundColor: 'transparent' }}
214
+ >
215
+ <Box
216
+ className='checkbox-circle'
217
+ sx={{
218
+ position: 'relative',
219
+ width: 36,
220
+ height: 36,
221
+ display: 'flex',
222
+ alignItems: 'center',
223
+ justifyContent: 'center',
224
+ }}
225
+ >
226
+ <CheckboxOutline
227
+ className='checkbox-circle-outline'
228
+ isChecked={checked}
229
+ isHovering={isHovering}
230
+ />
231
+ <CheckIcon
232
+ className='checkbox-icon'
292
233
  sx={{
293
- overflow: 'hidden',
294
- textOverflow: 'ellipsis',
295
- whiteSpace: 'nowrap',
234
+ fontSize: 24,
235
+ color: checked ? 'white' : uncheckedIconColor,
236
+ position: 'relative',
237
+ zIndex: 1,
238
+ transition: 'color 0.2s',
296
239
  }}
297
- >
298
- Scan End
299
- </Typography>
300
- {isRunning(scan.status) ? (
301
- <ConditionalTooltip title={`${scan.status}...`} arrow>
302
- <Typography
303
- variant='body2'
304
- sx={{
305
- display: 'block',
306
- background: theme.palette.gray.gradient,
307
- backgroundSize: '200% 100%',
308
- backgroundClip: 'text',
309
- WebkitBackgroundClip: 'text',
310
- color: 'transparent',
311
- animation: 'shimmer 2s infinite',
312
- '@keyframes shimmer': {
313
- '0%': { backgroundPosition: '200% 0' },
314
- '100%': { backgroundPosition: '-200% 0' },
315
- },
316
- overflow: 'hidden',
317
- textOverflow: 'ellipsis',
318
- whiteSpace: 'nowrap',
319
- }}
320
- >
321
- {scan.status}...
322
- </Typography>
323
- </ConditionalTooltip>
324
- ) : scan.dateScanEnded ? (
325
- <ConditionalTooltip title={formatDateTime(scan.dateScanEnded)} arrow>
326
- <Typography
327
- variant='body2'
328
- sx={{
329
- display: 'block',
330
- fontSize: '0.8rem',
331
- overflow: 'hidden',
332
- textOverflow: 'ellipsis',
333
- whiteSpace: 'nowrap',
334
- }}
335
- >
336
- {formatDateTime(scan.dateScanEnded)}
337
- </Typography>
338
- </ConditionalTooltip>
339
- ) : (
340
- <Typography variant='body2' color='text.disabled'>
341
- N/A
342
- </Typography>
343
- )}
240
+ />
344
241
  </Box>
242
+ </IconButton>
243
+ );
244
+ };
245
+
246
+ /** Scan start timestamp */
247
+ const ScanStartCell: FunctionComponent<{ dateScanStarted: string }> = ({ dateScanStarted }) => (
248
+ <>
249
+ <CaptionLabel>Scan Start</CaptionLabel>
250
+ <ConditionalTooltip title={formatDateTime(dateScanStarted)} arrow>
251
+ <EllipsisText variant='body2' sx={{ fontSize: '0.8rem' }}>
252
+ {formatDateTime(dateScanStarted)}
253
+ </EllipsisText>
254
+ </ConditionalTooltip>
255
+ </>
256
+ );
257
+
258
+ /** Scan end timestamp with shimmer animation for running scans */
259
+ const ScanEndCell: FunctionComponent<{ status: ScanResult['status']; dateScanEnded: string | null }> = ({
260
+ status,
261
+ dateScanEnded,
262
+ }) => {
263
+ const renderValue = () => {
264
+ if (isRunning(status)) {
265
+ return (
266
+ <ConditionalTooltip title={`${status}...`} arrow>
267
+ <ShimmerText variant='body2'>{status}...</ShimmerText>
268
+ </ConditionalTooltip>
269
+ );
270
+ }
271
+ if (dateScanEnded) {
272
+ return (
273
+ <ConditionalTooltip title={formatDateTime(dateScanEnded)} arrow>
274
+ <EllipsisText variant='body2' sx={{ fontSize: '0.8rem' }}>
275
+ {formatDateTime(dateScanEnded)}
276
+ </EllipsisText>
277
+ </ConditionalTooltip>
278
+ );
279
+ }
280
+ return <NotAvailable />;
281
+ };
282
+
283
+ return (
284
+ <>
285
+ <CaptionLabel>Scan End</CaptionLabel>
286
+ {renderValue()}
287
+ </>
288
+ );
289
+ };
290
+
291
+ /** Scan duration with live counter for running scans */
292
+ const ScanDurationCell: FunctionComponent<{
293
+ status: ScanResult['status'];
294
+ dateScanStarted: string;
295
+ dateScanEnded: string | null;
296
+ liveDuration: string;
297
+ }> = ({ status, dateScanStarted, dateScanEnded, liveDuration }) => (
298
+ <>
299
+ <CaptionLabel>Scan Duration</CaptionLabel>
300
+ {isRunning(status) ? (
301
+ <ConditionalTooltip title={liveDuration} arrow>
302
+ <ShimmerText variant='body2'>{liveDuration}</ShimmerText>
303
+ </ConditionalTooltip>
304
+ ) : (
305
+ <ConditionalTooltip title={formatDuration(dateScanStarted, dateScanEnded || undefined)} arrow>
306
+ <EllipsisText variant='body2'>
307
+ {formatDuration(dateScanStarted, dateScanEnded || undefined)}
308
+ </EllipsisText>
309
+ </ConditionalTooltip>
310
+ )}
311
+ </>
312
+ );
313
+
314
+ /** Admin decision status for quarantined extensions */
315
+ const DecisionStatusCell: FunctionComponent<{ adminDecision: ScanResult['adminDecision'] }> = ({ adminDecision }) => {
316
+ const theme = useTheme();
345
317
 
346
- {/* Column 4: Scan Duration */}
347
- <Box sx={{ gridRow: '3', gridColumn: '4', minWidth: 0 }}>
318
+ if (adminDecision) {
319
+ const isAllowed = adminDecision.decision.toLowerCase() === 'allowed';
320
+ return (
321
+ <Tooltip
322
+ title={`Decided by ${adminDecision.decidedBy} on ${formatDateTime(adminDecision.dateDecided)}`}
323
+ arrow
324
+ disableInteractive
325
+ PopperProps={{ disablePortal: true, sx: { pointerEvents: 'none' } }}
326
+ >
348
327
  <Typography
349
- variant='caption'
350
- color='text.secondary'
351
- display='block'
328
+ variant='h6'
352
329
  sx={{
353
- overflow: 'hidden',
354
- textOverflow: 'ellipsis',
330
+ fontWeight: 700,
331
+ color: isAllowed ? theme.palette.allowed : theme.palette.blocked,
355
332
  whiteSpace: 'nowrap',
333
+ cursor: 'help',
356
334
  }}
357
335
  >
358
- Scan Duration
336
+ {isAllowed ? 'ALLOWED' : 'BLOCKED'}
359
337
  </Typography>
360
- {isRunning(scan.status) ? (
361
- <ConditionalTooltip title={liveDuration} arrow>
362
- <Typography
363
- variant='body2'
364
- sx={{
365
- display: 'block',
366
- background: theme.palette.gray.gradient,
367
- backgroundSize: '200% 100%',
368
- backgroundClip: 'text',
369
- WebkitBackgroundClip: 'text',
370
- color: 'transparent',
371
- animation: 'shimmer 2s infinite',
372
- '@keyframes shimmer': {
373
- '0%': { backgroundPosition: '200% 0' },
374
- '100%': { backgroundPosition: '-200% 0' },
375
- },
376
- overflow: 'hidden',
377
- textOverflow: 'ellipsis',
378
- whiteSpace: 'nowrap',
379
- }}
380
- >
381
- {liveDuration}
382
- </Typography>
383
- </ConditionalTooltip>
384
- ) : (
385
- <ConditionalTooltip title={formatDuration(scan.dateScanStarted, scan.dateScanEnded || undefined)} arrow>
386
- <Typography
387
- variant='body2'
388
- sx={{
389
- display: 'block',
390
- overflow: 'hidden',
391
- textOverflow: 'ellipsis',
392
- whiteSpace: 'nowrap',
393
- }}
394
- >
395
- {formatDuration(scan.dateScanStarted, scan.dateScanEnded || undefined)}
396
- </Typography>
397
- </ConditionalTooltip>
398
- )}
399
- </Box>
338
+ </Tooltip>
339
+ );
340
+ }
400
341
 
401
- {/* Column 5: Decision Status */}
402
- {scan.status === 'QUARANTINED' && scan.adminDecision && (
403
- <Box sx={{
404
- gridRow: '3',
405
- gridColumn: '5',
406
- display: 'flex',
407
- justifyContent: 'flex-end',
408
- alignSelf: 'end',
409
- minWidth: 0,
410
- }}>
411
- <Tooltip
412
- title={`Decided by ${scan.adminDecision.decidedBy} on ${formatDateTime(scan.adminDecision.dateDecided)}`}
413
- arrow
414
- disableInteractive
415
- PopperProps={{
416
- disablePortal: true,
417
- sx: { pointerEvents: 'none' },
418
- }}
419
- >
420
- <Typography
421
- variant='h6'
422
- sx={{
423
- fontWeight: 700,
424
- color: scan.adminDecision.decision.toLowerCase() === 'allowed' ? theme.palette.allowed : theme.palette.blocked,
425
- whiteSpace: 'nowrap',
426
- cursor: 'help',
427
- }}
428
- >
429
- {scan.adminDecision.decision.toLowerCase() === 'allowed' ? 'ALLOWED' : 'BLOCKED'}
430
- </Typography>
431
- </Tooltip>
432
- </Box>
433
- )}
434
- {scan.status === 'QUARANTINED' && !scan.adminDecision && (
435
- <Box sx={{
436
- gridRow: '3',
437
- gridColumn: '5',
438
- display: 'flex',
439
- justifyContent: 'flex-end',
440
- alignSelf: 'end',
441
- minWidth: 0,
442
- }}>
443
- <Typography
444
- variant='h6'
445
- sx={{
446
- fontWeight: 700,
447
- color: theme.palette.review,
448
- whiteSpace: 'nowrap',
449
- }}
450
- >
451
- NEEDS REVIEW
452
- </Typography>
453
- </Box>
454
- )}
455
- </>
342
+ return (
343
+ <Typography
344
+ variant='h6'
345
+ sx={{
346
+ fontWeight: 700,
347
+ color: theme.palette.review,
348
+ whiteSpace: 'nowrap',
349
+ }}
350
+ >
351
+ NEEDS REVIEW
352
+ </Typography>
456
353
  );
457
354
  };
355
+
356
+ interface ScanCardContentProps {
357
+ scan: ScanResult;
358
+ showCheckbox?: boolean;
359
+ checked?: boolean;
360
+ onCheckboxChange?: (id: string, checked: boolean) => void;
361
+ liveDuration: string;
362
+ }
363
+
364
+ /**
365
+ * Content section of the ScanCard containing:
366
+ * - Publisher, Version, Platform, Download (Row 2)
367
+ * - Scan Start, Scan End, Duration, Decision Status (Row 3)
368
+ * - Checkbox for selection
369
+ */
370
+ export const ScanCardContent: FunctionComponent<ScanCardContentProps> = ({
371
+ scan,
372
+ showCheckbox,
373
+ checked,
374
+ onCheckboxChange,
375
+ liveDuration,
376
+ }) => (
377
+ <>
378
+ {/* ROW 2 - Publisher, Version, Platform, Download, Checkbox */}
379
+ <GridCell row={2} column={2}>
380
+ <PublisherCell publisher={scan.publisher} publisherUrl={scan.publisherUrl} />
381
+ </GridCell>
382
+ <GridCell row={2} column={3}>
383
+ <VersionCell version={scan.version} />
384
+ </GridCell>
385
+ <GridCell row={2} column={4}>
386
+ <PlatformCell targetPlatform={scan.targetPlatform} />
387
+ </GridCell>
388
+ <GridCell row={2} column={5}>
389
+ <DownloadCell scan={scan} />
390
+ </GridCell>
391
+ <GridCell
392
+ row={2}
393
+ column={6}
394
+ sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}
395
+ >
396
+ {showCheckbox && (
397
+ <SelectionCheckbox
398
+ checked={checked}
399
+ onChange={(newChecked) => onCheckboxChange?.(scan.id, newChecked)}
400
+ />
401
+ )}
402
+ </GridCell>
403
+
404
+ {/* ROW 3 - Scan Start, Scan End, Duration, Decision Status */}
405
+ <GridCell row={3} column={2}>
406
+ <ScanStartCell dateScanStarted={scan.dateScanStarted} />
407
+ </GridCell>
408
+ <GridCell row={3} column={3}>
409
+ <ScanEndCell status={scan.status} dateScanEnded={scan.dateScanEnded} />
410
+ </GridCell>
411
+ <GridCell row={3} column={4}>
412
+ <ScanDurationCell
413
+ status={scan.status}
414
+ dateScanStarted={scan.dateScanStarted}
415
+ dateScanEnded={scan.dateScanEnded}
416
+ liveDuration={liveDuration}
417
+ />
418
+ </GridCell>
419
+ {scan.status === 'QUARANTINED' && (
420
+ <GridCell
421
+ row={3}
422
+ column={5} columnSpan={2}
423
+ sx={{ display: 'flex', justifyContent: 'flex-end', alignSelf: 'end' }}
424
+ >
425
+ <DecisionStatusCell adminDecision={scan.adminDecision} />
426
+ </GridCell>
427
+ )}
428
+ </>
429
+ );