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.
- package/lib/components/error-dialog.d.ts.map +1 -1
- package/lib/components/error-dialog.js +3 -5
- package/lib/components/error-dialog.js.map +1 -1
- package/lib/components/scan-admin/scan-card/scan-card-content.d.ts +3 -3
- package/lib/components/scan-admin/scan-card/scan-card-content.d.ts.map +1 -1
- package/lib/components/scan-admin/scan-card/scan-card-content.js +146 -169
- package/lib/components/scan-admin/scan-card/scan-card-content.js.map +1 -1
- package/lib/components/scan-admin/scan-card/scan-card-header.d.ts.map +1 -1
- package/lib/components/scan-admin/scan-card/scan-card-header.js +17 -5
- package/lib/components/scan-admin/scan-card/scan-card-header.js.map +1 -1
- package/lib/components/scan-admin/scan-card/scan-card.js +1 -1
- package/lib/components/scan-admin/scan-card/scan-card.js.map +1 -1
- package/lib/components/timestamp.d.ts +1 -0
- package/lib/components/timestamp.d.ts.map +1 -1
- package/lib/components/timestamp.js +3 -2
- package/lib/components/timestamp.js.map +1 -1
- package/lib/default/menu-content.d.ts +1 -4
- package/lib/default/menu-content.d.ts.map +1 -1
- package/lib/default/menu-content.js +8 -16
- package/lib/default/menu-content.js.map +1 -1
- package/lib/extension-registry-service.js +1 -1
- package/lib/extension-registry-service.js.map +1 -1
- package/lib/extension-registry-types.d.ts +2 -0
- package/lib/extension-registry-types.d.ts.map +1 -1
- package/lib/extension-registry-types.js.map +1 -1
- package/lib/hooks/scan-admin/use-query-params-state.d.ts +15 -0
- package/lib/hooks/scan-admin/use-query-params-state.d.ts.map +1 -0
- package/lib/hooks/scan-admin/use-query-params-state.js +44 -0
- package/lib/hooks/scan-admin/use-query-params-state.js.map +1 -0
- package/lib/pages/admin-dashboard/admin-dashboard.d.ts.map +1 -1
- package/lib/pages/admin-dashboard/admin-dashboard.js +2 -2
- package/lib/pages/admin-dashboard/admin-dashboard.js.map +1 -1
- package/lib/pages/admin-dashboard/extension-admin.d.ts.map +1 -1
- package/lib/pages/admin-dashboard/extension-admin.js +4 -3
- package/lib/pages/admin-dashboard/extension-admin.js.map +1 -1
- package/lib/pages/admin-dashboard/namespace-input.d.ts +1 -0
- package/lib/pages/admin-dashboard/namespace-input.d.ts.map +1 -1
- package/lib/pages/admin-dashboard/namespace-input.js +2 -2
- package/lib/pages/admin-dashboard/namespace-input.js.map +1 -1
- package/lib/pages/extension-detail/extension-detail-overview.js +1 -1
- package/lib/pages/extension-detail/extension-detail-overview.js.map +1 -1
- package/lib/pages/extension-detail/extension-review-dialog.d.ts.map +1 -1
- package/lib/pages/extension-detail/extension-review-dialog.js +2 -11
- package/lib/pages/extension-detail/extension-review-dialog.js.map +1 -1
- package/lib/pages/user/avatar.d.ts.map +1 -1
- package/lib/pages/user/avatar.js +9 -9
- package/lib/pages/user/avatar.js.map +1 -1
- package/lib/pages/user/logout.d.ts +3 -2
- package/lib/pages/user/logout.d.ts.map +1 -1
- package/lib/pages/user/logout.js +5 -4
- package/lib/pages/user/logout.js.map +1 -1
- package/lib/pages/user/user-settings-extensions.js +1 -1
- package/lib/pages/user/user-settings-extensions.js.map +1 -1
- package/lib/pages/user/user-settings-tokens.d.ts.map +1 -1
- package/lib/pages/user/user-settings-tokens.js +1 -1
- package/lib/pages/user/user-settings-tokens.js.map +1 -1
- package/lib/server-request.d.ts.map +1 -1
- package/lib/server-request.js +5 -1
- package/lib/server-request.js.map +1 -1
- package/lib/utils.d.ts +1 -1
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +56 -22
- package/lib/utils.js.map +1 -1
- package/package.json +5 -4
- package/src/components/error-dialog.tsx +3 -5
- package/src/components/scan-admin/scan-card/scan-card-content.tsx +380 -408
- package/src/components/scan-admin/scan-card/scan-card-header.tsx +35 -6
- package/src/components/scan-admin/scan-card/scan-card.tsx +2 -2
- package/src/components/timestamp.tsx +3 -1
- package/src/default/menu-content.tsx +70 -96
- package/src/extension-registry-service.ts +1 -1
- package/src/extension-registry-types.ts +2 -0
- package/src/hooks/scan-admin/use-query-params-state.ts +55 -0
- package/src/pages/admin-dashboard/admin-dashboard.tsx +2 -1
- package/src/pages/admin-dashboard/extension-admin.tsx +4 -2
- package/src/pages/admin-dashboard/namespace-input.tsx +3 -1
- package/src/pages/extension-detail/extension-detail-overview.tsx +1 -1
- package/src/pages/extension-detail/extension-review-dialog.tsx +2 -12
- package/src/pages/user/avatar.tsx +30 -35
- package/src/pages/user/logout.tsx +6 -4
- package/src/pages/user/user-settings-extensions.tsx +1 -1
- package/src/pages/user/user-settings-tokens.tsx +1 -0
- package/src/server-request.ts +5 -1
- package/src/utils.ts +47 -19
|
@@ -11,447 +11,419 @@
|
|
|
11
11
|
* SPDX-License-Identifier: EPL-2.0
|
|
12
12
|
********************************************************************************/
|
|
13
13
|
|
|
14
|
-
import {
|
|
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 {
|
|
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
|
-
*
|
|
39
|
-
*
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
141
|
-
</
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
<
|
|
218
|
-
className='checkbox-circle-outline'
|
|
171
|
+
<WarningAmberIcon
|
|
219
172
|
sx={{
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
<
|
|
190
|
+
return (
|
|
191
|
+
<>
|
|
192
|
+
<CaptionLabel>Download</CaptionLabel>
|
|
193
|
+
<NotAvailable />
|
|
194
|
+
</>
|
|
195
|
+
);
|
|
196
|
+
};
|
|
255
197
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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='
|
|
350
|
-
color='text.secondary'
|
|
351
|
-
display='block'
|
|
328
|
+
variant='h6'
|
|
352
329
|
sx={{
|
|
353
|
-
|
|
354
|
-
|
|
330
|
+
fontWeight: 700,
|
|
331
|
+
color: isAllowed ? theme.palette.allowed : theme.palette.blocked,
|
|
355
332
|
whiteSpace: 'nowrap',
|
|
333
|
+
cursor: 'help',
|
|
356
334
|
}}
|
|
357
335
|
>
|
|
358
|
-
|
|
336
|
+
{isAllowed ? 'ALLOWED' : 'BLOCKED'}
|
|
359
337
|
</Typography>
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
+
);
|