feat: resource task add Image Manifest Url (#551)

Signed-off-by: zhaoxinxin <1186037180@qq.com>
This commit is contained in:
Zhaoxinxin 2025-07-02 23:38:47 +08:00 committed by GitHub
parent 5d943d8aea
commit 36678fbf10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 900 additions and 76 deletions

View File

@ -1,8 +1,9 @@
import createTaskJob from '../../../fixtures/job/task/create-task-job.json';
import task from '../../../fixtures/job/task/task.json';
import pendingTask from '../../../fixtures/job/task/pending-task.json';
import taskIDByTask from '../../../fixtures/job/task/task-id-by-task.json';
import noTask from '../../../fixtures/job/task/no-task.json';
import createTaskJob from '../../../fixtures/resource/task/create-task-job.json';
import task from '../../../fixtures/resource/task/task.json';
import pendingTask from '../../../fixtures/resource/task/pending-task.json';
import taskIDByTask from '../../../fixtures/resource/task/task-id-by-task.json';
import noTask from '../../../fixtures/resource/task/no-task.json';
import ImageManifest from '../../../fixtures/resource/task/image-manifest-url-task.json';
import _ from 'lodash';
describe('Clear', () => {
@ -13,56 +14,95 @@ describe('Clear', () => {
cy.viewport(1440, 1080);
});
it('when no data is loaded', () => {
cy.get('#no-task').should('not.exist');
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');
cy.get('#no-task-image').should('not.exist');
cy.get('#light').should('exist');
cy.get('#no-task-image').should('not.exist');
// Click the Toggle Light button.
cy.get('#light').click();
cy.get('#light').should('have.class', 'Mui-selected');
// Click the Toggle Light button.
cy.get('#light').click();
cy.get('#light').should('have.class', 'Mui-selected');
// Check if it is switched to light mode.
cy.get('#main').should('have.css', 'background-color', 'rgb(244, 246, 248)');
// Check if it is switched to light mode.
cy.get('#main').should('have.css', 'background-color', 'rgb(244, 246, 248)');
cy.get('#no-task-image').should('exist');
cy.get('#no-task-image').should('exist');
cy.get('#dark-no-task-image').should('not.exist');
cy.get('#dark-no-task-image').should('not.exist');
cy.intercept(
{
method: 'post',
url: '/api/v1/jobs',
},
(req) => {
req.reply({
statusCode: 200,
body: createTaskJob,
});
},
);
cy.intercept(
{
method: 'GET',
url: '/api/v1/jobs/1',
},
(req) => {
req.reply({
statusCode: 200,
body: noTask,
});
},
);
cy.intercept(
{
method: 'post',
url: '/api/v1/jobs',
},
(req) => {
req.reply({
statusCode: 200,
body: createTaskJob,
});
},
);
cy.intercept(
{
method: 'GET',
url: '/api/v1/jobs/1',
},
(req) => {
req.reply({
statusCode: 200,
body: noTask,
});
},
);
cy.get('#url').click();
cy.get('#url').click();
// Add url.
cy.get('#url').type('https://example.com/path/to/file');
// Add url.
cy.get('#url').type('https://example.com/path/to/file');
cy.get('#searchByURL').click();
cy.get('#searchByURL').click();
cy.get('#no-task').should('exist');
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: {
image: {
layers: [
{
url: 'https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:c7c72808bf776cd122bdaf4630a4a35ea319603d6a3b6cbffddd4c7fd6d2d269',
},
{
url: 'https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:9986a736f7d3d24bb01b0a560fa0f19c4b57e56c646e1f998941529d28710e6b',
},
],
},
peers: [],
},
});
},
);
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', () => {
@ -283,6 +323,42 @@ describe('Clear', () => {
// Pagination should not be displayed.
cy.get('#pagination-1').should('exist');
});
it('can search by image manifest url', () => {
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: ImageManifest,
});
},
);
cy.get('#image-manifest-url').type('https://example.com/path/to/file{enter}');
// Show is loading.
cy.get('#isLoading').should('exist');
// Display cache information.
cy.get('#blobs').should('have.text', 'Total: 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-proportion-0').should('contain', '60.00%');
// Should display URL.
cy.get('#scheduler-1-url-0').click();
});
});
describe('should handle API error response', () => {
@ -395,7 +471,7 @@ describe('Clear', () => {
cy.wait(60000);
});
it('Delete cache API error response', () => {
it('delete cache API error response', () => {
// Search by task id.
cy.get('#serach-task-id').click();
cy.intercept(
@ -434,6 +510,27 @@ describe('Clear', () => {
cy.get('.MuiAlert-action > .MuiButtonBase-root').click();
cy.get('.MuiAlert-message').should('not.exist');
});
it('search by image manifest url API error response', () => {
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({
forceNetworkError: true,
});
},
);
cy.get('#image-manifest-url').type('https://example.com/path/to/file{enter}');
// Show error message.
cy.get('.MuiAlert-message').should('be.visible').and('contain', 'Failed to fetch');
});
});
describe('delete', () => {
@ -625,10 +722,7 @@ describe('Clear', () => {
cy.get('#searchByURL').click();
cy.get(':nth-child(2) > .MuiPaper-root > .css-whqzh4 > .css-70qvj9 > .css-1y3f2j > #schedulerTotal').should(
'contain',
'2',
);
cy.get('#scheduler-id-1').should('contain', '2');
cy.get(':nth-child(2) > .MuiPaper-root > .css-whqzh4 > .MuiButtonBase-root').click();
@ -698,10 +792,7 @@ describe('Clear', () => {
'fe0c4a611d35e338efd342c346a2c671c358c5187c483a5fc7cd66c6685ce916{enter}',
);
cy.get(':nth-child(2) > .MuiPaper-root > .css-whqzh4 > .css-70qvj9 > .css-1y3f2j > #schedulerTotal').should(
'contain',
'2',
);
cy.get('#scheduler-id-1').should('contain', '2');
cy.get(':nth-child(2) > .MuiPaper-root > .css-whqzh4 > .MuiButtonBase-root').click();
@ -819,7 +910,7 @@ describe('Clear', () => {
.and('have.text', 'Fill in the characters, the length is 0-1000.');
});
it('try to verify url', () => {
it('try to verify content for calculating task id', () => {
const characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const contentForCalculatingTaskID = _.times(1001, () => _.sample(characters)).join('');
@ -831,5 +922,26 @@ describe('Clear', () => {
.should('be.visible')
.and('have.text', 'Fill in the characters, the length is 0-1000.');
});
it('try to verify image manifest url', () => {
const characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const url = _.times(1001, () => _.sample(characters)).join('');
cy.get('#serach-image-manifest-url').click();
cy.get('#image-manifest-url').click();
// Should display message url the validation error.
cy.get('#image-manifest-url').type(`https://docs${url}`);
cy.get('#image-manifest-url-helper-text')
.should('be.visible')
.and('have.text', 'Fill in the characters, the length is 1-1000.');
cy.get('#image-manifest-url').clear();
cy.get('#image-manifest-url').type('https://docs');
cy.get('#image-manifest-url-helper-text').should('not.exist');
});
});
});

View File

@ -1,7 +1,7 @@
import executions from '../../../fixtures/job/task/executions.json';
import execution from '../../../fixtures/job/task/execution.json';
import pendingExecution from '../../../fixtures/job/task/pending-execution.json';
import failureExecution from '../../../fixtures/job/task/failure-execution.json';
import executions from '../../../fixtures/resource/task/executions.json';
import execution from '../../../fixtures/resource/task/execution.json';
import pendingExecution from '../../../fixtures/resource/task/pending-execution.json';
import failureExecution from '../../../fixtures/resource/task/failure-execution.json';
describe('Executions', () => {
beforeEach(() => {

View File

@ -1,8 +1,8 @@
import executions from '../../../fixtures/job/task/executions.json';
import paginationExecutions from '../../../fixtures/job/task/pagination-executions.json';
import successExecutions from '../../../fixtures/job/task/success-executions.json';
import failureExecutions from '../../../fixtures/job/task/failure-executions.json';
import pendingExecutions from '../../../fixtures/job/task/pending-executions.json';
import executions from '../../../fixtures/resource/task/executions.json';
import paginationExecutions from '../../../fixtures/resource/task/pagination-executions.json';
import successExecutions from '../../../fixtures/resource/task/success-executions.json';
import failureExecutions from '../../../fixtures/resource/task/failure-executions.json';
import pendingExecutions from '../../../fixtures/resource/task/pending-executions.json';
describe('Executions', () => {
beforeEach(() => {

View File

@ -0,0 +1,133 @@
{
"image": {
"layers": [
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:f1f1039835051ecc04909f939530e86a20f02d2ce5ad7a81c0fa3616f7303944"
},
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:c1d6d1b2d5a367259e6e51a7f4d1ccd66a28cc9940d6599d8a8ea9544dd4b4a8"
},
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:871ab018db94b4ae7b137764837bc4504393a60656ba187189e985cd809064f7"
},
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:f1a1d290795d904815786e41d39a41dc1af5de68a9e9020baba8bd83b32d8f95"
},
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:f1ffc4b5459e82dc8e7ddd1d1a2ec469e85a1f076090c22851a1f2ce6f71e1a6"
}
]
},
"peers": [
{
"ip": "172.18.0.4",
"hostname": "kind-worker1",
"layers": [
{
"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
},
{
"ip": "172.18.0.4",
"hostname": "kind-worker2",
"layers": [
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:f1f1039835051ecc04909f939530e86a20f02d2ce5ad7a81c0fa3616f7303944"
},
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:871ab018db94b4ae7b137764837bc4504393a60656ba187189e985cd809064f7"
}
],
"scheduler_cluster_id": 1
},
{
"ip": "172.18.0.4",
"hostname": "kind-worker3",
"layers": [
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:f1ffc4b5459e82dc8e7ddd1d1a2ec469e85a1f076090c22851a1f2ce6f71e1a6"
},
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:c1d6d1b2d5a367259e6e51a7f4d1ccd66a28cc9940d6599d8a8ea9544dd4b4a8"
}
],
"scheduler_cluster_id": 1
},
{
"ip": "172.18.0.4",
"hostname": "kind-worker4",
"layers": [
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:f1f1039835051ecc04909f939530e86a20f02d2ce5ad7a81c0fa3616f7303944"
},
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:871ab018db94b4ae7b137764837bc4504393a60656ba187189e985cd809064f7"
}
],
"scheduler_cluster_id": 1
},
{
"ip": "172.18.0.4",
"hostname": "kind-worker5",
"layers": [
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:f1ffc4b5459e82dc8e7ddd1d1a2ec469e85a1f076090c22851a1f2ce6f71e1a6"
},
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:c1d6d1b2d5a367259e6e51a7f4d1ccd66a28cc9940d6599d8a8ea9544dd4b4a8"
}
],
"scheduler_cluster_id": 1
},
{
"ip": "172.18.0.4",
"hostname": "kind-worker6",
"layers": [
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:f1f1039835051ecc04909f939530e86a20f02d2ce5ad7a81c0fa3616f7303944"
},
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:871ab018db94b4ae7b137764837bc4504393a60656ba187189e985cd809064f7"
}
],
"scheduler_cluster_id": 1
},
{
"ip": "172.18.0.3",
"hostname": "kind-worker7",
"layers": [
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:f1f1039835051ecc04909f939530e86a20f02d2ce5ad7a81c0fa3616f7303944"
},
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:871ab018db94b4ae7b137764837bc4504393a60656ba187189e985cd809064f7"
}
],
"scheduler_cluster_id": 2
},
{
"ip": "172.18.0.3",
"hostname": "kind-worker8",
"layers": [
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:f1f1039835051ecc04909f939530e86a20f02d2ce5ad7a81c0fa3616f7303944"
},
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:871ab018db94b4ae7b137764837bc4504393a60656ba187189e985cd809064f7"
},
{
"url": "https://ghcr.io/v2/dragonflyoss/scheduler/blobs/sha256:771ab018db94b4ae7b137764837bc4504393a60656ba187189e985cd809064f7"
}
],
"scheduler_cluster_id": 2
}
]
}

View File

@ -0,0 +1,6 @@
<svg t="1751289209561" class="icon" viewBox="0 0 1191 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="48455" width="200" height="200">
<path
d="M1081.527579 542.078644A108.415332 108.415332 0 0 1 1189.942911 651.155045v264.42764a107.754263 107.754263 0 0 1-108.415332 108.415332H108.433866A107.754263 107.754263 0 0 1 0.018534 914.260546v-264.427639A108.415332 108.415332 0 0 1 108.433866 542.078644z m0 80.65043H108.433866a28.425971 28.425971 0 0 0-27.764902 27.103833v264.427639a27.103833 27.103833 0 0 0 27.103833 27.764903h973.754782a27.764902 27.764902 0 0 0 27.764902-27.103833v-264.42764a28.425971 28.425971 0 0 0-27.103833-28.425971zM207.594231 727.177992a33.714524 33.714524 0 0 1 34.375593 33.714524v37.680938a34.375593 34.375593 0 0 1-66.10691 0v-37.680938a33.714524 33.714524 0 0 1 31.731317-33.714524z m154.690169 0a33.714524 33.714524 0 0 1 34.375593 33.714524v37.680938a34.375593 34.375593 0 0 1-66.10691 0v-37.680938a33.714524 33.714524 0 0 1 31.731317-33.714524zM925.515272 727.177992a52.885528 52.885528 0 1 1 0 105.109986h-27.764902a52.885528 52.885528 0 1 1 0-105.109986z m156.012307-727.176009A107.754263 107.754263 0 0 1 1189.942911 108.417315v264.42764a108.415332 108.415332 0 0 1-108.415332 108.415332H108.433866A108.415332 108.415332 0 0 1 0.018534 372.183886v-264.42764A107.754263 107.754263 0 0 1 108.433866 0.001983z m0 80.65043H108.433866a27.764902 27.764902 0 0 0-27.764902 27.103833v264.42764a27.764902 27.764902 0 0 0 27.103833 27.764902h973.754782a27.764902 27.764902 0 0 0 27.764902-27.103833v-264.42764a27.764902 27.764902 0 0 0-27.103833-27.764902zM207.594231 185.7624a34.375593 34.375593 0 0 1 34.375593 34.375593v37.01987a34.375593 34.375593 0 1 1-66.10691 0v-37.01987a33.714524 33.714524 0 0 1 31.731317-34.375593z m154.690169 0a34.375593 34.375593 0 0 1 34.375593 34.375593v37.01987a34.375593 34.375593 0 1 1-66.10691 0v-37.01987a33.714524 33.714524 0 0 1 31.731317-34.375593z m561.247665 0a52.885528 52.885528 0 0 1 0 105.771056h-27.764903a52.885528 52.885528 0 1 1 0-105.771056z"
fill="currentColor" p-id="48456"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,9 @@
<svg t="1734589694524" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="53329" width="200" height="200">
<path
d="M537.362229 1024c-9.510836 0-15.851393-3.170279-22.19195-9.510836l-282.154799-278.98452c-120.470588-123.640867-158.513932-307.517028-91.938081-466.03096S362.996904 6.340557 534.19195 6.340557s329.708978 104.619195 393.114552 263.133127c66.575851 158.513932 28.532508 342.390093-91.938081 466.03096L559.55418 1014.489164c-6.340557 6.340557-12.681115 9.510836-22.191951 9.510836z m0-957.424149C391.529412 66.575851 258.377709 155.343653 201.312693 291.665635c-57.065015 136.321981-25.362229 291.665635 79.256966 396.284829l259.962849 256.79257 259.962848-259.962848c104.619195-104.619195 136.321981-259.962848 79.256966-396.28483-60.235294-133.151703-193.386997-221.919505-342.390093-221.919505z m0 0"
fill="currentColor" p-id="53330"></path>
<path
d="M382.018576 285.325077h63.405573v342.390093h-63.405573V285.325077z m244.111455 342.390093h-63.405573V317.027864c0-15.851393 12.681115-31.702786 31.702787-31.702787h66.575851c60.235294 0 107.789474 47.55418 107.789474 107.789474s-47.55418 107.789474-107.789474 107.789474h-34.873065v126.811145z m0-187.046439h34.873065c25.362229 0 47.55418-22.19195 47.55418-47.55418s-22.19195-47.55418-47.55418-47.554179h-34.873065v95.108359z m0 0"
fill="currentColor" p-id="53331"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

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,23 @@
<svg t="1751461120062" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="62198" width="200" height="200">
<path
d="M486.105058 51.989846C217.657489 51.989846 0.09998 269.547354 0.09998 537.994923c0 268.347588 217.557508 486.005077 486.005078 486.005077 268.347588 0 486.005077-217.557508 486.005077-486.005077H486.105058V51.989846z"
fill="currentColor" p-id="62199" data-spm-anchor-id="a313x.search_index.0.i52.46c83a815s2V0n" class="selected">
</path>
<path d="M537.894942 0.09998v486.005078h486.005078C1023.90002 217.657489 806.342511 0.09998 537.894942 0.09998z"
fill="rgb(178, 228, 193)" p-id="62200" data-spm-anchor-id="a313x.search_index.0.i54.46c83a815s2V0n" class="">
</path>
</svg>
<!-- <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: 2.1 KiB

View File

@ -62,6 +62,14 @@
justify-content: flex-start;
}
.schedulerClusterWrapper {
border: 1px solid #d5d2d2;
padding: 0.1rem 0.3rem;
border-radius: 0.3rem;
display: inline-flex;
align-items: center;
}
.schedulerClusterIcon {
width: 0.6rem;
height: 0.6rem;
@ -95,3 +103,129 @@
height: 1.25rem;
margin-right: 0.4rem;
}
.imageManifestCard {
margin-bottom: 2rem;
}
.imageManifestHeader {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.cacheHeader {
display: flex;
align-items: center;
margin: 1.5rem 0 1rem 0;
}
.bolbWrapper {
display: inline-flex;
align-items: center;
margin-bottom: 1.5rem;
color: var(--palette-table-title-text-color);
}
.bolbText {
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.3rem;
background-color: var(--palette-background-inactive);
padding: 0.2rem 0.4rem;
}
.hostnameContainer {
display: flex;
align-items: center;
padding: 0.4rem 0;
width: 80%;
}
.hostnameWrapper {
border-radius: 0.3rem;
background-color: var(--palette-background-inactive);
border: 0;
font-family: 'mabry-bold';
display: inline-flex;
padding: 0.3rem 0.5rem;
align-items: center;
}
.hostnameIcon {
width: 1.2rem;
height: 1.2rem;
}
.urlsWrapper {
border-color: var(--palette-palette-divider) !important;
background-color: var(--palette-background-inactive) !important;
border-radius: var(--menu-border-radius) !important;
padding: 0.5rem 0.6rem;
display: flex;
align-items: center;
overflow: hidden;
}
.url {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.urlIcon {
width: 8%;
height: 1.4rem !important;
color: var(--palette-detail-lable-color);
}
.layerWrapper {
display: flex;
align-items: center;
margin-bottom: 1.5rem;
}
.layerIcon {
width: 1.4rem;
height: 1.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;
}
.cardCantainer {
gap: calc(1.2rem);
display: grid;
grid-template-columns: repeat(2, 1fr);
}
.bolbProportionContainer {
display: flex;
align-items: center;
padding: 0.3rem 0.4rem;
background-color: var(--palette-button-color) !important;
border-radius: var(--menu-border-radius) !important;
margin-right: 0.4rem;
color: var(--palette-scopes-icon-color);
}
.bolbIcon {
width: 1.2rem;
height: 1.2rem;
}
.bolbProportionText {
padding-left: 0.4rem;
}

View File

@ -20,6 +20,9 @@ import {
Pagination,
useTheme,
InputAdornment,
Accordion,
AccordionSummary,
AccordionDetails,
} from '@mui/material';
import styles from './index.module.css';
import { useEffect, useState } from 'react';
@ -27,8 +30,15 @@ import ClearIcon from '@mui/icons-material/Clear';
import DeleteIcon from '@mui/icons-material/Delete';
import CloseIcon from '@mui/icons-material/Close';
import MoreTimeIcon from '@mui/icons-material/MoreTime';
import { getTaskJobResponse, createTaskJob, getTaskJob } from '../../../../lib/api';
import { getDatetime, getPaginatedList } from '../../../../lib/utils';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
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';
@ -46,6 +56,12 @@ import { ReactComponent as DarkNoTask } from '../../../../assets/images/resource
import { ReactComponent as Delete } from '../../../../assets/images/cluster/delete.svg';
import { ReactComponent as DeleteWarning } from '../../../../assets/images/cluster/delete-warning.svg';
import { ReactComponent as ContentForCalculatingTaskID } from '../../../../assets/images/resource/task/content-for-calculating-task-id.svg';
import { ReactComponent as ImageManifest } from '../../../../assets/images/resource/task/image-manifest.svg';
import { ReactComponent as IP } from '../../../../assets/images/resource/task/clear-ip.svg';
import { ReactComponent as Hostnames } from '../../../../assets/images/resource/task/clear-hostname.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}`]: {
@ -62,17 +78,71 @@ const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }) => ({
},
}));
type Layers = {
url: string;
};
type Image = {
layers: Layers[];
};
type OriginalPeer = {
ip: string;
hostname: string;
layers: Layers[];
scheduler_cluster_id?: number;
};
type ClusteredPeer = {
peer: Omit<OriginalPeer, 'scheduler_cluster_id'>[];
scheduler_cluster_id: number;
};
type TransformedImage = {
peers: ClusteredPeer[];
image: Image;
};
const transformImages = (images: createGetImageDistributionJobResponse): TransformedImage => {
const clusters = new Map<number, Omit<OriginalPeer, 'scheduler_cluster_id'>[]>();
for (const peer of images.peers || []) {
const clusterId = peer.scheduler_cluster_id ?? 1;
if (!clusters.has(clusterId)) {
clusters.set(clusterId, []);
}
const cleanedPeer = {
ip: peer.ip,
hostname: peer.hostname,
layers: peer.layers,
};
clusters.get(clusterId)!.push(cleanedPeer);
}
const resultPeers: ClusteredPeer[] = Array.from(clusters.entries()).map(([id, peers]) => ({
peer: peers,
scheduler_cluster_id: id,
}));
return { peers: resultPeers, image: images.image };
};
export default function Clear() {
const [errorMessage, setErrorMessage] = useState(false);
const [errorMessageText, setErrorMessageText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [searchTaskISLodaing, setSearchTaskISLodaing] = useState(false);
const [searchContentForCalculatingTaskIDISLodaing, setSearchContentForCalculatingTaskIDISLodaing] = useState(false);
const [searchImageManifestISLodaing, setSearchImageManifestISLodaing] = useState(false);
const [searchIconISLodaing, setSearchIconISLodaing] = useState(false);
const [openDeleteTask, setOpenDeleteTask] = useState(false);
const [deleteLoadingButton, setDeleteLoadingButton] = useState(false);
const [searchTask, setSearchTask] = useState('');
const [searchContentForCalculatingTaskID, setSearchContentForCalculatingTaskID] = useState('');
const [searchImageManifest, setSearchImageManifest] = useState('');
const [task, setTask] = useState<getTaskJobResponse | any>();
const [optional, setOptional] = useState(false);
const [deleteError, setDeleteError] = useState(false);
@ -97,6 +167,9 @@ export default function Clear() {
filtered_query_params: '',
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();
@ -493,6 +566,61 @@ export default function Clear() {
},
};
const imageManifestForm = {
formProps: {
id: 'image-manifest-url',
label: 'Image Manifest URL',
name: 'image-manifest-url',
required: true,
value: searchImageManifest,
autoComplete: 'family-name',
placeholder: 'Enter your image manifest URL',
helperText: contentForCalculatingTaskIDError ? 'Fill in the characters, the length is 1-1000.' : '',
error: contentForCalculatingTaskIDError,
InputProps: {
startAdornment: searchImageManifestISLodaing ? (
<Box className={styles.circularProgress}>
<SearchCircularProgress />
</Box>
) : (
<Box className={styles.circularProgress}>
<SearchIcon sx={{ color: '#9BA0A6' }} />
</Box>
),
endAdornment: searchImageManifest ? (
<IconButton
type="button"
aria-label="search"
onClick={() => {
setSearchImageManifest('');
setSearchImageManifestISLodaing(false);
}}
>
<ClearIcon />
</IconButton>
) : (
<></>
),
},
onChange: (e: any) => {
changeValidate(e.target.value, imageManifestForm);
setSearchImageManifest(e.target.value);
if (e.target.value === '') {
setSearchImageManifestISLodaing(false);
}
},
},
syncError: false,
setError: setContentForCalculatingTaskIDError,
validate: (value: string) => {
const reg = /^(?:https?|ftp):\/\/[^\s/$.?#].[^\s].{1,1000}$/;
return reg.test(value);
},
};
const result =
task?.result?.job_states?.map((item: any) => {
return item.results ? item.results.map((resultItem: any) => resultItem) : [];
@ -740,6 +868,37 @@ export default function Clear() {
}
};
const handleSearchByImageManifestURL = async (event: any) => {
setIsLoading(true);
setSearchImageManifestISLodaing(true);
try {
event.preventDefault();
const form = {
args: {
url: searchImageManifest,
},
type: 'get_image_distribution',
};
const imageManifest = await createGetImageDistributionJob(form);
const imageManifestTask = transformImages(imageManifest);
setImageManifestURL(imageManifestTask);
setLayer(imageManifestTask?.image?.layers?.length || 0);
setSearchImageManifestISLodaing(false);
setIsLoading(false);
} catch (error) {
if (error instanceof Error) {
setErrorMessage(true);
setErrorMessageText(error.message);
setIsLoading(false);
setSearchImageManifestISLodaing(false);
}
}
};
const handleClose = (_event: any, reason?: string) => {
if (reason === 'clickaway') {
return;
@ -765,6 +924,8 @@ export default function Clear() {
setOptional(false);
setSearchTask('');
setSearchDada({ url: '', tag: '', application: '', filtered_query_params: '', piece_length: 0 });
setSearchContentForCalculatingTaskID('');
setSearchImageManifest('');
};
const handlePageChange = (peerId: any, newPage: any) => {
@ -774,6 +935,13 @@ export default function Clear() {
}));
};
const handleImagePageChange = (schedulerClusterId: any, page: any) => {
setPageStates((prev: any) => ({
...prev,
[schedulerClusterId]: page,
}));
};
return (
<Box>
<Snackbar
@ -827,6 +995,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"
@ -890,6 +1082,15 @@ export default function Clear() {
>
<TextField fullWidth variant="outlined" size="small" {...calculatingTaskIDForm.formProps} sx={{ p: 0 }} />
</Box>
) : search === 'image-manifest-url' ? (
<Box
key="image-manifest-url"
component="form"
onSubmit={handleSearchByImageManifestURL}
sx={{ width: '38rem', height: '3rem' }}
>
<TextField fullWidth variant="outlined" size="small" {...imageManifestForm.formProps} sx={{ p: 0 }} />
</Box>
) : (
<Box sx={{ position: 'relative', height: '3rem' }}>
<Paper
@ -902,6 +1103,7 @@ export default function Clear() {
width: optional ? '45rem' : '36rem',
position: 'absolute',
backgroundColor: 'var(--palette-background-menu-paper)',
zIndex: '100001',
}}
>
<TextField
@ -1023,19 +1225,11 @@ export default function Clear() {
<Typography variant="subtitle1" mr="0.6rem" fontFamily="mabry-bold">
Scheduler Cluster
</Typography>
<Box
sx={{
border: '1px solid #d5d2d2',
p: '0.2rem 0.3rem',
borderRadius: '0.2rem',
display: 'inline-flex',
alignItems: 'center',
}}
>
<Box className={styles.schedulerClusterWrapper} id={`scheduler-id-${index}`}>
<SchedulerCluster className={styles.schedulerClusterIcon} />
<Typography
id="schedulerTotal"
variant="subtitle2"
variant="caption"
fontFamily="mabry-bold"
component="div"
pl="0.3rem"
@ -1242,6 +1436,184 @@ export default function Clear() {
</Box>
)}
</Box>
) : Array.isArray(imageManifestURL?.peers) ? (
imageManifestURL?.peers.length > 0 ? (
<Box>
<Box className={styles.cacheHeader}>
<Typography variant="h6" fontFamily="mabry-bold">
Cache
</Typography>
</Box>
<Box className={styles.bolbWrapper}>
<Typography variant="body2" fontFamily="mabry-bold" component="div" pr="0.4rem">
Blobs
</Typography>
<Typography
id="blobs"
variant="caption"
fontFamily="mabry-bold"
component="div"
className={styles.bolbText}
>
{`Total: ${layer || 0}`}
</Typography>
</Box>
{imageManifestURL?.peers.map((item, index) => {
const schedulerClusterId = item.scheduler_cluster_id;
const totalPage = Math.ceil(item.peer.length / 5);
const currentPage = pageStates[schedulerClusterId] || 1;
const paginatedPeers = getPaginatedList(item.peer, currentPage, 5);
return (
<Box mb="2rem" key={index}>
<Card key={index} className={styles.imageManifestCard}>
<Box sx={{ display: 'flex', alignItems: 'center', p: '1rem' }}>
<Typography variant="subtitle1" mr="0.6rem" fontFamily="mabry-bold">
Scheduler Cluster
</Typography>
<Box className={styles.schedulerClusterWrapper} id={`scheduler-id-${index}`}>
<SchedulerCluster className={styles.schedulerClusterIcon} />
<Typography
variant="caption"
fontFamily="mabry-bold"
component="div"
pl="0.3rem"
lineHeight="1rem"
>
ID&nbsp;:&nbsp; {item?.scheduler_cluster_id || '0'}
</Typography>
</Box>
</Box>
<Divider />
{paginatedPeers?.map((items, peerIndex) => {
return (
<Box key={peerIndex}>
<Accordion
disableGutters
elevation={0}
square
sx={{
'&:not(:last-child)': {
borderBottom: 0,
},
'&:before': {
display: 'none',
},
backgroundColor: 'var(--palette-background-paper)',
}}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1-content"
id={`scheduler-${item?.scheduler_cluster_id}-url-${peerIndex}`}
>
<Box className={styles.imageManifestHeader}>
<Box className={styles.hostnameContainer}>
<Box width="18%">
<Box
className={styles.hostnameWrapper}
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-${peerIndex}`}
>
<IP className={styles.hostnameIcon} />
<Typography variant="subtitle2" ml="0.4rem" fontFamily="mabry-bold">
{items?.ip}
</Typography>
</Box>
</Box>
<Box
className={styles.bolbProportionContainer}
id={`scheduler-${item?.scheduler_cluster_id}-proportion-${peerIndex}`}
>
<Proportion className={styles.bolbIcon} />
<Typography
component="div"
variant="body2"
fontFamily="mabry-bold"
className={styles.bolbProportionText}
>
{`${((items?.layers?.length / layer) * 100).toFixed(2) || 0}%`}
</Typography>
</Box>
</Box>
</AccordionSummary>
<AccordionDetails
key={peerIndex}
sx={{
padding: '1rem',
backgroundColor: 'var(--palette-background-paper)',
}}
>
<Box>
<Box className={styles.layerWrapper}>
<Paper variant="outlined" className={styles.bolbIconWrapper}>
<Layer className={styles.layerIcon} />
</Paper>
<Typography component="div" variant="body2" fontFamily="mabry-bold" ml="0.5rem">
Blobs
</Typography>
</Box>
<Box className={styles.cardCantainer}>
{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>
))}
</Box>
</Box>
</AccordionDetails>
</Accordion>
{peerIndex !== paginatedPeers.length - 1 && <Divider />}
</Box>
);
})}
</Card>
{totalPage > 1 && (
<Box display="flex" justifyContent="flex-end" sx={{ marginTop: '2rem' }}>
<Pagination
id={`pagination-${index}`}
count={Math.ceil(item.peer.length / 5)}
page={currentPage}
onChange={(e, page) => handleImagePageChange(schedulerClusterId, page)}
color="primary"
size="small"
/>
</Box>
)}
</Box>
);
})}
</Box>
) : (
<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">
You don't find any results!
</Typography>
</Box>
</Box>
)
) : (
<Box
sx={{

View File

@ -982,12 +982,36 @@ export interface createTaskJobResponse {
scheduler_clusters: Array<scheduler_clusters>;
}
interface layers {
url: string;
}
interface ImageDistributionpeers {
ip: string;
hostname: string;
layers: layers[];
scheduler_cluster_id: number;
}
export interface createGetImageDistributionJobResponse {
image: { layers: layers[] };
peers: ImageDistributionpeers[];
}
export async function createTaskJob(request: createTaskJobResquest): Promise<createTaskJobResponse> {
const url = new URL(`/api/v1/jobs`, API_URL);
const response = await post(url, request);
return await response.json();
}
export async function createGetImageDistributionJob(
request: createTaskJobResquest,
): Promise<createGetImageDistributionJobResponse> {
const url = new URL(`/api/v1/jobs`, API_URL);
const response = await post(url, request);
return await response.json();
}
export interface peers {
created_at: string;
host_type: string;

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] : '';
};