From 29ff705f58d7610f3c595c36b450fd34f946d343 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Fri, 1 Nov 2024 15:07:30 +0100 Subject: [PATCH] Add unit tests and fix minor issues --- .github/workflows/playwright.yml | 10 - .github/workflows/unit-testing.yml | 37 ++ e2e/availabilities.spec.ts | 39 +- e2e/download.spec.ts | 6 +- e2e/onboarding.spec.ts | 46 ++- e2e/settings.spec.ts | 43 +- e2e/storage-requests.spec.ts | 49 +-- e2e/upload.spec.ts | 8 +- package-lock.json | 368 +++++++++++++++++- package.json | 6 +- src/components/AppBar/AppBar.tsx | 13 +- src/components/CardNumbers/CardNumbers.css | 2 +- .../ErrorCircleIcon/ErrorCircleIcon.tsx | 1 + src/components/HealthChecks/HealthChecks.tsx | 70 ++-- .../HealthChecks/health-check.util.test.ts | 148 +++++++ .../HealthChecks/health-check.util.ts | 51 +++ .../OnBoarding/OnBoardingLayout.css | 8 + src/components/Peers/PeerCountryCell.tsx | 12 +- .../peers.css => components/Peers/Peers.css} | 3 +- .../Peers/Peers.tsx} | 116 ++---- src/components/Peers/peers.util.test.ts | 68 ++++ src/components/Peers/peers.util.ts | 64 +++ src/components/Peers/types.ts | 4 - src/components/RefreshIcon/RefreshIcon.tsx | 9 +- .../SuccessCheckIcon/SuccessCheckIcon.tsx | 1 + src/components/UserInfo/UserInfo.tsx | 2 + src/components/Versions/Versions.tsx | 8 +- src/components/Versions/versions.util.test.ts | 9 + src/components/Versions/versions.util.ts | 8 + .../port-forwarding.util.ts} | 8 +- src/hooks/useCodexConnection.tsx | 2 + src/hooks/usePersistence.tsx | 5 +- src/hooks/usePortForwarding.tsx | 11 +- src/routeTree.gen.ts | 56 ++- src/routes/dashboard.tsx | 13 +- src/routes/dashboard/peers.tsx | 15 + src/utils/echo.ts | 5 - src/utils/network.ts | 6 - src/utils/strings.ts | 5 +- vite.config.ts | 3 +- vitest.config.ts | 8 + 41 files changed, 1033 insertions(+), 313 deletions(-) create mode 100644 .github/workflows/unit-testing.yml create mode 100644 src/components/HealthChecks/health-check.util.test.ts create mode 100644 src/components/HealthChecks/health-check.util.ts rename src/{routes/dashboard/peers.css => components/Peers/Peers.css} (98%) rename src/{routes/dashboard/peers.lazy.tsx => components/Peers/Peers.tsx} (53%) create mode 100644 src/components/Peers/peers.util.test.ts create mode 100644 src/components/Peers/peers.util.ts delete mode 100644 src/components/Peers/types.ts create mode 100644 src/components/Versions/versions.util.test.ts create mode 100644 src/components/Versions/versions.util.ts rename src/{utils/debug.ts => hooks/port-forwarding.util.ts} (71%) create mode 100644 src/routes/dashboard/peers.tsx delete mode 100644 src/utils/echo.ts delete mode 100644 src/utils/network.ts create mode 100644 vitest.config.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index bbde8c4..a6e9332 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -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 diff --git a/.github/workflows/unit-testing.yml b/.github/workflows/unit-testing.yml new file mode 100644 index 0000000..d20c823 --- /dev/null +++ b/.github/workflows/unit-testing.yml @@ -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 diff --git a/e2e/availabilities.spec.ts b/e2e/availabilities.spec.ts index 2b9ec81..6306b8a 100644 --- a/e2e/availabilities.spec.ts +++ b/e2e/availabilities.spec.ts @@ -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(); }) diff --git a/e2e/download.spec.ts b/e2e/download.spec.ts index 145650f..672549b 100644 --- a/e2e/download.spec.ts +++ b/e2e/download.spec.ts @@ -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(); diff --git a/e2e/onboarding.spec.ts b/e2e/onboarding.spec.ts index 01cb38d..3f624be 100644 --- a/e2e/onboarding.spec.ts +++ b/e2e/onboarding.spec.ts @@ -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() -}); \ No newline at end of file +}); + +// 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'); \ No newline at end of file diff --git a/e2e/settings.spec.ts b/e2e/settings.spec.ts index f1f16a9..6d35e47 100644 --- a/e2e/settings.spec.ts +++ b/e2e/settings.spec.ts @@ -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() }) \ No newline at end of file diff --git a/e2e/storage-requests.spec.ts b/e2e/storage-requests.spec.ts index 578de72..3dd6edf 100644 --- a/e2e/storage-requests.spec.ts +++ b/e2e/storage-requests.spec.ts @@ -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() }) diff --git a/e2e/upload.spec.ts b/e2e/upload.spec.ts index ebbf05e..c8b9582 100644 --- a/e2e/upload.spec.ts +++ b/e2e/upload.spec.ts @@ -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(); // }); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8918c89..0e8c42c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index c5a89c8..6cbdcd3 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index 809d844..abb52d2 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -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 ( <> diff --git a/src/components/CardNumbers/CardNumbers.css b/src/components/CardNumbers/CardNumbers.css index 3230fde..00e2a65 100644 --- a/src/components/CardNumbers/CardNumbers.css +++ b/src/components/CardNumbers/CardNumbers.css @@ -59,7 +59,7 @@ align-items: center; } -.cardNumber-dataContainer .buttonIcon { +.cardNumber-dataContainer .button-icon { position: relative; top: 3px; } diff --git a/src/components/ErrorCircleIcon/ErrorCircleIcon.tsx b/src/components/ErrorCircleIcon/ErrorCircleIcon.tsx index c696124..43720b7 100644 --- a/src/components/ErrorCircleIcon/ErrorCircleIcon.tsx +++ b/src/components/ErrorCircleIcon/ErrorCircleIcon.tsx @@ -1,6 +1,7 @@ export function ErrorCircleIcon() { return ( { - 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) => { 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) {
- +
diff --git a/src/components/HealthChecks/health-check.util.test.ts b/src/components/HealthChecks/health-check.util.test.ts new file mode 100644 index 0000000..534a851 --- /dev/null +++ b/src/components/HealthChecks/health-check.util.test.ts @@ -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); + }); +}) \ No newline at end of file diff --git a/src/components/HealthChecks/health-check.util.ts b/src/components/HealthChecks/health-check.util.ts new file mode 100644 index 0000000..65250a4 --- /dev/null +++ b/src/components/HealthChecks/health-check.util.ts @@ -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 { + 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 } + } +} \ No newline at end of file diff --git a/src/components/OnBoarding/OnBoardingLayout.css b/src/components/OnBoarding/OnBoardingLayout.css index 71013e8..f0032de 100644 --- a/src/components/OnBoarding/OnBoardingLayout.css +++ b/src/components/OnBoarding/OnBoardingLayout.css @@ -149,6 +149,14 @@ label { margin-top: 1rem; } + + .health-checks { + .address { + .refresh { + top: 22px; + } + } + } } .get-started { diff --git a/src/components/Peers/PeerCountryCell.tsx b/src/components/Peers/PeerCountryCell.tsx index b4a3d10..632d390 100644 --- a/src/components/Peers/PeerCountryCell.tsx +++ b/src/components/Peers/PeerCountryCell.tsx @@ -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) {
{data ? ( <> - {!!data && getFlagEmoji(data.country_iso)} + {!!data && PeerUtils.geCountryEmoji(data.country_iso)} {data?.country} ) : ( diff --git a/src/routes/dashboard/peers.css b/src/components/Peers/Peers.css similarity index 98% rename from src/routes/dashboard/peers.css rename to src/components/Peers/Peers.css index 43d81bf..b540a7f 100644 --- a/src/routes/dashboard/peers.css +++ b/src/components/Peers/Peers.css @@ -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; } } diff --git a/src/routes/dashboard/peers.lazy.tsx b/src/components/Peers/Peers.tsx similarity index 53% rename from src/routes/dashboard/peers.lazy.tsx rename to src/components/Peers/Peers.tsx index 3768135..471118b 100644 --- a/src/routes/dashboard/peers.lazy.tsx +++ b/src/components/Peers/Peers.tsx @@ -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>({}); const [pins, setPins] = useState<[PeerPin, number][]>([]); - const [sortFn, setSortFn] = useState(() => - sortByBooleanValue("desc") + const [sortFn, setSortFn] = useState(() => + 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 = () => { ]}> )); - 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 (
@@ -217,14 +162,3 @@ const Peers = () => {
); }; - -export const Route = createLazyFileRoute("/dashboard/peers")({ - component: () => ( - ( - - )}> - - - ), -}); diff --git a/src/components/Peers/peers.util.test.ts b/src/components/Peers/peers.util.test.ts new file mode 100644 index 0000000..95d4dbf --- /dev/null +++ b/src/components/Peers/peers.util.test.ts @@ -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"), "🇫🇷") + }); +}) \ No newline at end of file diff --git a/src/components/Peers/peers.util.ts b/src/components/Peers/peers.util.ts new file mode 100644 index 0000000..7d40ed5 --- /dev/null +++ b/src/components/Peers/peers.util.ts @@ -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) => + (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); + } + +} diff --git a/src/components/Peers/types.ts b/src/components/Peers/types.ts deleted file mode 100644 index a3e1bea..0000000 --- a/src/components/Peers/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type PeerPin = { - lat: number; - lng: number; -}; \ No newline at end of file diff --git a/src/components/RefreshIcon/RefreshIcon.tsx b/src/components/RefreshIcon/RefreshIcon.tsx index dd73da3..75d0f8d 100644 --- a/src/components/RefreshIcon/RefreshIcon.tsx +++ b/src/components/RefreshIcon/RefreshIcon.tsx @@ -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 ( ); diff --git a/src/components/SuccessCheckIcon/SuccessCheckIcon.tsx b/src/components/SuccessCheckIcon/SuccessCheckIcon.tsx index 4577050..a12a696 100644 --- a/src/components/SuccessCheckIcon/SuccessCheckIcon.tsx +++ b/src/components/SuccessCheckIcon/SuccessCheckIcon.tsx @@ -7,6 +7,7 @@ export function SuccessCheckIcon({ variant }: Props) { return ( diff --git a/src/components/Versions/Versions.tsx b/src/components/Versions/Versions.tsx index 234b615..08e8ea8 100644 --- a/src/components/Versions/Versions.tsx +++ b/src/components/Versions/Versions.tsx @@ -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 (
@@ -20,7 +20,7 @@ export function Versions() {

Vault

- VER. {import.meta.env.PACKAGE_VERSION} + VER. {VersionsUtil.codexVersion()}
diff --git a/src/components/Versions/versions.util.test.ts b/src/components/Versions/versions.util.test.ts new file mode 100644 index 0000000..63b1805 --- /dev/null +++ b/src/components/Versions/versions.util.test.ts @@ -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") + }) +}) \ No newline at end of file diff --git a/src/components/Versions/versions.util.ts b/src/components/Versions/versions.util.ts new file mode 100644 index 0000000..151ecc3 --- /dev/null +++ b/src/components/Versions/versions.util.ts @@ -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]; + } +} \ No newline at end of file diff --git a/src/utils/debug.ts b/src/hooks/port-forwarding.util.ts similarity index 71% rename from src/utils/debug.ts rename to src/hooks/port-forwarding.util.ts index 3e4ca68..6833ca9 100644 --- a/src/utils/debug.ts +++ b/src/hooks/port-forwarding.util.ts @@ -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 { 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 } } + } \ No newline at end of file diff --git a/src/hooks/useCodexConnection.tsx b/src/hooks/useCodexConnection.tsx index e5f1bb5..57fb340 100644 --- a/src/hooks/useCodexConnection.tsx +++ b/src/hooks/useCodexConnection.tsx @@ -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 }; diff --git a/src/hooks/usePersistence.tsx b/src/hooks/usePersistence.tsx index 00ad58c..b66d68d 100644 --- a/src/hooks/usePersistence.tsx +++ b/src/hooks/usePersistence.tsx @@ -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 }; diff --git a/src/hooks/usePortForwarding.tsx b/src/hooks/usePortForwarding.tsx index 0c66fec..f77beb1 100644 --- a/src/hooks/usePortForwarding.tsx +++ b/src/hooks/usePortForwarding.tsx @@ -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 => { - 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 }; diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index dedbeb0..b0bed7f 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -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" diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx index 39ef1c2..c11cfa2 100644 --- a/src/routes/dashboard.tsx +++ b/src/routes/dashboard.tsx @@ -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 = diff --git a/src/routes/dashboard/peers.tsx b/src/routes/dashboard/peers.tsx new file mode 100644 index 0000000..a9c8827 --- /dev/null +++ b/src/routes/dashboard/peers.tsx @@ -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: () => ( + ( + + )}> + + + ), +}); diff --git a/src/utils/echo.ts b/src/utils/echo.ts deleted file mode 100644 index e582a60..0000000 --- a/src/utils/echo.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const Echo = { - portForwarding: (port: number) => fetch(import.meta.env.VITE_GEO_IP_URL + "/port/" + port) - .then((res) => res.json()) - -} \ No newline at end of file diff --git a/src/utils/network.ts b/src/utils/network.ts deleted file mode 100644 index d191611..0000000 --- a/src/utils/network.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const Network = { - getIp(address: string) { - const [ip] = address.split(":") - return ip - } -} \ No newline at end of file diff --git a/src/utils/strings.ts b/src/utils/strings.ts index dcacf91..95c4ea1 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -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] - } + }; diff --git a/vite.config.ts b/vite.config.ts index 7dee5e1..f9f0fd2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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) { diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..d73ab47 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +/// +import { defineConfig } from 'vite' + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"] + }, +})