Merge branch 'releases/v0.0.13' into mock

This commit is contained in:
Arnaud 2024-11-28 17:51:39 +01:00
commit 4602feb486
No known key found for this signature in database
GPG Key ID: 69D6CE281FCAE663
55 changed files with 900 additions and 214 deletions

View File

@ -8,9 +8,9 @@ on:
workflow_dispatch:
env:
codex_version: v0.1.8
circuit_version: v0.1.8
marketplace_address: "0x5Bd66fA15Eb0E546cd26808248867a572cFF5706"
codex_version: v0.1.9
circuit_version: v0.1.9
marketplace_address: "0xAB03b6a58C5262f530D54146DA2a552B1C0F7648"
eth_provider: "https://rpc.testnet.codex.storage"
VITE_CODEX_API_URL: ${{ secrets.VITE_CODEX_API_URL }}
VITE_GEO_IP_URL: ${{ secrets.VITE_GEO_IP_URL }}

View File

@ -1,11 +1,16 @@
import test, { expect } from "@playwright/test";
import { Bytes } from "../src/utils/bytes"
import { GB } from "../src/utils/constants"
test('create an availability', async ({ page }) => {
await page.goto('/dashboard/availabilities');
await page.waitForTimeout(500);
await page.locator('.availability-edit button').first().click();
await page.getByLabel('Total size').click();
await page.getByLabel('Total size').fill('0.50');
const value = (Math.random() * 0.5) + 0.1;
await page.getByLabel('Total size').fill(value.toFixed(1));
await page.getByLabel('Duration').click();
await page.getByLabel('Duration').fill('30');
await page.getByLabel('Min price').click();
@ -20,7 +25,7 @@ test('create an availability', async ({ page }) => {
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Success', { exact: true })).toBeVisible();
await page.getByRole('button', { name: 'Finish' }).click();
await expect(page.getByText('512.0 MB allocated for the').first()).toBeVisible();
await expect(page.getByText(Bytes.pretty(parseFloat(value.toFixed(1)) * GB)).first()).toBeVisible();
})
test('availability navigation buttons', async ({ page }) => {
@ -54,3 +59,63 @@ test('availability navigation buttons', async ({ page }) => {
await page.getByRole('button', { name: 'Finish' }).click();
await expect(page.locator('.modal--open')).not.toBeVisible();
})
test('create an availability with changing the duration to months', async ({ page }) => {
await page.goto('/dashboard/availabilities');
await page.waitForTimeout(500);
await page.locator('.availability-edit button').first().click();
await page.getByLabel('Total size').click();
await page.getByLabel('Total size').fill("0.1");
await page.getByLabel('Duration').click();
await page.getByLabel('Duration').fill("3");
await page.getByRole('combobox').nth(1).selectOption('months');
await page.getByLabel('Min price').click();
await page.getByLabel('Min price').fill('5');
await page.getByLabel('Max collateral').click();
await page.getByLabel('Max collateral').fill('30');
await page.getByLabel('Min price').fill('5');
await page.getByLabel('Nickname').click();
await page.getByLabel('Nickname').fill('test');
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Confirm your new sale')).toBeVisible();
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Success', { exact: true })).toBeVisible();
await page.getByRole('button', { name: 'Finish' }).click();
await expect(page.getByText("3 months").first()).toBeVisible();
})
test('create an availability after checking max size and invalid input', async ({ page }) => {
await page.goto('/dashboard/availabilities');
await page.waitForTimeout(500);
await page.locator('.availability-edit button').first().click();
await page.getByLabel('Total size').click();
await page.getByLabel('Total size').fill("9999");
await expect(page.getByLabel('Total size')).toHaveAttribute("aria-invalid");
await page.getByText("Use max size").click()
await expect(page.getByLabel('Total size')).not.toHaveAttribute("aria-invalid");
const value = (Math.random() * 0.5);
await page.getByLabel('Total size').fill(value.toFixed(1));
await page.getByLabel('Duration').click();
await page.getByLabel('Duration').fill('30');
await page.getByLabel('Min price').click();
await page.getByLabel('Min price').fill('5');
await page.getByLabel('Max collateral').click();
await page.getByLabel('Max collateral').fill('30');
await page.getByLabel('Min price').fill('5');
await page.getByLabel('Nickname').click();
await page.getByLabel('Nickname').fill('test');
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Confirm your new sale')).toBeVisible();
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Success', { exact: true })).toBeVisible();
await page.getByRole('button', { name: 'Finish' }).click();
await expect(page.getByText(Bytes.pretty(parseFloat(value.toFixed(1)) * GB)).first()).toBeVisible();
})

View File

@ -76,3 +76,58 @@ test('remove the CID when the file is deleted', async ({ page }) => {
await page.locator('.button-icon--small').nth(1).click();
await expect(page.locator('#cid')).toBeEmpty()
})
test('create a storage request by using decimal values', async ({ page }) => {
await page.goto('/dashboard');
await page.locator('a').filter({ hasText: 'Purchases' }).click();
await page.getByRole('button', { name: 'Storage Request' }).click();
await page.locator('div').getByTestId("upload").setInputFiles([
path.join(__dirname, "assets", 'chatgpt.jpg'),
]);
await expect(page.locator('#cid')).not.toBeEmpty()
await expect(page.getByText('Success, the CID has been')).toBeVisible();
await page.getByRole('button', { name: 'Next' }).click();
await page.getByLabel("Full period of the contract").fill("10")
await expect(page.locator('footer .button--primary')).toHaveAttribute("disabled");
await page.getByLabel("Full period of the contract").fill("1")
await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled");
await page.getByLabel("Full period of the contract").fill("0")
await expect(page.locator('footer .button--primary')).toHaveAttribute("disabled");
const value = (Math.random() * 7);
await page.getByLabel("Full period of the contract").fill(value.toFixed(1))
await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled");
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Your request is being processed.')).toBeVisible();
await page.getByRole('button', { name: 'Finish' }).click();
await expect(page.getByText('No data.')).not.toBeVisible();
await expect(page.getByText(value.toFixed(1) + " days").first()).toBeVisible();
})
// test('create a storage request by using months', async ({ page }) => {
// await page.goto('/dashboard');
// await page.locator('a').filter({ hasText: 'Purchases' }).click();
// await page.getByRole('button', { name: 'Storage Request' }).click();
// await page.locator('div').getByTestId("upload").setInputFiles([
// path.join(__dirname, "assets", 'chatgpt.jpg'),
// ]);
// await expect(page.locator('#cid')).not.toBeEmpty()
// await expect(page.getByText('Success, the CID has been')).toBeVisible();
// await page.getByRole('button', { name: 'Next' }).click();
// await page.getByLabel("Full period of the contract").fill("3")
// await page.getByRole('combobox').selectOption('months');
// await expect(page.getByLabel("Full period of the contract")).toHaveValue("3")
// await page.getByRole('button', { name: 'Next' }).click();
// await expect(page.getByText('Your request is being processed.')).toBeVisible();
// await page.getByRole('button', { name: 'Finish' }).click();
// await expect(page.getByText('No data.')).not.toBeVisible();
// await expect(page.getByText("3 months").first()).toBeVisible();
// })

8
package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "0.0.13",
"license": "MIT",
"dependencies": {
"@codex-storage/marketplace-ui-components": "^0.0.50",
"@codex-storage/marketplace-ui-components": "^0.0.51",
"@codex-storage/sdk-js": "^0.0.16",
"@sentry/browser": "^8.32.0",
"@sentry/react": "^8.31.0",
@ -395,9 +395,9 @@
}
},
"node_modules/@codex-storage/marketplace-ui-components": {
"version": "0.0.50",
"resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.50.tgz",
"integrity": "sha512-pijYw8Wd/ns64Q7iSLQ444U5Q7Ql0U88OuxBycuxIOVp3obITJk6SBgdZpxHIj5kWShsUvXU/nJl0V0oKt9ieQ==",
"version": "0.0.51",
"resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.51.tgz",
"integrity": "sha512-KPPFlcpx3a83WCBSLbRONrF/yr4J/ctyTfFPxMaRSMTRD1LtfIE0uPy3QxtHs6tigOts2h4DEz6Kn2ynHdfKPg==",
"engines": {
"node": ">=18"
},

View File

@ -26,7 +26,7 @@
"React"
],
"dependencies": {
"@codex-storage/marketplace-ui-components": "^0.0.50",
"@codex-storage/marketplace-ui-components": "^0.0.51",
"@codex-storage/sdk-js": "^0.0.16",
"@sentry/browser": "^8.32.0",
"@sentry/react": "^8.31.0",

85
public/icons/aud-flag.svg Normal file
View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 512 512"
style="enable-background: new 0 0 512 512"
xml:space="preserve">
<path
style="fill: #0052b4"
d="M512,256c0,141.384-114.616,256-256,256S0,397.384,0,256C0,256.06,256,0.029,256,0
C397.384,0,512,114.616,512,256z" />
<g>
<path
style="fill: #f0f0f0"
d="M256,0c-0.014,0-0.029,0.001-0.043,0.001L256,0L256,0z" />
<path
style="fill: #f0f0f0"
d="M255.315,256H256c0-0.232,0-0.454,0-0.685C255.772,255.544,255.544,255.772,255.315,256z" />
<path
style="fill: #f0f0f0"
d="M256,133.566c0-45.045,0-74.562,0-133.565h-0.043C114.592,0.024,0,114.629,0,256h133.565v-75.212
L208.777,256h46.539c0.229-0.228,0.457-0.456,0.685-0.685c0-17.247,0-32.636,0-46.536l-75.213-75.213H256z" />
</g>
<g>
<path
style="fill: #d80027"
d="M129.515,33.391C89.476,56.19,56.19,89.476,33.391,129.515V256h66.783V100.175v-0.001H256
c0-21.063,0-41.129,0-66.783H129.515z" />
<path
style="fill: #d80027"
d="M256,224.519l-90.953-90.952h-31.481c0-0.001,0,0,0,0L255.999,256H256
C256,256,256,234.295,256,224.519z" />
</g>
<g>
<polygon
style="fill: #f0f0f0"
points="154.395,300.522 168.445,329.9 200.172,322.567 185.964,351.869 211.478,372.102
179.711,379.262 179.8,411.826 154.395,391.453 128.991,411.826 129.08,379.262 97.312,372.102 122.827,351.869 108.617,322.567
140.346,329.9 " />
<polygon
style="fill: #f0f0f0"
points="383.284,356.174 390.309,370.863 406.173,367.196 399.068,381.847 411.826,391.964
395.942,395.544 395.986,411.826 383.284,401.639 370.582,411.826 370.626,395.544 354.743,391.964 367.5,381.847 360.396,367.196
376.259,370.863 " />
<polygon
style="fill: #f0f0f0"
points="317.933,200.348 324.957,215.038 340.821,211.37 333.717,226.021 346.474,236.138
330.591,239.718 330.634,256 317.933,245.813 305.231,256 305.274,239.718 289.391,236.138 302.148,226.021 295.044,211.37
310.908,215.038 " />
<polygon
style="fill: #f0f0f0"
points="383.284,111.304 390.309,125.994 406.173,122.327 399.069,136.978 411.825,147.094
395.942,150.675 395.986,166.957 383.284,156.77 370.582,166.957 370.626,150.675 354.743,147.094 367.499,136.978
360.396,122.327 376.259,125.994 " />
<polygon
style="fill: #f0f0f0"
points="440.368,178.087 447.392,192.777 463.256,189.109 456.152,203.76 468.909,213.877
453.025,217.458 453.069,233.739 440.368,223.553 427.666,233.739 427.709,217.458 411.826,213.877 424.583,203.76
417.479,189.109 433.342,192.777 " />
<polygon
style="fill: #f0f0f0"
points="399.55,256 405.075,273.006 422.957,273.006 408.49,283.517 414.017,300.522
399.55,290.012 385.084,300.522 390.609,283.517 376.143,273.006 394.024,273.006 " />
</g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
<g></g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

28
public/icons/btc-flag.svg Normal file
View File

@ -0,0 +1,28 @@
<svg
xmlns="http://www.w3.org/2000/svg"
xml:space="preserve"
width="100%"
height="100%"
version="1.1"
shape-rendering="geometricPrecision"
text-rendering="geometricPrecision"
image-rendering="optimizeQuality"
fill-rule="evenodd"
clip-rule="evenodd"
viewBox="0 0 4091.27 4091.73"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<g id="Layer_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer" />
<g id="_1421344023328">
<path
fill="#F7931A"
fill-rule="nonzero"
d="M4030.06 2540.77c-273.24,1096.01 -1383.32,1763.02 -2479.46,1489.71 -1095.68,-273.24 -1762.69,-1383.39 -1489.33,-2479.31 273.12,-1096.13 1383.2,-1763.19 2479,-1489.95 1096.06,273.24 1763.03,1383.51 1489.76,2479.57l0.02 -0.02z" />
<path
fill="white"
fill-rule="nonzero"
d="M2947.77 1754.38c40.72,-272.26 -166.56,-418.61 -450,-516.24l91.95 -368.8 -224.5 -55.94 -89.51 359.09c-59.02,-14.72 -119.63,-28.59 -179.87,-42.34l90.16 -361.46 -224.36 -55.94 -92 368.68c-48.84,-11.12 -96.81,-22.11 -143.35,-33.69l0.26 -1.16 -309.59 -77.31 -59.72 239.78c0,0 166.56,38.18 163.05,40.53 90.91,22.69 107.35,82.87 104.62,130.57l-104.74 420.15c6.26,1.59 14.38,3.89 23.34,7.49 -7.49,-1.86 -15.46,-3.89 -23.73,-5.87l-146.81 588.57c-11.11,27.62 -39.31,69.07 -102.87,53.33 2.25,3.26 -163.17,-40.72 -163.17,-40.72l-111.46 256.98 292.15 72.83c54.35,13.63 107.61,27.89 160.06,41.3l-92.9 373.03 224.24 55.94 92 -369.07c61.26,16.63 120.71,31.97 178.91,46.43l-91.69 367.33 224.51 55.94 92.89 -372.33c382.82,72.45 670.67,43.24 791.83,-303.02 97.63,-278.78 -4.86,-439.58 -206.26,-544.44 146.69,-33.83 257.18,-130.31 286.64,-329.61l-0.07 -0.05zm-512.93 719.26c-69.38,278.78 -538.76,128.08 -690.94,90.29l123.28 -494.2c152.17,37.99 640.17,113.17 567.67,403.91zm69.43 -723.3c-63.29,253.58 -453.96,124.75 -580.69,93.16l111.77 -448.21c126.73,31.59 534.85,90.55 468.94,355.05l-0.02 0z" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

24
public/icons/cad-flag.svg Normal file
View File

@ -0,0 +1,24 @@
<svg
height="800px"
width="800px"
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512"
xml:space="preserve">
<circle style="fill: #f0f0f0" cx="256" cy="256" r="256" />
<g>
<path
style="fill: #d80027"
d="M512,256c0-101.494-59.065-189.19-144.696-230.598v461.195C452.935,445.19,512,357.494,512,256z" />
<path
style="fill: #d80027"
d="M0,256c0,101.494,59.065,189.19,144.696,230.598V25.402C59.065,66.81,0,154.506,0,256z" />
<polygon
style="fill: #d80027"
points="300.522,289.391 345.043,267.13 322.783,256 322.783,233.739 278.261,256 300.522,211.478
278.261,211.478 256,178.087 233.739,211.478 211.478,211.478 233.739,256 189.217,233.739 189.217,256 166.957,267.13
211.478,289.391 200.348,311.652 244.87,311.652 244.87,345.043 267.13,345.043 267.13,311.652 311.652,311.652 " />
</g>
</svg>

After

Width:  |  Height:  |  Size: 936 B

45
public/icons/cny-flag.svg Normal file
View File

@ -0,0 +1,45 @@
<svg
version="1.1"
id="圖層_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
width="311.81px"
height="311.81px"
viewBox="0 0 311.81 311.81"
enable-background="new 0 0 311.81 311.81"
xml:space="preserve">
<circle
fill="#DC2E27"
stroke="#000000"
stroke-miterlimit="10"
cx="157"
cy="157"
r="150" />
<path
id="path3374_1_"
fill="#F7DC15"
d="M191.928,83.868l7.479,12.46l-9.544,10.964l14.164-3.26l7.478,12.459l1.279-14.476
l14.159-3.26l-13.371-5.685l1.271-14.479l-9.538,10.964L191.928,83.868z" />
<path
id="path3433_1_"
fill="#F7DC15"
d="M107.009,97l-13.46,41.46H49.94l35.271,25.628L71.743,205.54l35.276-25.616l35.265,25.612
l-13.473-41.444l35.264-25.64l-43.6,0.012L107.009,97z" />
<path
id="path3447_1_"
fill="#F7DC15"
d="M238.217,119.048l2.061,14.38l-13.047,6.399l14.314,2.492L243.6,156.7l6.797-12.844
l14.313,2.488l-10.12-10.424l6.788-12.848l-13.045,6.404L238.217,119.048z" />
<path
id="path3453_1_"
fill="#F7DC15"
d="M214.076,218.288L205,229.636l-13.6-5.128l7.982,12.141l-9.074,11.344l14.012-3.84
l7.984,12.132l0.672-14.507l14.012-3.85l-13.586-5.123L214.076,218.288z" />
<path
id="path3475_1_"
fill="#F7DC15"
d="M246.305,177.012l-3.99,13.973l-14.527,0.521l12.057,8.116l-3.99,13.968l11.442-8.956
l12.054,8.108l-4.984-13.644l11.439-8.965l-14.523,0.524L246.305,177.012z" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

46
public/icons/eth-flag.svg Normal file
View File

@ -0,0 +1,46 @@
<svg
xmlns="http://www.w3.org/2000/svg"
xml:space="preserve"
width="100%"
height="100%"
version="1.1"
shape-rendering="geometricPrecision"
text-rendering="geometricPrecision"
image-rendering="optimizeQuality"
fill-rule="evenodd"
clip-rule="evenodd"
viewBox="0 0 784.37 1277.39"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<g id="Layer_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer" />
<g id="_1421394342400">
<g>
<polygon
fill="#343434"
fill-rule="nonzero"
points="392.07,0 383.5,29.11 383.5,873.74 392.07,882.29 784.13,650.54 " />
<polygon
fill="#8C8C8C"
fill-rule="nonzero"
points="392.07,0 -0,650.54 392.07,882.29 392.07,472.33 " />
<polygon
fill="#3C3C3B"
fill-rule="nonzero"
points="392.07,956.52 387.24,962.41 387.24,1263.28 392.07,1277.38 784.37,724.89 " />
<polygon
fill="#8C8C8C"
fill-rule="nonzero"
points="392.07,1277.38 392.07,956.52 -0,724.89 " />
<polygon
fill="#141414"
fill-rule="nonzero"
points="392.07,882.29 784.13,650.54 392.07,472.33 " />
<polygon
fill="#393939"
fill-rule="nonzero"
points="0,650.54 392.07,882.29 392.07,472.33 " />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

14
src/assets/icons/dots.svg Normal file
View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#969696"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<circle cx="12" cy="12" r="1" />
<circle cx="12" cy="5" r="1" />
<circle cx="12" cy="19" r="1" />
</svg>

After

Width:  |  Height:  |  Size: 306 B

View File

@ -28,7 +28,7 @@ type Props = {
};
const icons: Record<string, ReactElement> = {
dashboard: <DashboardIcon />,
dashboard: <DashboardIcon width={24} />,
peers: <PeersIcon width={24} />,
settings: <SettingsIcon width={24} />,
files: <FilesIcon width={24} />,
@ -74,16 +74,16 @@ export function AppBar({ onIconClick, onExpanded }: Props) {
const title =
location.pathname.split("/")[2] || location.pathname.split("/")[1];
const networkIconColor = online ? "#3EE089" : "var(-codex-color-error)";
const networkIconColor = online ? "#3EE089" : "var(--codex-color-error)";
const nodesIconColor =
codex.enabled === false
? "var(-codex-color-error)"
? "var(--codex-color-error)"
: persistence.enabled
? "#3EE089"
: "var(--codex-input-color-warning)";
const icon = isMobile ? (
<Logo onClick={() => onExpanded(true)}></Logo>
<Logo onClick={() => onExpanded(true)} width={30}></Logo>
) : (
icons[title]
);

View File

@ -45,6 +45,12 @@
sans-serif;
letter-spacing: -0.006em;
color: rgba(150, 150, 150, 0.8);
@media (max-width: 800px) {
& {
display: none;
}
}
}
> div {
@ -88,7 +94,7 @@
}
@media (max-width: 800px) {
aside {
.wallet-login {
display: none;
}
}

View File

@ -4,7 +4,7 @@ import {
Table,
TabSortState,
} from "@codex-storage/marketplace-ui-components";
import { PrettyBytes } from "../../utils/bytes";
import { Bytes } from "../../utils/bytes";
import { AvailabilityActionsCell } from "./AvailabilityActionsCell";
import { CodexAvailability, CodexNodeSpace } from "@codex-storage/sdk-js/async";
import { Times } from "../../utils/times";
@ -86,7 +86,7 @@ export function AvailabilitiesTable({ availabilities, space }: Props) {
)}
</Cell>,
<AvailabilityIdCell value={a} />,
<Cell>{PrettyBytes(a.totalSize)}</Cell>,
<Cell>{Bytes.pretty(a.totalSize)}</Cell>,
<Cell>{Times.pretty(a.duration)}</Cell>,
<Cell>{a.minPrice.toString()}</Cell>,
<Cell>{a.maxCollateral.toString()}</Cell>,

View File

@ -1,5 +1,5 @@
import { Cell, Row } from "@codex-storage/marketplace-ui-components";
import { PrettyBytes } from "../../utils/bytes";
import { Bytes } from "../../utils/bytes";
import { classnames } from "../../utils/classnames";
import HardriveIcon from "../../assets/icons/hardrive.svg?react";
@ -19,7 +19,7 @@ export function AvailabilityDiskRow({ bytes }: Props) {
<HardriveIcon />
<div>
<b>Node</b>
<small>{PrettyBytes(bytes)} allocated for the node</small>
<small>{Bytes.pretty(bytes)} allocated for the node</small>
</div>
</div>
</Cell>,

View File

@ -10,7 +10,7 @@ import { CodexNodeSpace } from "@codex-storage/sdk-js";
import { AvailabilityConfirm } from "./AvailabilityConfirmation";
import { WebStorage } from "../../utils/web-storage";
import { AvailabilityState } from "./types";
import { GB, STEPPER_DURATION } from "../../utils/constants";
import { STEPPER_DURATION } from "../../utils/constants";
import { useAvailabilityMutation } from "./useAvailabilityMutation";
import { AvailabilitySuccess } from "./AvailabilitySuccess";
import { AvailabilityError } from "./AvailabilityError";
@ -28,8 +28,8 @@ type Props = {
const CONFIRM_STATE = 2;
const defaultAvailabilityData: AvailabilityState = {
totalSize: 0.5 * GB,
duration: Times.unitValue("days"),
totalSize: 0.5,
duration: 1,
minPrice: 0,
maxCollateral: 0,
totalSizeUnit: "gb",
@ -52,7 +52,7 @@ export function AvailabilityEdit({
useEffect(() => {
Promise.all([
WebStorage.get<number>("availability-step"),
WebStorage.get<AvailabilityState>("availability"),
WebStorage.get<AvailabilityState>("availability-1"),
]).then(([s, a]) => {
if (s) {
dispatch({
@ -62,31 +62,11 @@ export function AvailabilityEdit({
}
if (a) {
setAvailability(a);
setAvailability({ ...defaultAvailabilityData, ...a });
}
});
}, [dispatch]);
// We use a custom event to not re render the sunburst component
// useEffect(() => {
// const onAvailabilityIdChange = (e: Event) => {
// const custom = e as CustomEvent;
// setAvailabilityId(custom.detail);
// };
// document.addEventListener(
// "codexavailabilityid",
// onAvailabilityIdChange,
// false
// );
// return () =>
// document.removeEventListener(
// "codexavailabilityid",
// onAvailabilityIdChange
// );
// }, []);
const components = [
AvailabilityForm,
AvailabilityConfirm,
@ -157,7 +137,7 @@ export function AvailabilityEdit({
editAvailabilityValue.current = a.totalSize;
WebStorage.set("availability-step", 0);
WebStorage.set("availability", a);
WebStorage.set("availability-1", a);
const unit = Times.unit(a.duration);

View File

@ -11,7 +11,6 @@ import NodesIcon from "../../assets/icons/nodes.svg?react";
import InfoIcon from "../../assets/icons/info.svg?react";
import { attributes } from "../../utils/attributes";
import { AvailabilityUtils } from "./availability.utils";
import { Times } from "../../utils/times";
export function AvailabilityForm({
dispatch,
@ -39,27 +38,24 @@ export function AvailabilityForm({
const element = e.currentTarget;
onAvailabilityChange({
totalSize: 0,
totalSizeUnit: element.value as "tb" | "gb",
});
};
const onDurationChange = async (e: ChangeEvent<HTMLInputElement>) => {
const element = e.currentTarget;
const unitValue = Times.unitValue(availability.durationUnit);
onAvailabilityChange({
duration: parseInt(element.value) * unitValue,
duration: parseInt(element.value),
});
};
const onDurationUnitChange = async (e: ChangeEvent<HTMLSelectElement>) => {
const element = e.currentTarget;
const unit = element.value as "hours" | "days" | "months";
const unitValue = Times.unitValue(unit);
onAvailabilityChange({
duration: unitValue,
duration: availability.duration,
durationUnit: unit,
});
};
@ -67,10 +63,9 @@ export function AvailabilityForm({
const onAvailablityChange = async (e: ChangeEvent<HTMLInputElement>) => {
const element = e.currentTarget;
const v = element.value;
const unit = AvailabilityUtils.unitValue(availability.totalSizeUnit);
onAvailabilityChange({
totalSize: parseFloat(v) * unit,
totalSize: parseFloat(v),
});
};
@ -87,7 +82,12 @@ export function AvailabilityForm({
const available = AvailabilityUtils.maxValue(space);
onAvailabilityChange({
totalSize: available,
totalSize:
Math.floor(
((available - 1) /
AvailabilityUtils.unitValue(availability.totalSizeUnit)) *
10
) / 10,
});
};
@ -96,20 +96,17 @@ export function AvailabilityForm({
available += editAvailabilityValue;
}
const isValid =
availability.totalSize > 0 && available >= availability.totalSize;
const totalSizeInBytes =
availability.totalSize *
AvailabilityUtils.unitValue(availability.totalSizeUnit);
const isValid = totalSizeInBytes > 0 && available >= totalSizeInBytes;
const helper = isValid
? "Total size of sale's storage in bytes"
: "The total size cannot exceed the space available.";
const value = AvailabilityUtils.toUnit(
availability.totalSize,
availability.totalSizeUnit
).toFixed(2);
const unitValue = Times.unitValue(availability.durationUnit);
const duration = availability.duration / unitValue;
const duration = availability.duration;
return (
<div className="availability-form">
@ -143,13 +140,12 @@ export function AvailabilityForm({
name="totalSize"
type="number"
label="Total size"
min={0.01}
isInvalid={!isValid}
max={available.toFixed(2)}
onChange={onAvailablityChange}
onGroupChange={onTotalSizeUnitChange}
step={"0.01"}
value={value}
value={availability.totalSize.toString()}
min={"0"}
group={[
["gb", "GB"],
// ["tb", "TB"],

View File

@ -1,6 +1,6 @@
import { Strings } from "../../utils/strings";
import { Cell } from "@codex-storage/marketplace-ui-components";
import { PrettyBytes } from "../../utils/bytes";
import { Bytes } from "../../utils/bytes";
import { AvailabilityWithSlots } from "./types";
import AvailbilityIcon from "../../assets/icons/availability.svg?react";
@ -18,7 +18,7 @@ export function AvailabilityIdCell({ value }: Props) {
<b>{value.name || Strings.shortId(value.id)}</b>
</div>
<small className="text--light">
{PrettyBytes(value.totalSize)} allocated for the availability
{Bytes.pretty(value.totalSize)} allocated for the availability
</small>
<br />
<small className="text--light">

View File

@ -1,5 +1,5 @@
import { Cell, Row } from "@codex-storage/marketplace-ui-components";
import { PrettyBytes } from "../../utils/bytes";
import { Bytes } from "../../utils/bytes";
import "./SlotRow.css";
import { classnames } from "../../utils/classnames";
import { attributes } from "../../utils/attributes";
@ -47,7 +47,7 @@ export function SlotRow({ bytes, active, id }: Props) {
<SlotIcon />
<div>
<b>Slot {id}</b>
<small>{PrettyBytes(bytes)} allocated for the slot</small>
<small>{Bytes.pretty(bytes)} allocated for the slot</small>
</div>
</div>
</Cell>,

View File

@ -1,7 +1,7 @@
import { CodexNodeSpace } from "@codex-storage/sdk-js";
import { Times } from "../../utils/times";
import { Strings } from "../../utils/strings";
import { PrettyBytes } from "../../utils/bytes";
import { Bytes } from "../../utils/bytes";
import { useEffect, useRef, useState } from "react";
import { CallbackDataParams, ECBasicOption } from "echarts/types/dist/shared";
import * as echarts from "echarts/core";
@ -36,6 +36,16 @@ export function Sunburst({ availabilities, space }: Props) {
}
}, [chart, div]);
useEffect(() => {
const refresh = () => chart.current?.resize();
window.addEventListener("resize", refresh);
return () => {
window.removeEventListener("resize", refresh);
};
}, []);
const data = availabilities.map((a, index) => {
return {
name: Strings.shortId(a.id),
@ -65,7 +75,7 @@ export function Sunburst({ availabilities, space }: Props) {
a.minPrice +
"<br/>" +
"Size " +
PrettyBytes(a.totalSize)
Bytes.pretty(a.totalSize)
);
},
},
@ -87,7 +97,7 @@ export function Sunburst({ availabilities, space }: Props) {
params.marker +
"Slot " +
slot.id +
PrettyBytes(parseFloat(slot.size))
Bytes.pretty(parseFloat(slot.size))
);
},
},
@ -121,7 +131,7 @@ export function Sunburst({ availabilities, space }: Props) {
return (
params.marker +
" Space remaining " +
PrettyBytes(
Bytes.pretty(
space.quotaMaxBytes -
space.quotaReservedBytes -
space.quotaUsedBytes
@ -170,6 +180,8 @@ export function Sunburst({ availabilities, space }: Props) {
};
chart.current.setOption(option);
chart.current?.resize();
// chart.current.off("click");
// chart.current.on("click", function (params) {
// // console.info(params.componentIndex);
@ -196,7 +208,6 @@ export function Sunburst({ availabilities, space }: Props) {
ref={div}
className="sunburst"
style={{
width: size,
height: size,
}}></div>
);

View File

@ -173,16 +173,18 @@ describe("files", () => {
quotaUsedBytes: GB,
totalBlocks: 0
}
assert.deepEqual(AvailabilityUtils.maxValue(space), 5 * GB);
assert.deepEqual(AvailabilityUtils.maxValue(space), 5 * GB - 1);
})
it("checks the availability max value", async () => {
const availability = {
totalSize: GB
totalSizeUnit: "gb",
totalSize: 1
} as AvailabilityState
assert.deepEqual(AvailabilityUtils.isValid(availability, GB * 2), true);
assert.deepEqual(AvailabilityUtils.isValid({ ...availability, totalSize: -1 }, GB), false);
assert.deepEqual(AvailabilityUtils.isValid({ ...availability, totalSize: 2 * GB }, GB), false);
assert.deepEqual(AvailabilityUtils.isValid({ ...availability, totalSize: GB }, 2 * GB), false);
})
it("toggles item in array", async () => {

View File

@ -39,7 +39,8 @@ export const AvailabilityUtils = {
return bytes / this.unitValue(unit || "gb")
},
maxValue(space: CodexNodeSpace) {
return space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes
// Remove 1 byte to allow to create an availability with the max space possible
return space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes - 1
},
unitValue(unit: "gb" | "tb") {
return unit === "tb" ? TB : GB
@ -47,7 +48,7 @@ export const AvailabilityUtils = {
isValid: (
availability: AvailabilityState,
max: number
) => availability.totalSize > 0 && availability.totalSize <= max
) => availability.totalSize > 0 && availability.totalSize * AvailabilityUtils.unitValue(availability.totalSizeUnit) <= max
,
toggle: <T>(arr: Array<T>, value: T) =>
arr.includes(value) ? arr.filter(i => i !== value) : [...arr, value],
@ -88,5 +89,5 @@ export const AvailabilityUtils = {
"#D2493C22",
"#D2493C11",
"#D2493C00",
]
],
}

View File

@ -9,6 +9,8 @@ import {
} from "@codex-storage/marketplace-ui-components";
import { CodexSdk } from "../../sdk/codex";
import { CodexAvailabilityCreateResponse } from "@codex-storage/sdk-js";
import { Times } from "../../utils/times";
import { AvailabilityUtils } from "./availability.utils";
export function useAvailabilityMutation(
@ -42,15 +44,15 @@ export function useAvailabilityMutation(
return fn({
...input,
duration,
totalSize: Math.trunc(totalSize),
duration: Times.value(durationUnit) * duration,
totalSize: Math.trunc(totalSize * AvailabilityUtils.unitValue(totalSizeUnit)),
});
},
onSuccess: (res, body) => {
queryClient.invalidateQueries({ queryKey: ["availabilities"] });
queryClient.invalidateQueries({ queryKey: ["space"] });
WebStorage.delete("availability");
WebStorage.delete("availability-1");
WebStorage.delete("availability-step");
if (typeof res === "object" && body.name) {

View File

@ -4,6 +4,13 @@
padding: 16px;
background-color: rgb(35, 35, 35);
@media (max-width: 800px) {
&,
td:nth-child(2) {
padding: 8px;
}
}
> header {
display: flex;
align-items: center;

View File

@ -147,18 +147,38 @@
box-shadow: 0 0 0 3px rgba(150, 150, 150, 0.2);
}
&:has(option[value="USD"]:checked) {
background-image: url(/icons/us-flag.svg);
&:has(option:checked) {
background-position: 10px;
background-repeat: no-repeat;
background-size: 16px;
}
&:has(option[value="USD"]:checked) {
background-image: url(/icons/us-flag.svg);
}
&:has(option[value="EUR"]:checked) {
background-image: url(/icons/euro-flag.svg);
background-position: 10px;
background-repeat: no-repeat;
background-size: 16px;
}
&:has(option[value="AUD"]:checked) {
background-image: url(/icons/aud-flag.svg);
}
&:has(option[value="CAD"]:checked) {
background-image: url(/icons/cad-flag.svg);
}
&:has(option[value="CNY"]:checked) {
background-image: url(/icons/cny-flag.svg);
}
&:has(option[value="BTC"]:checked) {
background-image: url(/icons/btc-flag.svg);
}
&:has(option[value="ETH"]:checked) {
background-image: url(/icons/eth-flag.svg);
}
option {
@ -205,6 +225,5 @@
.lines {
height: 200;
transform: scale(1.25);
}
}

View File

@ -33,24 +33,52 @@ export function WalletCard({ tab }: Props) {
}
}, [chart, div]);
useEffect(() => {
const refresh = () => chart.current?.resize();
window.addEventListener("resize", refresh);
return () => {
window.removeEventListener("resize", refresh);
};
}, []);
const onCurrencyChange = async (e: ChangeEvent<HTMLSelectElement>) => {
const value = e.currentTarget.value;
setCurrency(value);
if (value === "USD") {
setTokenValue(1540);
} else {
} else if (["BTC", "ETH"].includes(value) === false) {
const json = await fetch(
"https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json"
).then((res) => res.json());
setTokenValue(defaultTokenValue * json.usd.eur);
setTokenValue(defaultTokenValue * json.usd[value.toLocaleLowerCase()]);
} else {
const json = await fetch(
"https://api.coinbase.com/v2/prices/" +
value.toLocaleLowerCase() +
"-USD/spot.json"
).then((res) => res.json());
setTokenValue(defaultTokenValue / json.data.amount);
}
};
if (chart.current) {
const today = new Date();
const startOfWeek = today.getDate() - today.getDay() + 1;
const startDates = [];
today.setDate(startOfWeek);
for (let i = 0; i < 5; i++) {
startDates.push(today.toISOString().split("T")[0]);
today.setDate(today.getDate() + 7);
}
const data = {
daily: ["MON", "TUE", "WED", "THU", "WED", "SAT", "SUN"],
weekly: ["1", "2", "3", "4", "5", "6"],
weekly: startDates,
monthly: ["JAN", "FEB", "MAR", "APR", "MAY", "JUN"],
}[tab];
@ -72,6 +100,14 @@ export function WalletCard({ tab }: Props) {
type: "value",
show: false,
},
grid: [
{
left: 0,
right: 0,
top: 50,
bottom: 50,
},
],
series: [
{
data: [220, 932, 401, 934, 1290, 1330, 1450].slice(0, data.length),
@ -89,7 +125,7 @@ export function WalletCard({ tab }: Props) {
},
};
chart.current.setOption(option);
chart.current.setOption(option, true);
}
return (
@ -115,7 +151,10 @@ export function WalletCard({ tab }: Props) {
<section>
{/* <WalletChart></WalletChart> */}
{/* <WalletLines></WalletLines> */}
<div className="lines" ref={div} style={{ height: 200 }}></div>
<div
className="lines"
ref={div}
style={{ height: 200, width: "100%" }}></div>
</section>
</main>
@ -124,14 +163,20 @@ export function WalletCard({ tab }: Props) {
<span>TOKEN</span>
<div className="row">
<var>
{tokenValue.toFixed(2)} {currency}
{tokenValue.toFixed(["BTC", "ETH"].includes(currency) ? 8 : 2)}{" "}
{currency}
</var>
<small>+ 5%</small>
</div>
</div>
<select defaultValue={currency} onChange={onCurrencyChange}>
<option value={"USD"}>USD</option>
<option value={"BTC"}>BTC</option>
<option value={"ETH"}>ETH</option>
<option value={"EUR"}>EUR</option>
<option value={"AUD"}>AUD</option>
<option value={"CAD"}>CAD</option>
<option value={"CNY"}>CNY</option>
</select>
</footer>
</div>

View File

@ -10,6 +10,37 @@
padding: 10px;
}
ul {
transition: bottom 0.35s;
position: fixed;
bottom: -500px;
left: 0;
right: 0;
background-color: rgba(47, 47, 47, 1);
border-top: 1px solid rgba(150, 150, 150, 0.2);
border-top-left-radius: 12px;
border-top-right-radius: 12px;
padding: 16px;
&[aria-expanded] {
bottom: 0;
z-index: 11;
}
li {
display: flex;
align-items: center;
gap: 16px;
}
li + li {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid rgba(150, 150, 150, 0.2);
}
}
@media (max-width: 800px) {
.folder-button {
display: none;

View File

@ -1,10 +1,19 @@
import { ButtonIcon, Cell } from "@codex-storage/marketplace-ui-components";
import {
Backdrop,
ButtonIcon,
Cell,
} from "@codex-storage/marketplace-ui-components";
import { FolderButton } from "./FolderButton";
import { CodexDataContent } from "@codex-storage/sdk-js";
import { CodexSdk } from "../../sdk/codex";
import "./FileActions.css";
import DownloadIcon from "../../assets/icons/download-file.svg?react";
import InfoFileIcon from "../../assets/icons/info-file.svg?react";
import DotsIcon from "../../assets/icons/dots.svg?react";
import { useIsMobile } from "../../hooks/useMobile";
import { useState } from "react";
import { attributes } from "../../utils/attributes";
import CopyIcon from "../../assets/icons/copy.svg?react";
type Props = {
content: CodexDataContent;
@ -19,7 +28,53 @@ export function FileActions({
onFolderToggle,
onDetails,
}: Props) {
const isMobile = useIsMobile();
const url = CodexSdk.url() + "/api/codex/v1/data/";
const [isExpanded, setIsExpanded] = useState(false);
const onClose = () => setIsExpanded(false);
const onOpen = () => setIsExpanded(true);
const onCopy = (cid: string) => {
setIsExpanded(false);
navigator.clipboard.writeText(cid);
};
if (isMobile) {
return (
<Cell className="file-actions">
<>
<ButtonIcon
variant="small"
onClick={onOpen}
Icon={() => (
<DotsIcon width={24} height={24}></DotsIcon>
)}></ButtonIcon>
<ul {...attributes({ "aria-expanded": isExpanded })}>
<li
onClick={() => {
window.open(url + content.cid, "_blank");
setIsExpanded(false);
}}>
<DownloadIcon width={20}></DownloadIcon> Download
</li>
<li
onClick={() => {
onDetails(content.cid);
setIsExpanded(false);
}}>
<InfoFileIcon width={20}></InfoFileIcon> Details
</li>
<li onClick={() => onCopy(content.cid)}>
<CopyIcon width={20}></CopyIcon> Copy
</li>
</ul>
<Backdrop open={isExpanded} onClose={onClose}></Backdrop>
</>
</Cell>
);
}
return (
<Cell className="file-actions">

View File

@ -17,6 +17,12 @@
height: 40px;
background-color: rgba(20, 20, 20, 0.6);
border: 1px solid rgba(150, 150, 150, 0.2);
@media (max-width: 800px) {
& {
display: none;
}
}
}
}
}

View File

@ -14,11 +14,27 @@ type Props = {
};
export function FileCell({ content }: Props) {
const [toast, setToast] = useState({ time: 0, message: "" });
const [toast, setToast] = useState({
time: 0,
message: "",
variant: "success" as "success" | "error",
});
const onCopy = (cid: string) => {
navigator.clipboard.writeText(cid);
setToast({ message: "CID copied to the clipboard.", time: Date.now() });
if (navigator.clipboard) {
navigator.clipboard.writeText(cid);
setToast({
message: "CID copied to the clipboard.",
time: Date.now(),
variant: "success" as "success",
});
} else {
setToast({
message: "Sorry the CID cannot be copied to the clipboard.",
time: Date.now(),
variant: "error" as "error",
});
}
};
return (
@ -44,7 +60,11 @@ export function FileCell({ content }: Props) {
)}></ButtonIcon>
</div>
<Toast message={toast.message} time={toast.time} variant={"success"} />
<Toast
message={toast.message}
time={toast.time}
variant={toast.variant}
/>
</Cell>
</>
);

View File

@ -21,17 +21,6 @@
span {
flex-grow: 1;
}
.button-icon {
background-color: rgb(47, 47, 47);
border: 1px solid rgba(150, 150, 150, 0.2);
svg {
position: relative;
left: -3px;
top: -1px;
}
}
}
.preview {

View File

@ -5,7 +5,7 @@ import {
WebFileIcon,
} from "@codex-storage/marketplace-ui-components";
import { CodexDataContent, CodexPurchase } from "@codex-storage/sdk-js";
import { PrettyBytes } from "../../utils/bytes";
import { Bytes } from "../../utils/bytes";
import { CidCopyButton } from "./CidCopyButton";
import "./FileDetails.css";
import { CodexSdk } from "../../sdk/codex";
@ -136,7 +136,7 @@ export function FileDetails({ onClose, details }: Props) {
<li>
<p>Size:</p>
<p>{PrettyBytes(details.manifest.datasetSize)}</p>
<p>{Bytes.pretty(details.manifest.datasetSize)}</p>
</li>
<li>

View File

@ -1,5 +1,5 @@
import { ChangeEvent, useEffect, useState } from "react";
import { PrettyBytes } from "../../utils/bytes";
import { Bytes } from "../../utils/bytes";
import "./Files.css";
import {
Tabs,
@ -164,7 +164,7 @@ export function Files({ limit }: Props) {
<Row
cells={[
<FileCell content={c}></FileCell>,
<Cell>{PrettyBytes(c.manifest.datasetSize)}</Cell>,
<Cell>{Bytes.pretty(c.manifest.datasetSize)}</Cell>,
<Cell>
{FilesUtils.formatDate(c.manifest.uploadedAt).toString()}
</Cell>,

View File

@ -294,8 +294,8 @@ describe("files", () => {
});
it("formats date", async () => {
const utcDate = new Date(Date.UTC(2024, 10, 20, 11, 36));
assert.equal(FilesUtils.formatDate(1732102577), "20 Nov 2024, 11:36");
assert.equal(FilesUtils.formatDate(1732102577), "20 Nov 2024, " + utcDate.getHours() + ":" + utcDate.getMinutes());
})
})

View File

@ -7,6 +7,12 @@
> div:first-child {
position: relative;
@media (max-width: 800px) {
& {
flex: 1;
}
}
svg {
position: absolute;
top: 11px;
@ -22,5 +28,11 @@
select {
border-color: rgba(150, 150, 150, 1);
padding-left: 40px;
@media (max-width: 800px) {
& {
width: 100%;
}
}
}
}

View File

@ -16,6 +16,7 @@ import PurchaseIcon from "../../assets/icons/purchase.svg?react";
import HostIcon from "../../assets/icons/host.svg?react";
import LogsIcon from "../../assets/icons/logs.svg?react";
import SettingsIcon from "../../assets/icons/settings.svg?react";
import CloseIcon from "../../assets/icons/close.svg?react";
import HelpIcon from "../../assets/icons/help.svg?react";
import DisclaimerIcon from "../../assets/icons/disclaimer.svg?react";
import { NavLink } from "react-router-dom";
@ -65,6 +66,8 @@ export function Menu({ isExpanded, onExpanded }: Props) {
}
};
const Icon = isMobile ? CloseIcon : ExpandIcon;
return (
<>
<aside
@ -77,7 +80,7 @@ export function Menu({ isExpanded, onExpanded }: Props) {
<Logo onClick={onLogoClick} width={30} />
<Logotype height={34} />
<div>
<ExpandIcon onClick={onExpandMenu}></ExpandIcon>
<Icon onClick={onExpandMenu}></Icon>
</div>
</header>
@ -142,20 +145,20 @@ export function Menu({ isExpanded, onExpanded }: Props) {
</NavLink>
<NavLink onClick={onClose} to="/dashboard/availabilities">
<span>
<HostIcon />
<HostIcon width={20} height={20} />
</span>
<span>Host</span>
</NavLink>
<hr />
<NavLink onClick={onClose} to="/dashboard/peers">
<span>
<PeersIcon width={20} />
<PeersIcon width={20} height={20} />
</span>
<span>Peers</span>
</NavLink>
<NavLink onClick={onClose} to="/dashboard/logs">
<span>
<LogsIcon width={24} />
<LogsIcon width={20} height={20} />
</span>
<span>Log</span>
</NavLink>

View File

@ -19,6 +19,7 @@
left: -300px;
position: fixed;
z-index: 12;
width: 100%;
}
&[aria-expanded] {

View File

@ -36,6 +36,12 @@
section > *:first-child {
flex: 0.5;
@media (max-width: 800px) {
& {
flex: 0.2;
}
}
}
h1 {
@ -203,6 +209,12 @@
right: 6rem;
bottom: 16px;
@media (max-width: 800px) {
& {
right: 16px;
}
}
&:hover {
animation-name: example;
animation-duration: 2.5s;

View File

@ -18,6 +18,12 @@
top: -35px;
bottom: 0;
margin: auto;
@media (max-width: 800px) {
& {
transform: scale(0.8);
}
}
}
footer {

View File

@ -0,0 +1,7 @@
.commitment {
--codex-input-group-background-color: transparent;
span {
right: 155px;
}
}

View File

@ -0,0 +1,70 @@
import { InputGroup, Tooltip } from "@codex-storage/marketplace-ui-components";
import "../CardNumbers/CardNumbers.css";
import "./Commitment.css";
import { ChangeEvent, useState } from "react";
import { classnames } from "../../utils/classnames";
import InfoIcon from "../../assets/icons/info.svg?react";
import { attributes } from "../../utils/attributes";
type Props = {
unit: "months" | "days";
value: string;
onChange: (value: string, unit: "months" | "days") => void;
onValidation?: (value: string) => string;
};
const TESTNET_MAX_VALUE = 7;
export function Commitment({ unit, value, onValidation, onChange }: Props) {
const [error, setError] = useState("");
const onValueChange = (e: ChangeEvent<HTMLInputElement>) => {
onValueOrUnitChange(e.currentTarget.value, unit);
};
const onUnitChange = (e: ChangeEvent<HTMLSelectElement>) => {
onValueOrUnitChange(value, e.currentTarget.value as "months" | "days");
};
const onValueOrUnitChange = (val: string, unit: "months" | "days") => {
onChange(val, unit);
const msg = onValidation?.(val);
if (msg) {
setError(msg);
return;
}
setError("");
};
return (
<div
className={classnames(["card-number cardNumber-container commitment"])}
{...attributes({ "aria-invalid": !!error })}>
<InputGroup
id="duration"
name="duration"
type="number"
label="Full period of the contract"
isInvalid={!!error}
onChange={onValueChange}
onGroupChange={onUnitChange}
value={value}
min={0}
max={TESTNET_MAX_VALUE}
group={[
["days", "days"],
// ["months", "months"],
]}
groupValue={unit}
/>
<Tooltip message={error || "The duration of the request in months"}>
<InfoIcon></InfoIcon>
</Tooltip>
<span>{"Contract duration"}</span>
</div>
);
}

View File

@ -22,8 +22,8 @@ const CONFIRM_STATE = 2;
const defaultStorageRequest: StorageRequest = {
cid: "",
availabilityUnit: "months",
availability: 1,
availabilityUnit: "days",
tolerance: 1,
proofProbability: 1,
nodes: 3,
@ -43,7 +43,7 @@ export function StorageRequestCreate() {
useEffect(() => {
Promise.all([
WebStorage.get<number>("storage-request-step"),
WebStorage.get<StorageRequest>("storage-request"),
WebStorage.get<StorageRequest>("storage-request-3"),
]).then(([s, data]) => {
if (s) {
dispatch({
@ -83,11 +83,11 @@ export function StorageRequestCreate() {
WebStorage.set("storage-request-step", step);
if (step == CONFIRM_STATE) {
const { availabilityUnit, availability, expiration, ...rest } =
const { availability, availabilityUnit, expiration, ...rest } =
storageRequest;
mutateAsync({
...rest,
duration: Times.toSeconds(availability, availabilityUnit),
duration: availability * Times.value(availabilityUnit),
expiry: expiration * 60,
});
} else {
@ -101,7 +101,7 @@ export function StorageRequestCreate() {
const onStorageRequestChange = (data: Partial<StorageRequest>) => {
const val = { ...storageRequest, ...data };
WebStorage.set("storage-request", val);
WebStorage.set("storage-request-3", val);
setStorageRequest(val);
};

View File

@ -14,6 +14,7 @@ import CommitmentIcon from "../../assets/icons/commitment.svg?react";
import RequestDurationIcon from "../../assets/icons/request-duration.svg?react";
import { attributes } from "../../utils/attributes";
import { Strings } from "../../utils/strings";
import { Commitment } from "./Commitment";
type Durability = {
nodes: number;
@ -27,6 +28,8 @@ const durabilities = [
{ nodes: 5, tolerance: 2, proofProbability: 4 },
];
const TESTNET_MAX_VALUE = 7;
const findDurabilityIndex = (d: Durability) => {
const s = JSON.stringify({
nodes: d.nodes,
@ -57,10 +60,10 @@ export function StorageRequestReview({
);
useEffect(() => {
const invalid = isInvalidConstrainst(
storageRequest.nodes,
storageRequest.tolerance
);
const invalid =
isInvalidConstrainst(storageRequest.nodes, storageRequest.tolerance) ||
storageRequest.availability > TESTNET_MAX_VALUE ||
storageRequest.availability == 0;
dispatch({
type: "toggle-buttons",
@ -133,26 +136,29 @@ export function StorageRequestReview({
const isInvalidAvailability = (data: string) => {
const [value] = data.split(" ");
const error = isInvalidNumber(value);
const error =
isInvalidNumber(value) ||
isInvalidAvailabilityNumber(value) ||
isRequiredNumber(value);
if (error) {
return error;
}
// if (!unit.endsWith("s")) {
// unit += "s";
// }
// if (!units.includes(unit)) {
// return "Invalid unit must one of: minutes, hours, days, months, years";
// }
return "";
};
const isInvalidNumber = (value: string) =>
isNaN(Number(value)) ? "The value is not a number" : "";
const isRequiredNumber = (value: string) =>
value == "0" ? "The value has to be more than 0." : "";
const isInvalidAvailabilityNumber = (value: string) =>
parseInt(value, 10) > TESTNET_MAX_VALUE
? "The maximum value is on the current Testnet is 7 days"
: "";
const onNodesChange = (value: string) =>
onUpdateDurability({ nodes: Number(value) });
@ -162,16 +168,12 @@ export function StorageRequestReview({
const onProofProbabilityChange = (value: string) =>
onUpdateDurability({ proofProbability: Number(value) });
const onAvailabilityChange = (value: string) => {
const onAvailabilityChange = (value: string, unit: "days" | "months") => {
const [availability] = value.split(" ");
// if (!availabilityUnit.endsWith("s")) {
// availabilityUnit += "s";
// }
onStorageRequestChange({
availability: Number(availability),
availabilityUnit: "months",
availabilityUnit: unit,
});
};
@ -184,18 +186,6 @@ export function StorageRequestReview({
const onCollateralChange = (value: string) =>
onStorageRequestChange({ collateral: Number(value) });
// const pluralizeUnit = () => {
// if (data.availability > 1 && !data.availabilityUnit.endsWith("s")) {
// return data.availability + " " +data.availabilityUnit + "s";
// }
// if (data.availability <= 1 && data.availabilityUnit.endsWith("s")) {
// return data.availabilityUnit.slice(0, -1);
// }
// return data.availabilityUnit;
// };
const availability = storageRequest.availability;
return (
@ -286,22 +276,19 @@ export function StorageRequestReview({
</div>
<div className="grid">
<CardNumbers
helper="The duration of the request in months"
id="duration"
title={"Full period of the contract"}
<Commitment
unit={storageRequest.availabilityUnit}
value={availability.toString()}
onChange={onAvailabilityChange}
onValidation={isInvalidAvailability}
unit="Contract duration"></CardNumbers>
onValidation={isInvalidAvailability}></Commitment>
<CardNumbers
helper="Represents how much collateral is asked from hosts that wants to fill a slots"
helper="Represents how much collateral is asked from hosts if they don't fulfill the contract."
id="collateral"
unit={"Collateral"}
value={storageRequest.collateral.toString()}
onChange={onCollateralChange}
onValidation={isInvalidNumber}
title="Reward tokens for hosts"></CardNumbers>
title="Penality tokens"></CardNumbers>
<CardNumbers
helper="The maximum amount of tokens paid per second per slot to hosts the client is willing to pay."
id="reward"
@ -309,7 +296,7 @@ export function StorageRequestReview({
value={storageRequest.reward.toString()}
onChange={onRewardChange}
onValidation={isInvalidNumber}
title="Penality tokens"></CardNumbers>
title="Reward tokens for hosts"></CardNumbers>
</div>
<div className="row">

View File

@ -16,29 +16,14 @@ export type StoragePriceStepValue = {
expiration: number;
};
export type StorageAvailabilityUnit =
| "days"
| "months"
| "years"
| "minutes"
| "hours";
export type StorageAvailabilityValue = {
value: number;
unit: StorageAvailabilityUnit;
};
export type AvailabilityUnit =
| "days"
| "months"
| "years"
| "minutes"
| "hours";
export type StorageRequest = {
cid: string;
availability: number;
availabilityUnit: AvailabilityUnit;
availabilityUnit: "months" | "days";
tolerance: number;
proofProbability: number;
nodes: number;

View File

@ -29,7 +29,7 @@ export function useStorageRequestMutation(
// }
WebStorage.delete("storage-request-step");
WebStorage.delete("storage-request");
WebStorage.delete("storage-request-3");
setError(null);

View File

@ -9,7 +9,7 @@ export function usePersistence(isCodexOnline: boolean) {
queryKey: [],
queryFn: async () => {
return CodexSdk.marketplace()
.purchases()
.activeSlots()
.then((data) => Promises.rejectOnError(data, report));
},

View File

@ -6,6 +6,12 @@
> .card {
flex: 1 1 50%;
@media (max-width: 800px) {
& {
flex: 1 1 100%;
}
}
}
.table {

View File

@ -10,7 +10,7 @@ import "./availabilities.css";
import { AvailabilitiesTable } from "../../components/Availability/AvailabilitiesTable";
import { AvailabilityEdit } from "../../components/Availability/AvailabilityEdit";
import { Strings } from "../../utils/strings";
import { PrettyBytes } from "../../utils/bytes";
import { Bytes } from "../../utils/bytes";
import { Sunburst } from "../../components/Availability/Sunburst";
import { Errors } from "../../utils/errors";
import { AvailabilityWithSlots } from "../../components/Availability/types";
@ -111,7 +111,7 @@ export function AvailabilitiesRoute() {
(a, index) => ({
title: Strings.shortId(a.id),
size: a.totalSize,
tooltip: a.id + "\u000D\u000A" + PrettyBytes(a.totalSize),
tooltip: a.id + "\u000D\u000A" + Bytes.pretty(a.totalSize),
color: AvailabilityUtils.availabilityColors[index],
})
);
@ -170,7 +170,7 @@ export function AvailabilitiesRoute() {
<footer>
<b>Node</b>
<small>
{PrettyBytes(space.quotaMaxBytes)} allocated for the node
{Bytes.pretty(space.quotaMaxBytes)} allocated for the node
</small>
</footer>
</div>

View File

@ -35,8 +35,19 @@
}
.button {
width: 187px;
gap: 8px;
@media (min-width: 801px) {
& {
width: 187px;
}
}
@media (max-width: 800px) {
span {
display: none;
}
}
}
}

View File

@ -175,4 +175,12 @@
.gauge {
margin: auto;
}
@media (max-width: 800px) {
.peers-chart {
transform: scale(0.8);
margin: auto;
left: 0;
}
}
}

View File

@ -8,9 +8,9 @@
background-color: rgb(20, 20, 20);
> div {
padding: 16px;
padding: 8px;
@media (min-width: 1000px) {
@media (min-width: 800px) {
padding: 24px 48px;
}
}

12
src/utils/bytes.test.ts Normal file
View File

@ -0,0 +1,12 @@
import { assert, describe, it } from "vitest";
import { Bytes } from "./bytes";
import { GB } from "./constants";
describe("bytes", () => {
it("display the bytes", async () => {
assert.equal(Bytes.pretty(0), "0 B");
assert.equal(Bytes.pretty(512), "512.0 B");
assert.equal(Bytes.pretty(1025), "1.0 KB");
assert.equal(Bytes.pretty(GB), "1.0 GB");
});
})

View File

@ -1,14 +1,17 @@
export const PrettyBytes = (bytes: number) => {
const sizes = ["bytes", "KB", "MB", "GB", "TB"];
if (bytes == 0) {
return "0 b";
export const Bytes = {
pretty(bytes: number) {
const sizes = ["B", "KB", "MB", "GB", "TB"];
if (bytes == 0) {
return "0 B";
}
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString());
if (i == 0) {
return bytes.toFixed(1) + " " + sizes[i];
}
return (bytes / Math.pow(1024, i)).toFixed(1) + " " + sizes[i];
}
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString());
if (i == 0) {
return bytes + " " + sizes[i];
}
return (bytes / Math.pow(1024, i)).toFixed(1) + " " + sizes[i];
};
}

28
src/utils/times.test.ts Normal file
View File

@ -0,0 +1,28 @@
import { assert, describe, it } from "vitest";
import { Times } from "./times";
describe("times", () => {
it("display the times", async () => {
assert.equal(Times.pretty(0), "0 second");
assert.equal(Times.pretty(2), "2 seconds");
assert.equal(Times.pretty(60), "1 minute");
assert.equal(Times.pretty(90), "1.5 minutes");
assert.equal(Times.pretty(3600), "1 hour");
assert.equal(Times.pretty(3600 * 2), "2 hours");
assert.equal(Times.pretty(3600 * 24), "1 day");
assert.equal(Times.pretty(3600 * 36), "1.5 days");
assert.equal(Times.pretty(3600 * 24 * 30), "1 month");
});
it("guess the time unit", async () => {
assert.equal(Times.unit(0), "hours");
assert.equal(Times.unit(3600 * 24), "days");
assert.equal(Times.unit(3600 * 24 * 30), "months");
})
it("get the seconds for a time unit given", async () => {
assert.equal(Times.value("hours"), 3600);
assert.equal(Times.value("days"), 3600 * 24);
assert.equal(Times.value("months"), 3600 * 24 * 30);
})
})

View File

@ -6,31 +6,33 @@ export type TimesUnit =
| "hours"
| "seconds";
const plural = (value: number, unit: TimesUnit) =>
value > 1 ? value + ` ${unit}` : value + ` ${unit.slice(0, -1)}`;
const plural = (value: number, unit: TimesUnit) => {
const val = Number.isInteger(value) ? value : value.toFixed(1)
return value > 1 ? val + ` ${unit}` : val + ` ${unit.slice(0, -1)}`;
}
export const Times = {
toSeconds(value: number, unit: TimesUnit) {
let seconds = value;
let val = value;
/* eslint-disable no-fallthrough */
switch (unit) {
// @ts-expect-error - We don't want to break
case "years":
seconds *= 365;
val *= 365;
// @ts-expect-error - We don't want to break
case "months":
seconds *= 30;
val *= 30;
// @ts-expect-error - We don't want to break
case "days":
seconds *= 24;
val *= 24;
// @ts-expect-error - We don't want to break
case "hours":
seconds *= 60;
val *= 60;
case "minutes":
seconds *= 60;
val *= 60;
}
return seconds;
return val;
},
pretty(value: number) {
@ -62,7 +64,7 @@ export const Times = {
return plural(value, "seconds");
},
unit(value: number) {
unit(value: number): "months" | "days" | "hours" {
let seconds = 30 * 24 * 60 * 60;
if (value >= seconds) {
@ -77,7 +79,7 @@ export const Times = {
return "hours"
},
unitValue(unit: "hours" | "days" | "months") {
value(unit: "hours" | "days" | "months") {
switch (unit) {
case "months": {
return 30 * 24 * 60 * 60
@ -90,4 +92,5 @@ export const Times = {
}
}
}
};