mirror of
https://github.com/logos-storage/logos-storage-marketplace-ui.git
synced 2026-08-07 10:53:23 +00:00
Merge branch 'feat/ui/integration' into mock
This commit is contained in:
commit
ad35caee9c
10
.github/workflows/playwright.yml
vendored
10
.github/workflows/playwright.yml
vendored
@ -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
|
||||
|
||||
|
||||
39
.github/workflows/unit-testing.yml
vendored
Normal file
39
.github/workflows/unit-testing.yml
vendored
Normal file
@ -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
|
||||
@ -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();
|
||||
})
|
||||
|
||||
@ -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()
|
||||
});
|
||||
// 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()
|
||||
// });
|
||||
@ -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()
|
||||
});
|
||||
});
|
||||
|
||||
// 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');
|
||||
@ -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()
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
|
||||
@ -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();
|
||||
// });
|
||||
386
package-lock.json
generated
386
package-lock.json
generated
@ -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",
|
||||
|
||||
10
package.json
10
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"
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -59,7 +59,7 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cardNumber-dataContainer .buttonIcon {
|
||||
.cardNumber-dataContainer .button-icon {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export function ErrorCircleIcon() {
|
||||
return (
|
||||
<svg
|
||||
data-testid="icon-error"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
|
||||
@ -5,43 +5,57 @@ import {
|
||||
WebFileIcon,
|
||||
} from "@codex-storage/marketplace-ui-components";
|
||||
import "./FileCell.css";
|
||||
import { FileMetadata, FilesStorage } from "../../utils/file-storage";
|
||||
import { PurchaseStorage } from "../../utils/purchases-storage";
|
||||
import { WebStorage } from "../../utils/web-storage";
|
||||
import { CodexDataContent } from "@codex-storage/sdk-js";
|
||||
|
||||
type FileMetadata = {
|
||||
mimetype: string | null;
|
||||
uploadedAt: number;
|
||||
filename: string | null;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
requestId: string;
|
||||
purchaseCid: string;
|
||||
index: number;
|
||||
data: CodexDataContent[];
|
||||
};
|
||||
|
||||
export function FileCell({ requestId, purchaseCid }: Props) {
|
||||
export function FileCell({ requestId, purchaseCid, data }: Props) {
|
||||
const [cid, setCid] = useState(purchaseCid);
|
||||
const [metadata, setMetadata] = useState<FileMetadata>({
|
||||
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<FileMetadata>(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 (
|
||||
<Cell>
|
||||
<div className="fileCell">
|
||||
<WebFileIcon type={metadata.mimetype} />
|
||||
<WebFileIcon type={metadata.mimetype || "-"} />
|
||||
<div>
|
||||
<span className="fileCell-title">
|
||||
<Tooltip message={metadata.name}>{name}</Tooltip>
|
||||
<Tooltip message={filename}>{filename}</Tooltip>
|
||||
</span>
|
||||
<span className="fileCell-subtitle">
|
||||
<Tooltip message={cid}>
|
||||
|
||||
@ -24,7 +24,7 @@ export function FileCell({ content }: Props) {
|
||||
<>
|
||||
<Cell>
|
||||
<div className="files-cell-file">
|
||||
<WebFileIcon type={content.manifest.mimetype} />
|
||||
<WebFileIcon type={content.manifest.mimetype || ""} />
|
||||
|
||||
<div>
|
||||
<b>{content.manifest.filename}</b>
|
||||
|
||||
@ -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) && (
|
||||
<div className="fileDetails-imageContainer">
|
||||
<img className="fileDetails-image" src={"/chatgpt.jpg"} />
|
||||
<img className="fileDetails-image" src={url + details.cid} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -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())
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -3,16 +3,17 @@ import { useEffect, useState } from "react";
|
||||
import { useDebug } from "../../hooks/useDebug";
|
||||
import { usePersistence } from "../../hooks/usePersistence";
|
||||
import { usePortForwarding } from "../../hooks/usePortForwarding";
|
||||
import { CodexSdk } from "../../proxy";
|
||||
import { ErrorCircleIcon } from "../ErrorCircleIcon/ErrorCircleIcon";
|
||||
import { SuccessCheckIcon } from "../SuccessCheckIcon/SuccessCheckIcon";
|
||||
import { WarningIcon } from "../WarningIcon/WarningIcon";
|
||||
import { HealthCheckIcon } from "./HealthCheckIcon";
|
||||
import { Input } from "@codex-storage/marketplace-ui-components";
|
||||
import { classnames } from "../../utils/classnames";
|
||||
import { DebugUtils } from "../../utils/debug";
|
||||
import { RefreshIcon } from "../RefreshIcon/RefreshIcon";
|
||||
import "./HealthChecks.css";
|
||||
import { CodexSdk } from "../../sdk/codex";
|
||||
import { HealthCheckUtil } from "./health-check.util";
|
||||
import { PortForwardingUtil } from "../../hooks/port-forwarding.util";
|
||||
|
||||
type Props = {
|
||||
online: boolean;
|
||||
@ -29,45 +30,40 @@ export function HealthChecks({ online, onStepValid }: Props) {
|
||||
const [isAddressInvalid, setIsAddressInvalid] = useState(false);
|
||||
const [isPortInvalid, setIsPortInvalid] = useState(false);
|
||||
const [address, setAddress] = useState(
|
||||
CodexSdk.url().split(":")[0] + ":" + CodexSdk.url().split(":")[1]
|
||||
);
|
||||
const [port, setPort] = useState(
|
||||
parseInt(CodexSdk.url().split(":")[2] || "80", 10)
|
||||
HealthCheckUtil.removePort(CodexSdk.url())
|
||||
);
|
||||
const [port, setPort] = useState(HealthCheckUtil.getPort(CodexSdk.url()));
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (codex.isSuccess) {
|
||||
persistence.refetch();
|
||||
portForwarding.refetch().then(({ data }) => {
|
||||
onStepValid(data?.reachable || false);
|
||||
});
|
||||
} else {
|
||||
onStepValid(false);
|
||||
}
|
||||
}, [
|
||||
persistence.refetch,
|
||||
onStepValid,
|
||||
portForwarding.refetch,
|
||||
codex.isSuccess,
|
||||
]);
|
||||
useEffect(
|
||||
() => {
|
||||
if (codex.isSuccess) {
|
||||
persistence.refetch();
|
||||
portForwarding.refetch().then(({ data }) => {
|
||||
onStepValid(data?.reachable || false);
|
||||
});
|
||||
} else {
|
||||
onStepValid(false);
|
||||
}
|
||||
},
|
||||
// We really do not want to add persistence and portForwarding as
|
||||
// dependencies because it will cause a re-render every time.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[persistence.refetch, onStepValid, portForwarding.refetch, codex.isSuccess]
|
||||
);
|
||||
|
||||
const onAddressChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
const element = e.currentTarget;
|
||||
const parts = e.currentTarget.value.split(":");
|
||||
const value = e.currentTarget.value;
|
||||
|
||||
setIsAddressInvalid(!element.checkValidity());
|
||||
|
||||
if (parts.length > 2) {
|
||||
const [protocol, addr, port] = parts;
|
||||
setAddress(protocol + ":" + addr);
|
||||
const address = HealthCheckUtil.removePort(value);
|
||||
setAddress(address);
|
||||
|
||||
const p = parseInt(port, 10);
|
||||
if (!isNaN(p)) {
|
||||
setPort(p);
|
||||
}
|
||||
} else {
|
||||
setAddress(parts.join(":"));
|
||||
if (HealthCheckUtil.containsPort(value)) {
|
||||
const p = HealthCheckUtil.getPort(value);
|
||||
setPort(p);
|
||||
}
|
||||
};
|
||||
|
||||
@ -80,11 +76,13 @@ export function HealthChecks({ online, onStepValid }: Props) {
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
if (isAddressInvalid || isPortInvalid) {
|
||||
const url = address + ":" + port;
|
||||
|
||||
if (HealthCheckUtil.isUrlInvalid(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
CodexSdk.updateURL(address + ":" + port)
|
||||
CodexSdk.updateURL(url)
|
||||
.then(() => queryClient.invalidateQueries())
|
||||
.then(() => codex.refetch());
|
||||
};
|
||||
@ -92,7 +90,7 @@ export function HealthChecks({ online, onStepValid }: Props) {
|
||||
let forwardingPortValue = defaultPort;
|
||||
|
||||
if (codex.isSuccess && codex.data) {
|
||||
const port = DebugUtils.getTcpPort(codex.data);
|
||||
const port = PortForwardingUtil.getTcpPort(codex.data);
|
||||
if (!port.error) {
|
||||
forwardingPortValue = port.data;
|
||||
}
|
||||
@ -135,7 +133,9 @@ export function HealthChecks({ online, onStepValid }: Props) {
|
||||
</div>
|
||||
|
||||
<div className="refresh">
|
||||
<RefreshIcon onClick={onSave}></RefreshIcon>
|
||||
<RefreshIcon
|
||||
onClick={onSave}
|
||||
disabled={isAddressInvalid || isPortInvalid}></RefreshIcon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
148
src/components/HealthChecks/health-check.util.test.ts
Normal file
148
src/components/HealthChecks/health-check.util.test.ts
Normal file
@ -0,0 +1,148 @@
|
||||
import { assert, describe, it } from "vitest";
|
||||
import { HealthCheckUtil } from "./health-check.util";
|
||||
|
||||
describe("health check", () => {
|
||||
it("remove the port from an url", async () => {
|
||||
assert.deepEqual(HealthCheckUtil.removePort("http://localhost:8080"), "http://localhost");
|
||||
});
|
||||
|
||||
it("get the port from an url", async () => {
|
||||
assert.deepEqual(HealthCheckUtil.getPort("http://localhost:8080"), 8080);
|
||||
});
|
||||
|
||||
it("get the default port when the url does not contain the port", async () => {
|
||||
assert.deepEqual(HealthCheckUtil.getPort("http://localhost"), 80);
|
||||
});
|
||||
|
||||
it("returns true when the url contains a port", async () => {
|
||||
assert.deepEqual(HealthCheckUtil.containsPort("http://localhost:8080"), true);
|
||||
});
|
||||
|
||||
it("returns false when the url does not contain a port", async () => {
|
||||
assert.deepEqual(HealthCheckUtil.containsPort("http://localhost"), false);
|
||||
});
|
||||
|
||||
|
||||
it("returns true when the url is invalid", async () => {
|
||||
assert.deepEqual(HealthCheckUtil.isUrlInvalid("http://"), true);
|
||||
});
|
||||
|
||||
it("returns false when the url is valid", async () => {
|
||||
assert.deepEqual(HealthCheckUtil.isUrlInvalid("http://localhost:8080"), false);
|
||||
});
|
||||
|
||||
it("returns the tcp port", async () => {
|
||||
const debug = {
|
||||
"id": "a",
|
||||
"addrs": [
|
||||
"/ip4/127.0.0.1/tcp/8070"
|
||||
],
|
||||
"repo": "",
|
||||
"spr": "",
|
||||
"announceAddresses": [
|
||||
"/ip4/127.0.0.1/tcp/8070"
|
||||
],
|
||||
"table": {
|
||||
"localNode": {
|
||||
"nodeId": "",
|
||||
"peerId": "",
|
||||
"record": "",
|
||||
"address": "0.0.0.0:8090",
|
||||
"seen": false
|
||||
},
|
||||
"nodes": []
|
||||
},
|
||||
"codex": {
|
||||
"version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7",
|
||||
"revision": "2fb7031e"
|
||||
}
|
||||
}
|
||||
assert.deepEqual(HealthCheckUtil.getTcpPort(debug), { error: false, data: 8070 });
|
||||
});
|
||||
|
||||
it("returns an error when the addr is empty", async () => {
|
||||
const debug = {
|
||||
"id": "a",
|
||||
"addrs": [
|
||||
],
|
||||
"repo": "",
|
||||
"spr": "",
|
||||
"announceAddresses": [
|
||||
"/ip4/127.0.0.1/tcp/8070"
|
||||
],
|
||||
"table": {
|
||||
"localNode": {
|
||||
"nodeId": "",
|
||||
"peerId": "",
|
||||
"record": "",
|
||||
"address": "0.0.0.0:8090",
|
||||
"seen": false
|
||||
},
|
||||
"nodes": []
|
||||
},
|
||||
"codex": {
|
||||
"version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7",
|
||||
"revision": "2fb7031e"
|
||||
}
|
||||
}
|
||||
assert.deepEqual(HealthCheckUtil.getTcpPort(debug).error, true);
|
||||
});
|
||||
|
||||
it("returns an error when the addr is misformated", async () => {
|
||||
const debug = {
|
||||
"id": "a",
|
||||
"addrs": [
|
||||
"/ip4/127.0.0.1/tcp/hello"
|
||||
],
|
||||
"repo": "",
|
||||
"spr": "",
|
||||
"announceAddresses": [
|
||||
"/ip4/127.0.0.1/tcp/8070"
|
||||
],
|
||||
"table": {
|
||||
"localNode": {
|
||||
"nodeId": "",
|
||||
"peerId": "",
|
||||
"record": "",
|
||||
"address": "0.0.0.0:8090",
|
||||
"seen": false
|
||||
},
|
||||
"nodes": []
|
||||
},
|
||||
"codex": {
|
||||
"version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7",
|
||||
"revision": "2fb7031e"
|
||||
}
|
||||
}
|
||||
assert.deepEqual(HealthCheckUtil.getTcpPort(debug).error, true);
|
||||
});
|
||||
|
||||
it("returns an error when the port is misformated", async () => {
|
||||
const debug = {
|
||||
"id": "a",
|
||||
"addrs": [
|
||||
"hello"
|
||||
],
|
||||
"repo": "",
|
||||
"spr": "",
|
||||
"announceAddresses": [
|
||||
"/ip4/127.0.0.1/tcp/8070"
|
||||
],
|
||||
"table": {
|
||||
"localNode": {
|
||||
"nodeId": "",
|
||||
"peerId": "",
|
||||
"record": "",
|
||||
"address": "0.0.0.0:8090",
|
||||
"seen": false
|
||||
},
|
||||
"nodes": []
|
||||
},
|
||||
"codex": {
|
||||
"version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7",
|
||||
"revision": "2fb7031e"
|
||||
}
|
||||
}
|
||||
assert.deepEqual(HealthCheckUtil.getTcpPort(debug).error, true);
|
||||
});
|
||||
})
|
||||
51
src/components/HealthChecks/health-check.util.ts
Normal file
51
src/components/HealthChecks/health-check.util.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { CodexDebugInfo, SafeValue, CodexError } from "@codex-storage/sdk-js"
|
||||
|
||||
export const HealthCheckUtil = {
|
||||
removePort(url: string) {
|
||||
const parts = url.split(":")
|
||||
return parts[0] + ":" + parts[1]
|
||||
},
|
||||
|
||||
/*
|
||||
* Extract the port from a protocol + ip + port string
|
||||
*/
|
||||
getPort(url: string) {
|
||||
return parseInt(url.split(":")[2] || "80", 10)
|
||||
},
|
||||
|
||||
containsPort(url: string) {
|
||||
return url.split(":").length > 2
|
||||
},
|
||||
|
||||
isUrlInvalid(url: string) {
|
||||
try {
|
||||
new URL(url)
|
||||
return false
|
||||
// We do not need to manage the error because we want to check
|
||||
// if the URL is valid or not only.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (_) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
|
||||
getTcpPort(info: CodexDebugInfo): SafeValue<number> {
|
||||
if (info.addrs.length === 0) {
|
||||
return { error: true, data: new CodexError("Not existing address") }
|
||||
}
|
||||
|
||||
const parts = info.addrs[0].split("/")
|
||||
|
||||
if (parts.length < 2) {
|
||||
return { error: true, data: new CodexError("Address misformated") }
|
||||
}
|
||||
|
||||
const port = parseInt(parts[parts.length - 1], 10)
|
||||
|
||||
if (isNaN(port)) {
|
||||
return { error: true, data: new CodexError("Port misformated") }
|
||||
}
|
||||
|
||||
return { error: false, data: port }
|
||||
}
|
||||
}
|
||||
@ -149,6 +149,14 @@
|
||||
label {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.health-checks {
|
||||
.address {
|
||||
.refresh {
|
||||
top: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.get-started {
|
||||
|
||||
@ -1,22 +1,14 @@
|
||||
import { Cell } from "@codex-storage/marketplace-ui-components";
|
||||
import { PeerPin } from "./types";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import "./PeerCountryCell.css";
|
||||
import { useEffect } from "react";
|
||||
import { PeerPin, PeerUtils } from "./peers.util";
|
||||
|
||||
export type Props = {
|
||||
address: string;
|
||||
onPinAdd: (pin: PeerPin & { countryIso: string; ip: string }) => void;
|
||||
};
|
||||
|
||||
const getFlagEmoji = (countryCode: string) => {
|
||||
const codePoints = countryCode
|
||||
.toUpperCase()
|
||||
.split("")
|
||||
.map((char) => 127397 + char.charCodeAt(0));
|
||||
return String.fromCodePoint(...codePoints);
|
||||
};
|
||||
|
||||
export function PeerCountryCell({ address, onPinAdd }: Props) {
|
||||
const { data } = useQuery({
|
||||
queryFn: () => {
|
||||
@ -61,7 +53,7 @@ export function PeerCountryCell({ address, onPinAdd }: Props) {
|
||||
<div className="peer-country">
|
||||
{data ? (
|
||||
<>
|
||||
<span> {!!data && getFlagEmoji(data.country_iso)}</span>
|
||||
<span> {!!data && PeerUtils.geCountryEmoji(data.country_iso)}</span>
|
||||
<span>{data?.country}</span>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,78 +1,36 @@
|
||||
import {
|
||||
Cell,
|
||||
Row,
|
||||
Table,
|
||||
TabSortState,
|
||||
Row,
|
||||
Cell,
|
||||
Table,
|
||||
} from "@codex-storage/marketplace-ui-components";
|
||||
import { getMapJSON } from "dotted-map";
|
||||
import DottedMap from "dotted-map/without-countries";
|
||||
import { Promises } from "../../utils/promises";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { PeerCountryCell } from "../../components/Peers/PeerCountryCell";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { PeerPin } from "../../components/Peers/types";
|
||||
import "./peers.css";
|
||||
import { CodexSdk } from "../../sdk/codex";
|
||||
import { ErrorBoundary } from "@sentry/react";
|
||||
import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder";
|
||||
import { PeersIcon } from "../../components/Menu/PeersIcon";
|
||||
import { SuccessCheckIcon } from "../../components/SuccessCheckIcon/SuccessCheckIcon";
|
||||
import { ErrorCircleIcon } from "../../components/ErrorCircleIcon/ErrorCircleIcon";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { Network } from "../../utils/network";
|
||||
import { useRef, useState, useCallback } from "react";
|
||||
import { ErrorCircleIcon } from "../ErrorCircleIcon/ErrorCircleIcon";
|
||||
import { PeersIcon } from "../Menu/PeersIcon";
|
||||
import { PeerCountryCell } from "./PeerCountryCell";
|
||||
import { SuccessCheckIcon } from "../SuccessCheckIcon/SuccessCheckIcon";
|
||||
import { useDebug } from "../../hooks/useDebug";
|
||||
import { getMapJSON } from "dotted-map";
|
||||
import "./Peers.css";
|
||||
import { PeerPin, PeerSortFn, PeerUtils } from "./peers.util";
|
||||
|
||||
// This function accepts the same arguments as DottedMap in the example above.
|
||||
const mapJsonString = getMapJSON({ height: 60, grid: "diagonal" });
|
||||
|
||||
type CustomCSSProperties = React.CSSProperties & {
|
||||
"--codex-peers-percent": number;
|
||||
"--codex-peers-degrees": number;
|
||||
};
|
||||
|
||||
type Node = {
|
||||
nodeId: string;
|
||||
peerId: string;
|
||||
record: string;
|
||||
address: string;
|
||||
seen: boolean;
|
||||
};
|
||||
const throwOnError = true;
|
||||
|
||||
type SortFn = (a: Node, b: Node) => number;
|
||||
|
||||
const sortByBooleanValue = (state: TabSortState) => {
|
||||
return (a: Node, b: Node) => {
|
||||
const order = state === "desc" ? 1 : -1;
|
||||
return a?.seen === b?.seen ? 0 : b?.seen ? order : -order;
|
||||
};
|
||||
};
|
||||
|
||||
const Peers = () => {
|
||||
export const Peers = () => {
|
||||
const ips = useRef<Record<string, string>>({});
|
||||
const [pins, setPins] = useState<[PeerPin, number][]>([]);
|
||||
const [sortFn, setSortFn] = useState<SortFn | null>(() =>
|
||||
sortByBooleanValue("desc")
|
||||
const [sortFn, setSortFn] = useState<PeerSortFn | null>(() =>
|
||||
PeerUtils.sortByBoolean("desc")
|
||||
);
|
||||
const { data } = useQuery({
|
||||
queryFn: () =>
|
||||
CodexSdk.debug()
|
||||
.info()
|
||||
.then((s) => Promises.rejectOnError(s)),
|
||||
queryKey: ["debug"],
|
||||
|
||||
// No need to retry because if the connection to the node
|
||||
// is back again, all the queries will be invalidated.
|
||||
retry: false,
|
||||
|
||||
// The client node should be local, so display the cache value while
|
||||
// making a background request looks good.
|
||||
staleTime: 0,
|
||||
|
||||
// Refreshing when focus returns can be useful if a user comes back
|
||||
// to the UI after performing an operation in the terminal.
|
||||
refetchOnWindowFocus: true,
|
||||
|
||||
// Throw the error to the error boundary
|
||||
throwOnError: true,
|
||||
});
|
||||
const { data } = useDebug(throwOnError);
|
||||
|
||||
const onPinAdd = useCallback(
|
||||
({
|
||||
@ -80,11 +38,7 @@ const Peers = () => {
|
||||
ip,
|
||||
...pin
|
||||
}: PeerPin & { countryIso: string; ip: string }) => {
|
||||
setPins((val) => {
|
||||
const [, quantity = 0] =
|
||||
val.find(([p]) => p.lat === pin.lat && p.lng == pin.lng) || [];
|
||||
return [...val, [pin, quantity + 1]];
|
||||
});
|
||||
setPins((val) => PeerUtils.incPin(val, pin));
|
||||
ips.current[ip] = countryIso;
|
||||
},
|
||||
[]
|
||||
@ -115,24 +69,16 @@ const Peers = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
setSortFn(() => (a: Node, b: Node) => {
|
||||
const countryA = ips.current[Network.getIp(a.address)] || "";
|
||||
const countryB = ips.current[Network.getIp(b.address)] || "";
|
||||
|
||||
return state === "desc"
|
||||
? countryA.localeCompare(countryB)
|
||||
: countryB.localeCompare(countryA);
|
||||
});
|
||||
setSortFn(() => PeerUtils.sortByCountry(state, ips.current));
|
||||
};
|
||||
|
||||
const onSortActive = (state: TabSortState) => {
|
||||
console.info("fdf");
|
||||
if (!state) {
|
||||
setSortFn(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setSortFn(() => sortByBooleanValue(state));
|
||||
setSortFn(() => PeerUtils.sortByBoolean(state));
|
||||
};
|
||||
|
||||
const headers = [
|
||||
@ -165,15 +111,14 @@ const Peers = () => {
|
||||
]}></Row>
|
||||
));
|
||||
|
||||
const actives = sorted.reduce((acc, cur) => acc + (cur.seen ? 1 : 0), 0) || 0;
|
||||
const total = data?.table.nodes.length || 1;
|
||||
const actives = PeerUtils.countActives(sorted);
|
||||
const degrees = PeerUtils.calculareDegrees(sorted);
|
||||
const good = actives > 0;
|
||||
|
||||
const styles: CustomCSSProperties = {
|
||||
"--codex-peers-percent": (actives / total) * 180,
|
||||
"--codex-peers-degrees": degrees,
|
||||
};
|
||||
|
||||
const good = actives > 0;
|
||||
|
||||
return (
|
||||
<div className="peers">
|
||||
<div>
|
||||
@ -217,14 +162,3 @@ const Peers = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createLazyFileRoute("/dashboard/peers")({
|
||||
component: () => (
|
||||
<ErrorBoundary
|
||||
fallback={({ error }) => (
|
||||
<ErrorPlaceholder error={error} subtitle="Cannot retrieve the data." />
|
||||
)}>
|
||||
<Peers />
|
||||
</ErrorBoundary>
|
||||
),
|
||||
});
|
||||
68
src/components/Peers/peers.util.test.ts
Normal file
68
src/components/Peers/peers.util.test.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { assert, describe, it } from "vitest";
|
||||
import { PeerUtils } from "./peers.util";
|
||||
|
||||
describe("peers", () => {
|
||||
it("sorts by boolean", async () => {
|
||||
const a = { nodeId: "a", peerId: "", record: "", address: "", seen: false }
|
||||
const b = { nodeId: "a", peerId: "", record: "", address: "", seen: true }
|
||||
const items = [a, b,]
|
||||
|
||||
const descSorted = items.slice().sort(PeerUtils.sortByBoolean("desc"))
|
||||
|
||||
assert.deepEqual(descSorted, [b, a]);
|
||||
|
||||
const ascSorted = items.slice().sort(PeerUtils.sortByBoolean("asc"))
|
||||
|
||||
assert.deepEqual(ascSorted, [a, b]);
|
||||
});
|
||||
|
||||
it("sorts by country", async () => {
|
||||
const a = { nodeId: "a", peerId: "", record: "", address: "127.0.0.1", seen: false }
|
||||
const b = { nodeId: "a", peerId: "", record: "", address: "127.0.0.2", seen: true }
|
||||
const table = {
|
||||
"127.0.0.1": "US",
|
||||
"127.0.0.2": "France",
|
||||
}
|
||||
const items = [a, b,]
|
||||
|
||||
const descSorted = items.slice().sort(PeerUtils.sortByCountry("desc", table))
|
||||
|
||||
assert.deepEqual(descSorted, [b, a]);
|
||||
|
||||
const ascSorted = items.slice().sort(PeerUtils.sortByCountry("asc", table))
|
||||
|
||||
assert.deepEqual(ascSorted, [a, b]);
|
||||
});
|
||||
|
||||
it("adds a new pin", async () => {
|
||||
const latLng = { lat: 0, lng: 0 }
|
||||
const values = PeerUtils.incPin([], latLng)
|
||||
|
||||
assert.deepEqual(values, [[latLng, 1]]);
|
||||
});
|
||||
|
||||
it("increments an existing pin", async () => {
|
||||
const latLng = { lat: 0, lng: 0 }
|
||||
const values = PeerUtils.incPin([[latLng, 1]], latLng)
|
||||
|
||||
assert.deepEqual(values, [[latLng, 2]]);
|
||||
});
|
||||
|
||||
it("count active peers nodes", async () => {
|
||||
const a = { nodeId: "a", peerId: "", record: "", address: "127.0.0.1", seen: false }
|
||||
const b = { nodeId: "a", peerId: "", record: "", address: "127.0.0.2", seen: true }
|
||||
|
||||
assert.equal(PeerUtils.countActives([a, b]), 1)
|
||||
});
|
||||
|
||||
it("calculates active peers nodes degrees", async () => {
|
||||
const a = { nodeId: "a", peerId: "", record: "", address: "127.0.0.1", seen: false }
|
||||
const b = { nodeId: "a", peerId: "", record: "", address: "127.0.0.2", seen: true }
|
||||
|
||||
assert.equal(PeerUtils.calculareDegrees([a, b]), 90)
|
||||
});
|
||||
|
||||
it("returns the country flag", async () => {
|
||||
assert.equal(PeerUtils.geCountryEmoji("FR"), "🇫🇷")
|
||||
});
|
||||
})
|
||||
64
src/components/Peers/peers.util.ts
Normal file
64
src/components/Peers/peers.util.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { TabSortState } from "@codex-storage/marketplace-ui-components";
|
||||
|
||||
export type PeerNode = {
|
||||
nodeId: string;
|
||||
peerId: string;
|
||||
record: string;
|
||||
address: string;
|
||||
seen: boolean;
|
||||
};
|
||||
|
||||
export type PeerPin = {
|
||||
lat: number;
|
||||
lng: number;
|
||||
};
|
||||
|
||||
export type PeerSortFn = (a: PeerNode, b: PeerNode) => number;
|
||||
|
||||
export const PeerUtils = {
|
||||
sortByBoolean: (state: TabSortState) => (a: PeerNode, b: PeerNode) => {
|
||||
const order = state === "desc" ? 1 : -1;
|
||||
return a?.seen === b?.seen ? 0 : b?.seen ? order : -order;
|
||||
},
|
||||
|
||||
sortByCountry: (state: TabSortState, ipTable: Record<string, string>) =>
|
||||
(a: PeerNode, b: PeerNode) => {
|
||||
const [ipA = ""] = a.address.split(":")
|
||||
const [ipB = ""] = b.address.split(":")
|
||||
const countryA = ipTable[ipA] || "";
|
||||
const countryB = ipTable[ipB] || "";
|
||||
|
||||
return state === "desc"
|
||||
? countryA.localeCompare(countryB)
|
||||
: countryB.localeCompare(countryA);
|
||||
},
|
||||
|
||||
/**
|
||||
* Increments the number of pin for a location
|
||||
*/
|
||||
incPin(val: [PeerPin, number][], pin: PeerPin): [PeerPin, number][] {
|
||||
const [, quantity = 0] =
|
||||
val.find(([p]) => p.lat === pin.lat && p.lng == pin.lng) || [];
|
||||
const rest = val.filter(([p]) => p.lat !== pin.lat || p.lng !== pin.lng)
|
||||
return [...rest, [pin, quantity + 1]];
|
||||
},
|
||||
|
||||
countActives: (peers: PeerNode[]) =>
|
||||
peers.reduce((acc, cur) => acc + (cur.seen ? 1 : 0), 0) || 0,
|
||||
|
||||
calculareDegrees: (peers: PeerNode[]) => {
|
||||
const actives = PeerUtils.countActives(peers);
|
||||
const total = peers.length || 1;
|
||||
|
||||
return (actives / total) * 180
|
||||
},
|
||||
|
||||
geCountryEmoji: (countryCode: string) => {
|
||||
const codePoints = countryCode
|
||||
.toUpperCase()
|
||||
.split("")
|
||||
.map((char) => 127397 + char.charCodeAt(0));
|
||||
return String.fromCodePoint(...codePoints);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
export type PeerPin = {
|
||||
lat: number;
|
||||
lng: number;
|
||||
};
|
||||
@ -1,11 +1,16 @@
|
||||
import { attributes } from "../../utils/attributes";
|
||||
|
||||
type Props = {
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export function RefreshIcon({ className, onClick }: Props) {
|
||||
export function RefreshIcon({ className, onClick, disabled = false }: Props) {
|
||||
const color = disabled ? "#494949" : "var(--codex-color-primary)";
|
||||
return (
|
||||
<svg
|
||||
{...attributes({ "aria-disabled": disabled })}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
width="24"
|
||||
@ -15,7 +20,7 @@ export function RefreshIcon({ className, onClick }: Props) {
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 24C5.3724 24 0 18.6276 0 12C0 5.3724 5.3724 0 12 0C18.6276 0 24 5.3724 24 12C24 18.6276 18.6276 24 12 24ZM17.784 18.0912C19.2325 16.7182 20.1449 14.8744 20.3576 12.8899C20.5703 10.9055 20.0695 8.91012 18.9449 7.26133C17.8203 5.61253 16.1454 4.41803 14.2202 3.89182C12.295 3.36561 10.2453 3.54208 8.4384 4.3896L9.6084 6.4956C10.5215 6.09874 11.519 5.93535 12.511 6.02015C13.503 6.10495 14.4584 6.43528 15.2909 6.98135C16.1234 7.52743 16.8069 8.27209 17.2798 9.14821C17.7528 10.0243 18.0003 11.0044 18 12H14.4L17.784 18.0912ZM15.5616 19.6104L14.3916 17.5044C13.4785 17.9013 12.481 18.0646 11.489 17.9798C10.497 17.8951 9.54165 17.5647 8.70914 17.0186C7.87664 16.4726 7.1931 15.7279 6.72016 14.8518C6.24722 13.9757 5.99973 12.9956 6 12H9.6L6.216 5.9088C4.76747 7.28176 3.85511 9.12563 3.64239 11.1101C3.42966 13.0945 3.93047 15.0899 5.05508 16.7387C6.17969 18.3875 7.85462 19.582 9.77982 20.1082C11.705 20.6344 13.7547 20.4579 15.5616 19.6104V19.6104Z"
|
||||
fill="var(--codex-color-primary)"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@ -48,8 +48,8 @@ export function StorageRequestFileChooser({
|
||||
const options =
|
||||
files.map((f) => {
|
||||
return {
|
||||
Icon: () => <WebFileIcon type={f.manifest.mimetype} size={24} />,
|
||||
title: f.manifest.filename,
|
||||
Icon: () => <WebFileIcon type={f.manifest.mimetype || ""} size={24} />,
|
||||
title: f.manifest.filename || "",
|
||||
subtitle: f.cid,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
@ -7,6 +7,7 @@ export function SuccessCheckIcon({ variant }: Props) {
|
||||
|
||||
return (
|
||||
<svg
|
||||
data-testid="icon-success"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { ChangeEvent, useState } from "react";
|
||||
import "./UserInfo.css";
|
||||
import { OnBoardingUtils } from "../../utils/onboarding";
|
||||
import { Input } from "@codex-storage/marketplace-ui-components";
|
||||
import EmojiPicker, {
|
||||
EmojiClickData,
|
||||
EmojiStyle,
|
||||
Theme,
|
||||
} from "emoji-picker-react";
|
||||
import { WebStorage } from "../../utils/web-storage";
|
||||
|
||||
type Props = {
|
||||
onNameChange?: (value: string) => 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<HTMLInputElement>) => {
|
||||
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
|
||||
}
|
||||
/>
|
||||
|
||||
@ -2,14 +2,14 @@ import { useDebug } from "../../hooks/useDebug";
|
||||
import { AlphaText } from "../AlphaText/AlphaText";
|
||||
import { AlphaIcon } from "../OnBoarding/AlphaIcon";
|
||||
import "./Versions.css";
|
||||
import { VersionsUtil } from "./versions.util";
|
||||
|
||||
const throwOnError = true;
|
||||
const throwOnError = false;
|
||||
|
||||
export function Versions() {
|
||||
const debug = useDebug(throwOnError);
|
||||
|
||||
const parts = debug.data?.codex.version.split("\n") || [""];
|
||||
const version = parts[parts.length - 1];
|
||||
const version = VersionsUtil.clientVersion(debug.data?.codex.version);
|
||||
|
||||
return (
|
||||
<div className="versions">
|
||||
@ -20,7 +20,7 @@ export function Versions() {
|
||||
</div>
|
||||
<div>
|
||||
<p>Vault</p>
|
||||
<small>VER. {import.meta.env.PACKAGE_VERSION}</small>
|
||||
<small>VER. {VersionsUtil.codexVersion()}</small>
|
||||
<AlphaText variant="failure" width={37}></AlphaText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
9
src/components/Versions/versions.util.test.ts
Normal file
9
src/components/Versions/versions.util.test.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { assert, describe, it } from "vitest";
|
||||
import { VersionsUtil } from "./versions.util";
|
||||
|
||||
describe("versions", () => {
|
||||
it("gets the last client version", async () => {
|
||||
const version = "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7"
|
||||
assert.equal(VersionsUtil.clientVersion(version), "v0.1.7")
|
||||
})
|
||||
})
|
||||
8
src/components/Versions/versions.util.ts
Normal file
8
src/components/Versions/versions.util.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const VersionsUtil = {
|
||||
codexVersion: () => import.meta.env.PACKAGE_VERSION,
|
||||
|
||||
clientVersion: (version: string | undefined) => {
|
||||
const parts = version?.split("\n") || [""];
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,9 @@
|
||||
import { CodexDebugInfo, CodexError, SafeValue } from "@codex-storage/sdk-js";
|
||||
import { CodexDebugInfo, SafeValue, CodexError } from "@codex-storage/sdk-js"
|
||||
|
||||
export const PortForwardingUtil = {
|
||||
check: (port: number) => fetch(import.meta.env.VITE_GEO_IP_URL + "/port/" + port)
|
||||
.then((res) => res.json()),
|
||||
|
||||
export const DebugUtils = {
|
||||
getTcpPort(info: CodexDebugInfo): SafeValue<number> {
|
||||
if (info.addrs.length === 0) {
|
||||
return { error: true, data: new CodexError("Not existing address") }
|
||||
@ -20,4 +23,5 @@ export const DebugUtils = {
|
||||
|
||||
return { error: false, data: port }
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 };
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -1,20 +1,21 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Echo } from "../utils/echo";
|
||||
import { Errors } from "../utils/errors";
|
||||
import { CodexDebugInfo } from "@codex-storage/sdk-js";
|
||||
import { DebugUtils } from "../utils/debug";
|
||||
import { PortForwardingUtil } from "./port-forwarding.util";
|
||||
|
||||
type PortForwardingResponse = { reachable: boolean };
|
||||
|
||||
export function usePortForwarding(info: CodexDebugInfo | undefined) {
|
||||
const { data, isFetching, refetch } = useQuery({
|
||||
queryFn: (): Promise<PortForwardingResponse> => {
|
||||
const port = DebugUtils.getTcpPort(info!);
|
||||
const port = PortForwardingUtil.getTcpPort(info!);
|
||||
if (port.error) {
|
||||
Errors.report(port);
|
||||
return Promise.resolve({ reachable: false });
|
||||
} else {
|
||||
return Echo.portForwarding(port.data).catch((e) => Errors.report(e));
|
||||
return PortForwardingUtil.check(port.data).catch((e) =>
|
||||
Errors.report(e)
|
||||
);
|
||||
}
|
||||
},
|
||||
queryKey: ["port-forwarding"],
|
||||
@ -33,6 +34,8 @@ export function usePortForwarding(info: CodexDebugInfo | undefined) {
|
||||
// The user may try to change the port forwarding and go back
|
||||
// to the tab
|
||||
refetchOnWindowFocus: true,
|
||||
|
||||
throwOnError: false,
|
||||
});
|
||||
|
||||
return { enabled: data.reachable, isFetching, refetch };
|
||||
|
||||
178
src/proxy.ts
178
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<SafeValue<string>>((resolve) => {
|
||||
// xhr.upload.onprogress = (evt) => {
|
||||
// if (evt.lengthComputable) {
|
||||
// onProgress?.(evt.loaded, evt.total);
|
||||
// }
|
||||
// };
|
||||
// // const promise = new Promise<SafeValue<string>>((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<SafeValue<CodexDataResponse>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -33,7 +33,7 @@ const About = () => {
|
||||
<div className="container manifest-main-content">
|
||||
<div className="manifest" key={c.cid}>
|
||||
<div className="row">
|
||||
<WebFileIcon type={c.manifest.mimetype} />
|
||||
<WebFileIcon type={c.manifest.mimetype || ""} />
|
||||
<div className="manifest-data grow">
|
||||
<div>
|
||||
<b>{c.manifest.filename}</b>
|
||||
|
||||
@ -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: () => (
|
||||
<ErrorBoundary
|
||||
fallback={({ error }) => (
|
||||
@ -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 (
|
||||
<>
|
||||
|
||||
15
src/routes/dashboard/peers.tsx
Normal file
15
src/routes/dashboard/peers.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { ErrorBoundary } from "@sentry/react";
|
||||
import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Peers } from "../../components/Peers/Peers";
|
||||
|
||||
export const Route = createFileRoute("/dashboard/peers")({
|
||||
component: () => (
|
||||
<ErrorBoundary
|
||||
fallback={({ error }) => (
|
||||
<ErrorPlaceholder error={error} subtitle="Cannot retrieve the data." />
|
||||
)}>
|
||||
<Peers />
|
||||
</ErrorBoundary>
|
||||
),
|
||||
});
|
||||
@ -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}
|
||||
/>,
|
||||
<TruncateCell value={r.id} />,
|
||||
<Cell>{Times.pretty(duration)}</Cell>,
|
||||
|
||||
@ -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" });
|
||||
|
||||
|
||||
@ -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" });
|
||||
|
||||
|
||||
@ -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<string>(key, store);
|
||||
},
|
||||
|
||||
delete(key: string) {
|
||||
return del(key, store);
|
||||
},
|
||||
|
||||
async add(key: string, value: string) {
|
||||
return set(key, value, store);
|
||||
},
|
||||
};
|
||||
@ -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));
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
export const Echo = {
|
||||
portForwarding: (port: number) => fetch(import.meta.env.VITE_GEO_IP_URL + "/port/" + port)
|
||||
.then((res) => res.json())
|
||||
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
import { createStore, del, keys, set } from "idb-keyval";
|
||||
|
||||
const store = createStore("favorites", "favorites");
|
||||
|
||||
export const FavoriteStorage = {
|
||||
list() {
|
||||
return keys<string>(store);
|
||||
},
|
||||
|
||||
delete(key: string) {
|
||||
return del(key, store);
|
||||
},
|
||||
|
||||
async add(key: string) {
|
||||
return set(key, "1", store);
|
||||
},
|
||||
};
|
||||
@ -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<string, FileMetadata>(store);
|
||||
},
|
||||
|
||||
async get<T>(cid: string) {
|
||||
return get<T>(cid, store);
|
||||
},
|
||||
|
||||
async set(cid: string, metadata: FileMetadata) {
|
||||
return set(cid, metadata, store);
|
||||
},
|
||||
};
|
||||
@ -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)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
export const Network = {
|
||||
getIp(address: string) {
|
||||
const [ip] = address.split(":")
|
||||
return ip
|
||||
}
|
||||
}
|
||||
@ -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") || "🤖"
|
||||
},
|
||||
}
|
||||
@ -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<string>(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<string>(key, storeDates);
|
||||
},
|
||||
|
||||
async set(key: string, date: string) {
|
||||
return set(key, date, storeDates);
|
||||
},
|
||||
};
|
||||
@ -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]
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
@ -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<string, string[]>(folders) || []
|
||||
const items = await entries<string, string[]>(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<string[]>(folder, folders) || []
|
||||
const files = await get<string[]>(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<string[]>(folder, folders) || []
|
||||
const files = await get<string[]>(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<string>(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<string>(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<string>(key, this.store);
|
||||
},
|
||||
|
||||
async set(key: string, date: string) {
|
||||
return set(key, date, this.store);
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
8
vitest.config.ts
Normal file
8
vitest.config.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/// <reference types="vitest" />
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ["src/**/*.test.ts"]
|
||||
},
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user