Add unit tests and fix minor issues

This commit is contained in:
Arnaud 2024-11-01 15:07:30 +01:00
parent 3260ae5839
commit 29ff705f58
No known key found for this signature in database
GPG Key ID: 69D6CE281FCAE663
41 changed files with 1033 additions and 313 deletions

View File

@ -112,16 +112,6 @@ jobs:
- name: Build
run: npm run build
# - uses: actions/cache@v4
# id: playwright-browsers-cache
# with:
# path: ~/.cache/ms-playwright
# key: ${{ runner.os }}-node-${{ hashFiles('**/playwright.config.ts') }}
# - if: steps.playwright-browsers-cache.outputs.cache-hit != 'true'
# name: Install Playwright Browsers
# run: npx playwright install --with-deps
- name: Install Playwright Browsers
run: npx playwright install --with-deps

37
.github/workflows/unit-testing.yml vendored Normal file
View File

@ -0,0 +1,37 @@
name: Unit Tests
on:
push:
branches: *
pull_request:
branches: *
workflow_dispatch:
env:
VITE_CODEX_API_URL: ${{ secrets.VITE_CODEX_API_URL }}
VITE_GEO_IP_URL: ${{ secrets.VITE_GEO_IP_URL }}
jobs:
tests:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- name: Install dependencies
run: sudo apt update
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- uses: actions/cache@v4
id: npm-cache
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit

View File

@ -1,10 +1,9 @@
import test, { expect } from "@playwright/test";
test('create an availability', async ({ page }) => {
await page.goto('/dashboard');
await page.getByRole('link', { name: 'Sales' }).click();
await page.goto('/dashboard/availabilities');
await page.waitForTimeout(500);
await page.getByRole('button').first().click();
await page.locator('.availabilities-create').first().click();
await page.getByLabel('Total size').click();
await page.getByLabel('Total size').fill('0.50');
await page.getByLabel('Duration').click();
@ -18,7 +17,7 @@ test('create an availability', async ({ page }) => {
await page.getByLabel('Nickname').fill('test');
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Confirm your new sale')).toBeVisible();
await page.locator('small').filter({ hasText: /^512\.0 MB$/ }).click();
await expect(page.getByText('512.0 MB').first()).toBeVisible();
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Success', { exact: true })).toBeVisible();
await page.getByRole('button', { name: 'Finish' }).click();
@ -28,31 +27,31 @@ test('create an availability', async ({ page }) => {
test('availability navigation buttons', async ({ page }) => {
await page.goto('/dashboard/availabilities');
await page.waitForTimeout(500);
await page.getByRole('button').first().click();
await page.locator('.availabilities-create').first().click();
await expect(page.locator('.stepper-number-done')).not.toBeVisible()
await expect(page.locator('.stepper-number-active')).toBeVisible()
await expect(page.locator('.stepper-buttons .button--primary')).not.toHaveAttribute("disabled");
await expect(page.locator('.stepper-buttons .button--outline')).not.toHaveAttribute("disabled");
await expect(page.locator('.step--active')).toBeVisible()
await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled");
await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled");
await page.getByLabel('Total size').click();
await page.getByLabel('Total size').fill('19');
await expect(page.locator('.stepper-buttons .button--outline')).not.toHaveAttribute("disabled");
await expect(page.locator('.stepper-buttons .button--primary')).toHaveAttribute("disabled");
await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled");
await expect(page.locator('footer .button--primary')).toHaveAttribute("disabled");
await page.getByLabel('Total size').click();
await page.getByLabel('Total size').fill('0.5');
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.locator('.stepper-buttons .button--outline')).not.toHaveAttribute("disabled");
await expect(page.locator('.stepper-buttons .button--primary')).not.toHaveAttribute("disabled");
await expect(page.locator('.stepper-number-done')).toBeVisible()
await expect(page.locator('.stepper-number-active')).toBeVisible()
await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled");
await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled");
await expect(page.locator('.step--done')).toBeVisible()
await expect(page.locator('.step--active')).toBeVisible()
await page.getByRole('button', { name: 'Back' }).click();
await expect(page.locator('.stepper-number-done')).not.toBeVisible()
await expect(page.locator('.stepper-number-active')).toBeVisible()
await expect(page.locator('.step--done')).not.toBeVisible()
await expect(page.locator('.step--active')).toBeVisible()
await page.getByRole('button', { name: 'Next' }).click();
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.locator('.stepper-number-done')).toHaveCount(2)
await expect(page.locator('.stepper-number-active')).toBeVisible()
await expect(page.locator('.stepper-buttons .button--outline')).toHaveAttribute("disabled");
await expect(page.locator('.stepper-buttons .button--primary')).not.toHaveAttribute("disabled");
await expect(page.locator('.step--done')).toHaveCount(2)
await expect(page.locator('.step--active')).toBeVisible()
await expect(page.locator('footer .button--outline').first()).toHaveAttribute("disabled");
await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled");
await page.getByRole('button', { name: 'Finish' }).click();
await expect(page.locator('.modal--open')).not.toBeVisible();
})

View File

@ -19,9 +19,9 @@ test('download a file', async ({ page, browserName }) => {
await page.getByRole('button', { name: 'Copy CID' }).click();
const handle = await page.evaluateHandle(() => navigator.clipboard.readText());
const cid = await handle.jsonValue()
await page.locator('.sheets-container > .backdrop').click();
await page.getByPlaceholder('CID').click();
await page.getByPlaceholder('CID').fill(cid);
await page.locator('.sheets > .backdrop').click();
await page.locator('.download-input input').click();
await page.locator('.download-input input').fill(cid);
// const page1Promise = page.waitForEvent('popup');
const downloadPromise = page.waitForEvent('download');
await page.locator('div').filter({ hasText: /^Download a fileDownload$/ }).getByRole('button').click();

View File

@ -3,17 +3,39 @@ import { test, expect } from '@playwright/test';
test('onboarding steps', async ({ page }) => {
await page.context().setOffline(false)
await page.goto('/');
await expect(page.locator('#root')).toContainText('Network connected');
await page.locator('a').nth(2).click();
await expect(page.getByText("Codex is a durable, decentralised data storage protocol, created so the world community can preserve its most important knowledge without risk of censorship.")).toBeVisible()
await page.locator('.navigation').click();
await expect(page.locator('.navigation')).toHaveAttribute("aria-disabled");
await page.getByLabel('Preferred name').fill('Arnaud');
await expect(page.locator('.navigation')).not.toHaveAttribute("aria-disabled");
await page.locator('.navigation').click();
// Network
await expect(page.locator(".health-checks ul li").nth(1).getByTestId("icon-error")).not.toBeVisible()
await expect(page.locator(".health-checks ul li").nth(1).getByTestId("icon-success")).toBeVisible()
// Port forwarding
await expect(page.locator(".health-checks ul li").nth(2).getByTestId("icon-error")).toBeVisible()
await expect(page.locator(".health-checks ul li").nth(2).getByTestId("icon-success")).not.toBeVisible()
// Codex node
await expect(page.locator(".health-checks ul li").nth(3).getByTestId("icon-error")).not.toBeVisible()
await expect(page.locator(".health-checks ul li").nth(3).getByTestId("icon-success")).toBeVisible()
// Marketplace
await expect(page.locator(".health-checks ul li").nth(4).getByTestId("icon-error")).not.toBeVisible()
await expect(page.locator(".health-checks ul li").nth(4).getByTestId("icon-success")).toBeVisible()
await page.context().setOffline(true)
await expect(page.locator('#root')).toContainText('Network disconnected');
await page.getByLabel('Display name').click();
await page.getByLabel('Display name').fill('Arnaud');
await page.locator('a').click();
await page.locator('div').filter({ hasText: /^Internet connectionStatus indicator for the Internet\.$/ }).first().click();
await expect(page.getByTestId("network").locator(".onboarding-check-icon--valid")).not.toBeInViewport()
await expect(page.getByTestId("network").locator(".onboarding-check-icon--invalid")).toBeInViewport()
// Network
await expect(page.locator(".health-checks ul li").nth(1).getByTestId("icon-error")).toBeVisible()
await expect(page.locator(".health-checks ul li").nth(1).getByTestId("icon-success")).not.toBeVisible()
await page.context().setOffline(false)
await expect(page.getByTestId("network").locator(".onboarding-check-icon--valid")).toBeInViewport()
await expect(page.getByTestId("network").locator(".onboarding-check-icon--invalid")).not.toBeInViewport()
});
});
// await expect(page.locator('#root')).toContainText('Network connected');
// await page.locator('a').nth(2).click();
// await page.context().setOffline(true)
// await expect(page.locator('#root')).toContainText('Network disconnected');

View File

@ -1,25 +1,30 @@
import test, { expect } from "@playwright/test";
test('update the log level', async ({ page }) => {
await page.goto('/dashboard');
await page.getByRole('link', { name: 'Settings' }).click();
await page.getByLabel('Log level').selectOption('TRACE');
await page.getByRole('main').locator('div').filter({ hasText: 'Log' }).getByRole('button').click();
await expect(page.locator('span').filter({ hasText: 'success ! The log level has' }).locator('b')).toBeVisible();
})
// test('update the log level', async ({ page }) => {
// await page.goto('/dashboard');
// await page.getByRole('link', { name: 'Settings' }).click();
// await page.getByLabel('Log level').selectOption('TRACE');
// await page.getByRole('main').locator('div').filter({ hasText: 'Log' }).getByRole('button').click();
// await expect(page.locator('span').filter({ hasText: 'success ! The log level has' }).locator('b')).toBeVisible();
// })
test('update the URL with wrong URL applies', async ({ page }) => {
await page.goto('/dashboard');
await page.getByRole('link', { name: 'Settings' }).click();
await page.getByLabel('Codex client node URL').click();
await page.getByLabel('Codex client node URL').fill('hello');
await expect.soft(page.getByText("The URL is not valid")).toBeVisible()
await expect.soft(page.locator(".settings-url-button")).toBeDisabled()
await page.getByLabel('Codex client node URL').fill('http://127.0.0.1:8079');
await expect.soft(page.getByText("The URL is not valid")).not.toBeVisible()
await expect.soft(page.locator(".settings-url-button")).not.toBeDisabled()
await page.getByRole('button', { name: 'Save changes' }).nth(1).click();
await expect.soft(page.getByText("Cannot retrieve the data")).toBeVisible()
await page.getByLabel('Codex client node URL').fill('http://127.0.0.1:8080');
await page.getByRole('button', { name: 'Save changes' }).nth(1).click();
await page.locator('a').filter({ hasText: 'Settings' }).click();
await page.getByLabel('Address').click();
await page.getByLabel('Address').fill('hello');
await expect(page.getByLabel('Address')).toHaveAttribute("aria-invalid")
await expect(page.locator(".refresh svg")).toHaveAttribute("aria-disabled")
await page.getByLabel('Address').fill('http://127.0.0.1:8079');
await expect(page.getByLabel('Address')).not.toHaveAttribute("aria-invalid")
await expect(page.locator(".refresh svg")).not.toHaveAttribute("aria-disabled")
await expect(page.getByLabel('Address')).toHaveValue("http://127.0.0.1")
await expect(page.getByLabel('Port')).toHaveValue("8079")
await page.locator(".refresh").click()
await expect(page.locator(".health-checks ul li").nth(3).getByTestId("icon-error")).toBeVisible()
await expect(page.locator(".health-checks ul li").nth(3).getByTestId("icon-success")).not.toBeVisible()
await page.getByLabel('Address').fill('http://127.0.0.1:8080');
await page.locator(".refresh").click()
await expect(page.locator(".health-checks ul li").nth(3).getByTestId("icon-error")).not.toBeVisible()
await expect(page.locator(".health-checks ul li").nth(3).getByTestId("icon-success")).toBeVisible()
})

View File

@ -7,12 +7,13 @@ const __dirname = dirname(__filename);
test('create a storage request', async ({ page }) => {
await page.goto('/dashboard');
await page.getByRole('link', { name: 'Purchases' }).click();
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')).toHaveValue("zDvZRwzkvwapyNeL4mzw5gBsZvyn7x8F8Y9n4RYSC7ETBssDYpGe")
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.getByRole('button', { name: 'Next' }).click();
@ -27,51 +28,51 @@ test('select a uploaded cid when creating a storage request', async ({ page }) =
await page.locator('div').getByTestId("upload").setInputFiles([
path.join(__dirname, "assets", 'chatgpt.jpg'),
]);
await page.getByRole('link', { name: 'Purchases' }).click();
await page.locator('a').filter({ hasText: 'Purchases' }).click();
await page.getByRole('button', { name: 'Storage Request' }).click();
await page.getByPlaceholder('Select or type your CID').click();
await page.locator('.dropdown-option').nth(1).click();
await page.locator('.dropdown ul li').nth(1).click();
await expect(page.getByText('button[disabled]')).not.toBeVisible();
})
test('storage request navigation buttons', async ({ page }) => {
await page.goto('/dashboard/purchases');
await page.getByRole('button', { name: 'Storage Request' }).click();
await expect(page.locator('.stepper-number-done')).not.toBeVisible()
await expect(page.locator('.stepper-number-active')).toBeVisible()
await expect(page.locator('.stepper-buttons .button--primary')).toHaveAttribute("disabled");
await expect(page.locator('.stepper-buttons .button--outline')).not.toHaveAttribute("disabled");
await expect(page.locator('.step--done')).not.toBeVisible()
await expect(page.locator('.step--active')).toBeVisible()
await expect(page.locator('footer .button--primary')).toHaveAttribute("disabled");
await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled");
await page.locator('div').getByTestId("upload").setInputFiles([
path.join(__dirname, "assets", 'chatgpt.jpg'),
]);
await expect(page.locator('.stepper-buttons .button--outline')).not.toHaveAttribute("disabled");
await expect(page.locator('.stepper-buttons .button--primary')).not.toHaveAttribute("disabled");
await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled");
await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled");
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.locator('.stepper-buttons .button--outline')).not.toHaveAttribute("disabled");
await expect(page.locator('.stepper-buttons .button--primary')).not.toHaveAttribute("disabled");
await expect(page.locator('.stepper-number-done')).toBeVisible()
await expect(page.locator('.stepper-number-active')).toBeVisible()
await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled");
await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled");
await expect(page.locator('.step--done')).toBeVisible()
await expect(page.locator('.step--active')).toBeVisible()
await page.getByRole('button', { name: 'Back' }).click();
await expect(page.locator('.stepper-number-done')).not.toBeVisible()
await expect(page.locator('.stepper-number-active')).toBeVisible()
await expect(page.locator('.step--done')).not.toBeVisible()
await expect(page.locator('.step--active')).toBeVisible()
await page.getByRole('button', { name: 'Next' }).click();
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.locator('.stepper-number-done')).toHaveCount(2)
await expect(page.locator('.stepper-number-active')).toBeVisible()
await expect(page.locator('.stepper-buttons .button--outline')).toHaveAttribute("disabled");
await expect(page.locator('.stepper-buttons .button--primary')).not.toHaveAttribute("disabled");
await expect(page.locator('.step--done')).toHaveCount(2)
await expect(page.locator('.step--active')).toBeVisible()
await expect(page.locator('footer .button--outline').first()).toHaveAttribute("disabled");
await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled");
await page.getByRole('button', { name: 'Finish' }).click();
await expect(page.locator('.modal--open')).not.toBeVisible();
})
test('remove the CID when the file is deleted', async ({ page }) => {
await page.goto('/dashboard');
await page.getByRole('link', { name: 'Purchases' }).click();
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')).toHaveValue("zDvZRwzkvwapyNeL4mzw5gBsZvyn7x8F8Y9n4RYSC7ETBssDYpGe")
await page.locator('.uploadFile-infoRight .buttonIcon--small').click();
await expect(page.locator('#cid')).toHaveValue("")
await expect(page.locator('#cid')).not.toBeEmpty()
await page.locator('.button-icon--small').click();
await expect(page.locator('#cid')).toBeEmpty()
})

View File

@ -26,8 +26,8 @@ test('multiple files upload', async ({ page }) => {
await expect(page.getByText('File uploaded successfully').nth(1)).toBeVisible();
await page.locator('.uploadFile-infoRight > .buttonIcon').first().click();
await page.locator('.uploadFile-infoRight > .buttonIcon').click();
await page.locator('.upload-file .button-icon--small').first().click();
await page.locator('.upload-file .button-icon--small').click();
await expect(page.getByText('File uploaded successfully').first()).not.toBeVisible();
await expect(page.getByText('File uploaded successfully').nth(1)).not.toBeVisible();
@ -42,7 +42,7 @@ test('drag and drop file', async ({ page }) => {
const dataTransfer = await page.evaluateHandle((data) => {
const dt = new DataTransfer();
// Convert the buffer to a hex array
const file = new File([data.toString('hex')], 'chat.jpg', { type: 'image/jpg' });
const file = new File([data.toString('hex')], 'chat.jpg', { type: 'image/jpeg' });
dt.items.add(file);
return dt;
}, buffer);
@ -63,7 +63,7 @@ test('drag and drop file', async ({ page }) => {
// mimeType: 'text/plain'
// });
// await page.locator('.uploadFile-infoRight > .buttonIcon--small').click();
// await page.locator('.uploadFile-infoRight > .button-icon--small').click();
// await expect(page.getByText('The upload has been cancelled')).toBeVisible();
// });

368
package-lock.json generated
View File

@ -41,7 +41,8 @@
"postcss-nesting": "^13.0.1",
"prettier": "^3.3.3",
"typescript": "5.5.4",
"vite": "^5.4.7"
"vite": "^5.4.7",
"vitest": "^2.1.4"
},
"engines": {
"node": ">=18"
@ -1994,6 +1995,112 @@
"vite": "^4.2.0 || ^5.0.0"
}
},
"node_modules/@vitest/expect": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.4.tgz",
"integrity": "sha512-DOETT0Oh1avie/D/o2sgMHGrzYUFFo3zqESB2Hn70z6QB1HrS2IQ9z5DfyTqU8sg4Bpu13zZe9V4+UTNQlUeQA==",
"dev": true,
"dependencies": {
"@vitest/spy": "2.1.4",
"@vitest/utils": "2.1.4",
"chai": "^5.1.2",
"tinyrainbow": "^1.2.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/mocker": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.4.tgz",
"integrity": "sha512-Ky/O1Lc0QBbutJdW0rqLeFNbuLEyS+mIPiNdlVlp2/yhJ0SbyYqObS5IHdhferJud8MbbwMnexg4jordE5cCoQ==",
"dev": true,
"dependencies": {
"@vitest/spy": "2.1.4",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.12"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"msw": "^2.4.9",
"vite": "^5.0.0"
},
"peerDependenciesMeta": {
"msw": {
"optional": true
},
"vite": {
"optional": true
}
}
},
"node_modules/@vitest/pretty-format": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.4.tgz",
"integrity": "sha512-L95zIAkEuTDbUX1IsjRl+vyBSLh3PwLLgKpghl37aCK9Jvw0iP+wKwIFhfjdUtA2myLgjrG6VU6JCFLv8q/3Ww==",
"dev": true,
"dependencies": {
"tinyrainbow": "^1.2.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/runner": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.4.tgz",
"integrity": "sha512-sKRautINI9XICAMl2bjxQM8VfCMTB0EbsBc/EDFA57V6UQevEKY/TOPOF5nzcvCALltiLfXWbq4MaAwWx/YxIA==",
"dev": true,
"dependencies": {
"@vitest/utils": "2.1.4",
"pathe": "^1.1.2"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.4.tgz",
"integrity": "sha512-3Kab14fn/5QZRog5BPj6Rs8dc4B+mim27XaKWFWHWA87R56AKjHTGcBFKpvZKDzC4u5Wd0w/qKsUIio3KzWW4Q==",
"dev": true,
"dependencies": {
"@vitest/pretty-format": "2.1.4",
"magic-string": "^0.30.12",
"pathe": "^1.1.2"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/spy": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.4.tgz",
"integrity": "sha512-4JOxa+UAizJgpZfaCPKK2smq9d8mmjZVPMt2kOsg/R8QkoRzydHH1qHxIYNvr1zlEaFj4SXiaaJWxq/LPLKaLg==",
"dev": true,
"dependencies": {
"tinyspy": "^3.0.2"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/utils": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.4.tgz",
"integrity": "sha512-MXDnZn0Awl2S86PSNIim5PWXgIAx8CIkzu35mBdSApUip6RFOGXBCf3YFyeEu8n1IHk4bWD46DeYFu9mQlFIRg==",
"dev": true,
"dependencies": {
"@vitest/pretty-format": "2.1.4",
"loupe": "^3.1.2",
"tinyrainbow": "^1.2.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/acorn": {
"version": "8.12.1",
"dev": true,
@ -2065,6 +2172,15 @@
"dev": true,
"license": "Python-2.0"
},
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/babel-dead-code-elimination": {
"version": "1.0.6",
"dev": true,
@ -2152,6 +2268,15 @@
"optional": true,
"peer": true
},
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"dev": true,
@ -2180,6 +2305,22 @@
}
]
},
"node_modules/chai": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz",
"integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==",
"dev": true,
"dependencies": {
"assertion-error": "^2.0.1",
"check-error": "^2.1.1",
"deep-eql": "^5.0.1",
"loupe": "^3.1.0",
"pathval": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@ -2194,6 +2335,15 @@
"node": ">=4"
}
},
"node_modules/check-error": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
"dev": true,
"engines": {
"node": ">= 16"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"dev": true,
@ -2317,6 +2467,15 @@
}
}
},
"node_modules/deep-eql": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"dev": true,
@ -2672,6 +2831,15 @@
"node": ">=4.0"
}
},
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
"dependencies": {
"@types/estree": "^1.0.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"dev": true,
@ -2680,6 +2848,15 @@
"node": ">=0.10.0"
}
},
"node_modules/expect-type": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz",
"integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==",
"dev": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"dev": true,
@ -3103,6 +3280,12 @@
"loose-envify": "cli.js"
}
},
"node_modules/loupe": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz",
"integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==",
"dev": true
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -3120,6 +3303,15 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
}
},
"node_modules/magic-string": {
"version": "0.30.12",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz",
"integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -3289,6 +3481,21 @@
"node": ">=8"
}
},
"node_modules/pathe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
"dev": true
},
"node_modules/pathval": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
"dev": true,
"engines": {
"node": ">= 14.16"
}
},
"node_modules/picocolors": {
"version": "1.1.0",
"license": "ISC"
@ -4070,6 +4277,12 @@
"node": ">=8"
}
},
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
"dev": true
},
"node_modules/source-map-js": {
"version": "1.2.1",
"license": "BSD-3-Clause",
@ -4077,6 +4290,18 @@
"node": ">=0.10.0"
}
},
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
"dev": true
},
"node_modules/std-env": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
"integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==",
"dev": true
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"dev": true,
@ -4124,6 +4349,45 @@
"version": "1.0.3",
"license": "MIT"
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
"dev": true
},
"node_modules/tinyexec": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz",
"integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==",
"dev": true
},
"node_modules/tinypool": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz",
"integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==",
"dev": true,
"engines": {
"node": "^18.0.0 || >=20.0.0"
}
},
"node_modules/tinyrainbow": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
"integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
"dev": true,
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tinyspy": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
"integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
"dev": true,
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"dev": true,
@ -4347,6 +4611,27 @@
}
}
},
"node_modules/vite-node": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.4.tgz",
"integrity": "sha512-kqa9v+oi4HwkG6g8ufRnb5AeplcRw8jUF6/7/Qz1qRQOXHImG8YnLbB+LLszENwFnoBl9xIf9nVdCFzNd7GQEg==",
"dev": true,
"dependencies": {
"cac": "^6.7.14",
"debug": "^4.3.7",
"pathe": "^1.1.2",
"vite": "^5.0.0"
},
"bin": {
"vite-node": "vite-node.mjs"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/vite/node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"cpu": [
@ -4765,6 +5050,71 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/vitest": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.4.tgz",
"integrity": "sha512-eDjxbVAJw1UJJCHr5xr/xM86Zx+YxIEXGAR+bmnEID7z9qWfoxpHw0zdobz+TQAFOLT+nEXz3+gx6nUJ7RgmlQ==",
"dev": true,
"dependencies": {
"@vitest/expect": "2.1.4",
"@vitest/mocker": "2.1.4",
"@vitest/pretty-format": "^2.1.4",
"@vitest/runner": "2.1.4",
"@vitest/snapshot": "2.1.4",
"@vitest/spy": "2.1.4",
"@vitest/utils": "2.1.4",
"chai": "^5.1.2",
"debug": "^4.3.7",
"expect-type": "^1.1.0",
"magic-string": "^0.30.12",
"pathe": "^1.1.2",
"std-env": "^3.7.0",
"tinybench": "^2.9.0",
"tinyexec": "^0.3.1",
"tinypool": "^1.0.1",
"tinyrainbow": "^1.2.0",
"vite": "^5.0.0",
"vite-node": "2.1.4",
"why-is-node-running": "^2.3.0"
},
"bin": {
"vitest": "vitest.mjs"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "2.1.4",
"@vitest/ui": "2.1.4",
"happy-dom": "*",
"jsdom": "*"
},
"peerDependenciesMeta": {
"@edge-runtime/vm": {
"optional": true
},
"@types/node": {
"optional": true
},
"@vitest/browser": {
"optional": true
},
"@vitest/ui": {
"optional": true
},
"happy-dom": {
"optional": true
},
"jsdom": {
"optional": true
}
}
},
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
"dev": true,
@ -4784,6 +5134,22 @@
"node": ">= 8"
}
},
"node_modules/why-is-node-running": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
"dev": true,
"dependencies": {
"siginfo": "^2.0.0",
"stackback": "0.0.2"
},
"bin": {
"why-is-node-running": "cli.js"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wkt-parser": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.3.3.tgz",

View File

@ -14,7 +14,8 @@
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview --host 127.0.0.1 --port 5173",
"format": "prettier --write ./src",
"test": "npx playwright test"
"test": "npx playwright test",
"test:unit": "vitest run"
},
"keywords": [
"Codex",
@ -56,7 +57,8 @@
"postcss-nesting": "^13.0.1",
"prettier": "^3.3.3",
"typescript": "5.5.4",
"vite": "^5.4.7"
"vite": "^5.4.7",
"vitest": "^2.1.4"
},
"engines": {
"node": ">=18"

View File

@ -8,7 +8,7 @@ import { ReactElement, useEffect } from "react";
import { useCodexConnection } from "../../hooks/useCodexConnection";
import { NodesIcon } from "../Menu/NodesIcon";
import { usePersistence } from "../../hooks/usePersistence";
import { useNavigate, useRouterState } from "@tanstack/react-router";
import { useLocation, useNavigate } from "@tanstack/react-router";
import { PeersIcon } from "../Menu/PeersIcon";
import { SettingsIcon } from "../Menu/SettingsIcon";
@ -33,8 +33,8 @@ export function AppBar({ onIconClick }: Props) {
const queryClient = useQueryClient();
const codex = useCodexConnection();
const persistence = usePersistence(codex.enabled);
const router = useRouterState();
const navigate = useNavigate({ from: router.location.pathname });
const location = useLocation();
const navigate = useNavigate({ from: location.pathname });
useEffect(() => {
queryClient.invalidateQueries({
@ -43,13 +43,12 @@ export function AppBar({ onIconClick }: Props) {
});
}, [queryClient, codex.enabled]);
const onNodeClick = () => navigate({ to: "/dashboard/settings" });
const offline = !online || !codex.enabled;
const onNodeClick = () => navigate({ to: "/dashboard/settings" });
const title =
router.location.pathname.split("/")[2] ||
router.location.pathname.split("/")[1];
location.pathname.split("/")[2] || location.pathname.split("/")[1];
return (
<>

View File

@ -59,7 +59,7 @@
align-items: center;
}
.cardNumber-dataContainer .buttonIcon {
.cardNumber-dataContainer .button-icon {
position: relative;
top: 3px;
}

View File

@ -1,6 +1,7 @@
export function ErrorCircleIcon() {
return (
<svg
data-testid="icon-error"
width="16"
height="16"
viewBox="0 0 16 16"

View File

@ -3,16 +3,17 @@ import { useEffect, useState } from "react";
import { useDebug } from "../../hooks/useDebug";
import { usePersistence } from "../../hooks/usePersistence";
import { usePortForwarding } from "../../hooks/usePortForwarding";
import { CodexSdk } from "../../proxy";
import { ErrorCircleIcon } from "../ErrorCircleIcon/ErrorCircleIcon";
import { SuccessCheckIcon } from "../SuccessCheckIcon/SuccessCheckIcon";
import { WarningIcon } from "../WarningIcon/WarningIcon";
import { HealthCheckIcon } from "./HealthCheckIcon";
import { Input } from "@codex-storage/marketplace-ui-components";
import { classnames } from "../../utils/classnames";
import { DebugUtils } from "../../utils/debug";
import { RefreshIcon } from "../RefreshIcon/RefreshIcon";
import "./HealthChecks.css";
import { CodexSdk } from "../../sdk/codex";
import { HealthCheckUtil } from "./health-check.util";
import { PortForwardingUtil } from "../../hooks/port-forwarding.util";
type Props = {
online: boolean;
@ -29,45 +30,40 @@ export function HealthChecks({ online, onStepValid }: Props) {
const [isAddressInvalid, setIsAddressInvalid] = useState(false);
const [isPortInvalid, setIsPortInvalid] = useState(false);
const [address, setAddress] = useState(
CodexSdk.url().split(":")[0] + ":" + CodexSdk.url().split(":")[1]
);
const [port, setPort] = useState(
parseInt(CodexSdk.url().split(":")[2] || "80", 10)
HealthCheckUtil.removePort(CodexSdk.url())
);
const [port, setPort] = useState(HealthCheckUtil.getPort(CodexSdk.url()));
const queryClient = useQueryClient();
useEffect(() => {
if (codex.isSuccess) {
persistence.refetch();
portForwarding.refetch().then(({ data }) => {
onStepValid(data?.reachable || false);
});
} else {
onStepValid(false);
}
}, [
persistence.refetch,
onStepValid,
portForwarding.refetch,
codex.isSuccess,
]);
useEffect(
() => {
if (codex.isSuccess) {
persistence.refetch();
portForwarding.refetch().then(({ data }) => {
onStepValid(data?.reachable || false);
});
} else {
onStepValid(false);
}
},
// We really do not want to add persistence and portForwarding as
// dependencies because it will cause a re-render every time.
// eslint-disable-next-line react-hooks/exhaustive-deps
[persistence.refetch, onStepValid, portForwarding.refetch, codex.isSuccess]
);
const onAddressChange = (e: React.FormEvent<HTMLInputElement>) => {
const element = e.currentTarget;
const parts = e.currentTarget.value.split(":");
const value = e.currentTarget.value;
setIsAddressInvalid(!element.checkValidity());
if (parts.length > 2) {
const [protocol, addr, port] = parts;
setAddress(protocol + ":" + addr);
const address = HealthCheckUtil.removePort(value);
setAddress(address);
const p = parseInt(port, 10);
if (!isNaN(p)) {
setPort(p);
}
} else {
setAddress(parts.join(":"));
if (HealthCheckUtil.containsPort(value)) {
const p = HealthCheckUtil.getPort(value);
setPort(p);
}
};
@ -80,11 +76,13 @@ export function HealthChecks({ online, onStepValid }: Props) {
};
const onSave = () => {
if (isAddressInvalid || isPortInvalid) {
const url = address + ":" + port;
if (HealthCheckUtil.isUrlInvalid(url)) {
return;
}
CodexSdk.updateURL(address + ":" + port)
CodexSdk.updateURL(url)
.then(() => queryClient.invalidateQueries())
.then(() => codex.refetch());
};
@ -92,7 +90,7 @@ export function HealthChecks({ online, onStepValid }: Props) {
let forwardingPortValue = defaultPort;
if (codex.isSuccess && codex.data) {
const port = DebugUtils.getTcpPort(codex.data);
const port = PortForwardingUtil.getTcpPort(codex.data);
if (!port.error) {
forwardingPortValue = port.data;
}
@ -135,7 +133,9 @@ export function HealthChecks({ online, onStepValid }: Props) {
</div>
<div className="refresh">
<RefreshIcon onClick={onSave}></RefreshIcon>
<RefreshIcon
onClick={onSave}
disabled={isAddressInvalid || isPortInvalid}></RefreshIcon>
</div>
</div>

View File

@ -0,0 +1,148 @@
import { assert, describe, it } from "vitest";
import { HealthCheckUtil } from "./health-check.util";
describe("health check", () => {
it("remove the port from an url", async () => {
assert.deepEqual(HealthCheckUtil.removePort("http://localhost:8080"), "http://localhost");
});
it("get the port from an url", async () => {
assert.deepEqual(HealthCheckUtil.getPort("http://localhost:8080"), 8080);
});
it("get the default port when the url does not contain the port", async () => {
assert.deepEqual(HealthCheckUtil.getPort("http://localhost"), 80);
});
it("returns true when the url contains a port", async () => {
assert.deepEqual(HealthCheckUtil.containsPort("http://localhost:8080"), true);
});
it("returns false when the url does not contain a port", async () => {
assert.deepEqual(HealthCheckUtil.containsPort("http://localhost"), false);
});
it("returns true when the url is invalid", async () => {
assert.deepEqual(HealthCheckUtil.isUrlInvalid("http://"), true);
});
it("returns false when the url is valid", async () => {
assert.deepEqual(HealthCheckUtil.isUrlInvalid("http://localhost:8080"), false);
});
it("returns the tcp port", async () => {
const debug = {
"id": "a",
"addrs": [
"/ip4/127.0.0.1/tcp/8070"
],
"repo": "",
"spr": "",
"announceAddresses": [
"/ip4/127.0.0.1/tcp/8070"
],
"table": {
"localNode": {
"nodeId": "",
"peerId": "",
"record": "",
"address": "0.0.0.0:8090",
"seen": false
},
"nodes": []
},
"codex": {
"version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7",
"revision": "2fb7031e"
}
}
assert.deepEqual(HealthCheckUtil.getTcpPort(debug), { error: false, data: 8070 });
});
it("returns an error when the addr is empty", async () => {
const debug = {
"id": "a",
"addrs": [
],
"repo": "",
"spr": "",
"announceAddresses": [
"/ip4/127.0.0.1/tcp/8070"
],
"table": {
"localNode": {
"nodeId": "",
"peerId": "",
"record": "",
"address": "0.0.0.0:8090",
"seen": false
},
"nodes": []
},
"codex": {
"version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7",
"revision": "2fb7031e"
}
}
assert.deepEqual(HealthCheckUtil.getTcpPort(debug).error, true);
});
it("returns an error when the addr is misformated", async () => {
const debug = {
"id": "a",
"addrs": [
"/ip4/127.0.0.1/tcp/hello"
],
"repo": "",
"spr": "",
"announceAddresses": [
"/ip4/127.0.0.1/tcp/8070"
],
"table": {
"localNode": {
"nodeId": "",
"peerId": "",
"record": "",
"address": "0.0.0.0:8090",
"seen": false
},
"nodes": []
},
"codex": {
"version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7",
"revision": "2fb7031e"
}
}
assert.deepEqual(HealthCheckUtil.getTcpPort(debug).error, true);
});
it("returns an error when the port is misformated", async () => {
const debug = {
"id": "a",
"addrs": [
"hello"
],
"repo": "",
"spr": "",
"announceAddresses": [
"/ip4/127.0.0.1/tcp/8070"
],
"table": {
"localNode": {
"nodeId": "",
"peerId": "",
"record": "",
"address": "0.0.0.0:8090",
"seen": false
},
"nodes": []
},
"codex": {
"version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7",
"revision": "2fb7031e"
}
}
assert.deepEqual(HealthCheckUtil.getTcpPort(debug).error, true);
});
})

View File

@ -0,0 +1,51 @@
import { CodexDebugInfo, SafeValue, CodexError } from "@codex-storage/sdk-js"
export const HealthCheckUtil = {
removePort(url: string) {
const parts = url.split(":")
return parts[0] + ":" + parts[1]
},
/*
* Extract the port from a protocol + ip + port string
*/
getPort(url: string) {
return parseInt(url.split(":")[2] || "80", 10)
},
containsPort(url: string) {
return url.split(":").length > 2
},
isUrlInvalid(url: string) {
try {
new URL(url)
return false
// We do not need to manage the error because we want to check
// if the URL is valid or not only.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_) {
return true
}
},
getTcpPort(info: CodexDebugInfo): SafeValue<number> {
if (info.addrs.length === 0) {
return { error: true, data: new CodexError("Not existing address") }
}
const parts = info.addrs[0].split("/")
if (parts.length < 2) {
return { error: true, data: new CodexError("Address misformated") }
}
const port = parseInt(parts[parts.length - 1], 10)
if (isNaN(port)) {
return { error: true, data: new CodexError("Port misformated") }
}
return { error: false, data: port }
}
}

View File

@ -149,6 +149,14 @@
label {
margin-top: 1rem;
}
.health-checks {
.address {
.refresh {
top: 22px;
}
}
}
}
.get-started {

View File

@ -1,22 +1,14 @@
import { Cell } from "@codex-storage/marketplace-ui-components";
import { PeerPin } from "./types";
import { useQuery } from "@tanstack/react-query";
import "./PeerCountryCell.css";
import { useEffect } from "react";
import { PeerPin, PeerUtils } from "./peers.util";
export type Props = {
address: string;
onPinAdd: (pin: PeerPin & { countryIso: string; ip: string }) => void;
};
const getFlagEmoji = (countryCode: string) => {
const codePoints = countryCode
.toUpperCase()
.split("")
.map((char) => 127397 + char.charCodeAt(0));
return String.fromCodePoint(...codePoints);
};
export function PeerCountryCell({ address, onPinAdd }: Props) {
const { data } = useQuery({
queryFn: () => {
@ -61,7 +53,7 @@ export function PeerCountryCell({ address, onPinAdd }: Props) {
<div className="peer-country">
{data ? (
<>
<span> {!!data && getFlagEmoji(data.country_iso)}</span>
<span> {!!data && PeerUtils.geCountryEmoji(data.country_iso)}</span>
<span>{data?.country}</span>
</>
) : (

View File

@ -2,6 +2,7 @@
max-width: 1320px;
margin-left: auto;
margin-right: auto;
padding-bottom: 32px;
> div {
max-width: 1320px;
@ -194,7 +195,7 @@
div:nth-child(1) {
border-color: var(--codex-color-primary);
transform: rotate(calc(var(--codex-peers-percent) * 1deg));
transform: rotate(calc(var(--codex-peers-degrees) * 1deg));
background-color: transparent;
}
}

View File

@ -1,78 +1,36 @@
import {
Cell,
Row,
Table,
TabSortState,
Row,
Cell,
Table,
} from "@codex-storage/marketplace-ui-components";
import { getMapJSON } from "dotted-map";
import DottedMap from "dotted-map/without-countries";
import { Promises } from "../../utils/promises";
import { useQuery } from "@tanstack/react-query";
import { PeerCountryCell } from "../../components/Peers/PeerCountryCell";
import { useCallback, useRef, useState } from "react";
import { PeerPin } from "../../components/Peers/types";
import "./peers.css";
import { CodexSdk } from "../../sdk/codex";
import { ErrorBoundary } from "@sentry/react";
import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder";
import { PeersIcon } from "../../components/Menu/PeersIcon";
import { SuccessCheckIcon } from "../../components/SuccessCheckIcon/SuccessCheckIcon";
import { ErrorCircleIcon } from "../../components/ErrorCircleIcon/ErrorCircleIcon";
import { createLazyFileRoute } from "@tanstack/react-router";
import { Network } from "../../utils/network";
import { useRef, useState, useCallback } from "react";
import { ErrorCircleIcon } from "../ErrorCircleIcon/ErrorCircleIcon";
import { PeersIcon } from "../Menu/PeersIcon";
import { PeerCountryCell } from "./PeerCountryCell";
import { SuccessCheckIcon } from "../SuccessCheckIcon/SuccessCheckIcon";
import { useDebug } from "../../hooks/useDebug";
import { getMapJSON } from "dotted-map";
import "./Peers.css";
import { PeerPin, PeerSortFn, PeerUtils } from "./peers.util";
// This function accepts the same arguments as DottedMap in the example above.
const mapJsonString = getMapJSON({ height: 60, grid: "diagonal" });
type CustomCSSProperties = React.CSSProperties & {
"--codex-peers-percent": number;
"--codex-peers-degrees": number;
};
type Node = {
nodeId: string;
peerId: string;
record: string;
address: string;
seen: boolean;
};
const throwOnError = true;
type SortFn = (a: Node, b: Node) => number;
const sortByBooleanValue = (state: TabSortState) => {
return (a: Node, b: Node) => {
const order = state === "desc" ? 1 : -1;
return a?.seen === b?.seen ? 0 : b?.seen ? order : -order;
};
};
const Peers = () => {
export const Peers = () => {
const ips = useRef<Record<string, string>>({});
const [pins, setPins] = useState<[PeerPin, number][]>([]);
const [sortFn, setSortFn] = useState<SortFn | null>(() =>
sortByBooleanValue("desc")
const [sortFn, setSortFn] = useState<PeerSortFn | null>(() =>
PeerUtils.sortByBoolean("desc")
);
const { data } = useQuery({
queryFn: () =>
CodexSdk.debug()
.info()
.then((s) => Promises.rejectOnError(s)),
queryKey: ["debug"],
// No need to retry because if the connection to the node
// is back again, all the queries will be invalidated.
retry: false,
// The client node should be local, so display the cache value while
// making a background request looks good.
staleTime: 0,
// Refreshing when focus returns can be useful if a user comes back
// to the UI after performing an operation in the terminal.
refetchOnWindowFocus: true,
// Throw the error to the error boundary
throwOnError: true,
});
const { data } = useDebug(throwOnError);
const onPinAdd = useCallback(
({
@ -80,11 +38,7 @@ const Peers = () => {
ip,
...pin
}: PeerPin & { countryIso: string; ip: string }) => {
setPins((val) => {
const [, quantity = 0] =
val.find(([p]) => p.lat === pin.lat && p.lng == pin.lng) || [];
return [...val, [pin, quantity + 1]];
});
setPins((val) => PeerUtils.incPin(val, pin));
ips.current[ip] = countryIso;
},
[]
@ -115,24 +69,16 @@ const Peers = () => {
return;
}
setSortFn(() => (a: Node, b: Node) => {
const countryA = ips.current[Network.getIp(a.address)] || "";
const countryB = ips.current[Network.getIp(b.address)] || "";
return state === "desc"
? countryA.localeCompare(countryB)
: countryB.localeCompare(countryA);
});
setSortFn(() => PeerUtils.sortByCountry(state, ips.current));
};
const onSortActive = (state: TabSortState) => {
console.info("fdf");
if (!state) {
setSortFn(null);
return;
}
setSortFn(() => sortByBooleanValue(state));
setSortFn(() => PeerUtils.sortByBoolean(state));
};
const headers = [
@ -165,15 +111,14 @@ const Peers = () => {
]}></Row>
));
const actives = sorted.reduce((acc, cur) => acc + (cur.seen ? 1 : 0), 0) || 0;
const total = data?.table.nodes.length || 1;
const actives = PeerUtils.countActives(sorted);
const degrees = PeerUtils.calculareDegrees(sorted);
const good = actives > 0;
const styles: CustomCSSProperties = {
"--codex-peers-percent": (actives / total) * 180,
"--codex-peers-degrees": degrees,
};
const good = actives > 0;
return (
<div className="peers">
<div>
@ -217,14 +162,3 @@ const Peers = () => {
</div>
);
};
export const Route = createLazyFileRoute("/dashboard/peers")({
component: () => (
<ErrorBoundary
fallback={({ error }) => (
<ErrorPlaceholder error={error} subtitle="Cannot retrieve the data." />
)}>
<Peers />
</ErrorBoundary>
),
});

View File

@ -0,0 +1,68 @@
import { assert, describe, it } from "vitest";
import { PeerUtils } from "./peers.util";
describe("peers", () => {
it("sorts by boolean", async () => {
const a = { nodeId: "a", peerId: "", record: "", address: "", seen: false }
const b = { nodeId: "a", peerId: "", record: "", address: "", seen: true }
const items = [a, b,]
const descSorted = items.slice().sort(PeerUtils.sortByBoolean("desc"))
assert.deepEqual(descSorted, [b, a]);
const ascSorted = items.slice().sort(PeerUtils.sortByBoolean("asc"))
assert.deepEqual(ascSorted, [a, b]);
});
it("sorts by country", async () => {
const a = { nodeId: "a", peerId: "", record: "", address: "127.0.0.1", seen: false }
const b = { nodeId: "a", peerId: "", record: "", address: "127.0.0.2", seen: true }
const table = {
"127.0.0.1": "US",
"127.0.0.2": "France",
}
const items = [a, b,]
const descSorted = items.slice().sort(PeerUtils.sortByCountry("desc", table))
assert.deepEqual(descSorted, [b, a]);
const ascSorted = items.slice().sort(PeerUtils.sortByCountry("asc", table))
assert.deepEqual(ascSorted, [a, b]);
});
it("adds a new pin", async () => {
const latLng = { lat: 0, lng: 0 }
const values = PeerUtils.incPin([], latLng)
assert.deepEqual(values, [[latLng, 1]]);
});
it("increments an existing pin", async () => {
const latLng = { lat: 0, lng: 0 }
const values = PeerUtils.incPin([[latLng, 1]], latLng)
assert.deepEqual(values, [[latLng, 2]]);
});
it("count active peers nodes", async () => {
const a = { nodeId: "a", peerId: "", record: "", address: "127.0.0.1", seen: false }
const b = { nodeId: "a", peerId: "", record: "", address: "127.0.0.2", seen: true }
assert.equal(PeerUtils.countActives([a, b]), 1)
});
it("calculates active peers nodes degrees", async () => {
const a = { nodeId: "a", peerId: "", record: "", address: "127.0.0.1", seen: false }
const b = { nodeId: "a", peerId: "", record: "", address: "127.0.0.2", seen: true }
assert.equal(PeerUtils.calculareDegrees([a, b]), 90)
});
it("returns the country flag", async () => {
assert.equal(PeerUtils.geCountryEmoji("FR"), "🇫🇷")
});
})

View File

@ -0,0 +1,64 @@
import { TabSortState } from "@codex-storage/marketplace-ui-components";
export type PeerNode = {
nodeId: string;
peerId: string;
record: string;
address: string;
seen: boolean;
};
export type PeerPin = {
lat: number;
lng: number;
};
export type PeerSortFn = (a: PeerNode, b: PeerNode) => number;
export const PeerUtils = {
sortByBoolean: (state: TabSortState) => (a: PeerNode, b: PeerNode) => {
const order = state === "desc" ? 1 : -1;
return a?.seen === b?.seen ? 0 : b?.seen ? order : -order;
},
sortByCountry: (state: TabSortState, ipTable: Record<string, string>) =>
(a: PeerNode, b: PeerNode) => {
const [ipA = ""] = a.address.split(":")
const [ipB = ""] = b.address.split(":")
const countryA = ipTable[ipA] || "";
const countryB = ipTable[ipB] || "";
return state === "desc"
? countryA.localeCompare(countryB)
: countryB.localeCompare(countryA);
},
/**
* Increments the number of pin for a location
*/
incPin(val: [PeerPin, number][], pin: PeerPin): [PeerPin, number][] {
const [, quantity = 0] =
val.find(([p]) => p.lat === pin.lat && p.lng == pin.lng) || [];
const rest = val.filter(([p]) => p.lat !== pin.lat || p.lng !== pin.lng)
return [...rest, [pin, quantity + 1]];
},
countActives: (peers: PeerNode[]) =>
peers.reduce((acc, cur) => acc + (cur.seen ? 1 : 0), 0) || 0,
calculareDegrees: (peers: PeerNode[]) => {
const actives = PeerUtils.countActives(peers);
const total = peers.length || 1;
return (actives / total) * 180
},
geCountryEmoji: (countryCode: string) => {
const codePoints = countryCode
.toUpperCase()
.split("")
.map((char) => 127397 + char.charCodeAt(0));
return String.fromCodePoint(...codePoints);
}
}

View File

@ -1,4 +0,0 @@
export type PeerPin = {
lat: number;
lng: number;
};

View File

@ -1,11 +1,16 @@
import { attributes } from "../../utils/attributes";
type Props = {
onClick?: () => void;
className?: string;
disabled?: boolean;
};
export function RefreshIcon({ className, onClick }: Props) {
export function RefreshIcon({ className, onClick, disabled = false }: Props) {
const color = disabled ? "#494949" : "var(--codex-color-primary)";
return (
<svg
{...attributes({ "aria-disabled": disabled })}
className={className}
onClick={onClick}
width="24"
@ -15,7 +20,7 @@ export function RefreshIcon({ className, onClick }: Props) {
xmlns="http://www.w3.org/2000/svg">
<path
d="M12 24C5.3724 24 0 18.6276 0 12C0 5.3724 5.3724 0 12 0C18.6276 0 24 5.3724 24 12C24 18.6276 18.6276 24 12 24ZM17.784 18.0912C19.2325 16.7182 20.1449 14.8744 20.3576 12.8899C20.5703 10.9055 20.0695 8.91012 18.9449 7.26133C17.8203 5.61253 16.1454 4.41803 14.2202 3.89182C12.295 3.36561 10.2453 3.54208 8.4384 4.3896L9.6084 6.4956C10.5215 6.09874 11.519 5.93535 12.511 6.02015C13.503 6.10495 14.4584 6.43528 15.2909 6.98135C16.1234 7.52743 16.8069 8.27209 17.2798 9.14821C17.7528 10.0243 18.0003 11.0044 18 12H14.4L17.784 18.0912ZM15.5616 19.6104L14.3916 17.5044C13.4785 17.9013 12.481 18.0646 11.489 17.9798C10.497 17.8951 9.54165 17.5647 8.70914 17.0186C7.87664 16.4726 7.1931 15.7279 6.72016 14.8518C6.24722 13.9757 5.99973 12.9956 6 12H9.6L6.216 5.9088C4.76747 7.28176 3.85511 9.12563 3.64239 11.1101C3.42966 13.0945 3.93047 15.0899 5.05508 16.7387C6.17969 18.3875 7.85462 19.582 9.77982 20.1082C11.705 20.6344 13.7547 20.4579 15.5616 19.6104V19.6104Z"
fill="var(--codex-color-primary)"
fill={color}
/>
</svg>
);

View File

@ -7,6 +7,7 @@ export function SuccessCheckIcon({ variant }: Props) {
return (
<svg
data-testid="icon-success"
width="16"
height="16"
viewBox="0 0 16 16"

View File

@ -54,6 +54,8 @@ export function UserInfo({ onNameChange }: Props) {
"objects",
"symbols",
"flags",
// The type does not allow a string array but the api yes
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any
}
/>

View File

@ -2,14 +2,14 @@ import { useDebug } from "../../hooks/useDebug";
import { AlphaText } from "../AlphaText/AlphaText";
import { AlphaIcon } from "../OnBoarding/AlphaIcon";
import "./Versions.css";
import { VersionsUtil } from "./versions.util";
const throwOnError = true;
const throwOnError = false;
export function Versions() {
const debug = useDebug(throwOnError);
const parts = debug.data?.codex.version.split("\n") || [""];
const version = parts[parts.length - 1];
const version = VersionsUtil.clientVersion(debug.data?.codex.version);
return (
<div className="versions">
@ -20,7 +20,7 @@ export function Versions() {
</div>
<div>
<p>Vault</p>
<small>VER. {import.meta.env.PACKAGE_VERSION}</small>
<small>VER. {VersionsUtil.codexVersion()}</small>
<AlphaText variant="failure" width={37}></AlphaText>
</div>
</div>

View File

@ -0,0 +1,9 @@
import { assert, describe, it } from "vitest";
import { VersionsUtil } from "./versions.util";
describe("versions", () => {
it("gets the last client version", async () => {
const version = "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7"
assert.equal(VersionsUtil.clientVersion(version), "v0.1.7")
})
})

View File

@ -0,0 +1,8 @@
export const VersionsUtil = {
codexVersion: () => import.meta.env.PACKAGE_VERSION,
clientVersion: (version: string | undefined) => {
const parts = version?.split("\n") || [""];
return parts[parts.length - 1];
}
}

View File

@ -1,6 +1,9 @@
import { CodexDebugInfo, CodexError, SafeValue } from "@codex-storage/sdk-js";
import { CodexDebugInfo, SafeValue, CodexError } from "@codex-storage/sdk-js"
export const PortForwardingUtil = {
check: (port: number) => fetch(import.meta.env.VITE_GEO_IP_URL + "/port/" + port)
.then((res) => res.json()),
export const DebugUtils = {
getTcpPort(info: CodexDebugInfo): SafeValue<number> {
if (info.addrs.length === 0) {
return { error: true, data: new CodexError("Not existing address") }
@ -20,4 +23,5 @@ export const DebugUtils = {
return { error: false, data: port }
}
}

View File

@ -27,6 +27,8 @@ export function useCodexConnection() {
// Cache is not useful for the spr endpoint
gcTime: 0,
throwOnError: false,
});
return { enabled: !isError && !!data, isFetching, refetch };

View File

@ -6,12 +6,13 @@ const report = false;
export function usePersistence(isCodexOnline: boolean) {
const { data, isError, isFetching, refetch } = useQuery({
queryKey: ["availabilities"],
queryKey: [],
queryFn: async () => {
return CodexSdk.marketplace()
.purchases()
.then((data) => Promises.rejectOnError(data, report));
},
refetchInterval: 5000,
// Enable only when the use has an internet connection
@ -27,6 +28,8 @@ export function usePersistence(isCodexOnline: boolean) {
// Refreshing when focus returns can be useful if a user comes back
// to the UI after performing an operation in the terminal.
refetchOnWindowFocus: true,
throwOnError: false,
});
return { enabled: !isError && !!data, isFetching, refetch };

View File

@ -1,20 +1,21 @@
import { useQuery } from "@tanstack/react-query";
import { Echo } from "../utils/echo";
import { Errors } from "../utils/errors";
import { CodexDebugInfo } from "@codex-storage/sdk-js";
import { DebugUtils } from "../utils/debug";
import { PortForwardingUtil } from "./port-forwarding.util";
type PortForwardingResponse = { reachable: boolean };
export function usePortForwarding(info: CodexDebugInfo | undefined) {
const { data, isFetching, refetch } = useQuery({
queryFn: (): Promise<PortForwardingResponse> => {
const port = DebugUtils.getTcpPort(info!);
const port = PortForwardingUtil.getTcpPort(info!);
if (port.error) {
Errors.report(port);
return Promise.resolve({ reachable: false });
} else {
return Echo.portForwarding(port.data).catch((e) => Errors.report(e));
return PortForwardingUtil.check(port.data).catch((e) =>
Errors.report(e)
);
}
},
queryKey: ["port-forwarding"],
@ -33,6 +34,8 @@ export function usePortForwarding(info: CodexDebugInfo | undefined) {
// The user may try to change the port forwarding and go back
// to the tab
refetchOnWindowFocus: true,
throwOnError: false,
});
return { enabled: data.reachable, isFetching, refetch };

View File

@ -22,6 +22,7 @@ import { Route as DashboardWalletImport } from './routes/dashboard/wallet'
import { Route as DashboardSettingsImport } from './routes/dashboard/settings'
import { Route as DashboardRequestsImport } from './routes/dashboard/requests'
import { Route as DashboardPurchasesImport } from './routes/dashboard/purchases'
import { Route as DashboardPeersImport } from './routes/dashboard/peers'
import { Route as DashboardNodesImport } from './routes/dashboard/nodes'
import { Route as DashboardLogsImport } from './routes/dashboard/logs'
import { Route as DashboardHelpImport } from './routes/dashboard/help'
@ -34,7 +35,6 @@ import { Route as DashboardAboutImport } from './routes/dashboard/about'
// Create Virtual Routes
const DashboardPeersLazyImport = createFileRoute('/dashboard/peers')()
const DashboardAvailabilitiesLazyImport = createFileRoute(
'/dashboard/availabilities',
)()
@ -71,14 +71,6 @@ const DashboardIndexRoute = DashboardIndexImport.update({
getParentRoute: () => DashboardRoute,
} as any)
const DashboardPeersLazyRoute = DashboardPeersLazyImport.update({
id: '/peers',
path: '/peers',
getParentRoute: () => DashboardRoute,
} as any).lazy(() =>
import('./routes/dashboard/peers.lazy').then((d) => d.Route),
)
const DashboardAvailabilitiesLazyRoute =
DashboardAvailabilitiesLazyImport.update({
id: '/availabilities',
@ -112,6 +104,12 @@ const DashboardPurchasesRoute = DashboardPurchasesImport.update({
getParentRoute: () => DashboardRoute,
} as any)
const DashboardPeersRoute = DashboardPeersImport.update({
id: '/peers',
path: '/peers',
getParentRoute: () => DashboardRoute,
} as any)
const DashboardNodesRoute = DashboardNodesImport.update({
id: '/nodes',
path: '/nodes',
@ -261,6 +259,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DashboardNodesImport
parentRoute: typeof DashboardImport
}
'/dashboard/peers': {
id: '/dashboard/peers'
path: '/peers'
fullPath: '/dashboard/peers'
preLoaderRoute: typeof DashboardPeersImport
parentRoute: typeof DashboardImport
}
'/dashboard/purchases': {
id: '/dashboard/purchases'
path: '/purchases'
@ -296,13 +301,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DashboardAvailabilitiesLazyImport
parentRoute: typeof DashboardImport
}
'/dashboard/peers': {
id: '/dashboard/peers'
path: '/peers'
fullPath: '/dashboard/peers'
preLoaderRoute: typeof DashboardPeersLazyImport
parentRoute: typeof DashboardImport
}
'/dashboard/': {
id: '/dashboard/'
path: '/'
@ -325,12 +323,12 @@ interface DashboardRouteChildren {
DashboardHelpRoute: typeof DashboardHelpRoute
DashboardLogsRoute: typeof DashboardLogsRoute
DashboardNodesRoute: typeof DashboardNodesRoute
DashboardPeersRoute: typeof DashboardPeersRoute
DashboardPurchasesRoute: typeof DashboardPurchasesRoute
DashboardRequestsRoute: typeof DashboardRequestsRoute
DashboardSettingsRoute: typeof DashboardSettingsRoute
DashboardWalletRoute: typeof DashboardWalletRoute
DashboardAvailabilitiesLazyRoute: typeof DashboardAvailabilitiesLazyRoute
DashboardPeersLazyRoute: typeof DashboardPeersLazyRoute
DashboardIndexRoute: typeof DashboardIndexRoute
}
@ -344,12 +342,12 @@ const DashboardRouteChildren: DashboardRouteChildren = {
DashboardHelpRoute: DashboardHelpRoute,
DashboardLogsRoute: DashboardLogsRoute,
DashboardNodesRoute: DashboardNodesRoute,
DashboardPeersRoute: DashboardPeersRoute,
DashboardPurchasesRoute: DashboardPurchasesRoute,
DashboardRequestsRoute: DashboardRequestsRoute,
DashboardSettingsRoute: DashboardSettingsRoute,
DashboardWalletRoute: DashboardWalletRoute,
DashboardAvailabilitiesLazyRoute: DashboardAvailabilitiesLazyRoute,
DashboardPeersLazyRoute: DashboardPeersLazyRoute,
DashboardIndexRoute: DashboardIndexRoute,
}
@ -371,12 +369,12 @@ export interface FileRoutesByFullPath {
'/dashboard/help': typeof DashboardHelpRoute
'/dashboard/logs': typeof DashboardLogsRoute
'/dashboard/nodes': typeof DashboardNodesRoute
'/dashboard/peers': typeof DashboardPeersRoute
'/dashboard/purchases': typeof DashboardPurchasesRoute
'/dashboard/requests': typeof DashboardRequestsRoute
'/dashboard/settings': typeof DashboardSettingsRoute
'/dashboard/wallet': typeof DashboardWalletRoute
'/dashboard/availabilities': typeof DashboardAvailabilitiesLazyRoute
'/dashboard/peers': typeof DashboardPeersLazyRoute
'/dashboard/': typeof DashboardIndexRoute
}
@ -393,12 +391,12 @@ export interface FileRoutesByTo {
'/dashboard/help': typeof DashboardHelpRoute
'/dashboard/logs': typeof DashboardLogsRoute
'/dashboard/nodes': typeof DashboardNodesRoute
'/dashboard/peers': typeof DashboardPeersRoute
'/dashboard/purchases': typeof DashboardPurchasesRoute
'/dashboard/requests': typeof DashboardRequestsRoute
'/dashboard/settings': typeof DashboardSettingsRoute
'/dashboard/wallet': typeof DashboardWalletRoute
'/dashboard/availabilities': typeof DashboardAvailabilitiesLazyRoute
'/dashboard/peers': typeof DashboardPeersLazyRoute
'/dashboard': typeof DashboardIndexRoute
}
@ -417,12 +415,12 @@ export interface FileRoutesById {
'/dashboard/help': typeof DashboardHelpRoute
'/dashboard/logs': typeof DashboardLogsRoute
'/dashboard/nodes': typeof DashboardNodesRoute
'/dashboard/peers': typeof DashboardPeersRoute
'/dashboard/purchases': typeof DashboardPurchasesRoute
'/dashboard/requests': typeof DashboardRequestsRoute
'/dashboard/settings': typeof DashboardSettingsRoute
'/dashboard/wallet': typeof DashboardWalletRoute
'/dashboard/availabilities': typeof DashboardAvailabilitiesLazyRoute
'/dashboard/peers': typeof DashboardPeersLazyRoute
'/dashboard/': typeof DashboardIndexRoute
}
@ -442,12 +440,12 @@ export interface FileRouteTypes {
| '/dashboard/help'
| '/dashboard/logs'
| '/dashboard/nodes'
| '/dashboard/peers'
| '/dashboard/purchases'
| '/dashboard/requests'
| '/dashboard/settings'
| '/dashboard/wallet'
| '/dashboard/availabilities'
| '/dashboard/peers'
| '/dashboard/'
fileRoutesByTo: FileRoutesByTo
to:
@ -463,12 +461,12 @@ export interface FileRouteTypes {
| '/dashboard/help'
| '/dashboard/logs'
| '/dashboard/nodes'
| '/dashboard/peers'
| '/dashboard/purchases'
| '/dashboard/requests'
| '/dashboard/settings'
| '/dashboard/wallet'
| '/dashboard/availabilities'
| '/dashboard/peers'
| '/dashboard'
id:
| '__root__'
@ -485,12 +483,12 @@ export interface FileRouteTypes {
| '/dashboard/help'
| '/dashboard/logs'
| '/dashboard/nodes'
| '/dashboard/peers'
| '/dashboard/purchases'
| '/dashboard/requests'
| '/dashboard/settings'
| '/dashboard/wallet'
| '/dashboard/availabilities'
| '/dashboard/peers'
| '/dashboard/'
fileRoutesById: FileRoutesById
}
@ -542,12 +540,12 @@ export const routeTree = rootRoute
"/dashboard/help",
"/dashboard/logs",
"/dashboard/nodes",
"/dashboard/peers",
"/dashboard/purchases",
"/dashboard/requests",
"/dashboard/settings",
"/dashboard/wallet",
"/dashboard/availabilities",
"/dashboard/peers",
"/dashboard/"
]
},
@ -593,6 +591,10 @@ export const routeTree = rootRoute
"filePath": "dashboard/nodes.tsx",
"parent": "/dashboard"
},
"/dashboard/peers": {
"filePath": "dashboard/peers.tsx",
"parent": "/dashboard"
},
"/dashboard/purchases": {
"filePath": "dashboard/purchases.tsx",
"parent": "/dashboard"
@ -613,10 +615,6 @@ export const routeTree = rootRoute
"filePath": "dashboard/availabilities.lazy.tsx",
"parent": "/dashboard"
},
"/dashboard/peers": {
"filePath": "dashboard/peers.lazy.tsx",
"parent": "/dashboard"
},
"/dashboard/": {
"filePath": "dashboard/index.tsx",
"parent": "/dashboard"

View File

@ -1,17 +1,12 @@
import {
createFileRoute,
Outlet,
useRouterState,
} from "@tanstack/react-router";
import { createFileRoute, Outlet } from "@tanstack/react-router";
import "./layout.css";
import { Menu } from "../components/Menu/Menu";
import { useEffect, useState } from "react";
import { useState } from "react";
import { AppBar } from "../components/AppBar/AppBar";
import { Backdrop } from "@codex-storage/marketplace-ui-components";
const Layout = () => {
const [hasMobileMenu, setHasMobileMenu] = useState(false);
const router = useRouterState();
const onIconClick = () => {
if (window.innerWidth <= 999) {
@ -19,10 +14,6 @@ const Layout = () => {
}
};
useEffect(() => {
setHasMobileMenu(false);
}, [router.location.pathname]);
const onClose = () => setHasMobileMenu(false);
const isMobileMenuDisplayed =

View File

@ -0,0 +1,15 @@
import { ErrorBoundary } from "@sentry/react";
import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder";
import { createFileRoute } from "@tanstack/react-router";
import { Peers } from "../../components/Peers/Peers";
export const Route = createFileRoute("/dashboard/peers")({
component: () => (
<ErrorBoundary
fallback={({ error }) => (
<ErrorPlaceholder error={error} subtitle="Cannot retrieve the data." />
)}>
<Peers />
</ErrorBoundary>
),
});

View File

@ -1,5 +0,0 @@
export const Echo = {
portForwarding: (port: number) => fetch(import.meta.env.VITE_GEO_IP_URL + "/port/" + port)
.then((res) => res.json())
}

View File

@ -1,6 +0,0 @@
export const Network = {
getIp(address: string) {
const [ip] = address.split(":")
return ip
}
}

View File

@ -1,11 +1,8 @@
export const Strings = {
shortId: (id: string) => id.slice(0, 5) + "..." + id.slice(-5),
splitURLAndPort(url: string) {
const [protocol, hostname = "", port = ""] = url.split(":")
return [protocol + ":" + hostname, port]
}
};

View File

@ -15,7 +15,8 @@ export default defineConfig({
output: {
manualChunks: {
"@sentry/react": ["@sentry/react"],
"emoji-picker-react": ["emoji-picker-react"]
"emoji-picker-react": ["emoji-picker-react"],
"dotted-map": ["dotted-map"],
}
},
onwarn(warning, defaultHandler) {

8
vitest.config.ts Normal file
View File

@ -0,0 +1,8 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite'
export default defineConfig({
test: {
include: ["src/**/*.test.ts"]
},
})