feat: resource task add Image Manifest Url

Signed-off-by: zhaoxinxin <1186037180@qq.com>
This commit is contained in:
zhaoxinxin 2025-07-02 20:25:33 +08:00
parent 4f67755fa7
commit f033feccee
No known key found for this signature in database
GPG Key ID: F6AA8D23FFB07092
8 changed files with 332 additions and 127 deletions

View File

@ -14,7 +14,8 @@ describe('Clear', () => {
cy.viewport(1440, 1080);
});
it('when no data is loaded', () => {
describe('when no data is loaded', () => {
it('when search by url has no data to load', () => {
cy.get('#no-task').should('not.exist');
cy.get('#light').should('exist');
@ -66,6 +67,32 @@ describe('Clear', () => {
cy.get('#no-task').should('exist');
});
it('when search by image manifest url has no data to load', () => {
cy.get('#no-task').should('not.exist');
cy.get('#serach-image-manifest-url').click();
cy.intercept(
{
method: 'post',
url: '/api/v1/jobs',
},
async (req) => {
await new Promise((resolve) => setTimeout(resolve, 200));
req.reply({
statusCode: 200,
body: {},
});
},
);
cy.get('#image-manifest-url').type('https://example.com/path/to/file{enter}');
// Shou You don't find any results!
cy.get('#no-image-manifest-URL-task').should('exist').and('contain', `You don't find any results!`);
});
});
describe('when data is loaded', () => {
it('click the `CANCEL button', () => {
cy.get('#url').click();
@ -309,13 +336,13 @@ describe('Clear', () => {
// Show is loading.
cy.get('#isLoading').should('exist');
// Display cache information.
cy.get('#blobs').should('have.text', '5');
cy.get('#scheduler-id-0').should('exist', 'ID : 1');
cy.get('#isLoading').should('not.exist');
cy.get('#scheduler-1-hostname-0').should('have.text', 'kind-worker1');
cy.get('#scheduler-1-ip-0').should('have.text', '172.18.0.4');
cy.get('#scheduler-1-total-0').should('have.text', 'Total 2');
cy.get('#scheduler-1-proportion-0').should('contain', '60.00%');
// Should display URL.
cy.get('#scheduler-1-url-0').click();

View File

@ -24,10 +24,13 @@
"hostname": "kind-worker1",
"layers": [
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:f1ffc4b5459e82dc8e7ddd1d1a2ec469e85a1f076090c22851a1f2ce6f71e1a6"
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:f1ffc4b5459e82dc8e7ddd1d1a2ec469e85a1f076090c22851a1f2ce6f71e1a6?format=json"
},
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:c1d6d1b2d5a367259e6e51a7f4d1ccd66a28cc9940d6599d8a8ea9544dd4b4a8"
},
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:c1d6d1b2d5a367259e6e51a7f4d1ccd66a28cc9940d6599d8a8ea9544dd4b4a7"
}
],
"scheduler_cluster_id": 1

View File

@ -0,0 +1,6 @@
<svg t="1751448383394" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="56778" width="200" height="200">
<path
d="M118.979048 637.074286l137.99619 66.243047 255.171048 123.587048 246.076952-119.222857 147.163429-70.485334a73.142857 73.142857 0 0 1-34.230857 97.109334l-327.119239 158.427428a73.142857 73.142857 0 0 1-63.780571 0L153.136762 734.305524A73.142857 73.142857 0 0 1 118.979048 637.074286z m786.090666-153.063619a73.142857 73.142857 0 0 1-33.913904 97.767619L544.01219 740.205714a73.142857 73.142857 0 0 1-63.780571 0L153.136762 581.778286A73.142857 73.142857 0 0 1 117.51619 487.862857l362.300953 170.886095 32.329143 15.652572 327.119238-158.427429 65.80419-31.939047zM544.036571 139.190857l327.094858 158.403048a73.142857 73.142857 0 0 1 0 131.657143l-327.094858 158.427428a73.142857 73.142857 0 0 1-63.780571 0L153.136762 429.251048a73.142857 73.142857 0 0 1 0-131.657143L480.256 139.215238a73.142857 73.142857 0 0 1 63.780571 0z m-31.890285 65.828572L185.027048 363.422476l327.119238 158.427429 327.119238-158.427429L512.146286 205.04381z"
p-id="56779" fill="currentColor"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,12 @@
<svg t="1751448525528" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="61090" width="200" height="200">
<path
d="M539.450983 15.110813h-0.503689v467.927137h468.934515C1007.579596 224.544724 798.044947 15.110813 539.450983 15.110813z"
fill="currentColor" p-id="61091"></path>
<path
d="M486.563632 82.101458h-15.110672c-62.961132 0-125.2171 12.592226-183.141342 37.172252C114.337417 193.013789 1.309592 363.663642 1.208854 552.647778c-0.201476 260.105031 210.542027 471.150747 470.747795 471.352222h0.705165c259.903555-0.201476 470.445582-211.045716 470.244106-470.949271v-15.614361H486.563632v-455.33491z m425.919469 485.556254c-1.712543 54.196943-13.498867 107.487245-34.553069 157.4532-94.794281 224.141632-353.388244 328.908956-577.429139 234.114675C142.644742 892.33568 37.877417 740.221584 31.631673 569.068041c-8.76419-243.281816 181.328062-447.678837 424.710615-456.443026v455.032697h456.140813z"
fill="currentColor" p-id="61092"></path>
<path
d="M1022.992481 482.836474C1022.791005 215.98201 806.305447-0.201335 539.450983 0.000141H523.836623v498.14848h499.155858v-15.110671-0.201476zM554.057966 467.927278V30.42296c238.647877 7.454598 430.452671 198.856441 438.511696 437.504318H554.057966z"
fill="currentColor" p-id="61093"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -64,7 +64,7 @@
.schedulerClusterWrapper {
border: 1px solid #d5d2d2;
padding: 0.2rem 0.4rem;
padding: 0.1rem 0.3rem;
border-radius: 0.3rem;
display: inline-flex;
align-items: center;
@ -115,6 +115,29 @@
width: 100%;
}
.cacheHeader {
display: flex;
align-items: center;
margin: 1.5rem 0 1rem 0;
}
.bolbWrapper {
display: flex;
align-items: center;
margin-bottom: 1rem;
color: var(--palette-table-title-text-color);
}
.bolbText {
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
border-radius: 0.3rem;
background-color: var(--palette-background-inactive);
}
.hostnameContainer {
display: flex;
align-items: center;
@ -130,7 +153,6 @@
display: inline-flex;
padding: 0.3rem 0.5rem;
align-items: center;
margin-right: 1rem;
}
.hostnameIcon {
@ -142,7 +164,7 @@
border-color: var(--palette-palette-divider) !important;
background-color: var(--palette-background-inactive) !important;
border-radius: var(--menu-border-radius) !important;
padding: 0.6rem 0.4rem;
padding: 0.6rem 0.6rem;
display: flex;
align-items: center;
overflow: hidden;
@ -155,31 +177,57 @@
}
.urlIcon {
height: 1.2rem !important;
width: 8%;
height: 1.4rem !important;
color: var(--palette-detail-lable-color);
}
.clusterWrapper {
display: flex;
align-items: center;
margin-bottom: 1.5rem;
}
.cluster {
width: 1.5rem;
height: 1.5rem;
}
.cardCantainer {
gap: calc(1rem);
gap: calc(1.2rem);
display: grid;
grid-template-columns: repeat(2, 1fr);
}
.totalContainer {
.bolbProportionContainer {
display: flex;
align-items: center;
padding: 0.3rem;
background-color: var(--palette-grey-background-color) !important;
background-color: var(--palette-button-color) !important;
border-radius: var(--menu-border-radius) !important;
margin-right: 0.4rem;
color: var(--palette-scopes-icon-color);
}
.totalIcon {
.bolbIcon {
width: 1rem;
height: 1rem;
/* color: var(--palette-description-color); */
}
.totalText {
color: var(--palette-description-color);
padding-left: 0.3rem;
.bolbProportionText {
padding-left: 0.4rem;
}
.bolbIconWrapper {
display: inline-flex;
align-items: center;
border-radius: 0.6rem !important;
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) !important;
z-index: 0;
color: var(--palette-color) !important;
background-image: none;
padding: 0.4rem;
background-color: var(--palette-background-paper) !important;
box-shadow: var(--palette-card-box-shadow) !important;
}

View File

@ -31,8 +31,14 @@ import DeleteIcon from '@mui/icons-material/Delete';
import CloseIcon from '@mui/icons-material/Close';
import MoreTimeIcon from '@mui/icons-material/MoreTime';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { getTaskJobResponse, createTaskJob, getTaskJob, createGetImageDistributionJob } from '../../../../lib/api';
import { getDatetime, getPaginatedList } from '../../../../lib/utils';
import {
getTaskJobResponse,
createTaskJob,
getTaskJob,
createGetImageDistributionJob,
createGetImageDistributionJobResponse,
} from '../../../../lib/api';
import { extractSHA256Regex, getDatetime, getPaginatedList } from '../../../../lib/utils';
import _ from 'lodash';
import SearchTaskAnimation from '../../../search-task-animation';
import { useNavigate } from 'react-router-dom';
@ -55,6 +61,9 @@ import { ReactComponent as IP } from '../../../../assets/images/resource/task/cl
import { ReactComponent as Hostnames } from '../../../../assets/images/resource/task/clear-hostname.svg';
import { ReactComponent as URL } from '../../../../assets/images/job/preheat/url.svg';
import { ReactComponent as Total } from '../../../../assets/images/cluster/total.svg';
import { ReactComponent as Proportion } from '../../../../assets/images/resource/task/proportion.svg';
import { ReactComponent as Layer } from '../../../../assets/images/resource/task/layer.svg';
const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }) => ({
[`& .${toggleButtonGroupClasses.grouped}`]: {
@ -71,14 +80,18 @@ const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }) => ({
},
}));
type Layer = {
type Layers = {
url: string;
};
type Image = {
layers: Layers[];
};
type OriginalPeer = {
ip: string;
hostname: string;
layers: Layer[];
layers: Layers[];
scheduler_cluster_id?: number;
};
@ -89,12 +102,13 @@ type ClusteredPeer = {
type TransformedImage = {
peers: ClusteredPeer[];
image: Image;
};
function transformImages(images: { peers: OriginalPeer[] }): TransformedImage {
function transformImages(images: createGetImageDistributionJobResponse): TransformedImage {
const clusters = new Map<number, Omit<OriginalPeer, 'scheduler_cluster_id'>[]>();
for (const peer of images.peers) {
for (const peer of images.peers || []) {
const clusterId = peer.scheduler_cluster_id ?? 1;
if (!clusters.has(clusterId)) {
@ -115,9 +129,65 @@ function transformImages(images: { peers: OriginalPeer[] }): TransformedImage {
scheduler_cluster_id: id,
}));
return { peers: resultPeers };
return { peers: resultPeers, image: images.image };
}
const img = {
image: {
layers: [
{
url: 'https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:c7c72808bf776cd122bdaf4630a4a35ea319603d6a3b6cbffddd4c7fd6d2d269',
},
{
url: 'https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:9986a736f7d3d24bb01b0a560fa0f19c4b57e56c646e1f998941529d28710e6b',
},
{
url: 'https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:fdb99fa1c8f89464e911dce77afd5e26c6c09e9db625014431a4df3be4021359',
},
{
url: 'https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:fc5951fb196d09e569f4592b50e3a71ad01d11da229b8a500fea278eba0170c5',
},
{
url: 'https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:edbf1aa1d62d9c17605c1ee2d9dff43489bc0f8ae056367734386c35bfae226a',
},
{
url: 'https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:f7307687fd72fb79eadd7f38f8cb9675b76480e32365a5d282a06f788944e9f2',
},
],
},
peers: [
{
ip: '172.18.0.2',
hostname: 'kind-worker',
layers: [
{
url: 'https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:9986a736f7d3d24bb01b0a560fa0f19c4b57e56c646e1f998941529d28710e6b?token=abc123&format=json',
},
{
url: 'https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:fdb99fa1c8f89464e911dce77afd5e26c6c09e9db625014431a4df3be4021359',
},
{
url: 'https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:c7c72808bf776cd122bdaf4630a4a35ea319603d6a3b6cbffddd4c7fd6d2d269?token=abc123&format=json',
},
],
scheduler_cluster_id: 1,
},
{
ip: '172.18.0.4',
hostname: 'kind-worker2',
layers: [
{
url: 'https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:fdb99fa1c8f89464e911dce77afd5e26c6c09e9db625014431a4df3be4021359',
},
{
url: 'https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:c7c72808bf776cd122bdaf4630a4a35ea319603d6a3b6cbffddd4c7fd6d2d269',
},
],
scheduler_cluster_id: 1,
},
],
};
export default function Clear() {
const [errorMessage, setErrorMessage] = useState(false);
const [errorMessageText, setErrorMessageText] = useState('');
@ -156,12 +226,15 @@ export default function Clear() {
piece_length: 0,
});
const [imageManifestURL, setImageManifestURL] = useState<TransformedImage>();
const [layer, setLayer] = useState(0);
const [pageStates, setPageStates] = useState<any>({});
const { url, tag, application, filtered_query_params } = searchData;
const navigate = useNavigate();
const theme = useTheme();
const imagem = transformImages(img);
useEffect(() => {
const fetchJob = async () => {
try {
@ -861,7 +934,6 @@ export default function Clear() {
try {
event.preventDefault();
const data = new FormData(event.currentTarget);
const form = {
args: {
@ -871,12 +943,11 @@ export default function Clear() {
};
const imageManifest = await createGetImageDistributionJob(form);
const imageManifestTask = transformImages(imageManifest);
setImageManifestURL(imageManifestTask);
setLayer(imageManifestTask?.image?.layers?.length || 0);
setSearchImageManifestISLodaing(false);
const res = transformImages(imageManifest);
setImageManifestURL(res);
setIsLoading(false);
} catch (error) {
if (error instanceof Error) {
@ -984,6 +1055,30 @@ export default function Clear() {
<LinkOutlinedIcon sx={{ mr: '0.4rem' }} />
Search by URL
</ToggleButton>
<ToggleButton
id="serach-image-manifest-url"
value="image-manifest-url"
size="small"
sx={{
'&.Mui-selected': {
backgroundColor: 'var(--palette-save-color)',
color: '#FFFFFF',
boxShadow: 'rgba(145, 158, 171, 0.2) 0px 0px 2px 0px, rgba(145, 158, 171, 0.12) 0px 12px 24px -4px',
'&:hover': {
backgroundColor: 'var(--palette-save-color)',
},
},
'&:hover': {
backgroundColor: 'transparent',
},
p: '0.3rem 0.5rem',
color: 'var(--palette-dark-400Channel)',
textTransform: 'none',
}}
>
<ImageManifest className={styles.contentForCalculatingTaskIDIcon} />
Search by Image Manifest URL
</ToggleButton>
<ToggleButton
id="serach-task-id"
value="task-id"
@ -1032,30 +1127,6 @@ export default function Clear() {
<ContentForCalculatingTaskID className={styles.contentForCalculatingTaskIDIcon} />
Search by Calculating Task ID
</ToggleButton>
<ToggleButton
id="serach-image-manifest-url"
value="image-manifest-url"
size="small"
sx={{
'&.Mui-selected': {
backgroundColor: 'var(--palette-save-color)',
color: '#FFFFFF',
boxShadow: 'rgba(145, 158, 171, 0.2) 0px 0px 2px 0px, rgba(145, 158, 171, 0.12) 0px 12px 24px -4px',
'&:hover': {
backgroundColor: 'var(--palette-save-color)',
},
},
'&:hover': {
backgroundColor: 'transparent',
},
p: '0.3rem 0.5rem',
color: 'var(--palette-dark-400Channel)',
textTransform: 'none',
}}
>
<ImageManifest className={styles.contentForCalculatingTaskIDIcon} />
Search by Image Manifest URL
</ToggleButton>
</StyledToggleButtonGroup>
</Paper>
{search === 'task-id' ? (
@ -1218,7 +1289,7 @@ export default function Clear() {
<SchedulerCluster className={styles.schedulerClusterIcon} />
<Typography
id="schedulerTotal"
variant="subtitle2"
variant="caption"
fontFamily="mabry-bold"
component="div"
pl="0.3rem"
@ -1428,9 +1499,25 @@ export default function Clear() {
) : Array.isArray(imageManifestURL?.peers) ? (
imageManifestURL?.peers.length > 0 ? (
<Box>
<Typography variant="h6" m="1rem 0" fontFamily="mabry-bold">
<Box className={styles.cacheHeader}>
<Typography variant="h6" fontFamily="mabry-bold" mr="0.6rem">
Cache
</Typography>
</Box>
<Box className={styles.bolbWrapper}>
<Typography variant="body2" fontFamily="mabry-bold" component="div" pr="0.3rem">
Blobs
</Typography>
<Typography
id="blobs"
variant="body2"
fontFamily="mabry-bold"
component="div"
className={styles.bolbText}
>
{layer || 0}
</Typography>
</Box>
{imageManifestURL?.peers.map((item, index) => {
const schedulerClusterId = item.scheduler_cluster_id;
const totalPage = Math.ceil(item.peer.length / 5);
@ -1446,7 +1533,7 @@ export default function Clear() {
<Box className={styles.schedulerClusterWrapper} id={`scheduler-id-${index}`}>
<SchedulerCluster className={styles.schedulerClusterIcon} />
<Typography
variant="subtitle2"
variant="caption"
fontFamily="mabry-bold"
component="div"
pl="0.3rem"
@ -1457,9 +1544,9 @@ export default function Clear() {
</Box>
</Box>
<Divider />
{paginatedPeers?.map((items, index) => {
{paginatedPeers?.map((items, peerIndex) => {
return (
<Box key={index}>
<Box key={peerIndex}>
<Accordion
disableGutters
elevation={0}
@ -1477,22 +1564,24 @@ export default function Clear() {
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1-content"
id={`scheduler-${item?.scheduler_cluster_id}-url-${index}`}
id={`scheduler-${item?.scheduler_cluster_id}-url-${peerIndex}`}
>
<Box className={styles.imageManifestHeader}>
<Box className={styles.hostnameContainer}>
<Box width="16%">
<Box
className={styles.hostnameWrapper}
id={`scheduler-${item?.scheduler_cluster_id}-hostname-${index}`}
id={`scheduler-${item?.scheduler_cluster_id}-hostname-${peerIndex}`}
>
<Hostnames className={styles.hostnameIcon} />
<Typography variant="subtitle2" ml="0.4rem" fontFamily="mabry-bold">
{items?.hostname}
</Typography>
</Box>
</Box>
<Box
className={styles.hostnameWrapper}
id={`scheduler-${item?.scheduler_cluster_id}-ip-${index}`}
id={`scheduler-${item?.scheduler_cluster_id}-ip-${peerIndex}`}
>
<IP className={styles.hostnameIcon} />
<Typography variant="subtitle2" ml="0.4rem" fontFamily="mabry-bold">
@ -1501,34 +1590,47 @@ export default function Clear() {
</Box>
</Box>
<Box
className={styles.totalContainer}
id={`scheduler-${item?.scheduler_cluster_id}-total-${index}`}
className={styles.bolbProportionContainer}
id={`scheduler-${item?.scheduler_cluster_id}-proportion-${peerIndex}`}
>
<Total className={styles.totalIcon} />
<Typography component="span" variant="subtitle2" fontFamily="mabry-bold" pl="0.3rem">
{`Total ${items?.layers.length}`}
<Proportion className={styles.bolbIcon} />
<Typography
component="span"
variant="subtitle2"
className={styles.bolbProportionText}
>
{`Bolb: ${((items?.layers?.length / layer) * 100).toFixed(2) || 0}%`}
</Typography>
</Box>
</Box>
</AccordionSummary>
<AccordionDetails
key={index}
key={peerIndex}
sx={{
padding: '1rem',
backgroundColor: 'var(--palette-background-paper)',
}}
>
<Box>
<Typography component="div" variant="subtitle1" fontFamily="mabry-bold" mb="1rem">
URL
<Box className={styles.clusterWrapper}>
<Paper variant="outlined" className={styles.bolbIconWrapper}>
<Layer className={styles.cluster} />
</Paper>
<Typography component="div" variant="body1" fontFamily="mabry-bold" ml="0.5rem">
Blob
</Typography>
</Box>
<Box className={styles.cardCantainer}>
{items?.layers.map((item: any, urlIndex: any) => (
<Box key={index} className={styles.urlsWrapper}>
<URL className={styles.urlIcon} />
<Tooltip title={item?.url || '-'} placement="top">
<Typography id={`url-${index}`} className={styles.url} variant="body2">
{item?.url}
{items?.layers.map((item: any, bolbIndex: any) => (
<Box key={bolbIndex} className={styles.urlsWrapper}>
<Tooltip title={extractSHA256Regex(item?.url) || '-'} placement="top">
<Typography
id={`url-${bolbIndex}`}
className={styles.url}
fontFamily="mabry-bold"
variant="body2"
>
{extractSHA256Regex(item?.url)}
</Typography>
</Tooltip>
</Box>
@ -1537,8 +1639,7 @@ export default function Clear() {
</Box>
</AccordionDetails>
</Accordion>
{index !== paginatedPeers.length - 1 && <Divider />}
{peerIndex !== paginatedPeers.length - 1 && <Divider />}
</Box>
);
})}
@ -1560,7 +1661,10 @@ export default function Clear() {
})}
</Box>
) : (
<Box id="no-task" sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', mt: '6rem' }}>
<Box
id="no-image-manifest-URL-task"
sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', mt: '6rem' }}
>
<NoSearch className={styles.noSearch} />
<Box>
<Typography variant="h5" component="span">

View File

@ -994,7 +994,7 @@ interface ImageDistributionpeers {
}
export interface createGetImageDistributionJobResponse {
image: layers[];
image: { layers: layers[] };
peers: ImageDistributionpeers[];
}

View File

@ -168,3 +168,8 @@ export const parseTimeDuration = (input: string) => {
};
}
};
export const extractSHA256Regex = (url: string) => {
const match = url.match(/\/sha256:([a-f0-9]{64})/);
return match ? match[1] : null;
};