From 36678fbf10121b55074d619b8b107c3343b8d866 Mon Sep 17 00:00:00 2001 From: Zhaoxinxin <107842350+Liam-Zhao@users.noreply.github.com> Date: Wed, 2 Jul 2025 23:38:47 +0800 Subject: [PATCH] feat: resource task add Image Manifest Url (#551) Signed-off-by: zhaoxinxin <1186037180@qq.com> --- cypress/e2e/resource/task/clear.cy.ts | 222 +++++++--- cypress/e2e/resource/task/execution.cy.ts | 8 +- cypress/e2e/resource/task/executions.cy.ts | 10 +- .../task/create-task-job.json | 0 .../{job => resource}/task/execution.json | 0 .../{job => resource}/task/executions.json | 0 .../task/failure-execution.json | 0 .../task/failure-executions.json | 0 .../task/image-manifest-url-task.json | 133 ++++++ .../{job => resource}/task/no-task.json | 0 .../task/pagination-executions.json | 0 .../task/pending-execution.json | 0 .../task/pending-executions.json | 0 .../{job => resource}/task/pending-task.json | 0 .../task/success-executions.json | 0 .../task/task-id-by-task.json | 0 .../fixtures/{job => resource}/task/task.json | 0 .../images/resource/task/clear-hostname.svg | 6 + src/assets/images/resource/task/clear-ip.svg | 9 + src/assets/images/resource/task/layer.svg | 6 + .../images/resource/task/proportion.svg | 23 + .../resource/task/clear/index.module.css | 134 ++++++ src/components/resource/task/clear/index.tsx | 396 +++++++++++++++++- src/lib/api.ts | 24 ++ src/lib/utils.ts | 5 + 25 files changed, 900 insertions(+), 76 deletions(-) rename cypress/fixtures/{job => resource}/task/create-task-job.json (100%) rename cypress/fixtures/{job => resource}/task/execution.json (100%) rename cypress/fixtures/{job => resource}/task/executions.json (100%) rename cypress/fixtures/{job => resource}/task/failure-execution.json (100%) rename cypress/fixtures/{job => resource}/task/failure-executions.json (100%) create mode 100644 cypress/fixtures/resource/task/image-manifest-url-task.json rename cypress/fixtures/{job => resource}/task/no-task.json (100%) rename cypress/fixtures/{job => resource}/task/pagination-executions.json (100%) rename cypress/fixtures/{job => resource}/task/pending-execution.json (100%) rename cypress/fixtures/{job => resource}/task/pending-executions.json (100%) rename cypress/fixtures/{job => resource}/task/pending-task.json (100%) rename cypress/fixtures/{job => resource}/task/success-executions.json (100%) rename cypress/fixtures/{job => resource}/task/task-id-by-task.json (100%) rename cypress/fixtures/{job => resource}/task/task.json (100%) create mode 100644 src/assets/images/resource/task/clear-hostname.svg create mode 100644 src/assets/images/resource/task/clear-ip.svg create mode 100644 src/assets/images/resource/task/layer.svg create mode 100644 src/assets/images/resource/task/proportion.svg diff --git a/cypress/e2e/resource/task/clear.cy.ts b/cypress/e2e/resource/task/clear.cy.ts index 566e8cd..074aea0 100644 --- a/cypress/e2e/resource/task/clear.cy.ts +++ b/cypress/e2e/resource/task/clear.cy.ts @@ -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'); + }); }); }); diff --git a/cypress/e2e/resource/task/execution.cy.ts b/cypress/e2e/resource/task/execution.cy.ts index c6b28e2..b2bc054 100644 --- a/cypress/e2e/resource/task/execution.cy.ts +++ b/cypress/e2e/resource/task/execution.cy.ts @@ -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(() => { diff --git a/cypress/e2e/resource/task/executions.cy.ts b/cypress/e2e/resource/task/executions.cy.ts index ba20abc..55de325 100644 --- a/cypress/e2e/resource/task/executions.cy.ts +++ b/cypress/e2e/resource/task/executions.cy.ts @@ -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(() => { diff --git a/cypress/fixtures/job/task/create-task-job.json b/cypress/fixtures/resource/task/create-task-job.json similarity index 100% rename from cypress/fixtures/job/task/create-task-job.json rename to cypress/fixtures/resource/task/create-task-job.json diff --git a/cypress/fixtures/job/task/execution.json b/cypress/fixtures/resource/task/execution.json similarity index 100% rename from cypress/fixtures/job/task/execution.json rename to cypress/fixtures/resource/task/execution.json diff --git a/cypress/fixtures/job/task/executions.json b/cypress/fixtures/resource/task/executions.json similarity index 100% rename from cypress/fixtures/job/task/executions.json rename to cypress/fixtures/resource/task/executions.json diff --git a/cypress/fixtures/job/task/failure-execution.json b/cypress/fixtures/resource/task/failure-execution.json similarity index 100% rename from cypress/fixtures/job/task/failure-execution.json rename to cypress/fixtures/resource/task/failure-execution.json diff --git a/cypress/fixtures/job/task/failure-executions.json b/cypress/fixtures/resource/task/failure-executions.json similarity index 100% rename from cypress/fixtures/job/task/failure-executions.json rename to cypress/fixtures/resource/task/failure-executions.json diff --git a/cypress/fixtures/resource/task/image-manifest-url-task.json b/cypress/fixtures/resource/task/image-manifest-url-task.json new file mode 100644 index 0000000..4ee614d --- /dev/null +++ b/cypress/fixtures/resource/task/image-manifest-url-task.json @@ -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 + } + ] +} diff --git a/cypress/fixtures/job/task/no-task.json b/cypress/fixtures/resource/task/no-task.json similarity index 100% rename from cypress/fixtures/job/task/no-task.json rename to cypress/fixtures/resource/task/no-task.json diff --git a/cypress/fixtures/job/task/pagination-executions.json b/cypress/fixtures/resource/task/pagination-executions.json similarity index 100% rename from cypress/fixtures/job/task/pagination-executions.json rename to cypress/fixtures/resource/task/pagination-executions.json diff --git a/cypress/fixtures/job/task/pending-execution.json b/cypress/fixtures/resource/task/pending-execution.json similarity index 100% rename from cypress/fixtures/job/task/pending-execution.json rename to cypress/fixtures/resource/task/pending-execution.json diff --git a/cypress/fixtures/job/task/pending-executions.json b/cypress/fixtures/resource/task/pending-executions.json similarity index 100% rename from cypress/fixtures/job/task/pending-executions.json rename to cypress/fixtures/resource/task/pending-executions.json diff --git a/cypress/fixtures/job/task/pending-task.json b/cypress/fixtures/resource/task/pending-task.json similarity index 100% rename from cypress/fixtures/job/task/pending-task.json rename to cypress/fixtures/resource/task/pending-task.json diff --git a/cypress/fixtures/job/task/success-executions.json b/cypress/fixtures/resource/task/success-executions.json similarity index 100% rename from cypress/fixtures/job/task/success-executions.json rename to cypress/fixtures/resource/task/success-executions.json diff --git a/cypress/fixtures/job/task/task-id-by-task.json b/cypress/fixtures/resource/task/task-id-by-task.json similarity index 100% rename from cypress/fixtures/job/task/task-id-by-task.json rename to cypress/fixtures/resource/task/task-id-by-task.json diff --git a/cypress/fixtures/job/task/task.json b/cypress/fixtures/resource/task/task.json similarity index 100% rename from cypress/fixtures/job/task/task.json rename to cypress/fixtures/resource/task/task.json diff --git a/src/assets/images/resource/task/clear-hostname.svg b/src/assets/images/resource/task/clear-hostname.svg new file mode 100644 index 0000000..597fdc6 --- /dev/null +++ b/src/assets/images/resource/task/clear-hostname.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/src/assets/images/resource/task/clear-ip.svg b/src/assets/images/resource/task/clear-ip.svg new file mode 100644 index 0000000..4f9673c --- /dev/null +++ b/src/assets/images/resource/task/clear-ip.svg @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/src/assets/images/resource/task/layer.svg b/src/assets/images/resource/task/layer.svg new file mode 100644 index 0000000..df4dfa8 --- /dev/null +++ b/src/assets/images/resource/task/layer.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/src/assets/images/resource/task/proportion.svg b/src/assets/images/resource/task/proportion.svg new file mode 100644 index 0000000..37ae573 --- /dev/null +++ b/src/assets/images/resource/task/proportion.svg @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/src/components/resource/task/clear/index.module.css b/src/components/resource/task/clear/index.module.css index 62f8d3b..b4f3648 100644 --- a/src/components/resource/task/clear/index.module.css +++ b/src/components/resource/task/clear/index.module.css @@ -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; +} diff --git a/src/components/resource/task/clear/index.tsx b/src/components/resource/task/clear/index.tsx index b0c90e7..967b8e9 100644 --- a/src/components/resource/task/clear/index.tsx +++ b/src/components/resource/task/clear/index.tsx @@ -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[]; + scheduler_cluster_id: number; +}; + +type TransformedImage = { + peers: ClusteredPeer[]; + image: Image; +}; + +const transformImages = (images: createGetImageDistributionJobResponse): TransformedImage => { + const clusters = new Map[]>(); + + 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(); 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(); + const [layer, setLayer] = useState(0); + const [pageStates, setPageStates] = useState({}); 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 ? ( + + + + ) : ( + + + + ), + endAdornment: searchImageManifest ? ( + { + setSearchImageManifest(''); + setSearchImageManifestISLodaing(false); + }} + > + + + ) : ( + <> + ), + }, + + 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 ( Search by URL + + + Search by Image Manifest URL + + ) : search === 'image-manifest-url' ? ( + + + ) : ( Scheduler Cluster - + )} + ) : Array.isArray(imageManifestURL?.peers) ? ( + imageManifestURL?.peers.length > 0 ? ( + + + + Cache + + + + + Blobs + + + {`Total: ${layer || 0}`} + + + {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 ( + + + + + Scheduler Cluster + + + + + ID :  {item?.scheduler_cluster_id || '0'} + + + + + {paginatedPeers?.map((items, peerIndex) => { + return ( + + + } + aria-controls="panel1-content" + id={`scheduler-${item?.scheduler_cluster_id}-url-${peerIndex}`} + > + + + + + + + {items?.hostname} + + + + + + + {items?.ip} + + + + + + + {`${((items?.layers?.length / layer) * 100).toFixed(2) || 0}%`} + + + + + + + + + + + + Blobs + + + + {items?.layers.map((item: any, bolbIndex: any) => ( + + + + {extractSHA256Regex(item?.url || '-') || '-'} + + + + ))} + + + + + {peerIndex !== paginatedPeers.length - 1 && } + + ); + })} + + {totalPage > 1 && ( + + handleImagePageChange(schedulerClusterId, page)} + color="primary" + size="small" + /> + + )} + + ); + })} + + ) : ( + + + + + You don't find any results! + + + + ) ) : ( ; } +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 { 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 { + 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; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index ad63659..6971152 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -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] : ''; +};