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..2ff3a3a --- /dev/null +++ b/.github/workflows/unit-testing.yml @@ -0,0 +1,39 @@ +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..4a625b0 100644 --- a/e2e/download.spec.ts +++ b/e2e/download.spec.ts @@ -1,31 +1,31 @@ -import { test, expect } from '@playwright/test'; -import path, { dirname } from 'path'; -import { fileURLToPath } from 'url'; +// import { test, expect } from '@playwright/test'; +// import path, { dirname } from 'path'; +// import { fileURLToPath } from 'url'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +// const __filename = fileURLToPath(import.meta.url); +// const __dirname = dirname(__filename); -test('download a file', async ({ page, browserName }) => { - // https://github.com/microsoft/playwright/issues/13037 - test.skip(browserName.toLowerCase() !== 'chromium', - `Test only for chromium!`); +// test('download a file', async ({ page, browserName }) => { +// // https://github.com/microsoft/playwright/issues/13037 +// test.skip(browserName.toLowerCase() !== 'chromium', +// `Test only for chromium!`); - await page.goto('/dashboard'); - await page.locator('div').getByTestId("upload").setInputFiles([ - path.join(__dirname, "assets", 'chatgpt.jpg'), - ]); - await page.context().grantPermissions(["clipboard-read", "clipboard-write"]); - await page.locator('.files-fileActions > button:nth-child(3)').first().click(); - 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); - // const page1Promise = page.waitForEvent('popup'); - const downloadPromise = page.waitForEvent('download'); - await page.locator('div').filter({ hasText: /^Download a fileDownload$/ }).getByRole('button').click(); - // const page1 = await page1Promise; - const download = await downloadPromise; - expect(await download.failure()).toBeNull() -}); \ No newline at end of file +// await page.goto('/dashboard'); +// await page.locator('div').getByTestId("upload").setInputFiles([ +// path.join(__dirname, "assets", 'chatgpt.jpg'), +// ]); +// await page.context().grantPermissions(["clipboard-read", "clipboard-write"]); +// await page.locator('.files-fileActions > button:nth-child(3)').first().click(); +// 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 > .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(); +// // const page1 = await page1Promise; +// const download = await downloadPromise; +// expect(await download.failure()).toBeNull() +// }); \ No newline at end of file diff --git a/e2e/onboarding.spec.ts b/e2e/onboarding.spec.ts index 01cb38d..a8d9e71 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")).not.toBeVisible() + await expect(page.locator(".health-checks ul li").nth(2).getByTestId("icon-success")).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 903147b..447e3fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "0.0.7", "license": "MIT", "dependencies": { - "@codex-storage/marketplace-ui-components": "^0.0.29", - "@codex-storage/sdk-js": "^0.0.12", + "@codex-storage/marketplace-ui-components": "^0.0.31", + "@codex-storage/sdk-js": "^0.0.15", "@sentry/browser": "^8.32.0", "@sentry/react": "^8.31.0", "@tanstack/react-query": "^5.51.15", @@ -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" @@ -378,9 +379,9 @@ "peer": true }, "node_modules/@codex-storage/marketplace-ui-components": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.29.tgz", - "integrity": "sha512-ctbjE/m2bOu3SXvlHcB8hulZyzFFymizjKPonNjHHriyCwlGMgryLeKLty2XpzAWw/JR/WZPSZTwlWJ7qOy0hQ==", + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.31.tgz", + "integrity": "sha512-jMAlqVRxC+AP3Q+C4PfrU27mHZsDMtH2CMqzsGvcRMfy/W/e4Q/B0Nvk0PlSlbdvMcWkWI2CQ+2kbf6gmOietA==", "dependencies": { "lucide-react": "^0.453.0" }, @@ -388,7 +389,7 @@ "node": ">=18" }, "peerDependencies": { - "@codex-storage/sdk-js": ">=0.0.12", + "@codex-storage/sdk-js": ">=0.0.14", "postcss-nesting": "^13.0.1", "react": "^18.3.1", "react-dom": "^18.3.1" @@ -403,9 +404,9 @@ } }, "node_modules/@codex-storage/sdk-js": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@codex-storage/sdk-js/-/sdk-js-0.0.12.tgz", - "integrity": "sha512-WjMZ9fkeVFBdDlFViWRDIirk2s4nX0U5r4WixAeieAkDBlRq5nqWcm9rqz5/8u8EQudgk/y3p0vPZc8cpyK4bA==", + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/@codex-storage/sdk-js/-/sdk-js-0.0.15.tgz", + "integrity": "sha512-asL59uhHNI2zPLEcygh6HnZEdQJebSjU2JXut6xZxAI87VVkWfN9Y1BxlyxgsF+wJF10DB0tnv3cPEI3wb2huQ==", "dependencies": { "valibot": "^0.32.0" }, @@ -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..5f984d5 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", @@ -24,8 +25,8 @@ "React" ], "dependencies": { - "@codex-storage/marketplace-ui-components": "^0.0.29", - "@codex-storage/sdk-js": "^0.0.12", + "@codex-storage/marketplace-ui-components": "^0.0.31", + "@codex-storage/sdk-js": "^0.0.15", "@sentry/browser": "^8.32.0", "@sentry/react": "^8.31.0", "@tanstack/react-query": "^5.51.15", @@ -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/Availability/useAvailabilityMutation.ts b/src/components/Availability/useAvailabilityMutation.ts index 9727de6..0f58949 100644 --- a/src/components/Availability/useAvailabilityMutation.ts +++ b/src/components/Availability/useAvailabilityMutation.ts @@ -9,7 +9,6 @@ import { } from "@codex-storage/marketplace-ui-components"; import { Times } from "../../utils/times"; import { CodexSdk } from "../../sdk/codex"; -import { AvailabilityStorage } from "../../utils/availabilities-storage"; import { CodexAvailabilityCreateResponse } from "@codex-storage/sdk-js"; @@ -58,7 +57,7 @@ export function useAvailabilityMutation( WebStorage.delete("availability-step"); if (typeof res === "object" && body.name) { - AvailabilityStorage.add(res.id, body.name) + WebStorage.availabilities.add(res.id, body.name) } setError(null); 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 ( ({ - name: "N/A.jpg", - mimetype: "N/A", - uploadedAt: new Date(0, 0, 0, 0, 0, 0).toJSON(), + filename: "-", + mimetype: "-", + uploadedAt: 0, }); useEffect(() => { - PurchaseStorage.get(requestId).then((cid) => { + WebStorage.purchases.get(requestId).then((cid) => { if (cid) { setCid(cid); - FilesStorage.get(cid).then((data) => { - if (data) { - setMetadata(data); - } - }); + const content = data.find((m) => m.cid === cid); + if (content) { + const { + filename = "-", + mimetype = "application/octet-stream", + uploadedAt = 0, + } = content.manifest; + setMetadata({ + filename, + mimetype, + uploadedAt, + }); + } } }); - }, [requestId]); + }, [requestId, data]); - let name = metadata.name.slice(0, 10); + let filename = metadata.filename || "-"; - if (metadata.name.length > 10) { - // const [filename, ext] = metadata.name.split("."); - // name = filename.slice(0, 10) + "..." + ext; - name += "..."; + if (filename.length > 10) { + const [name, ext] = filename.split("."); + filename = name.slice(0, 10) + "..." + ext; } // const cidTruncated = cid.slice(0, 5) + ".".repeat(5) + cid.slice(-5); @@ -50,10 +64,10 @@ export function FileCell({ requestId, purchaseCid }: Props) { return (
- +
- {name} + {filename} diff --git a/src/components/Files/FileCell.tsx b/src/components/Files/FileCell.tsx index d45d0b9..31c5a9e 100644 --- a/src/components/Files/FileCell.tsx +++ b/src/components/Files/FileCell.tsx @@ -24,7 +24,7 @@ export function FileCell({ content }: Props) { <>
- +
{content.manifest.filename} diff --git a/src/components/Files/FileDetails.tsx b/src/components/Files/FileDetails.tsx index 632d4b9..48464bb 100644 --- a/src/components/Files/FileDetails.tsx +++ b/src/components/Files/FileDetails.tsx @@ -13,7 +13,7 @@ import { DownloadIcon, X } from "lucide-react"; import { CodexSdk } from "../../sdk/codex"; import { Files } from "../../utils/files"; import { useEffect, useState } from "react"; -import { PurchaseStorage } from "../../utils/purchases-storage"; +import { WebStorage } from "../../utils/web-storage"; type Props = { details: CodexDataContent | null; @@ -24,11 +24,15 @@ export function FileDetails({ onClose, details }: Props) { const [purchases, setPurchases] = useState(0); useEffect(() => { - PurchaseStorage.entries().then((entries) => - setPurchases( - entries.filter((e) => e[1] === details?.cid).reduce((acc) => acc + 1, 0) - ) - ); + WebStorage.purchases + .entries() + .then((entries) => + setPurchases( + entries + .filter((e) => e[1] === details?.cid) + .reduce((acc) => acc + 1, 0) + ) + ); }, [details?.cid]); const url = CodexSdk.url() + "/api/codex/v1/data/"; @@ -49,7 +53,7 @@ export function FileDetails({ onClose, details }: Props) { {Files.isImage(details.manifest.mimetype) && (
- +
)} diff --git a/src/components/Files/Files.tsx b/src/components/Files/Files.tsx index 18c2d01..b419a75 100644 --- a/src/components/Files/Files.tsx +++ b/src/components/Files/Files.tsx @@ -149,14 +149,18 @@ export function Files() { } setSortFn( - () => (a: CodexDataContent, b: CodexDataContent) => - state === "desc" - ? b.manifest.filename - .toLocaleLowerCase() - .localeCompare(a.manifest.filename.toLocaleLowerCase()) - : a.manifest.filename - .toLocaleLowerCase() - .localeCompare(b.manifest.filename.toLocaleLowerCase()) + () => + ( + { manifest: { filename: afilename } }: CodexDataContent, + { manifest: { filename: bfilename } }: CodexDataContent + ) => + state === "desc" + ? (bfilename || "") + .toLocaleLowerCase() + .localeCompare((afilename || "").toLocaleLowerCase()) + : (afilename || "") + .toLocaleLowerCase() + .localeCompare((bfilename || "").toLocaleLowerCase()) ); }; diff --git a/src/components/HealthChecks/HealthChecks.tsx b/src/components/HealthChecks/HealthChecks.tsx index 4c6813a..cfa6684 100644 --- a/src/components/HealthChecks/HealthChecks.tsx +++ b/src/components/HealthChecks/HealthChecks.tsx @@ -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) => { 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/StorageRequestSetup/StorageRequestFileChooser.tsx b/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx index e5f7321..1ab77df 100644 --- a/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx +++ b/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx @@ -48,8 +48,8 @@ export function StorageRequestFileChooser({ const options = files.map((f) => { return { - Icon: () => , - title: f.manifest.filename, + Icon: () => , + title: f.manifest.filename || "", subtitle: f.cid, }; }) || []; 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 ( void; @@ -14,14 +14,14 @@ type Props = { export function UserInfo({ onNameChange }: Props) { const [displayName, setDisplayName] = useState( - OnBoardingUtils.getDisplayName() + WebStorage.onBoarding.getDisplayName() ); - const [emoji, setEmoji] = useState(OnBoardingUtils.getEmoji()); + const [emoji, setEmoji] = useState(WebStorage.onBoarding.getEmoji()); const [areEmojiVisible, setAreEmojiVisible] = useState(false); const onDisplayNameChange = (e: ChangeEvent) => { const value = e.currentTarget.value; - OnBoardingUtils.setDisplayName(value); + WebStorage.onBoarding.setDisplayName(value); setDisplayName(value); onNameChange?.(value); }; @@ -30,7 +30,7 @@ export function UserInfo({ onNameChange }: Props) { const onEmojiClick = (emojiData: EmojiClickData) => { setEmoji(emojiData.emoji); - OnBoardingUtils.setEmoji(emojiData.emoji); + WebStorage.onBoarding.setEmoji(emojiData.emoji); setAreEmojiVisible(false); }; @@ -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 } /> 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/proxy.ts b/src/proxy.ts index 0340507..4a0739b 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,127 +1,77 @@ import { CodexCreateStorageRequestInput, CodexData, - CodexDataResponse, CodexMarketplace, SafeValue, - UploadResponse, } from "@codex-storage/sdk-js"; import { CodexSdk as Sdk } from "./sdk/codex"; -import { FilesStorage } from "./utils/file-storage"; -import { PurchaseStorage } from "./utils/purchases-storage"; +import { PortForwardingUtil as PUtil } from "./hooks/port-forwarding.util"; +import { WebStorage } from "./utils/web-storage"; class CodexDataMock extends CodexData { - override upload( - file: File, - onProgress?: (loaded: number, total: number) => void - ): UploadResponse { - // const url = CodexSdk.url() + "/api/codex/v1/data"; + // override upload( + // file: File, + // onProgress?: (loaded: number, total: number) => void + // ): UploadResponse { + // // const url = CodexSdk.url() + "/api/codex/v1/data"; - // const xhr = new XMLHttpRequest(); + // // const xhr = new XMLHttpRequest(); - // const promise = new Promise>((resolve) => { - // xhr.upload.onprogress = (evt) => { - // if (evt.lengthComputable) { - // onProgress?.(evt.loaded, evt.total); - // } - // }; + // // const promise = new Promise>((resolve) => { + // // xhr.upload.onprogress = (evt) => { + // // if (evt.lengthComputable) { + // // onProgress?.(evt.loaded, evt.total); + // // } + // // }; - // xhr.open("POST", url, true); - // xhr.setRequestHeader("Content-Disposition", "attachment; filename=\"" + file.name + "\"") - // xhr.send(file); + // // xhr.open("POST", url, true); + // // xhr.setRequestHeader("Content-Disposition", "attachment; filename=\"" + file.name + "\"") + // // xhr.send(file); - // xhr.onload = function () { - // if (xhr.status != 200) { - // resolve({ - // error: true, - // data: new CodexError(xhr.responseText, { - // code: xhr.status, - // }), - // }); - // } else { - // resolve({ error: false, data: xhr.response }); - // } - // }; + // // xhr.onload = function () { + // // if (xhr.status != 200) { + // // resolve({ + // // error: true, + // // data: new CodexError(xhr.responseText, { + // // code: xhr.status, + // // }), + // // }); + // // } else { + // // resolve({ error: false, data: xhr.response }); + // // } + // // }; - // xhr.onerror = function () { - // resolve({ - // error: true, - // data: new CodexError("Something went wrong during the file upload."), - // }); - // }; - // }); + // // xhr.onerror = function () { + // // resolve({ + // // error: true, + // // data: new CodexError("Something went wrong during the file upload."), + // // }); + // // }; + // // }); - // return { - // result: promise, - // abort: () => { - // xhr.abort(); - // }, - // }; - const { result, abort } = super.upload(file, onProgress); + // // return { + // // result: promise, + // // abort: () => { + // // xhr.abort(); + // // }, + // // }; + // const { result, abort } = super.upload(file, onProgress); - return { - abort, - result: result.then((safe) => { - if (!safe.error) { - return FilesStorage.set(safe.data, { - mimetype: file.type, - name: file.name, - uploadedAt: new Date().toJSON(), - }).then(() => safe); - } + // return { + // abort, + // result: result.then((safe) => { + // if (!safe.error) { + // return WebStorage.files.set(safe.data, { + // mimetype: file.type, + // name: file.name, + // uploadedAt: new Date().toJSON(), + // }).then(() => safe); + // } - return safe; - }), - }; - } - - override async cids(): Promise> { - const res = await super.cids(); - - if (res.error) { - return res; - } - - const metadata = await FilesStorage.list(); - - const content = res.data.content.map((content, index) => { - if (content.manifest.filename) { - return content; - } - - const value = metadata.find(([cid]) => content.cid === cid); - - if (!value) { - return { - cid: content.cid, - manifest: { - ...content.manifest, - mimetype: "N/A", - uploadedAt: new Date(0, 0, 0, 0, 0, 0).toJSON(), - filename: "N/A" + index, - }, - }; - } - - const { - mimetype = "", - name = "", - uploadedAt = new Date(0, 0, 0, 0, 0, 0).toJSON(), - } = value[1]; - - return { - cid: content.cid, - manifest: { - ...content.manifest, - mimetype, - filename: name, - uploadedAt: uploadedAt, - }, - }; - }); - - return { error: false, data: { content } }; - } + // return safe; + // }), + // }; + // } } @@ -156,7 +106,7 @@ class CodexMarketplaceMock extends CodexMarketplace { return res } - await PurchaseStorage.set("0x" + res.data, input.cid) + await WebStorage.purchases.set("0x" + res.data, input.cid) // await PurchaseDatesStorage.set(res.data, new Date().toJSON()) @@ -224,3 +174,15 @@ export const CodexSdk = { marketplace: () => new CodexMarketplaceMock(CodexSdk.url()), data: () => new CodexDataMock(CodexSdk.url()), }; + + +export const PortForwardingUtil = { + ...PUtil, + check: (port: number) => { + if (import.meta.env.CI) { + return Promise.resolve({ reachable: true }) + } + + return PUtil.check(port) + } +} \ No newline at end of file diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index dedbeb0..d5991c6 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -8,8 +8,6 @@ // This file is auto-generated by TanStack Router -import { createFileRoute } from '@tanstack/react-router' - // Import Routes import { Route as rootRoute } from './routes/__root' @@ -22,6 +20,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' @@ -29,16 +28,10 @@ import { Route as DashboardFilesImport } from './routes/dashboard/files' import { Route as DashboardFavoritesImport } from './routes/dashboard/favorites' import { Route as DashboardDisclaimerImport } from './routes/dashboard/disclaimer' import { Route as DashboardDeviceImport } from './routes/dashboard/device' +import { Route as DashboardAvailabilitiesImport } from './routes/dashboard/availabilities' import { Route as DashboardAnalyticsImport } from './routes/dashboard/analytics' import { Route as DashboardAboutImport } from './routes/dashboard/about' -// Create Virtual Routes - -const DashboardPeersLazyImport = createFileRoute('/dashboard/peers')() -const DashboardAvailabilitiesLazyImport = createFileRoute( - '/dashboard/availabilities', -)() - // Create/Update Routes const OnboardingNameRoute = OnboardingNameImport.update({ @@ -71,23 +64,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', - path: '/availabilities', - getParentRoute: () => DashboardRoute, - } as any).lazy(() => - import('./routes/dashboard/availabilities.lazy').then((d) => d.Route), - ) - const DashboardWalletRoute = DashboardWalletImport.update({ id: '/wallet', path: '/wallet', @@ -112,6 +88,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', @@ -154,6 +136,12 @@ const DashboardDeviceRoute = DashboardDeviceImport.update({ getParentRoute: () => DashboardRoute, } as any) +const DashboardAvailabilitiesRoute = DashboardAvailabilitiesImport.update({ + id: '/availabilities', + path: '/availabilities', + getParentRoute: () => DashboardRoute, +} as any) + const DashboardAnalyticsRoute = DashboardAnalyticsImport.update({ id: '/analytics', path: '/analytics', @@ -212,6 +200,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DashboardAnalyticsImport parentRoute: typeof DashboardImport } + '/dashboard/availabilities': { + id: '/dashboard/availabilities' + path: '/availabilities' + fullPath: '/dashboard/availabilities' + preLoaderRoute: typeof DashboardAvailabilitiesImport + parentRoute: typeof DashboardImport + } '/dashboard/device': { id: '/dashboard/device' path: '/device' @@ -261,6 +256,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' @@ -289,20 +291,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DashboardWalletImport parentRoute: typeof DashboardImport } - '/dashboard/availabilities': { - id: '/dashboard/availabilities' - path: '/availabilities' - fullPath: '/dashboard/availabilities' - 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: '/' @@ -318,6 +306,7 @@ declare module '@tanstack/react-router' { interface DashboardRouteChildren { DashboardAboutRoute: typeof DashboardAboutRoute DashboardAnalyticsRoute: typeof DashboardAnalyticsRoute + DashboardAvailabilitiesRoute: typeof DashboardAvailabilitiesRoute DashboardDeviceRoute: typeof DashboardDeviceRoute DashboardDisclaimerRoute: typeof DashboardDisclaimerRoute DashboardFavoritesRoute: typeof DashboardFavoritesRoute @@ -325,18 +314,18 @@ 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 } const DashboardRouteChildren: DashboardRouteChildren = { DashboardAboutRoute: DashboardAboutRoute, DashboardAnalyticsRoute: DashboardAnalyticsRoute, + DashboardAvailabilitiesRoute: DashboardAvailabilitiesRoute, DashboardDeviceRoute: DashboardDeviceRoute, DashboardDisclaimerRoute: DashboardDisclaimerRoute, DashboardFavoritesRoute: DashboardFavoritesRoute, @@ -344,12 +333,11 @@ 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, } @@ -364,6 +352,7 @@ export interface FileRoutesByFullPath { '/onboarding-name': typeof OnboardingNameRoute '/dashboard/about': typeof DashboardAboutRoute '/dashboard/analytics': typeof DashboardAnalyticsRoute + '/dashboard/availabilities': typeof DashboardAvailabilitiesRoute '/dashboard/device': typeof DashboardDeviceRoute '/dashboard/disclaimer': typeof DashboardDisclaimerRoute '/dashboard/favorites': typeof DashboardFavoritesRoute @@ -371,12 +360,11 @@ 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 } @@ -386,6 +374,7 @@ export interface FileRoutesByTo { '/onboarding-name': typeof OnboardingNameRoute '/dashboard/about': typeof DashboardAboutRoute '/dashboard/analytics': typeof DashboardAnalyticsRoute + '/dashboard/availabilities': typeof DashboardAvailabilitiesRoute '/dashboard/device': typeof DashboardDeviceRoute '/dashboard/disclaimer': typeof DashboardDisclaimerRoute '/dashboard/favorites': typeof DashboardFavoritesRoute @@ -393,12 +382,11 @@ 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 } @@ -410,6 +398,7 @@ export interface FileRoutesById { '/onboarding-name': typeof OnboardingNameRoute '/dashboard/about': typeof DashboardAboutRoute '/dashboard/analytics': typeof DashboardAnalyticsRoute + '/dashboard/availabilities': typeof DashboardAvailabilitiesRoute '/dashboard/device': typeof DashboardDeviceRoute '/dashboard/disclaimer': typeof DashboardDisclaimerRoute '/dashboard/favorites': typeof DashboardFavoritesRoute @@ -417,12 +406,11 @@ 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 } @@ -435,6 +423,7 @@ export interface FileRouteTypes { | '/onboarding-name' | '/dashboard/about' | '/dashboard/analytics' + | '/dashboard/availabilities' | '/dashboard/device' | '/dashboard/disclaimer' | '/dashboard/favorites' @@ -442,12 +431,11 @@ 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: @@ -456,6 +444,7 @@ export interface FileRouteTypes { | '/onboarding-name' | '/dashboard/about' | '/dashboard/analytics' + | '/dashboard/availabilities' | '/dashboard/device' | '/dashboard/disclaimer' | '/dashboard/favorites' @@ -463,12 +452,11 @@ 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__' @@ -478,6 +466,7 @@ export interface FileRouteTypes { | '/onboarding-name' | '/dashboard/about' | '/dashboard/analytics' + | '/dashboard/availabilities' | '/dashboard/device' | '/dashboard/disclaimer' | '/dashboard/favorites' @@ -485,12 +474,11 @@ 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 } @@ -535,6 +523,7 @@ export const routeTree = rootRoute "children": [ "/dashboard/about", "/dashboard/analytics", + "/dashboard/availabilities", "/dashboard/device", "/dashboard/disclaimer", "/dashboard/favorites", @@ -542,12 +531,11 @@ 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/" ] }, @@ -565,6 +553,10 @@ export const routeTree = rootRoute "filePath": "dashboard/analytics.tsx", "parent": "/dashboard" }, + "/dashboard/availabilities": { + "filePath": "dashboard/availabilities.tsx", + "parent": "/dashboard" + }, "/dashboard/device": { "filePath": "dashboard/device.tsx", "parent": "/dashboard" @@ -593,6 +585,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" @@ -609,14 +605,6 @@ export const routeTree = rootRoute "filePath": "dashboard/wallet.tsx", "parent": "/dashboard" }, - "/dashboard/availabilities": { - "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/about.tsx b/src/routes/dashboard/about.tsx index ca4faa3..b18c86d 100644 --- a/src/routes/dashboard/about.tsx +++ b/src/routes/dashboard/about.tsx @@ -33,7 +33,7 @@ const About = () => {
- +
{c.manifest.filename} diff --git a/src/routes/dashboard/availabilities.lazy.tsx b/src/routes/dashboard/availabilities.tsx similarity index 95% rename from src/routes/dashboard/availabilities.lazy.tsx rename to src/routes/dashboard/availabilities.tsx index 9fc66cb..83b87e4 100644 --- a/src/routes/dashboard/availabilities.lazy.tsx +++ b/src/routes/dashboard/availabilities.tsx @@ -1,4 +1,4 @@ -import { createLazyFileRoute } from '@tanstack/react-router' +import { createFileRoute } from '@tanstack/react-router' import { ErrorBoundary } from '@sentry/react' import { ErrorPlaceholder } from '../../components/ErrorPlaceholder/ErrorPlaceholder' import { @@ -16,8 +16,8 @@ import { PrettyBytes } from '../../utils/bytes' import { AvailabilitySunburst } from '../../components/Availability/AvailabilitySunburst' import { Errors } from '../../utils/errors' import { availabilityColors } from '../../components/Availability/availability.colors' -import { AvailabilityStorage } from '../../utils/availabilities-storage' import { AvailabilityWithSlots } from '../../components/Availability/types' +import { WebStorage } from '../../utils/web-storage' const defaultSpace = { quotaMaxBytes: 0, @@ -51,7 +51,7 @@ export function Availabilities() { return { ...a, slots: res.data } }) .then((data) => - AvailabilityStorage.get(data.id).then((n) => ({ + WebStorage.availabilities.get(data.id).then((n) => ({ ...data, name: n || '', })), @@ -160,7 +160,7 @@ export function Availabilities() { } } -export const Route = createLazyFileRoute('/dashboard/availabilities')({ +export const Route = createFileRoute('/dashboard/availabilities')({ component: () => ( ( diff --git a/src/routes/dashboard/index.tsx b/src/routes/dashboard/index.tsx index f2acc09..944b966 100644 --- a/src/routes/dashboard/index.tsx +++ b/src/routes/dashboard/index.tsx @@ -8,9 +8,9 @@ import { ErrorBoundary } from "@sentry/react"; import { useQueryClient } from "@tanstack/react-query"; import { Download } from "../../components/Download/Download.tsx"; import { ManifestFetch } from "../../components/ManifestFetch/ManifestFetch.tsx"; -import { OnBoardingUtils } from "../../utils/onboarding.ts"; import "./index.css"; import { Versions } from "../../components/Versions/Versions.tsx"; +import { WebStorage } from "../../utils/web-storage.ts"; export const Route = createFileRoute("/dashboard/")({ component: Dashboard, @@ -23,9 +23,9 @@ function Dashboard() { queryClient.invalidateQueries({ queryKey: ["cids"] }); }; - const username = OnBoardingUtils.getDisplayName(); + const username = WebStorage.onBoarding.getDisplayName(); - const emoji = OnBoardingUtils.getEmoji(); + const emoji = WebStorage.onBoarding.getEmoji(); return ( <> 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/routes/dashboard/purchases.tsx b/src/routes/dashboard/purchases.tsx index 04eb9eb..08d6056 100644 --- a/src/routes/dashboard/purchases.tsx +++ b/src/routes/dashboard/purchases.tsx @@ -16,8 +16,10 @@ import { TruncateCell } from "../../components/TruncateCell/TruncateCell"; import { Times } from "../../utils/times"; import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder"; import { ErrorBoundary } from "@sentry/react"; +import { useData } from "../../hooks/useData"; const Purchases = () => { + const content = useData(); const { data, isPending } = useQuery({ queryFn: () => CodexSdk.marketplace() @@ -74,6 +76,7 @@ const Purchases = () => { requestId={r.id} purchaseCid={r.content.cid} index={index} + data={content} />, , {Times.pretty(duration)}, diff --git a/src/routes/onboarding-checks.tsx b/src/routes/onboarding-checks.tsx index 43d36f6..4148d68 100644 --- a/src/routes/onboarding-checks.tsx +++ b/src/routes/onboarding-checks.tsx @@ -1,16 +1,16 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { useState } from "react"; import { AlphaIcon } from "../components/OnBoarding/AlphaIcon"; -import { OnBoardingUtils } from "../utils/onboarding"; import { attributes } from "../utils/attributes"; import { ArrowRightCircle } from "../components/ArrowRightCircle/ArrowRightCircle"; import { OnBoardingLayout } from "../components/OnBoarding/OnBoardingLayout"; import { HealthChecks } from "../components/HealthChecks/HealthChecks"; import { useNetwork } from "../network/useNetwork"; +import { WebStorage } from "../utils/web-storage"; const OnBoardingChecks = () => { const online = useNetwork(); - const displayName = OnBoardingUtils.getDisplayName(); + const displayName = WebStorage.onBoarding.getDisplayName(); const [isStepValid, setIsStepValid] = useState(false); const navigate = useNavigate({ from: "/onboarding-checks" }); diff --git a/src/routes/onboarding-name.tsx b/src/routes/onboarding-name.tsx index 5ba1597..16a92ca 100644 --- a/src/routes/onboarding-name.tsx +++ b/src/routes/onboarding-name.tsx @@ -2,14 +2,14 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { useState } from "react"; import { AlphaIcon } from "../components/OnBoarding/AlphaIcon"; import { OnBoardingLayout } from "../components/OnBoarding/OnBoardingLayout"; -import { OnBoardingUtils } from "../utils/onboarding"; import { attributes } from "../utils/attributes"; import { ArrowRightCircle } from "../components/ArrowRightCircle/ArrowRightCircle"; import { UserInfo } from "../components/UserInfo/UserInfo"; +import { WebStorage } from "../utils/web-storage"; const OnBoardingName = () => { const [isStepValid, setIsStepValid] = useState( - !!OnBoardingUtils.getDisplayName() + !!WebStorage.onBoarding.getDisplayName() ); const navigate = useNavigate({ from: "/onboarding-name" }); diff --git a/src/utils/availabilities-storage.ts b/src/utils/availabilities-storage.ts deleted file mode 100644 index f28bd02..0000000 --- a/src/utils/availabilities-storage.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createStore, del, get, set } from "idb-keyval"; - -const store = createStore("availabilities", "availabilities"); - -export const AvailabilityStorage = { - get(key: string) { - return get(key, store); - }, - - delete(key: string) { - return del(key, store); - }, - - async add(key: string, value: string) { - return set(key, value, store); - }, -}; diff --git a/src/utils/dates.ts b/src/utils/dates.ts index 434c910..d34def9 100644 --- a/src/utils/dates.ts +++ b/src/utils/dates.ts @@ -1,12 +1,12 @@ export const Dates = { - format(date: string | Date) { + format(date: number) { if (!date) { - return "N/A"; + return "-"; } return new Intl.DateTimeFormat("en-GB", { dateStyle: "medium", timeStyle: "short", - }).format(new Date(date)); + }).format(new Date(date * 1000)); }, }; 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/favorite-storage.tsx b/src/utils/favorite-storage.tsx deleted file mode 100644 index a589b03..0000000 --- a/src/utils/favorite-storage.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createStore, del, keys, set } from "idb-keyval"; - -const store = createStore("favorites", "favorites"); - -export const FavoriteStorage = { - list() { - return keys(store); - }, - - delete(key: string) { - return del(key, store); - }, - - async add(key: string) { - return set(key, "1", store); - }, -}; diff --git a/src/utils/file-storage.ts b/src/utils/file-storage.ts deleted file mode 100644 index 7e9088f..0000000 --- a/src/utils/file-storage.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createStore, entries, get, set } from "idb-keyval"; - -const store = createStore("files", "files"); - -export type FileMetadata = { - mimetype: string; - uploadedAt: string; - name: string; -}; - -export const FilesStorage = { - list() { - return entries(store); - }, - - async get(cid: string) { - return get(cid, store); - }, - - async set(cid: string, metadata: FileMetadata) { - return set(cid, metadata, store); - }, -}; diff --git a/src/utils/files.ts b/src/utils/files.ts index d2bf24c..fdd43a4 100644 --- a/src/utils/files.ts +++ b/src/utils/files.ts @@ -10,15 +10,15 @@ const archiveMimetypes = [ ]; export const Files = { - isImage(type: string) { - return type.startsWith("image"); + isImage(type: string | null) { + return type && type.startsWith("image"); }, - type(mimetype: string) { - const [type] = mimetype.split("/") + type(mimetype: string | null) { + const [type] = mimetype?.split("/") || [] return type }, - isArchive(mimetype: string) { - return archiveMimetypes.includes(mimetype) + isArchive(mimetype: string | null) { + return mimetype && archiveMimetypes.includes(mimetype) } }; 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/onboarding.ts b/src/utils/onboarding.ts deleted file mode 100644 index 9a8d602..0000000 --- a/src/utils/onboarding.ts +++ /dev/null @@ -1,26 +0,0 @@ - -export const OnBoardingUtils = { - getStep() { - return parseInt(localStorage.getItem("onboarding-step") || "0", 10) - }, - - setStep(step: number) { - localStorage.setItem("onboarding-step", step.toString()) - }, - - setDisplayName(displayName: string) { - localStorage.setItem("display-name", displayName) - }, - - getDisplayName() { - return localStorage.getItem("display-name") || "" - }, - - setEmoji(emoji: string) { - localStorage.setItem("emoji", emoji) - }, - - getEmoji() { - return localStorage.getItem("emoji") || "🤖" - }, -} \ No newline at end of file diff --git a/src/utils/purchases-storage.ts b/src/utils/purchases-storage.ts deleted file mode 100644 index 3aafea1..0000000 --- a/src/utils/purchases-storage.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createStore, entries, get, set } from "idb-keyval"; - -const store = createStore("purchases", "purchases"); -const storeDates = createStore("purchases", "dates"); - -export const PurchaseStorage = { - async get(key: string) { - return get(key, store); - }, - - async set(key: string, cid: string) { - return set(key, cid, store); - }, - - async entries() { - return entries(store); - }, -}; - -export const PurchaseDatesStorage = { - async get(key: string) { - return get(key, storeDates); - }, - - async set(key: string, date: string) { - return set(key, date, storeDates); - }, -}; 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/src/utils/web-storage.ts b/src/utils/web-storage.ts index 56927cb..bdcbe2d 100644 --- a/src/utils/web-storage.ts +++ b/src/utils/web-storage.ts @@ -1,6 +1,5 @@ import { createStore, del, entries, get, set } from "idb-keyval"; -const folders = createStore("folders", "folders"); export const WebStorage = { set(key: string, value: unknown) { @@ -15,13 +14,41 @@ export const WebStorage = { return del(key); }, + onBoarding: { + getStep() { + return parseInt(localStorage.getItem("onboarding-step") || "0", 10) + }, + + setStep(step: number) { + localStorage.setItem("onboarding-step", step.toString()) + }, + + setDisplayName(displayName: string) { + localStorage.setItem("display-name", displayName) + }, + + getDisplayName() { + return localStorage.getItem("display-name") || "" + }, + + setEmoji(emoji: string) { + localStorage.setItem("emoji", emoji) + }, + + getEmoji() { + return localStorage.getItem("emoji") || "🤖" + }, + }, + folders: { + store: createStore("folders", "folders"), + create(folder: string) { - return set(folder, [], folders); + return set(folder, [], this.store); }, async list(): Promise<[string, string[]][]> { - const items = await entries(folders) || [] + const items = await entries(this.store) || [] if (items.length == 0) { return [["Favorites", []]] @@ -33,25 +60,66 @@ export const WebStorage = { return items - }, - delete(key: string) { - return del(key, folders); + return del(key, this.store); }, - - async addFile(folder: string, cid: string) { - const files = await get(folder, folders) || [] + const files = await get(folder, this.store) || [] - return set(folder, [...files, cid], folders) + return set(folder, [...files, cid], this.store) }, async deleteFile(folder: string, cid: string) { - const files = await get(folder, folders) || [] + const files = await get(folder, this.store) || [] - return set(folder, files.filter(item => item !== cid), folders) + return set(folder, files.filter(item => item !== cid), this.store) }, + }, + + availabilities: { + store: createStore("availabilities", "availabilities"), + + get(key: string) { + return get(key, this.store); + }, + + delete(key: string) { + return del(key, this.store); + }, + + async add(key: string, value: string) { + return set(key, value, this.store); + }, + }, + + purchases: { + store: createStore("purchases", "purchases"), + + + async get(key: string) { + return get(key, this.store); + }, + + async set(key: string, cid: string) { + return set(key, cid, this.store); + }, + + async entries() { + return entries(this.store); + }, + + dates: { + store: createStore("purchases", "dates"), + + async get(key: string) { + return get(key, this.store); + }, + + async set(key: string, date: string) { + return set(key, date, this.store); + }, + } } }; diff --git a/vite.config.ts b/vite.config.ts index 441b96c..c930fa3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -15,7 +15,9 @@ 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"], + "echarts": ["echarts"], } }, onwarn(warning, defaultHandler) { @@ -32,7 +34,8 @@ export default defineConfig({ alias: { "../sdk/codex": "../mock", "../../sdk/codex": "../../mock", - "../utils/echo": "../mock" + "../utils/echo": "../mock", + "./port-forwarding.util": "../proxy", }, }, }); 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"] + }, +})