mirror of
https://github.com/logos-storage/logos-storage-marketplace-ui.git
synced 2026-01-02 13:33:06 +00:00
Merge branch 'master' into feat/ui/integration
This commit is contained in:
commit
b2d78e3711
181
.github/workflows/docker-reusable.yml
vendored
Normal file
181
.github/workflows/docker-reusable.yml
vendored
Normal file
@ -0,0 +1,181 @@
|
||||
name: Docker - Reusable
|
||||
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
docker_file:
|
||||
default: docker/Dockerfile
|
||||
description: Dockerfile
|
||||
required: false
|
||||
type: string
|
||||
docker_repo:
|
||||
default: codexstorage/codex-marketplace-ui
|
||||
description: DockerHub repository
|
||||
required: false
|
||||
type: string
|
||||
tag_latest:
|
||||
default: true
|
||||
description: Set latest tag for Docker images
|
||||
required: false
|
||||
type: boolean
|
||||
tag_sha:
|
||||
default: true
|
||||
description: Set Git short commit as Docker tag
|
||||
required: false
|
||||
type: boolean
|
||||
tag_suffix:
|
||||
default: ''
|
||||
description: Suffix for Docker images tag
|
||||
required: false
|
||||
type: string
|
||||
|
||||
|
||||
env:
|
||||
DOCKER_FILE: ${{ inputs.docker_file }}
|
||||
DOCKER_REPO: ${{ inputs.docker_repo }}
|
||||
TAG_LATEST: ${{ inputs.tag_latest }}
|
||||
TAG_SHA: ${{ inputs.tag_sha }}
|
||||
TAG_SUFFIX: ${{ inputs.tag_suffix }}
|
||||
|
||||
|
||||
jobs:
|
||||
# Build platform specific image
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
target:
|
||||
- os: linux
|
||||
arch: amd64
|
||||
- os: linux
|
||||
arch: arm64
|
||||
include:
|
||||
- target:
|
||||
os: linux
|
||||
arch: amd64
|
||||
builder: ubuntu-22.04
|
||||
- target:
|
||||
os: linux
|
||||
arch: arm64
|
||||
builder: buildjet-4vcpu-ubuntu-2204-arm
|
||||
|
||||
name: Build ${{ matrix.target.os }}/${{ matrix.target.arch }}
|
||||
runs-on: ${{ matrix.builder }}
|
||||
env:
|
||||
PLATFORM: ${{ format('{0}/{1}', 'linux', matrix.target.arch) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker - Meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.DOCKER_REPO }}
|
||||
|
||||
- name: Docker - Set up Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker - Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Docker - Build and Push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ env.DOCKER_FILE }}
|
||||
platforms: ${{ env.PLATFORM }}
|
||||
push: true
|
||||
build-args: |
|
||||
VITE_CODEX_API_URL=${{ secrets.VITE_CODEX_API_URL }}
|
||||
VITE_GEO_IP_URL=${{ secrets.VITE_GEO_IP_URL }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.DOCKER_REPO }},push-by-digest=true,name-canonical=true,push=true
|
||||
|
||||
- name: Docker - Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Docker - Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ matrix.target.arch }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
|
||||
# Publish multi-platform image
|
||||
publish:
|
||||
name: Publish multi-platform image
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Docker - Variables
|
||||
run: |
|
||||
# Adjust custom suffix when set and
|
||||
if [[ -n "${{ env.TAG_SUFFIX }}" ]]; then
|
||||
echo "TAG_SUFFIX=-${{ env.TAG_SUFFIX }}" >>$GITHUB_ENV
|
||||
fi
|
||||
# Disable SHA tags on tagged release
|
||||
if [[ ${{ startsWith(github.ref, 'refs/tags/') }} == "true" ]]; then
|
||||
echo "TAG_SHA=false" >>$GITHUB_ENV
|
||||
fi
|
||||
# Handle latest and latest-custom using raw
|
||||
if [[ ${{ env.TAG_SHA }} == "false" ]]; then
|
||||
echo "TAG_LATEST=false" >>$GITHUB_ENV
|
||||
echo "TAG_RAW=true" >>$GITHUB_ENV
|
||||
if [[ -z "${{ env.TAG_SUFFIX }}" ]]; then
|
||||
echo "TAG_RAW_VALUE=latest" >>$GITHUB_ENV
|
||||
else
|
||||
echo "TAG_RAW_VALUE=latest-{{ env.TAG_SUFFIX }}" >>$GITHUB_ENV
|
||||
fi
|
||||
else
|
||||
echo "TAG_RAW=false" >>$GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Docker - Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
path: /tmp/digests
|
||||
|
||||
- name: Docker - Set up Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker - Meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.DOCKER_REPO }}
|
||||
flavor: |
|
||||
latest=${{ env.TAG_LATEST }}
|
||||
suffix=${{ env.TAG_SUFFIX }},onlatest=true
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,enable=${{ env.TAG_RAW }},value=latest
|
||||
type=sha,enable=${{ env.TAG_SHA }}
|
||||
|
||||
- name: Docker - Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Docker - Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.DOCKER_REPO }}@sha256:%s ' *)
|
||||
|
||||
- name: Docker - Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.DOCKER_REPO }}:${{ steps.meta.outputs.version }}
|
||||
28
.github/workflows/docker.yml
vendored
Normal file
28
.github/workflows/docker.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
name: Docker
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '.gitignore'
|
||||
- 'docker/**'
|
||||
- '!docker/Dockerfile'
|
||||
- '.github/**'
|
||||
- '!.github/workflows/docker.yml'
|
||||
- '!.github/workflows/docker-reusable.yml'
|
||||
- 'LICENSE-*'
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and Push
|
||||
uses: ./.github/workflows/docker-reusable.yml
|
||||
with:
|
||||
tag_latest: ${{ github.ref_name == github.event.repository.default_branch || startsWith(github.ref, 'refs/tags/') }}
|
||||
secrets: inherit
|
||||
4
.github/workflows/playwright.yml
vendored
4
.github/workflows/playwright.yml
vendored
@ -8,8 +8,8 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
codex_version: v0.1.6
|
||||
circuit_version: v0.1.6
|
||||
codex_version: v0.1.7
|
||||
circuit_version: v0.1.7
|
||||
marketplace_address: "0xfE822Df439d987849a90B64a4C0e26a297DBD47F"
|
||||
eth_provider: "https://rpc.testnet.codex.storage"
|
||||
VITE_CODEX_API_URL: ${{ secrets.VITE_CODEX_API_URL }}
|
||||
|
||||
47
docker/Dockerfile
Normal file
47
docker/Dockerfile
Normal file
@ -0,0 +1,47 @@
|
||||
# Variables
|
||||
ARG BUILDER=node:22-alpine
|
||||
ARG IMAGE=nginx:1.27-alpine-slim
|
||||
ARG APP_USER=root
|
||||
ARG BUILD_HOME=/app
|
||||
ARG BUILD_OUT=dist
|
||||
ARG APP_HOME=${BUILD_HOME}
|
||||
ARG APP_PORT=${APP_PORT:-80}
|
||||
ARG NGINX_TEMPLATE=docker/default.conf.template
|
||||
ARG VITE_CODEX_API_URL=${VITE_CODEX_API_URL:-http://127.0.0.1:8080}
|
||||
ARG VITE_GEO_IP_URL=${VITE_GEO_IP_URL:-http://127.0.0.1:8080}
|
||||
|
||||
|
||||
# Build
|
||||
FROM ${BUILDER} AS builder
|
||||
|
||||
ARG APP_USER
|
||||
ARG BUILD_HOME
|
||||
ARG VITE_CODEX_API_URL
|
||||
ARG VITE_GEO_IP_URL
|
||||
|
||||
WORKDIR ${BUILD_HOME}
|
||||
COPY --chown=${APP_USER}:${APP_USER} . .
|
||||
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
|
||||
# Create
|
||||
FROM ${IMAGE}
|
||||
|
||||
ARG APP_USER
|
||||
ARG BUILD_HOME
|
||||
ARG BUILD_OUT
|
||||
ARG APP_HOME
|
||||
ARG APP_PORT
|
||||
ARG NGINX_TEMPLATE
|
||||
|
||||
WORKDIR ${APP_HOME}
|
||||
RUN mkdir /etc/nginx/templates
|
||||
COPY ${NGINX_TEMPLATE} /etc/nginx/templates
|
||||
COPY --chown=${APP_USER}:${APP_USER} --from=builder ${BUILD_HOME}/${BUILD_OUT} .
|
||||
|
||||
ENV APP_HOME=${APP_HOME}
|
||||
ENV APP_PORT=${APP_PORT}
|
||||
|
||||
EXPOSE ${APP_PORT}
|
||||
59
docker/README.md
Normal file
59
docker/README.md
Normal file
@ -0,0 +1,59 @@
|
||||
# Codex Marketplace UI Docker images
|
||||
|
||||
## Description
|
||||
|
||||
We are shipping Codex Marketplace UI as a Docker image as well.
|
||||
|
||||
[Dockerfile](Dockerfile) is using multi-stage build and we use `alpine` image to speed up the build and to minimize the final Docker image size we are using a lightweight Nginx image.
|
||||
|
||||
|
||||
## Build locally
|
||||
|
||||
We can build image locally in the following way
|
||||
1. [Install Docker](https://docs.docker.com/engine/install)
|
||||
|
||||
2. Clone repository
|
||||
```shell
|
||||
git clone https://github.com/codex-storage/codex-marketplace-ui
|
||||
cd codex-marketplace-ui
|
||||
```
|
||||
|
||||
3. Build the image
|
||||
```shell
|
||||
# Variables
|
||||
VITE_CODEX_API_URL=<Default Codex API URL>
|
||||
VITE_GEO_IP_URL=<GeoIP API URL>
|
||||
|
||||
# Build
|
||||
docker build \
|
||||
--build-arg VITE_CODEX_API_URL=${VITE_CODEX_API_URL} \
|
||||
--build-arg VITE_GEO_IP_URL=${VITE_GEO_IP_URL} \
|
||||
--no-cache \
|
||||
-f docker/Dockerfile \
|
||||
-t codex-marketplace-ui:local .
|
||||
```
|
||||
|
||||
|
||||
## How to run
|
||||
|
||||
Base [Nginx image](https://hub.docker.com/_/nginx) is [exposing](https://docs.docker.com/reference/dockerfile/#expose) port 80 and we can [publish](https://docs.docker.com/get-started/docker-concepts/running-containers/publishing-ports/) it to a custom local port
|
||||
```shell
|
||||
docker run \
|
||||
--rm \
|
||||
--name codex-marketplace-ui \
|
||||
-p 3000:80 \
|
||||
codexstorage/codex-marketplace-ui:latest
|
||||
```
|
||||
|
||||
Access UI on http://localhost:3000.
|
||||
|
||||
And we also can set Nginx custom port using `APP_PORT` variable. That is useful when use the `host` network mode for a container
|
||||
```shell
|
||||
docker run \
|
||||
--rm \
|
||||
--name codex-marketplace-ui \
|
||||
--net=host \
|
||||
-e 'APP_PORT=3000' \
|
||||
-p 3000:3000 \
|
||||
codexstorage/codex-marketplace-ui:latest
|
||||
```
|
||||
15
docker/default.conf.template
Normal file
15
docker/default.conf.template
Normal file
@ -0,0 +1,15 @@
|
||||
server {
|
||||
listen ${APP_PORT};
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
location / {
|
||||
root ${APP_HOME};
|
||||
index index.html;
|
||||
|
||||
try_files $uri $uri/ /index.html =404;
|
||||
}
|
||||
}
|
||||
32
e2e/download.spec.ts
Normal file
32
e2e/download.spec.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { readFileSync } from 'fs';
|
||||
import path, { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
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!`);
|
||||
|
||||
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()
|
||||
});
|
||||
@ -6,4 +6,20 @@ test('update the log level', async ({ page }) => {
|
||||
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();
|
||||
})
|
||||
@ -30,7 +30,7 @@ test('select a uploaded cid when creating a storage request', async ({ page }) =
|
||||
await page.getByRole('link', { name: 'Purchases' }).click();
|
||||
await page.getByRole('button', { name: 'Storage Request' }).click();
|
||||
await page.getByPlaceholder('Select or type your CID').click();
|
||||
await page.getByText('N/A0').click();
|
||||
await page.locator('.dropdown-option').nth(1).click();
|
||||
await expect(page.getByText('button[disabled]')).not.toBeVisible();
|
||||
})
|
||||
|
||||
@ -63,3 +63,15 @@ test('storage request navigation buttons', async ({ page }) => {
|
||||
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.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("")
|
||||
})
|
||||
|
||||
812
package-lock.json
generated
812
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/codex-storage/codex-marketplace-ui"
|
||||
},
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 127.0.0.1 --port 5173",
|
||||
@ -24,7 +24,7 @@
|
||||
"React"
|
||||
],
|
||||
"dependencies": {
|
||||
"@codex-storage/marketplace-ui-components": "^0.0.22",
|
||||
"@codex-storage/marketplace-ui-components": "^0.0.24",
|
||||
"@codex-storage/sdk-js": "^0.0.8",
|
||||
"@sentry/browser": "^8.32.0",
|
||||
"@sentry/react": "^8.31.0",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { Button, Input, Toast } from "@codex-storage/marketplace-ui-components";
|
||||
import { CodexSdk } from "../../sdk/codex";
|
||||
@ -6,31 +6,49 @@ import { CodexSdk } from "../../sdk/codex";
|
||||
export function CodexUrlSettings() {
|
||||
const queryClient = useQueryClient();
|
||||
const [url, setUrl] = useState(CodexSdk.url);
|
||||
const [isInvalid, setIsInvalid] = useState(false);
|
||||
const [toast, setToast] = useState({ time: 0, message: "" });
|
||||
const { mutateAsync } = useMutation({
|
||||
mutationFn: (url: string) => CodexSdk.updateURL(url),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["spr"] });
|
||||
|
||||
setToast({ message: "Settings saved successfully.", time: Date.now() });
|
||||
},
|
||||
});
|
||||
|
||||
const onChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
const value = e.currentTarget.value;
|
||||
if (value) {
|
||||
setUrl(value);
|
||||
}
|
||||
const element = e.currentTarget;
|
||||
const value = element.value;
|
||||
|
||||
setUrl(value);
|
||||
setIsInvalid(!element.checkValidity());
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
CodexSdk.updateURL(url).then(() => {
|
||||
queryClient.invalidateQueries();
|
||||
setToast({ message: "Settigns saved successfully.", time: Date.now() });
|
||||
});
|
||||
if (isInvalid === false) {
|
||||
mutateAsync(url);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
id="url"
|
||||
label="Codex client node URL"
|
||||
onChange={onChange}
|
||||
value={url}
|
||||
inputClassName="settings-input"></Input>
|
||||
<Button variant="primary" label="Save changes" onClick={onClick}></Button>
|
||||
<div className="settings-input">
|
||||
<Input
|
||||
id="url"
|
||||
label="Codex client node URL"
|
||||
onChange={onChange}
|
||||
value={url}
|
||||
isInvalid={isInvalid}
|
||||
helper={isInvalid ? "The URL is not valid" : "Enter a valid URL"}
|
||||
type="url"></Input>
|
||||
</div>
|
||||
<Button
|
||||
className="settings-url-button"
|
||||
disabled={isInvalid}
|
||||
variant="primary"
|
||||
label="Save changes"
|
||||
onClick={onClick}></Button>
|
||||
<Toast message={toast.message} time={toast.time} variant="success" />
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CodexSdk } from "../../sdk/codex";
|
||||
import { Placeholder, Spinner } from "@codex-storage/marketplace-ui-components";
|
||||
import { Promises } from "../../utils/promises";
|
||||
import { CircleX } from "lucide-react";
|
||||
import { Spinner } from "@codex-storage/marketplace-ui-components";
|
||||
|
||||
export function Debug() {
|
||||
const { data, isPending, isError, error } = useQuery({
|
||||
const { data, isPending } = useQuery({
|
||||
queryFn: () =>
|
||||
CodexSdk.debug()
|
||||
.info()
|
||||
.then((s) => Promises.rejectOnError(s)),
|
||||
|
||||
queryKey: ["debug"],
|
||||
|
||||
// No need to retry because if the connection to the node
|
||||
@ -36,19 +36,10 @@ export function Debug() {
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Placeholder
|
||||
Icon={<CircleX size={"4em"}></CircleX>}
|
||||
title="Something went wrong"
|
||||
message={error.message}></Placeholder>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>Debug</h3>
|
||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
<pre key={Date.now()}>{JSON.stringify(data, null, 2)}</pre>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
13
src/components/Download/Download.css
Normal file
13
src/components/Download/Download.css
Normal file
@ -0,0 +1,13 @@
|
||||
.download {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.download-inputContainer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.download-input {
|
||||
width: 100%;
|
||||
}
|
||||
28
src/components/Download/Download.tsx
Normal file
28
src/components/Download/Download.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { Button, Input } from "@codex-storage/marketplace-ui-components";
|
||||
import "./Download.css";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
import { CodexSdk } from "../../sdk/codex";
|
||||
|
||||
export function Download() {
|
||||
const [cid, setCid] = useState("");
|
||||
const onDownload = () => {
|
||||
const url = CodexSdk.url() + "/api/codex/v1/data/";
|
||||
window.open(url + cid + "/network/stream", "_target");
|
||||
};
|
||||
|
||||
const onCidChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
setCid(e.currentTarget.value);
|
||||
|
||||
return (
|
||||
<div className="download">
|
||||
<div className="download-inputContainer">
|
||||
<Input
|
||||
id="cid"
|
||||
placeholder="CID"
|
||||
inputClassName="download-input"
|
||||
onChange={onCidChange}></Input>
|
||||
</div>
|
||||
<Button label="Download" onClick={onDownload}></Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
import { Cell } from "@codex-storage/marketplace-ui-components";
|
||||
import { PeerPin } from "./types";
|
||||
import { countriesCoordinates } from "./countries";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import "./PeerCountryCell.css";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export type Props = {
|
||||
address: string;
|
||||
@ -22,23 +22,12 @@ export function PeerCountryCell({ address, onPinAdd }: Props) {
|
||||
queryFn: () => {
|
||||
const [ip] = address.split(":");
|
||||
|
||||
return fetch(import.meta.env.VITE_GEO_IP_URL + "/" + ip)
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
const coordinate = countriesCoordinates.find(
|
||||
(c) => c.iso === json.country
|
||||
);
|
||||
|
||||
if (coordinate) {
|
||||
onPinAdd({
|
||||
lat: parseFloat(coordinate.lat),
|
||||
lng: parseFloat(coordinate.lng),
|
||||
});
|
||||
}
|
||||
|
||||
return coordinate;
|
||||
});
|
||||
return fetch(import.meta.env.VITE_GEO_IP_URL + "/json?ip=" + ip).then(
|
||||
(res) => res.json()
|
||||
);
|
||||
},
|
||||
refetchOnMount: true,
|
||||
|
||||
queryKey: [address],
|
||||
|
||||
// Enable only when the address exists
|
||||
@ -56,13 +45,22 @@ export function PeerCountryCell({ address, onPinAdd }: Props) {
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
onPinAdd({
|
||||
lat: data.latitude,
|
||||
lng: data.longitude,
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<Cell>
|
||||
<div className="peerCountry">
|
||||
{data ? (
|
||||
<>
|
||||
<span> {!!data && getFlagEmoji(data.iso)}</span>
|
||||
<span>{data?.name}</span>
|
||||
<span> {!!data && getFlagEmoji(data.country_iso)}</span>
|
||||
<span>{data?.country}</span>
|
||||
</>
|
||||
) : (
|
||||
<span>{address}</span>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -6,6 +6,7 @@
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.welcome-disclaimer {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Alert, SimpleText } from "@codex-storage/marketplace-ui-components";
|
||||
import { SimpleText } from "@codex-storage/marketplace-ui-components";
|
||||
import "./Welcome.css";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
@ -14,13 +14,6 @@ export function Welcome() {
|
||||
explore its features. Your feedback is invaluable as we continue to
|
||||
improve!
|
||||
</SimpleText>
|
||||
<Alert
|
||||
variant="warning"
|
||||
title="Disclaimer"
|
||||
className="welcome-disclaimer">
|
||||
The website and the content herein is not intended for public use and
|
||||
is for informational and demonstration purposes only.
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
<Link to="/dashboard/help" className="welcome-link">
|
||||
|
||||
@ -15,7 +15,7 @@ import * as Sentry from "@sentry/react";
|
||||
import { CodexSdk } from "./sdk/codex";
|
||||
import { ErrorPlaceholder } from "./components/ErrorPlaceholder/ErrorPlaceholder.tsx";
|
||||
|
||||
if (import.meta.env.PROD) {
|
||||
if (import.meta.env.PROD && !import.meta.env.CI) {
|
||||
Sentry.init({
|
||||
release: "codex-storage-marketplace-ui@" + import.meta.env.PACKAGE_VERSION,
|
||||
dsn: "https://22d77c59a27b8d5efc07132188b505b9@o4507855852011520.ingest.de.sentry.io/4507866758512720",
|
||||
|
||||
115
src/proxy.ts
115
src/proxy.ts
@ -3,12 +3,10 @@ import {
|
||||
CodexData,
|
||||
CodexDataResponse,
|
||||
CodexMarketplace,
|
||||
CodexReservation,
|
||||
SafeValue,
|
||||
UploadResponse,
|
||||
} from "@codex-storage/sdk-js";
|
||||
import { CodexSdk as Sdk } from "./sdk/codex";
|
||||
import { WebStorage } from "./utils/web-storage";
|
||||
import { FilesStorage } from "./utils/file-storage";
|
||||
import { PurchaseStorage } from "./utils/purchases-storage";
|
||||
|
||||
@ -17,15 +15,58 @@ class CodexDataMock extends CodexData {
|
||||
file: File,
|
||||
onProgress?: (loaded: number, total: number) => void
|
||||
): UploadResponse {
|
||||
// const url = CodexSdk.url() + "/api/codex/v1/data";
|
||||
|
||||
// const xhr = new XMLHttpRequest();
|
||||
|
||||
// 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.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."),
|
||||
// });
|
||||
// };
|
||||
// });
|
||||
|
||||
// return {
|
||||
// result: promise,
|
||||
// abort: () => {
|
||||
// xhr.abort();
|
||||
// },
|
||||
// };
|
||||
const { result, abort } = super.upload(file, onProgress);
|
||||
|
||||
return {
|
||||
abort,
|
||||
result: result.then((safe) => {
|
||||
if (!safe.error) {
|
||||
return WebStorage.set(safe.data, {
|
||||
type: file.type,
|
||||
return FilesStorage.set(safe.data, {
|
||||
mimetype: file.type,
|
||||
name: file.name,
|
||||
uploadedAt: new Date().toJSON(),
|
||||
}).then(() => safe);
|
||||
}
|
||||
|
||||
@ -143,39 +184,39 @@ class CodexMarketplaceMock extends CodexMarketplace {
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
override reservations(): Promise<SafeValue<CodexReservation[]>> {
|
||||
return Promise.resolve({
|
||||
error: false,
|
||||
data: [
|
||||
{
|
||||
id: "0x123456789",
|
||||
availabilityId: "0x12345678910",
|
||||
requestId: "0x1234567891011",
|
||||
/**
|
||||
* Size in bytes
|
||||
*/
|
||||
size: 500_000_000 + "",
|
||||
/**
|
||||
* Slot Index as hexadecimal string
|
||||
*/
|
||||
slotIndex: "2",
|
||||
},
|
||||
{
|
||||
id: "0x987654321",
|
||||
availabilityId: "0x9876543210",
|
||||
requestId: "0x98765432100",
|
||||
/**
|
||||
* Size in bytes
|
||||
*/
|
||||
size: 500_000_000 + "",
|
||||
/**
|
||||
* Slot Index as hexadecimal string
|
||||
*/
|
||||
slotIndex: "1",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
// override reservations(): Promise<SafeValue<CodexReservation[]>> {
|
||||
// return Promise.resolve({
|
||||
// error: false,
|
||||
// data: [
|
||||
// {
|
||||
// id: "0x123456789",
|
||||
// availabilityId: "0x12345678910",
|
||||
// requestId: "0x1234567891011",
|
||||
// /**
|
||||
// * Size in bytes
|
||||
// */
|
||||
// size: 500_000_000 + "",
|
||||
// /**
|
||||
// * Slot Index as hexadecimal string
|
||||
// */
|
||||
// slotIndex: "2",
|
||||
// },
|
||||
// {
|
||||
// id: "0x987654321",
|
||||
// availabilityId: "0x9876543210",
|
||||
// requestId: "0x98765432100",
|
||||
// /**
|
||||
// * Size in bytes
|
||||
// */
|
||||
// size: 500_000_000 + "",
|
||||
// /**
|
||||
// * Slot Index as hexadecimal string
|
||||
// */
|
||||
// slotIndex: "1",
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
export const CodexSdk = {
|
||||
|
||||
@ -5,6 +5,19 @@
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.dashboard-download {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.dashboard-welcome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dashboard-alert {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
.dashboard {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Files } from "../../components/Files/Files.tsx";
|
||||
import { Card, Upload } from "@codex-storage/marketplace-ui-components";
|
||||
import { Alert, Card, Upload } from "@codex-storage/marketplace-ui-components";
|
||||
import { CodexSdk } from "../../sdk/codex";
|
||||
import { Welcome } from "../../components/Welcome/Welcome.tsx";
|
||||
import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder.tsx";
|
||||
import { ErrorBoundary } from "@sentry/react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Download } from "../../components/Download/Download.tsx";
|
||||
|
||||
export const Route = createFileRoute("/dashboard/")({
|
||||
component: About,
|
||||
@ -21,21 +22,35 @@ function About() {
|
||||
return (
|
||||
<>
|
||||
<div className="dashboard">
|
||||
<ErrorBoundary
|
||||
fallback={({ error }) => (
|
||||
<ErrorPlaceholder
|
||||
error={error}
|
||||
subtitle="Cannot retrieve the data."
|
||||
/>
|
||||
)}>
|
||||
<Card title="Upload a file">
|
||||
<Upload
|
||||
multiple
|
||||
codexData={CodexSdk.data()}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
</Card>
|
||||
</ErrorBoundary>
|
||||
<div>
|
||||
<ErrorBoundary
|
||||
fallback={({ error }) => (
|
||||
<ErrorPlaceholder
|
||||
error={error}
|
||||
subtitle="Cannot retrieve the data."
|
||||
/>
|
||||
)}>
|
||||
<Card title="Upload a file">
|
||||
<Upload
|
||||
multiple
|
||||
codexData={CodexSdk.data()}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
</Card>
|
||||
</ErrorBoundary>
|
||||
|
||||
<ErrorBoundary
|
||||
fallback={({ error }) => (
|
||||
<ErrorPlaceholder
|
||||
error={error}
|
||||
subtitle="Cannot retrieve the data."
|
||||
/>
|
||||
)}>
|
||||
<Card title="Download a file" className="dashboard-download">
|
||||
<Download></Download>
|
||||
</Card>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
<ErrorBoundary
|
||||
fallback={({ error }) => (
|
||||
@ -44,7 +59,17 @@ function About() {
|
||||
subtitle="Cannot retrieve the data."
|
||||
/>
|
||||
)}>
|
||||
<Welcome />
|
||||
<div className="dashboard-welcome">
|
||||
<Welcome />
|
||||
|
||||
<Alert
|
||||
variant="warning"
|
||||
title="Disclaimer"
|
||||
className="welcome-disclaimer dashboard-alert">
|
||||
The website and the content herein is not intended for public use
|
||||
and is for informational and demonstration purposes only.
|
||||
</Alert>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
|
||||
@ -9,6 +9,8 @@ import { useCallback, 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";
|
||||
|
||||
// This function accepts the same arguments as DottedMap in the example above.
|
||||
const mapJsonString = getMapJSON({ height: 60, grid: "diagonal" });
|
||||
@ -102,5 +104,12 @@ const Peers = () => {
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/peers")({
|
||||
component: Peers,
|
||||
component: () => (
|
||||
<ErrorBoundary
|
||||
fallback={({ error }) => (
|
||||
<ErrorPlaceholder error={error} subtitle="Cannot retrieve the data." />
|
||||
)}>
|
||||
<Peers />
|
||||
</ErrorBoundary>
|
||||
),
|
||||
});
|
||||
|
||||
@ -5,6 +5,7 @@ import { Debug } from "../../components/Debug/Debug";
|
||||
import { CodexUrlSettings } from "../../components/CodexUrllSettings/CodexUrlSettings";
|
||||
import { ErrorBoundary } from "@sentry/react";
|
||||
import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings")({
|
||||
component: () => (
|
||||
@ -35,12 +36,25 @@ export const Route = createFileRoute("/dashboard/settings")({
|
||||
|
||||
<div className="settings">
|
||||
<ErrorBoundary
|
||||
fallback={({ error }) => (
|
||||
<ErrorPlaceholder
|
||||
error={error}
|
||||
subtitle="Cannot retrieve the data."
|
||||
/>
|
||||
)}>
|
||||
fallback={({ error, resetError }) => {
|
||||
useEffect(() => {
|
||||
document.addEventListener("codexinvalidatequeries", resetError);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener(
|
||||
"codexinvalidatequeries",
|
||||
resetError
|
||||
);
|
||||
};
|
||||
}, [resetError]);
|
||||
|
||||
return (
|
||||
<ErrorPlaceholder
|
||||
error={error}
|
||||
subtitle="Cannot retrieve the data."
|
||||
/>
|
||||
);
|
||||
}}>
|
||||
<Debug />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import { CodexError } from "@codex-storage/sdk-js";
|
||||
|
||||
|
||||
export const Errors = {
|
||||
report(safe: { error: true, data: CodexError }) {
|
||||
if (safe.data.code === 502) {
|
||||
// Ignore Gateway error
|
||||
return
|
||||
}
|
||||
|
||||
Sentry.captureException(safe.data, {
|
||||
extra: {
|
||||
code: safe.data.code,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user