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 && (
+
+
+ )}
+
+ );
+ })}
+
+ ) : (
+
+
+
+
+ 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] : '';
+};