mirror of
https://github.com/oceanprotocol/commons.git
synced 2023-03-15 18:03:00 +01:00
admin app with retire button
This commit is contained in:
parent
4c744d1ea9
commit
bf7fb99df1
2
admin/.dockerignore
Normal file
2
admin/.dockerignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
.env.local
|
54
admin/.env.local.example
Normal file
54
admin/.env.local.example
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
#
|
||||||
|
# When none of the following variables are set,
|
||||||
|
# Commons will default connecting to Nile
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Connect to Pacific
|
||||||
|
#
|
||||||
|
# REACT_APP_NODE_URI="https://pacific.oceanprotocol.com"
|
||||||
|
# REACT_APP_AQUARIUS_URI="https://aquarius.pacific.dev-ocean.com"
|
||||||
|
# REACT_APP_BRIZO_URI="https://brizo.pacific.dev-ocean.com"
|
||||||
|
# REACT_APP_SECRET_STORE_URI="https://secret-store.pacific.oceanprotocol.com"
|
||||||
|
# REACT_APP_FAUCET_URI="https://faucet.pacific.dev-ocean.com"
|
||||||
|
# REACT_APP_BRIZO_ADDRESS="0x008c25ed3594e094db4592f4115d5fa74c4f41ea"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Connect to Nile
|
||||||
|
#
|
||||||
|
REACT_APP_NODE_URI="https://nile.dev-ocean.com"
|
||||||
|
REACT_APP_AQUARIUS_URI="https://nginx-aquarius.dev-ocean.com"
|
||||||
|
REACT_APP_BRIZO_URI="https://nginx-brizo.dev-ocean.com"
|
||||||
|
REACT_APP_SECRET_STORE_URI="https://secret-store.dev-ocean.com"
|
||||||
|
REACT_APP_FAUCET_URI="https://faucet.nile.dev-ocean.com"
|
||||||
|
REACT_APP_BRIZO_ADDRESS="0x4aaab179035dc57b35e2ce066919048686f82972"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Connect to Duero
|
||||||
|
#
|
||||||
|
# REACT_APP_NODE_URI="https://duero.dev-ocean.com"
|
||||||
|
# REACT_APP_AQUARIUS_URI="https://aquarius.duero.dev-ocean.com"
|
||||||
|
# REACT_APP_BRIZO_URI="https://brizo.duero.dev-ocean.com"
|
||||||
|
# REACT_APP_SECRET_STORE_URI="https://secret-store.duero.dev-ocean.com"
|
||||||
|
# REACT_APP_FAUCET_URI="https://faucet.duero.dev-ocean.com"
|
||||||
|
# REACT_APP_BRIZO_ADDRESS="0x9d4ed58293f71122ad6a733c1603927a150735d0"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Connect to Nile Commons instances
|
||||||
|
#
|
||||||
|
# REACT_APP_NODE_URI="https://nile.dev-ocean.com"
|
||||||
|
# REACT_APP_AQUARIUS_URI="https://aquarius.marketplace.dev-ocean.com"
|
||||||
|
# REACT_APP_BRIZO_URI="https://brizo.marketplace.dev-ocean.com"
|
||||||
|
# REACT_APP_SECRET_STORE_URI="https://secret-store.dev-ocean.com"
|
||||||
|
# REACT_APP_FAUCET_URI="https://faucet.nile.dev-ocean.com"
|
||||||
|
# REACT_APP_BRIZO_ADDRESS="0x4aaab179035dc57b35e2ce066919048686f82972"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Connect to Spree (local with Barge)
|
||||||
|
#
|
||||||
|
# REACT_APP_NODE_URI="htts://localhost:8545"
|
||||||
|
# REACT_APP_AQUARIUS_URI="http://aquarius:5000"
|
||||||
|
# REACT_APP_BRIZO_URI="http://localhost:8030"
|
||||||
|
# REACT_APP_SECRET_STORE_URI="http://localhost:12001"
|
||||||
|
# REACT_APP_FAUCET_URI="http://localhost:3001"
|
||||||
|
# REACT_APP_BRIZO_ADDRESS="0x00bd138abd70e2f00903268f3db08f2d25677c9e"
|
25
admin/Dockerfile
Normal file
25
admin/Dockerfile
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
FROM node:11-alpine
|
||||||
|
LABEL maintainer="Ocean Protocol <devops@oceanprotocol.com>"
|
||||||
|
|
||||||
|
RUN apk add --no-cache --update\
|
||||||
|
bash\
|
||||||
|
g++\
|
||||||
|
gcc\
|
||||||
|
git\
|
||||||
|
gettext\
|
||||||
|
make\
|
||||||
|
python
|
||||||
|
|
||||||
|
COPY . /app/admin
|
||||||
|
WORKDIR /app/admin
|
||||||
|
|
||||||
|
RUN npm install -g npm serve
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Default ENV values
|
||||||
|
ENV LISTEN_ADDRESS='0.0.0.0'
|
||||||
|
ENV LISTEN_PORT='3000'
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/admin/scripts/docker-entrypoint.sh"]
|
||||||
|
|
2
admin/__mocks__/axios.js
Normal file
2
admin/__mocks__/axios.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import mockAxios from 'jest-mock-axios'
|
||||||
|
export default mockAxios
|
66
admin/__mocks__/ocean-mock.ts
Normal file
66
admin/__mocks__/ocean-mock.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
const oceanMock = {
|
||||||
|
ocean: {
|
||||||
|
accounts: {
|
||||||
|
list: () => ['xxx', 'xxx']
|
||||||
|
},
|
||||||
|
aquarius: {
|
||||||
|
queryMetadata: () => {
|
||||||
|
return {
|
||||||
|
results: [],
|
||||||
|
totalResults: 1,
|
||||||
|
totalPages: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
assets: {
|
||||||
|
resolve: jest.fn(),
|
||||||
|
order: () => {
|
||||||
|
return {
|
||||||
|
next: jest.fn()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
consume: jest.fn()
|
||||||
|
},
|
||||||
|
keeper: {
|
||||||
|
conditions: {
|
||||||
|
accessSecretStoreCondition: {
|
||||||
|
getGrantedDidByConsumer: () => {
|
||||||
|
return {
|
||||||
|
find: jest.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
get: jest.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
squid: {
|
||||||
|
name: 'Squid-js',
|
||||||
|
status: 'Working'
|
||||||
|
},
|
||||||
|
aquarius: {
|
||||||
|
name: 'Aquarius',
|
||||||
|
status: 'Working'
|
||||||
|
},
|
||||||
|
brizo: {
|
||||||
|
name: 'Brizo',
|
||||||
|
network: 'Nile',
|
||||||
|
status: 'Working',
|
||||||
|
contracts: {
|
||||||
|
hello: 'hello',
|
||||||
|
hello2: 'hello2'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
ok: true,
|
||||||
|
network: true,
|
||||||
|
contracts: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default oceanMock
|
33
admin/__mocks__/user-mock.ts
Normal file
33
admin/__mocks__/user-mock.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import oceanMock from './ocean-mock'
|
||||||
|
|
||||||
|
const userMock = {
|
||||||
|
isLogged: false,
|
||||||
|
isLoading: false,
|
||||||
|
isWeb3: false,
|
||||||
|
isOceanNetwork: false,
|
||||||
|
account: '',
|
||||||
|
web3: {},
|
||||||
|
...oceanMock,
|
||||||
|
balance: { eth: 0, ocn: 0 },
|
||||||
|
network: '',
|
||||||
|
requestFromFaucet: jest.fn(),
|
||||||
|
unlockAccounts: jest.fn(),
|
||||||
|
message: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const userMockConnected = {
|
||||||
|
isLogged: true,
|
||||||
|
isLoading: false,
|
||||||
|
isWeb3: true,
|
||||||
|
isOceanNetwork: true,
|
||||||
|
account: '0xxxxxx',
|
||||||
|
web3: {},
|
||||||
|
...oceanMock,
|
||||||
|
balance: { eth: 0, ocn: 0 },
|
||||||
|
network: '',
|
||||||
|
requestFromFaucet: jest.fn(),
|
||||||
|
unlockAccounts: jest.fn(),
|
||||||
|
message: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export { userMock, userMockConnected }
|
17250
admin/package-lock.json
generated
Normal file
17250
admin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
82
admin/package.json
Normal file
82
admin/package.json
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"name": "commons-admin",
|
||||||
|
"description": "Ocean Protocol marketplace admin to manage open data sets.",
|
||||||
|
"version": "0.5.4",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts --max_old_space_size=4096 build",
|
||||||
|
"test": "react-scripts test --coverage --watchAll=false",
|
||||||
|
"test:watch": "react-scripts test --coverage",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@oceanprotocol/art": "^2.2.0",
|
||||||
|
"@oceanprotocol/squid": "0.6.2",
|
||||||
|
"@oceanprotocol/typographies": "^0.1.0",
|
||||||
|
"@sindresorhus/slugify": "^0.9.1",
|
||||||
|
"axios": "^0.19.0",
|
||||||
|
"classnames": "^2.2.6",
|
||||||
|
"eslint-plugin-prettier": "^3.1.0",
|
||||||
|
"ethereum-blockies": "github:MyEtherWallet/blockies",
|
||||||
|
"filesize": "^4.1.2",
|
||||||
|
"history": "^4.9.0",
|
||||||
|
"is-url": "^1.2.4",
|
||||||
|
"moment": "^2.24.0",
|
||||||
|
"query-string": "^6.8.1",
|
||||||
|
"react": "^16.8.6",
|
||||||
|
"react-collapsed": "^2.0.1",
|
||||||
|
"react-datepicker": "^2.7.0",
|
||||||
|
"react-dom": "^16.8.6",
|
||||||
|
"react-dotdotdot": "^1.3.0",
|
||||||
|
"react-ga": "^2.6.0",
|
||||||
|
"react-helmet": "^5.2.1",
|
||||||
|
"react-markdown": "^4.1.0",
|
||||||
|
"react-moment": "^0.9.2",
|
||||||
|
"react-paginate": "^6.3.0",
|
||||||
|
"react-popper": "^1.3.3",
|
||||||
|
"react-router-dom": "^5.0.1",
|
||||||
|
"react-transition-group": "^4.1.1",
|
||||||
|
"web3": "1.0.0-beta.37"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@react-mock/state": "^0.1.8",
|
||||||
|
"@testing-library/react": "^8.0.4",
|
||||||
|
"@types/classnames": "^2.2.7",
|
||||||
|
"@types/filesize": "^4.1.0",
|
||||||
|
"@types/is-url": "^1.2.28",
|
||||||
|
"@types/jest": "^24.0.15",
|
||||||
|
"@types/react": "^16.8.22",
|
||||||
|
"@types/react-datepicker": "^2.3.0",
|
||||||
|
"@types/react-dom": "^16.8.4",
|
||||||
|
"@types/react-dotdotdot": "^1.2.0",
|
||||||
|
"@types/react-helmet": "^5.0.8",
|
||||||
|
"@types/react-paginate": "^6.2.1",
|
||||||
|
"@types/react-router-dom": "^4.3.4",
|
||||||
|
"@types/react-transition-group": "^2.9.2",
|
||||||
|
"@types/web3": "^1.0.19",
|
||||||
|
"jest-dom": "^3.5.0",
|
||||||
|
"jest-mock-axios": "^3.0.0",
|
||||||
|
"node-sass": "^4.12.0",
|
||||||
|
"react-scripts": "^3.0.0",
|
||||||
|
"stylelint-config-bigchaindb": "^1.2.2",
|
||||||
|
"typescript": "3.4.5"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/oceanprotocol/commons"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not ie <= 11",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"jest": {
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"src/**/*.{ts,tsx}",
|
||||||
|
"!src/serviceWorker.ts",
|
||||||
|
"!src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
BIN
admin/public/favicon.ico
Normal file
BIN
admin/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
BIN
admin/public/icons/favicon_128.png
Normal file
BIN
admin/public/icons/favicon_128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.7 KiB |
BIN
admin/public/icons/favicon_256.png
Normal file
BIN
admin/public/icons/favicon_256.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
admin/public/icons/favicon_512.png
Normal file
BIN
admin/public/icons/favicon_512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
49
admin/public/index.html
Normal file
49
admin/public/index.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||||
|
/>
|
||||||
|
<meta name="theme-color" content="#141414" />
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
|
||||||
|
<title>Commons</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.loader {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
margin-top: calc(50vh - 15px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader:before {
|
||||||
|
content: '';
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
top: -80%;
|
||||||
|
left: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-top: -10px;
|
||||||
|
margin-left: -10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #7b1173;
|
||||||
|
border-top-color: #e000cf;
|
||||||
|
animation: spinner 0.6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spinner {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"><span class="loader"></span></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
25
admin/public/manifest.json
Normal file
25
admin/public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"short_name": "Commons",
|
||||||
|
"name": "Commons Marketplace",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "icons/favicon_512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/favicon_256.png",
|
||||||
|
"sizes": "256x256",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/favicon_128.png",
|
||||||
|
"sizes": "128x128",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#141414",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
2
admin/public/robots.txt
Normal file
2
admin/public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /search
|
BIN
admin/public/share.png
Normal file
BIN
admin/public/share.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 167 KiB |
11
admin/scripts/docker-entrypoint.sh
Executable file
11
admin/scripts/docker-entrypoint.sh
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if [ "${LOCAL_CONTRACTS}" = "true" ]; then
|
||||||
|
echo "Waiting for contracts to be generated..."
|
||||||
|
while [ ! -f "/app/frontend/node_modules/@oceanprotocol/keeper-contracts/artifacts/ready" ]; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
npm run build
|
||||||
|
echo "Starting Commons..."
|
||||||
|
serve -l tcp://"${LISTEN_ADDRESS}":"${LISTEN_PORT}" -s /app/frontend/build/
|
5
admin/src/@types/ethereum-blockies/index.d.ts
vendored
Normal file
5
admin/src/@types/ethereum-blockies/index.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="node" />
|
||||||
|
|
||||||
|
declare module 'ethereum-blockies' {
|
||||||
|
export function toDataUrl(address: string): string
|
||||||
|
}
|
1
admin/src/@types/react-collapsed/index.d.ts
vendored
Normal file
1
admin/src/@types/react-collapsed/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare module 'react-collapsed'
|
27
admin/src/App.module.scss
Normal file
27
admin/src/App.module.scss
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
@import './styles/variables';
|
||||||
|
|
||||||
|
.app {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
// for sticky footer
|
||||||
|
display: flex;
|
||||||
|
min-height: calc(100vh - #{$page-frame * 2});
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 25vh;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
29
admin/src/App.test.tsx
Normal file
29
admin/src/App.test.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import App from './App'
|
||||||
|
import { User } from './context'
|
||||||
|
import { userMock, userMockConnected } from '../__mocks__/user-mock'
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
it('should be able to run tests', () => {
|
||||||
|
expect(1 + 2).toEqual(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={userMockConnected}>
|
||||||
|
<App />
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders loading state', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={{ ...userMock, isLoading: true }}>
|
||||||
|
<App />
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
expect(container.querySelector('.spinner')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
37
admin/src/App.tsx
Normal file
37
admin/src/App.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React, { Component } from 'react'
|
||||||
|
import { BrowserRouter as Router } from 'react-router-dom'
|
||||||
|
import Header from './components/organisms/Header'
|
||||||
|
import Footer from './components/organisms/Footer'
|
||||||
|
import Spinner from './components/atoms/Spinner'
|
||||||
|
import { User } from './context'
|
||||||
|
import Routes from './Routes'
|
||||||
|
import './styles/global.scss'
|
||||||
|
import styles from './App.module.scss'
|
||||||
|
|
||||||
|
export default class App extends Component {
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<div className={styles.app}>
|
||||||
|
<Router>
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<main className={styles.main}>
|
||||||
|
{this.context.isLoading ? (
|
||||||
|
<div className={styles.loader}>
|
||||||
|
<Spinner message={this.context.message} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Routes />
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
</Router>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
App.contextType = User
|
19
admin/src/Routes.test.tsx
Normal file
19
admin/src/Routes.test.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { BrowserRouter as Router } from 'react-router-dom'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import Routes from './Routes'
|
||||||
|
import { User } from './context'
|
||||||
|
import { userMockConnected } from '../__mocks__/user-mock'
|
||||||
|
|
||||||
|
describe('Routes', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={userMockConnected}>
|
||||||
|
<Router>
|
||||||
|
<Routes />
|
||||||
|
</Router>
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
21
admin/src/Routes.tsx
Normal file
21
admin/src/Routes.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Route, Switch } from 'react-router-dom'
|
||||||
|
|
||||||
|
import Home from './routes/Home'
|
||||||
|
import NotFound from './routes/NotFound'
|
||||||
|
import Search from './routes/Search'
|
||||||
|
import About from './routes/About'
|
||||||
|
|
||||||
|
import Asset from './components/templates/Asset'
|
||||||
|
|
||||||
|
const Routes = () => (
|
||||||
|
<Switch>
|
||||||
|
<Route component={Home} exact path="/" />
|
||||||
|
<Route component={Search} path="/search" />
|
||||||
|
<Route component={Asset} path="/asset/:did" />
|
||||||
|
<Route component={About} path="/about" />
|
||||||
|
<Route component={NotFound} />
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Routes
|
24
admin/src/components/atoms/Account.module.scss
Normal file
24
admin/src/components/atoms/Account.module.scss
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
.account {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-family: $font-family-monospace;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blockies {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: $spacer / 3;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
28
admin/src/components/atoms/Account.test.tsx
Normal file
28
admin/src/components/atoms/Account.test.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import { toDataUrl } from 'ethereum-blockies'
|
||||||
|
import Account from './Account'
|
||||||
|
|
||||||
|
describe('Account', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(<Account account={'0xxxxxxxxxxxxxxx'} />)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('outputs empty state without account', () => {
|
||||||
|
const { container } = render(<Account account={''} />)
|
||||||
|
expect(container.firstChild).toHaveTextContent('No account selected')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('outputs blockie img', () => {
|
||||||
|
const account = '0xxxxxxxxxxxxxxx'
|
||||||
|
const blockies = toDataUrl(account)
|
||||||
|
|
||||||
|
const { container } = render(<Account account={account} />)
|
||||||
|
expect(container.querySelector('.blockies')).toBeInTheDocument()
|
||||||
|
expect(container.querySelector('.blockies')).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
blockies
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
19
admin/src/components/atoms/Account.tsx
Normal file
19
admin/src/components/atoms/Account.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Dotdotdot from 'react-dotdotdot'
|
||||||
|
import { toDataUrl } from 'ethereum-blockies'
|
||||||
|
import styles from './Account.module.scss'
|
||||||
|
|
||||||
|
const Account = ({ account }: { account: string }) => {
|
||||||
|
const blockies = account && toDataUrl(account)
|
||||||
|
|
||||||
|
return account && blockies ? (
|
||||||
|
<div className={styles.account}>
|
||||||
|
<img className={styles.blockies} src={blockies} alt="Blockies" />
|
||||||
|
<Dotdotdot clamp={2}>{account}</Dotdotdot>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<em>No account selected</em>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Account
|
73
admin/src/components/atoms/Button.module.scss
Normal file
73
admin/src/components/atoms/Button.module.scss
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
.button {
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
width: fit-content;
|
||||||
|
padding: $spacer / 4 $spacer;
|
||||||
|
font-size: $font-size-base;
|
||||||
|
font-family: $font-family-base;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: .2s ease-out;
|
||||||
|
color: $brand-white;
|
||||||
|
background: $brand-grey-light;
|
||||||
|
box-shadow: 0 9px 18px 0 rgba(0, 0, 0, .1);
|
||||||
|
min-height: 45px;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: $brand-white;
|
||||||
|
background: $brand-grey-light;
|
||||||
|
text-decoration: none;
|
||||||
|
transform: translate3d(0, -.05rem, 0);
|
||||||
|
box-shadow: 0 12px 30px 0 rgba(0, 0, 0, .1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: $brand-grey-light;
|
||||||
|
transition: none;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: 0 5px 18px 0 rgba(0, 0, 0, .1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonPrimary {
|
||||||
|
composes: button;
|
||||||
|
background: $brand-gradient;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
background: $brand-gradient;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: $brand-gradient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
display: inline-block;
|
||||||
|
width: fit-content;
|
||||||
|
background: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: $brand-pink;
|
||||||
|
font-size: $font-size-base;
|
||||||
|
font-weight: $font-weight-base;
|
||||||
|
font-family: inherit;
|
||||||
|
box-shadow: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
54
admin/src/components/atoms/Button.test.tsx
Normal file
54
admin/src/components/atoms/Button.test.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import { BrowserRouter as Router } from 'react-router-dom'
|
||||||
|
import Button from './Button'
|
||||||
|
|
||||||
|
describe('Button', () => {
|
||||||
|
it('default renders correctly without crashing', () => {
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<Button data-testid="button-default">I am a default button</Button>
|
||||||
|
)
|
||||||
|
expect(getByTestId('button-default')).toHaveTextContent('default')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('primary renders correctly without crashing', () => {
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<Button data-testid="button-primary" primary>
|
||||||
|
I am a primary button
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
expect(getByTestId('button-primary')).toHaveTextContent('primary')
|
||||||
|
expect(getByTestId('button-primary').className).toMatch(/buttonPrimary/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Link renders correctly without crashing', () => {
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<Router>
|
||||||
|
<Button data-testid="button-to" to="https://hello.com">
|
||||||
|
I am a Link button
|
||||||
|
</Button>
|
||||||
|
</Router>
|
||||||
|
)
|
||||||
|
expect(getByTestId('button-to')).toHaveTextContent('Link')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('href renders correctly without crashing', () => {
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<Button data-testid="button-href" href="https://hello.com">
|
||||||
|
I am a href button
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
expect(getByTestId('button-href')).toHaveTextContent('href')
|
||||||
|
expect(getByTestId('button-href').nodeName).toBe('A')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('link renders correctly without crashing', () => {
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<Button data-testid="button-link" link>
|
||||||
|
I am a link button
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
expect(getByTestId('button-link')).toHaveTextContent('link')
|
||||||
|
expect(getByTestId('button-link').className).toMatch(/link/)
|
||||||
|
})
|
||||||
|
})
|
60
admin/src/components/atoms/Button.tsx
Normal file
60
admin/src/components/atoms/Button.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import React, { PureComponent } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import cx from 'classnames'
|
||||||
|
import styles from './Button.module.scss'
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
children: string
|
||||||
|
className?: string
|
||||||
|
primary?: boolean
|
||||||
|
link?: boolean
|
||||||
|
href?: string
|
||||||
|
onClick?: any
|
||||||
|
disabled?: boolean
|
||||||
|
to?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Button extends PureComponent<ButtonProps, any> {
|
||||||
|
public render() {
|
||||||
|
let classes
|
||||||
|
const {
|
||||||
|
primary,
|
||||||
|
link,
|
||||||
|
href,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
to,
|
||||||
|
...props
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
if (primary) {
|
||||||
|
classes = styles.buttonPrimary
|
||||||
|
} else if (link) {
|
||||||
|
classes = styles.link
|
||||||
|
} else {
|
||||||
|
classes = styles.button
|
||||||
|
}
|
||||||
|
|
||||||
|
if (to) {
|
||||||
|
return (
|
||||||
|
<Link to={to} className={cx(classes, className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (href) {
|
||||||
|
return (
|
||||||
|
<a href={href} className={cx(classes, className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={cx(classes, className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
25
admin/src/components/atoms/CategoryImage.module.scss
Normal file
25
admin/src/components/atoms/CategoryImage.module.scss
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
.categoryImage {
|
||||||
|
height: 4rem;
|
||||||
|
background-size: 100%;
|
||||||
|
background-position: center;
|
||||||
|
margin-bottom: $spacer / $line-height;
|
||||||
|
background-color: $body-background;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: .85;
|
||||||
|
transition: .2s ease-out;
|
||||||
|
border: 1px solid $brand-grey-lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
composes: categoryImage;
|
||||||
|
height: 8rem;
|
||||||
|
margin-top: $spacer / $line-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dimmed {
|
||||||
|
composes: categoryImage;
|
||||||
|
opacity: .6;
|
||||||
|
}
|
25
admin/src/components/atoms/CategoryImage.test.tsx
Normal file
25
admin/src/components/atoms/CategoryImage.test.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import CategoryImage from './CategoryImage'
|
||||||
|
import formPublish from '../../data/form-publish.json'
|
||||||
|
|
||||||
|
describe('CategoryImage', () => {
|
||||||
|
it('renders fallback image', () => {
|
||||||
|
const { container } = render(<CategoryImage category={''} />)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
expect(container.firstChild.style.backgroundImage).toMatch(
|
||||||
|
/jellyfish-back/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders all the category images', () => {
|
||||||
|
const { options } = formPublish.steps[1].fields
|
||||||
|
? formPublish.steps[1].fields.categories
|
||||||
|
: []
|
||||||
|
|
||||||
|
options.map((category: string) => {
|
||||||
|
const { container } = render(<CategoryImage category={category} />)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
168
admin/src/components/atoms/CategoryImage.tsx
Normal file
168
admin/src/components/atoms/CategoryImage.tsx
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import React, { PureComponent } from 'react'
|
||||||
|
import cx from 'classnames'
|
||||||
|
import styles from './CategoryImage.module.scss'
|
||||||
|
|
||||||
|
import agriculture from '../../img/categories/agriculture.jpg'
|
||||||
|
import anthroarche from '../../img/categories/anthroarche.jpg'
|
||||||
|
import astronomy from '../../img/categories/astronomy.jpg'
|
||||||
|
import biology from '../../img/categories/biology.jpg'
|
||||||
|
import business from '../../img/categories/business.jpg'
|
||||||
|
import chemistry from '../../img/categories/chemistry.jpg'
|
||||||
|
import communication from '../../img/categories/communication.jpg'
|
||||||
|
import computer from '../../img/categories/computer.jpg'
|
||||||
|
import dataofdata from '../../img/categories/dataofdata.jpg'
|
||||||
|
import deeplearning from '../../img/categories/deeplearning.jpg'
|
||||||
|
import demographics from '../../img/categories/demographics.jpg'
|
||||||
|
import earth from '../../img/categories/earth.jpg'
|
||||||
|
import economics from '../../img/categories/economics.jpg'
|
||||||
|
import engineering from '../../img/categories/engineering.jpg'
|
||||||
|
import history from '../../img/categories/history.jpg'
|
||||||
|
import imagesets from '../../img/categories/imagesets.jpg'
|
||||||
|
import language from '../../img/categories/language.jpg'
|
||||||
|
import law from '../../img/categories/law.jpg'
|
||||||
|
import mathematics from '../../img/categories/mathematics.jpg'
|
||||||
|
import medicine from '../../img/categories/medicine.jpg'
|
||||||
|
import other from '../../img/categories/other.jpg'
|
||||||
|
import performingarts from '../../img/categories/performingarts.jpg'
|
||||||
|
import philosophy from '../../img/categories/philosophy.jpg'
|
||||||
|
import physics from '../../img/categories/physics.jpg'
|
||||||
|
import politics from '../../img/categories/politics.jpg'
|
||||||
|
import psychology from '../../img/categories/psychology.jpg'
|
||||||
|
import sociology from '../../img/categories/sociology.jpg'
|
||||||
|
import sports from '../../img/categories/sports.jpg'
|
||||||
|
import theology from '../../img/categories/theology.jpg'
|
||||||
|
import transport from '../../img/categories/transport.jpg'
|
||||||
|
import urbanplanning from '../../img/categories/urbanplanning.jpg'
|
||||||
|
import visualart from '../../img/categories/visualart.jpg'
|
||||||
|
import aiforgood from '../../img/aiforgood.jpg'
|
||||||
|
import fallback from '@oceanprotocol/art/jellyfish/jellyfish-back.svg'
|
||||||
|
|
||||||
|
const categoryImageFile = (category: string) => {
|
||||||
|
switch (category) {
|
||||||
|
case 'Agriculture & Bio Engineering':
|
||||||
|
case 'agriculture':
|
||||||
|
return agriculture
|
||||||
|
case 'Anthropology & Archeology':
|
||||||
|
case 'anthroarche':
|
||||||
|
return anthroarche
|
||||||
|
case 'Space & Astronomy':
|
||||||
|
case 'astronomy':
|
||||||
|
return astronomy
|
||||||
|
case 'Biology':
|
||||||
|
case 'biology':
|
||||||
|
return biology
|
||||||
|
case 'Business & Management':
|
||||||
|
case 'business':
|
||||||
|
return business
|
||||||
|
case 'Chemistry':
|
||||||
|
case 'chemistry':
|
||||||
|
return chemistry
|
||||||
|
case 'Communication & Journalism':
|
||||||
|
case 'communication':
|
||||||
|
return communication
|
||||||
|
case 'Computer Technology':
|
||||||
|
case 'computer':
|
||||||
|
return computer
|
||||||
|
case 'Dataset Of Datasets':
|
||||||
|
case 'dataofdata':
|
||||||
|
return dataofdata
|
||||||
|
case 'Deep Learning':
|
||||||
|
case 'deeplearning':
|
||||||
|
return deeplearning
|
||||||
|
case 'Demography':
|
||||||
|
case 'demographics':
|
||||||
|
return demographics
|
||||||
|
case 'Earth & Climate':
|
||||||
|
case 'earth':
|
||||||
|
return earth
|
||||||
|
case 'Economics & Finance':
|
||||||
|
case 'economics-and-finance':
|
||||||
|
return economics
|
||||||
|
case 'Engineering':
|
||||||
|
case 'engineering':
|
||||||
|
return engineering
|
||||||
|
case 'History':
|
||||||
|
case 'history':
|
||||||
|
return history
|
||||||
|
case 'Image Recognition':
|
||||||
|
case 'imagesets':
|
||||||
|
return imagesets
|
||||||
|
case 'Language':
|
||||||
|
case 'language':
|
||||||
|
return language
|
||||||
|
case 'Law':
|
||||||
|
case 'law':
|
||||||
|
return law
|
||||||
|
case 'Mathematics':
|
||||||
|
case 'mathematics':
|
||||||
|
return mathematics
|
||||||
|
case 'Medicine':
|
||||||
|
case 'Health & Medicine':
|
||||||
|
case 'Health':
|
||||||
|
case 'medicine':
|
||||||
|
return medicine
|
||||||
|
case 'Other':
|
||||||
|
case 'other':
|
||||||
|
return other
|
||||||
|
case 'Performing Arts':
|
||||||
|
case 'performingarts':
|
||||||
|
return performingarts
|
||||||
|
case 'Philosophy':
|
||||||
|
case 'philosophy':
|
||||||
|
return philosophy
|
||||||
|
case 'Physics & Energy':
|
||||||
|
case 'physics':
|
||||||
|
return physics
|
||||||
|
case 'Politics':
|
||||||
|
case 'politics':
|
||||||
|
return politics
|
||||||
|
case 'Psychology':
|
||||||
|
case 'psychology':
|
||||||
|
return psychology
|
||||||
|
case 'Sociology':
|
||||||
|
case 'sociology':
|
||||||
|
return sociology
|
||||||
|
case 'Sports & Recreation':
|
||||||
|
case 'sports':
|
||||||
|
return sports
|
||||||
|
case 'Theology':
|
||||||
|
case 'theology':
|
||||||
|
return theology
|
||||||
|
case 'Transportation':
|
||||||
|
case 'transport':
|
||||||
|
return transport
|
||||||
|
case 'Urban Planning':
|
||||||
|
case 'urbanplanning':
|
||||||
|
return urbanplanning
|
||||||
|
case 'Visual Arts & Design':
|
||||||
|
case 'visualart':
|
||||||
|
return visualart
|
||||||
|
// technically no category
|
||||||
|
// but corresponding to title of a channel
|
||||||
|
case 'AI For Good':
|
||||||
|
return aiforgood
|
||||||
|
default:
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CategoryImage extends PureComponent<{
|
||||||
|
category: string
|
||||||
|
header?: boolean
|
||||||
|
dimmed?: boolean
|
||||||
|
}> {
|
||||||
|
public render() {
|
||||||
|
const image = categoryImageFile(this.props.category)
|
||||||
|
const classNames = cx(styles.categoryImage, {
|
||||||
|
[styles.header]: this.props.header,
|
||||||
|
[styles.dimmed]: this.props.dimmed
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames}
|
||||||
|
style={{ backgroundImage: `url(${image})` }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
23
admin/src/components/atoms/CategoryLink.tsx
Normal file
23
admin/src/components/atoms/CategoryLink.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
const CategoryLink = ({
|
||||||
|
category,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
category: string
|
||||||
|
children?: any
|
||||||
|
className?: string
|
||||||
|
}) => (
|
||||||
|
<Link
|
||||||
|
to={`/search?categories=${encodeURIComponent(category)}`}
|
||||||
|
className={className}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children || category}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default CategoryLink
|
16
admin/src/components/atoms/Content.module.scss
Normal file
16
admin/src/components/atoms/Content.module.scss
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 0 $spacer / 1.5;
|
||||||
|
max-width: 47rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--small) {
|
||||||
|
padding: 0 $spacer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wide {
|
||||||
|
composes: content;
|
||||||
|
max-width: $break-point--large;
|
||||||
|
}
|
8
admin/src/components/atoms/Content.tsx
Normal file
8
admin/src/components/atoms/Content.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styles from './Content.module.scss'
|
||||||
|
|
||||||
|
const Content = ({ wide, children }: { wide?: boolean; children: any }) => (
|
||||||
|
<div className={wide ? styles.wide : styles.content}>{children}</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Content
|
39
admin/src/components/atoms/Form/Form.module.scss
Normal file
39
admin/src/components/atoms/Form/Form.module.scss
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.form {
|
||||||
|
width: 100%;
|
||||||
|
background: $brand-white;
|
||||||
|
padding: $spacer / 1.5;
|
||||||
|
border: 1px solid $brand-grey-lighter;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--small) {
|
||||||
|
padding: $spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.formMinimal {
|
||||||
|
composes: form;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formHeader {
|
||||||
|
margin-bottom: $spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formTitle {
|
||||||
|
font-size: $font-size-h2;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formDescription {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: $spacer / 2;
|
||||||
|
}
|
29
admin/src/components/atoms/Form/Form.test.tsx
Normal file
29
admin/src/components/atoms/Form/Form.test.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import Form from './Form'
|
||||||
|
|
||||||
|
describe('Form', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(<Form>Hello</Form>)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders title & description when set', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Form title="Hello Title" description="Hello Description">
|
||||||
|
Hello
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
expect(container.querySelector('.formTitle')).toHaveTextContent(
|
||||||
|
'Hello Title'
|
||||||
|
)
|
||||||
|
expect(container.querySelector('.formDescription')).toHaveTextContent(
|
||||||
|
'Hello Description'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can switch to minimal', () => {
|
||||||
|
const { container } = render(<Form minimal>Hello</Form>)
|
||||||
|
expect(container.firstChild).toHaveClass('formMinimal')
|
||||||
|
})
|
||||||
|
})
|
36
admin/src/components/atoms/Form/Form.tsx
Normal file
36
admin/src/components/atoms/Form/Form.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styles from './Form.module.scss'
|
||||||
|
|
||||||
|
const Form = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
children,
|
||||||
|
onSubmit,
|
||||||
|
minimal,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
children: any
|
||||||
|
onSubmit?: any
|
||||||
|
minimal?: boolean
|
||||||
|
}) => (
|
||||||
|
<form
|
||||||
|
className={minimal ? styles.formMinimal : styles.form}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{title && (
|
||||||
|
<header className={styles.formHeader}>
|
||||||
|
<h1 className={styles.formTitle}>{title}</h1>
|
||||||
|
{description && (
|
||||||
|
<p className={styles.formDescription}>{description}</p>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Form
|
7
admin/src/components/atoms/Form/Help.module.scss
Normal file
7
admin/src/components/atoms/Form/Help.module.scss
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.help {
|
||||||
|
font-size: $font-size-small;
|
||||||
|
color: darken($brand-grey-light, 10%);
|
||||||
|
margin-top: $spacer / 4;
|
||||||
|
}
|
13
admin/src/components/atoms/Form/Help.test.tsx
Normal file
13
admin/src/components/atoms/Form/Help.test.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
|
||||||
|
import Help from './Help'
|
||||||
|
|
||||||
|
it('FormHelp renders without crashing', () => {
|
||||||
|
const div = document.createElement('div')
|
||||||
|
ReactDOM.render(
|
||||||
|
<Help>Price of your data set asset in Ocean Tokens.</Help>,
|
||||||
|
div
|
||||||
|
)
|
||||||
|
ReactDOM.unmountComponentAtNode(div)
|
||||||
|
})
|
8
admin/src/components/atoms/Form/Help.tsx
Normal file
8
admin/src/components/atoms/Form/Help.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styles from './Help.module.scss'
|
||||||
|
|
||||||
|
const FormHelp = ({ children }: { children: string }) => (
|
||||||
|
<div className={styles.help}>{children}</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default FormHelp
|
190
admin/src/components/atoms/Form/Input.module.scss
Normal file
190
admin/src/components/atoms/Form/Input.module.scss
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
@import './InputDate.module.scss';
|
||||||
|
|
||||||
|
.inputWrap {
|
||||||
|
background: $brand-gradient;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
padding: 2px;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.isFocused {
|
||||||
|
background: $brand-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div,
|
||||||
|
> div > div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputWrapSearch {
|
||||||
|
composes: inputWrap;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
padding-left: $spacer * 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
left: $spacer / 2;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -.6rem;
|
||||||
|
fill: rgba($brand-grey-light, .7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
font-size: $font-size-base;
|
||||||
|
font-family: $font-family-base;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $brand-black;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
width: 100%;
|
||||||
|
background: $brand-white;
|
||||||
|
padding: $spacer / 3;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
transition: .2s ease-out;
|
||||||
|
min-height: 43px;
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
font-family: $font-family-base;
|
||||||
|
font-size: $font-size-base;
|
||||||
|
color: $brand-grey-light;
|
||||||
|
font-weight: $font-weight-base;
|
||||||
|
transition: .2s ease-out;
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[readonly],
|
||||||
|
&[disabled] {
|
||||||
|
background-color: $brand-grey-lighter;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// &::-webkit-credentials-auto-fill-button,
|
||||||
|
// &::-webkit-caps-lock-indicator {
|
||||||
|
// background: $brand-white;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// &:-webkit-autofill,
|
||||||
|
// &:-webkit-autofill:hover,
|
||||||
|
// &:-webkit-autofill:focus {
|
||||||
|
// -webkit-text-fill-color: $brand-white;
|
||||||
|
// box-shadow: 0 0 0 1000px $brand-black inset;
|
||||||
|
// transition: background-color 5000s ease-in-out 0s;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
composes: input;
|
||||||
|
height: 43px;
|
||||||
|
padding-right: 3rem;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
// custom arrow
|
||||||
|
// stylelint-disable
|
||||||
|
background-image: linear-gradient(45deg, transparent 50%, $brand-purple 50%),
|
||||||
|
linear-gradient(135deg, $brand-purple 50%, transparent 50%),
|
||||||
|
linear-gradient(
|
||||||
|
to right,
|
||||||
|
$brand-pink 1px,
|
||||||
|
lighten($brand-grey-lighter, 5%) 2px,
|
||||||
|
lighten($brand-grey-lighter, 5%)
|
||||||
|
);
|
||||||
|
background-position: calc(100% - 18px) calc(1rem + 5px),
|
||||||
|
calc(100% - 13px) calc(1rem + 5px), 100% 0;
|
||||||
|
background-size: 5px 5px, 5px 5px, 2.5rem 3rem;
|
||||||
|
// stylelint-enable
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
font-family: $font-family-base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radioGroup {
|
||||||
|
margin-top: $spacer / 2;
|
||||||
|
margin-bottom: -2%;
|
||||||
|
|
||||||
|
@media screen and (min-width: $break-point--small) {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radioWrap {
|
||||||
|
position: relative;
|
||||||
|
padding: $spacer / 2;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2%;
|
||||||
|
|
||||||
|
@media screen and (min-width: $break-point--small) {
|
||||||
|
flex: 0 0 49%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio {
|
||||||
|
&:checked + label {
|
||||||
|
border-color: $brand-pink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radioLabel {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
line-height: 1.2;
|
||||||
|
border: 2px solid $brand-grey-lighter;
|
||||||
|
border-radius: .2rem;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
color: $brand-grey;
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size modifiers
|
||||||
|
|
||||||
|
.inputSmall {
|
||||||
|
composes: input;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: $spacer / 4;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
font-size: $font-size-small;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectSmall {
|
||||||
|
composes: select;
|
||||||
|
height: 32px;
|
||||||
|
padding-right: 2rem;
|
||||||
|
|
||||||
|
// custom arrow
|
||||||
|
background-position: calc(100% - 14px) 1rem, calc(100% - 9px) 1rem, 100% 0;
|
||||||
|
background-size: 5px 5px, 5px 5px, 2rem 3rem;
|
||||||
|
}
|
90
admin/src/components/atoms/Form/Input.test.tsx
Normal file
90
admin/src/components/atoms/Form/Input.test.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import Input from './Input'
|
||||||
|
|
||||||
|
describe('Input', () => {
|
||||||
|
it('renders default without crashing', () => {
|
||||||
|
const { container } = render(<Input name="my-input" label="My Input" />)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
expect(container.querySelector('.label')).toHaveTextContent('My Input')
|
||||||
|
expect(container.querySelector('.input')).toHaveAttribute(
|
||||||
|
'id',
|
||||||
|
'my-input'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders as text input by default', () => {
|
||||||
|
const { container } = render(<Input name="my-input" label="My Input" />)
|
||||||
|
expect(container.querySelector('.input')).toHaveAttribute(
|
||||||
|
'type',
|
||||||
|
'text'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders search', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Input name="my-input" label="My Input" type="search" />
|
||||||
|
)
|
||||||
|
expect(container.querySelector('.input')).toHaveAttribute(
|
||||||
|
'type',
|
||||||
|
'search'
|
||||||
|
)
|
||||||
|
expect(container.querySelector('label + div')).toHaveClass(
|
||||||
|
'inputWrapSearch'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders select', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Input
|
||||||
|
name="my-input"
|
||||||
|
label="My Input"
|
||||||
|
type="select"
|
||||||
|
options={['hello', 'hello2']}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(container.querySelector('select')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders textarea', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Input name="my-input" label="My Input" type="textarea" rows={40} />
|
||||||
|
)
|
||||||
|
expect(container.querySelector('textarea')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders radios', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Input
|
||||||
|
name="my-input"
|
||||||
|
label="My Input"
|
||||||
|
type="radio"
|
||||||
|
options={['hello', 'hello2']}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(container.querySelector('input[type=radio]')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders checkboxes', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Input
|
||||||
|
name="my-input"
|
||||||
|
label="My Input"
|
||||||
|
type="checkbox"
|
||||||
|
options={['hello', 'hello2']}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
container.querySelector('input[type=checkbox]')
|
||||||
|
).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders date picker', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Input name="my-input" label="My Input" type="date" />
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
container.querySelector('.react-datepicker-wrapper')
|
||||||
|
).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
221
admin/src/components/atoms/Form/Input.tsx
Normal file
221
admin/src/components/atoms/Form/Input.tsx
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import cx from 'classnames'
|
||||||
|
import React, { PureComponent, FormEvent, ChangeEvent } from 'react'
|
||||||
|
import slugify from '@sindresorhus/slugify'
|
||||||
|
import DatePicker from 'react-datepicker'
|
||||||
|
import { ReactComponent as SearchIcon } from '../../../img/search.svg'
|
||||||
|
import Help from './Help'
|
||||||
|
import Label from './Label'
|
||||||
|
import Row from './Row'
|
||||||
|
import InputGroup from './InputGroup'
|
||||||
|
import styles from './Input.module.scss'
|
||||||
|
|
||||||
|
interface InputProps {
|
||||||
|
name: string
|
||||||
|
label: string
|
||||||
|
placeholder?: string
|
||||||
|
required?: boolean
|
||||||
|
help?: string
|
||||||
|
tag?: string
|
||||||
|
type?: string
|
||||||
|
options?: string[]
|
||||||
|
additionalComponent?: any
|
||||||
|
value?: string
|
||||||
|
onChange?(
|
||||||
|
event:
|
||||||
|
| FormEvent<HTMLInputElement>
|
||||||
|
| ChangeEvent<HTMLInputElement>
|
||||||
|
| ChangeEvent<HTMLSelectElement>
|
||||||
|
| ChangeEvent<HTMLTextAreaElement>
|
||||||
|
): void
|
||||||
|
rows?: number
|
||||||
|
group?: any
|
||||||
|
multiple?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InputState {
|
||||||
|
isFocused: boolean
|
||||||
|
dateCreated?: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Input extends PureComponent<InputProps, InputState> {
|
||||||
|
public state: InputState = {
|
||||||
|
isFocused: false,
|
||||||
|
dateCreated: new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
public inputWrapClasses() {
|
||||||
|
if (this.props.type === 'search') {
|
||||||
|
return styles.inputWrapSearch
|
||||||
|
} else if (this.props.type === 'search' && this.state.isFocused) {
|
||||||
|
return cx(styles.inputWrapSearch, styles.isFocused)
|
||||||
|
} else if (this.state.isFocused && this.props.type !== 'search') {
|
||||||
|
return cx(styles.inputWrap, styles.isFocused)
|
||||||
|
} else {
|
||||||
|
return styles.inputWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleFocus = () => {
|
||||||
|
this.setState({ isFocused: !this.state.isFocused })
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDateChange = (date: Date) => {
|
||||||
|
this.setState({ dateCreated: date })
|
||||||
|
|
||||||
|
const event = {
|
||||||
|
currentTarget: {
|
||||||
|
name: 'dateCreated',
|
||||||
|
value: date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.props.onChange!(event as any) // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputComponent = () => {
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
options,
|
||||||
|
group,
|
||||||
|
name,
|
||||||
|
required,
|
||||||
|
onChange,
|
||||||
|
value
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
const wrapClass = this.inputWrapClasses()
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'select':
|
||||||
|
return (
|
||||||
|
<div className={wrapClass}>
|
||||||
|
<select
|
||||||
|
id={name}
|
||||||
|
className={styles.select}
|
||||||
|
name={name}
|
||||||
|
required={required}
|
||||||
|
onFocus={this.toggleFocus}
|
||||||
|
onBlur={this.toggleFocus}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
<option value="">---</option>
|
||||||
|
{options &&
|
||||||
|
options
|
||||||
|
.sort((a, b) => a.localeCompare(b))
|
||||||
|
.map((option: string, index: number) => (
|
||||||
|
<option key={index} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
case 'textarea':
|
||||||
|
return (
|
||||||
|
<div className={wrapClass}>
|
||||||
|
<textarea
|
||||||
|
id={name}
|
||||||
|
className={styles.input}
|
||||||
|
onFocus={this.toggleFocus}
|
||||||
|
onBlur={this.toggleFocus}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
case 'radio':
|
||||||
|
case 'checkbox':
|
||||||
|
return (
|
||||||
|
<div className={styles.radioGroup}>
|
||||||
|
{options &&
|
||||||
|
options.map((option: string, index: number) => (
|
||||||
|
<div className={styles.radioWrap} key={index}>
|
||||||
|
<input
|
||||||
|
className={styles.radio}
|
||||||
|
id={slugify(option)}
|
||||||
|
type={type}
|
||||||
|
name={name}
|
||||||
|
value={slugify(option)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className={styles.radioLabel}
|
||||||
|
htmlFor={slugify(option)}
|
||||||
|
>
|
||||||
|
{option}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
case 'date':
|
||||||
|
return (
|
||||||
|
<div className={wrapClass}>
|
||||||
|
<DatePicker
|
||||||
|
selected={this.state.dateCreated}
|
||||||
|
onChange={this.handleDateChange}
|
||||||
|
className={styles.input}
|
||||||
|
onFocus={this.toggleFocus}
|
||||||
|
onBlur={this.toggleFocus}
|
||||||
|
id={name}
|
||||||
|
name={name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<div className={wrapClass}>
|
||||||
|
{group ? (
|
||||||
|
<InputGroup>
|
||||||
|
<input
|
||||||
|
id={name}
|
||||||
|
type={type || 'text'}
|
||||||
|
className={styles.input}
|
||||||
|
onFocus={this.toggleFocus}
|
||||||
|
onBlur={this.toggleFocus}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
{group}
|
||||||
|
</InputGroup>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
id={name}
|
||||||
|
type={type || 'text'}
|
||||||
|
className={styles.input}
|
||||||
|
onFocus={this.toggleFocus}
|
||||||
|
onBlur={this.toggleFocus}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === 'search' && <SearchIcon />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
required,
|
||||||
|
help,
|
||||||
|
additionalComponent,
|
||||||
|
multiple
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<Label htmlFor={name} required={required}>
|
||||||
|
{label}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<this.InputComponent />
|
||||||
|
|
||||||
|
{help && <Help>{help}</Help>}
|
||||||
|
|
||||||
|
{multiple && 'hello'}
|
||||||
|
|
||||||
|
{additionalComponent && additionalComponent}
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
70
admin/src/components/atoms/Form/InputDate.module.scss
Normal file
70
admin/src/components/atoms/Form/InputDate.module.scss
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
@import '../../../../node_modules/react-datepicker/dist/react-datepicker-cssmodules.css';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Date picker
|
||||||
|
//
|
||||||
|
:global .react-datepicker {
|
||||||
|
font-family: inherit;
|
||||||
|
color: inherit;
|
||||||
|
border-color: $brand-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .react-datepicker__header {
|
||||||
|
background: $brand-black;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
.react-datepicker__day-name,
|
||||||
|
.react-datepicker__day,
|
||||||
|
.react-datepicker__time-name,
|
||||||
|
.react-datepicker__current-month,
|
||||||
|
.react-datepicker-time__header,
|
||||||
|
.react-datepicker-year-header {
|
||||||
|
color: $brand-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .react-datepicker__current-month,
|
||||||
|
:global .react-datepicker-time__header,
|
||||||
|
:global .react-datepicker-year-header,
|
||||||
|
:global .react-datepicker__day-name {
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .react-datepicker__month-container {
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .react-datepicker-popper {
|
||||||
|
max-width: 16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .react-datepicker-popper[data-placement^='top'] .react-datepicker__triangle:before,
|
||||||
|
:global .react-datepicker__year-read-view--down-arrow:before,
|
||||||
|
:global .react-datepicker__month-read-view--down-arrow:before,
|
||||||
|
:global .react-datepicker__month-year-read-view--down-arrow:before {
|
||||||
|
border-top-color: $brand-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .react-datepicker__day--selected,
|
||||||
|
:global .react-datepicker__day--in-selecting-range,
|
||||||
|
:global .react-datepicker__day--in-range,
|
||||||
|
:global .react-datepicker__month-text--selected,
|
||||||
|
:global .react-datepicker__month-text--in-selecting-range,
|
||||||
|
:global .react-datepicker__month-text--in-range {
|
||||||
|
background-color: $brand-black;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .react-datepicker__day:hover,
|
||||||
|
:global .react-datepicker__month-text:hover {
|
||||||
|
background-color: $brand-pink;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $brand-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .react-datepicker__day--outside-month {
|
||||||
|
color: $brand-grey-light;
|
||||||
|
}
|
39
admin/src/components/atoms/Form/InputGroup.module.scss
Normal file
39
admin/src/components/atoms/Form/InputGroup.module.scss
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.inputGroup {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media screen and (min-width: $break-point--small) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
> input {
|
||||||
|
@media screen and (min-width: $break-point--small) {
|
||||||
|
width: 75%;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> button {
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: -120%;
|
||||||
|
|
||||||
|
@media screen and (min-width: $break-point--small) {
|
||||||
|
position: relative;
|
||||||
|
bottom: auto;
|
||||||
|
width: 25%;
|
||||||
|
height: 100%;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
admin/src/components/atoms/Form/InputGroup.test.tsx
Normal file
10
admin/src/components/atoms/Form/InputGroup.test.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import InputGroup from './InputGroup'
|
||||||
|
|
||||||
|
describe('InputGroup', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(<InputGroup>Hello</InputGroup>)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
8
admin/src/components/atoms/Form/InputGroup.tsx
Normal file
8
admin/src/components/atoms/Form/InputGroup.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styles from './InputGroup.module.scss'
|
||||||
|
|
||||||
|
const InputGroup = ({ children }: { children: any }) => (
|
||||||
|
<div className={styles.inputGroup}>{children}</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default InputGroup
|
22
admin/src/components/atoms/Form/Label.module.scss
Normal file
22
admin/src/components/atoms/Form/Label.module.scss
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: $brand-grey;
|
||||||
|
font-size: $font-size-base;
|
||||||
|
font-family: $font-family-title;
|
||||||
|
line-height: 1.2;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: $spacer / 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
composes: label;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: '*';
|
||||||
|
font-size: $font-size-base;
|
||||||
|
color: $brand-grey-light;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: .1rem;
|
||||||
|
}
|
||||||
|
}
|
20
admin/src/components/atoms/Form/Label.test.tsx
Normal file
20
admin/src/components/atoms/Form/Label.test.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import Label from './Label'
|
||||||
|
|
||||||
|
describe('Label', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(<Label htmlFor="hello">Hello</Label>)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders required state', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Label required htmlFor="hello">
|
||||||
|
Hello
|
||||||
|
</Label>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toHaveAttribute('title', 'Required')
|
||||||
|
expect(container.firstChild).toHaveClass('required')
|
||||||
|
})
|
||||||
|
})
|
22
admin/src/components/atoms/Form/Label.tsx
Normal file
22
admin/src/components/atoms/Form/Label.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styles from './Label.module.scss'
|
||||||
|
|
||||||
|
const Label = ({
|
||||||
|
required,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
required?: boolean
|
||||||
|
children: string
|
||||||
|
htmlFor: string
|
||||||
|
}) => (
|
||||||
|
<label
|
||||||
|
className={required ? styles.required : styles.label}
|
||||||
|
title={required ? 'Required' : ''}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Label
|
5
admin/src/components/atoms/Form/Row.module.scss
Normal file
5
admin/src/components/atoms/Form/Row.module.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin-bottom: $spacer;
|
||||||
|
}
|
10
admin/src/components/atoms/Form/Row.test.tsx
Normal file
10
admin/src/components/atoms/Form/Row.test.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import Row from './Row'
|
||||||
|
|
||||||
|
describe('Row', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(<Row>Hello</Row>)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
8
admin/src/components/atoms/Form/Row.tsx
Normal file
8
admin/src/components/atoms/Form/Row.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styles from './Row.module.scss'
|
||||||
|
|
||||||
|
const Row = ({ children }: { children: any }) => (
|
||||||
|
<div className={styles.row}>{children}</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Row
|
10
admin/src/components/atoms/Markdown.test.tsx
Normal file
10
admin/src/components/atoms/Markdown.test.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import Markdown from './Markdown'
|
||||||
|
|
||||||
|
describe('Markdown', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(<Markdown text={'#hello'} />)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
18
admin/src/components/atoms/Markdown.tsx
Normal file
18
admin/src/components/atoms/Markdown.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactMarkdown from 'react-markdown'
|
||||||
|
|
||||||
|
const Markdown = ({
|
||||||
|
text,
|
||||||
|
className
|
||||||
|
}: {
|
||||||
|
text: string
|
||||||
|
className?: string
|
||||||
|
}) => {
|
||||||
|
// fix react-markdown \n transformation
|
||||||
|
// https://github.com/rexxars/react-markdown/issues/105#issuecomment-351585313
|
||||||
|
const textCleaned = text.replace(/\\n/g, '\n ')
|
||||||
|
|
||||||
|
return <ReactMarkdown source={textCleaned} className={className} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Markdown
|
71
admin/src/components/atoms/Seo.tsx
Normal file
71
admin/src/components/atoms/Seo.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Helmet from 'react-helmet'
|
||||||
|
import { withRouter, RouteComponentProps } from 'react-router-dom'
|
||||||
|
import meta from '../../data/meta.json'
|
||||||
|
import imageDefault from '../../img/share.png'
|
||||||
|
|
||||||
|
const MetaTags = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
url,
|
||||||
|
image
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
url: string
|
||||||
|
image: string
|
||||||
|
}) => (
|
||||||
|
<Helmet defaultTitle={meta.title} titleTemplate={`%s - ${meta.title}`}>
|
||||||
|
<html lang="en" />
|
||||||
|
|
||||||
|
{title && <title>{title}</title>}
|
||||||
|
|
||||||
|
{/* General tags */}
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
<meta name="image" content={image} />
|
||||||
|
<link rel="canonical" href={url} />
|
||||||
|
|
||||||
|
{/* OpenGraph tags */}
|
||||||
|
<meta property="og:url" content={url} />
|
||||||
|
<meta property="og:title" content={title} />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
<meta property="og:image" content={image} />
|
||||||
|
|
||||||
|
{/* Twitter Card tags */}
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:creator" content="@oceanprotocol" />
|
||||||
|
<meta name="twitter:title" content={title} />
|
||||||
|
<meta name="twitter:description" content={description} />
|
||||||
|
<meta name="twitter:image" content={image} />
|
||||||
|
|
||||||
|
{/* Prevent search engine indexing except for live */}
|
||||||
|
{window.location.hostname !== 'commons.oceanprotocol.com' && (
|
||||||
|
<meta name="robots" content="noindex,nofollow" />
|
||||||
|
)}
|
||||||
|
</Helmet>
|
||||||
|
)
|
||||||
|
|
||||||
|
interface SeoProps extends RouteComponentProps {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
shareImage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Seo = ({ title, description, shareImage, location }: SeoProps) => {
|
||||||
|
title = title || meta.title
|
||||||
|
description = description || meta.description
|
||||||
|
shareImage = shareImage || meta.url + imageDefault
|
||||||
|
|
||||||
|
const url = meta.url + location.pathname + location.search
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MetaTags
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
url={url}
|
||||||
|
image={shareImage}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(Seo)
|
50
admin/src/components/atoms/Spinner.module.scss
Normal file
50
admin/src/components/atoms/Spinner.module.scss
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: $spacer * $line-height;
|
||||||
|
margin-bottom: $spacer / 2;
|
||||||
|
line-height: 1.3;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-top: -20px;
|
||||||
|
margin-left: -10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid $brand-purple;
|
||||||
|
border-top-color: $brand-violet;
|
||||||
|
animation: spinner .6s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinnerMessage {
|
||||||
|
color: $brand-grey-light;
|
||||||
|
padding-top: $spacer / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
composes: spinner;
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
width: $font-size-small;
|
||||||
|
height: $font-size-small;
|
||||||
|
margin-top: -($font-size-small);
|
||||||
|
margin-left: -($font-size-small / 2);
|
||||||
|
border-width: .1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spinner {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
27
admin/src/components/atoms/Spinner.tsx
Normal file
27
admin/src/components/atoms/Spinner.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styles from './Spinner.module.scss'
|
||||||
|
|
||||||
|
const Spinner = ({
|
||||||
|
message,
|
||||||
|
small,
|
||||||
|
className
|
||||||
|
}: {
|
||||||
|
message?: string
|
||||||
|
small?: boolean
|
||||||
|
className?: string
|
||||||
|
}) => {
|
||||||
|
const classes = className || (small ? styles.small : styles.spinner)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes}>
|
||||||
|
{message && (
|
||||||
|
<div
|
||||||
|
className={styles.spinnerMessage}
|
||||||
|
dangerouslySetInnerHTML={{ __html: message }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Spinner
|
@ -0,0 +1,34 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.status {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
cursor: help;
|
||||||
|
padding: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// default: red square
|
||||||
|
.statusIndicator {
|
||||||
|
width: $font-size-small;
|
||||||
|
height: $font-size-small;
|
||||||
|
display: block;
|
||||||
|
background: $red;
|
||||||
|
}
|
||||||
|
|
||||||
|
// yellow triangle
|
||||||
|
.statusIndicatorCloseEnough {
|
||||||
|
composes: statusIndicator;
|
||||||
|
background: none;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: $font-size-small / 1.7 solid transparent;
|
||||||
|
border-right: $font-size-small / 1.7 solid transparent;
|
||||||
|
border-bottom: $font-size-small solid $yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// green circle
|
||||||
|
.statusIndicatorActive {
|
||||||
|
composes: statusIndicator;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $green;
|
||||||
|
}
|
35
admin/src/components/molecules/AccountStatus/Indicator.tsx
Normal file
35
admin/src/components/molecules/AccountStatus/Indicator.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import cx from 'classnames'
|
||||||
|
import { User } from '../../../context'
|
||||||
|
import styles from './Indicator.module.scss'
|
||||||
|
|
||||||
|
const Indicator = ({
|
||||||
|
className,
|
||||||
|
togglePopover,
|
||||||
|
forwardedRef
|
||||||
|
}: {
|
||||||
|
className?: string
|
||||||
|
togglePopover: () => void
|
||||||
|
forwardedRef: (ref: HTMLElement | null) => void
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className={cx(styles.status, className)}
|
||||||
|
onMouseOver={togglePopover}
|
||||||
|
onMouseOut={togglePopover}
|
||||||
|
ref={forwardedRef}
|
||||||
|
>
|
||||||
|
<User.Consumer>
|
||||||
|
{states =>
|
||||||
|
!states.isWeb3 ? (
|
||||||
|
<span className={styles.statusIndicator} />
|
||||||
|
) : !states.isLogged || !states.isOceanNetwork ? (
|
||||||
|
<span className={styles.statusIndicatorCloseEnough} />
|
||||||
|
) : states.isLogged ? (
|
||||||
|
<span className={styles.statusIndicatorActive} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
</User.Consumer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Indicator
|
@ -0,0 +1,56 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
$popoverWidth: 18rem;
|
||||||
|
|
||||||
|
.popover {
|
||||||
|
position: relative;
|
||||||
|
width: $popoverWidth;
|
||||||
|
padding: $spacer / 2;
|
||||||
|
background: $brand-black;
|
||||||
|
border-radius: .1rem;
|
||||||
|
border: .1rem solid $brand-grey-light;
|
||||||
|
box-shadow: 0 6px 16px 0 rgba($brand-black, .3);
|
||||||
|
color: $brand-grey-light;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
animation: showPopup .2s ease-in forwards;
|
||||||
|
white-space: initial;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes showPopup {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popoverInfoline {
|
||||||
|
border-bottom: .05rem solid $brand-grey;
|
||||||
|
padding: $spacer / 3 0;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: $font-size-small;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance {
|
||||||
|
font-size: $font-size-small;
|
||||||
|
margin-left: $spacer / 2;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import Popover from './Popover'
|
||||||
|
import { userMock, userMockConnected } from '../../../../__mocks__/user-mock'
|
||||||
|
import { User } from '../../../context'
|
||||||
|
|
||||||
|
describe('Popover', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={userMock}>
|
||||||
|
<Popover forwardedRef={() => null} style={{}} />
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders connected without crashing', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={userMockConnected}>
|
||||||
|
<Popover forwardedRef={() => null} style={{}} />
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders correct network', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={{ ...userMockConnected, network: 'Nile' }}>
|
||||||
|
<Popover forwardedRef={() => null} style={{}} />
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
expect(container.firstChild).toHaveTextContent('Connected to Nile')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders with wrong network', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider
|
||||||
|
value={{
|
||||||
|
...userMockConnected,
|
||||||
|
isOceanNetwork: false,
|
||||||
|
network: '1'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Popover forwardedRef={() => null} style={{}} />
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
expect(container.firstChild).toHaveTextContent(
|
||||||
|
'Please connect to Custom RPC'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
67
admin/src/components/molecules/AccountStatus/Popover.tsx
Normal file
67
admin/src/components/molecules/AccountStatus/Popover.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React, { PureComponent } from 'react'
|
||||||
|
import Account from '../../atoms/Account'
|
||||||
|
import { User } from '../../../context'
|
||||||
|
import styles from './Popover.module.scss'
|
||||||
|
|
||||||
|
export default class Popover extends PureComponent<{
|
||||||
|
forwardedRef: (ref: HTMLElement | null) => void
|
||||||
|
style: React.CSSProperties
|
||||||
|
}> {
|
||||||
|
public render() {
|
||||||
|
const {
|
||||||
|
account,
|
||||||
|
balance,
|
||||||
|
network,
|
||||||
|
isWeb3,
|
||||||
|
isOceanNetwork
|
||||||
|
} = this.context
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.popover}
|
||||||
|
ref={this.props.forwardedRef}
|
||||||
|
style={this.props.style}
|
||||||
|
>
|
||||||
|
{!isWeb3 ? (
|
||||||
|
<div className={styles.popoverInfoline}>
|
||||||
|
No Web3 detected. Use a browser with MetaMask installed
|
||||||
|
to publish assets.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className={styles.popoverInfoline}>
|
||||||
|
<Account account={account} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{account && balance && (
|
||||||
|
<div className={styles.popoverInfoline}>
|
||||||
|
<span
|
||||||
|
className={styles.balance}
|
||||||
|
title={(balance.eth / 1e18).toFixed(10)}
|
||||||
|
>
|
||||||
|
<strong>
|
||||||
|
{(balance.eth / 1e18)
|
||||||
|
.toFixed(3)
|
||||||
|
.slice(0, -1)}
|
||||||
|
</strong>{' '}
|
||||||
|
ETH
|
||||||
|
</span>
|
||||||
|
<span className={styles.balance}>
|
||||||
|
<strong>{balance.ocn}</strong> OCEAN
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.popoverInfoline}>
|
||||||
|
{network && !isOceanNetwork
|
||||||
|
? 'Please connect to Custom RPC\n https://pacific.oceanprotocol.com'
|
||||||
|
: network && `Connected to ${network} network`}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Popover.contextType = User
|
20
admin/src/components/molecules/AccountStatus/index.test.tsx
Normal file
20
admin/src/components/molecules/AccountStatus/index.test.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render, fireEvent } from '@testing-library/react'
|
||||||
|
import AccountStatus from '.'
|
||||||
|
|
||||||
|
describe('AccountStatus', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(<AccountStatus />)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('togglePopover fires', () => {
|
||||||
|
const { container } = render(<AccountStatus />)
|
||||||
|
|
||||||
|
const indicator = container.querySelector('.statusIndicator')
|
||||||
|
|
||||||
|
indicator && fireEvent.mouseOver(indicator)
|
||||||
|
expect(container.querySelector('.popover')).toBeInTheDocument()
|
||||||
|
indicator && fireEvent.mouseOut(indicator)
|
||||||
|
})
|
||||||
|
})
|
54
admin/src/components/molecules/AccountStatus/index.tsx
Normal file
54
admin/src/components/molecules/AccountStatus/index.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React, { PureComponent } from 'react'
|
||||||
|
import { Manager, Reference, Popper } from 'react-popper'
|
||||||
|
import AccountPopover from './Popover'
|
||||||
|
import AccountIndicator from './Indicator'
|
||||||
|
|
||||||
|
interface AccountStatusProps {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AccountStatusState {
|
||||||
|
isPopoverOpen: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AccountStatus extends PureComponent<
|
||||||
|
AccountStatusProps,
|
||||||
|
AccountStatusState
|
||||||
|
> {
|
||||||
|
public state = {
|
||||||
|
isPopoverOpen: false
|
||||||
|
}
|
||||||
|
|
||||||
|
private togglePopover() {
|
||||||
|
this.setState(prevState => ({
|
||||||
|
isPopoverOpen: !prevState.isPopoverOpen
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<Manager>
|
||||||
|
<Reference>
|
||||||
|
{({ ref }) => (
|
||||||
|
<AccountIndicator
|
||||||
|
togglePopover={() => this.togglePopover()}
|
||||||
|
className={this.props.className}
|
||||||
|
forwardedRef={ref}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Reference>
|
||||||
|
{this.state.isPopoverOpen && (
|
||||||
|
<Popper placement="auto">
|
||||||
|
{({ ref, style, placement }) => (
|
||||||
|
<AccountPopover
|
||||||
|
forwardedRef={ref}
|
||||||
|
style={style}
|
||||||
|
data-placement={placement}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Popper>
|
||||||
|
)}
|
||||||
|
</Manager>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
95
admin/src/components/molecules/AssetTeaser.module.scss
Normal file
95
admin/src/components/molecules/AssetTeaser.module.scss
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
.asset {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
> a {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
padding: $spacer;
|
||||||
|
border: 1px solid $brand-grey-lighter;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
background: $brand-white;
|
||||||
|
color: inherit;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: inherit;
|
||||||
|
border-color: $brand-pink;
|
||||||
|
transform: none;
|
||||||
|
|
||||||
|
// category image
|
||||||
|
> div:first-child {
|
||||||
|
opacity: 1;
|
||||||
|
background-size: 105%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: $font-size-large;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimal {
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.assetList {
|
||||||
|
> a {
|
||||||
|
color: $brand-grey-dark;
|
||||||
|
border-bottom: 1px solid $brand-grey-lighter;
|
||||||
|
padding-top: $spacer / 2;
|
||||||
|
padding-bottom: $spacer / 2;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: $brand-pink;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: $font-size-base;
|
||||||
|
color: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
&,
|
||||||
|
p,
|
||||||
|
strong,
|
||||||
|
a,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5 {
|
||||||
|
font-weight: $font-weight-base;
|
||||||
|
font-family: $font-family-base;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
color: $brand-grey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
font-size: $font-size-small;
|
||||||
|
color: $brand-grey-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assetFooter {
|
||||||
|
margin-top: $spacer / 2;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
color: $brand-grey-light;
|
||||||
|
}
|
58
admin/src/components/molecules/AssetTeaser.tsx
Normal file
58
admin/src/components/molecules/AssetTeaser.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import moment from 'moment'
|
||||||
|
import Dotdotdot from 'react-dotdotdot'
|
||||||
|
import cx from 'classnames'
|
||||||
|
import styles from './AssetTeaser.module.scss'
|
||||||
|
import CategoryImage from '../atoms/CategoryImage'
|
||||||
|
|
||||||
|
const AssetTeaser = ({
|
||||||
|
asset,
|
||||||
|
list,
|
||||||
|
minimal
|
||||||
|
}: {
|
||||||
|
asset: any
|
||||||
|
list?: boolean
|
||||||
|
minimal?: boolean
|
||||||
|
}) => {
|
||||||
|
const { metadata } = asset.findServiceByType('Metadata')
|
||||||
|
const { base } = metadata
|
||||||
|
|
||||||
|
return list ? (
|
||||||
|
<article className={styles.assetList}>
|
||||||
|
<Link to={`/asset/${asset.id}`}>
|
||||||
|
<h1>{base.name}</h1>
|
||||||
|
<div
|
||||||
|
className={styles.date}
|
||||||
|
title={`Published on ${base.datePublished}`}
|
||||||
|
>
|
||||||
|
{moment(base.datePublished, 'YYYYMMDD').fromNow()}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</article>
|
||||||
|
) : (
|
||||||
|
<article
|
||||||
|
className={
|
||||||
|
minimal ? cx(styles.asset, styles.minimal) : styles.asset
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Link to={`/asset/${asset.id}`}>
|
||||||
|
{base.categories && !minimal && (
|
||||||
|
<CategoryImage dimmed category={base.categories[0]} />
|
||||||
|
)}
|
||||||
|
<h1>{base.name}</h1>
|
||||||
|
|
||||||
|
{!minimal && (
|
||||||
|
<div className={styles.description}>
|
||||||
|
<Dotdotdot clamp={3}>{base.description}</Dotdotdot>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<footer className={styles.assetFooter}>
|
||||||
|
{base.categories && <div>{base.categories[0]}</div>}
|
||||||
|
</footer>
|
||||||
|
</Link>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AssetTeaser
|
72
admin/src/components/molecules/Pagination.module.scss
Normal file
72
admin/src/components/molecules/Pagination.module.scss
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: $spacer * 2;
|
||||||
|
margin-bottom: $spacer;
|
||||||
|
padding-left: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.number {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
padding: $spacer / 4 $spacer / 2;
|
||||||
|
margin-left: -1px;
|
||||||
|
margin-top: -1px;
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid $brand-grey-lighter;
|
||||||
|
min-width: 3.5rem;
|
||||||
|
|
||||||
|
&,
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
transform: none;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
background: lighten($brand-grey-lighter, 7%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.current,
|
||||||
|
.prev,
|
||||||
|
.next,
|
||||||
|
.break {
|
||||||
|
composes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current {
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&,
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
color: $brand-grey-light;
|
||||||
|
background: lighten($brand-grey-lighter, 7%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.next {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prevNextDisabled {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
17
admin/src/components/molecules/Pagination.test.tsx
Normal file
17
admin/src/components/molecules/Pagination.test.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import Pagination from './Pagination'
|
||||||
|
|
||||||
|
describe('Pagination', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Pagination
|
||||||
|
totalPages={20}
|
||||||
|
currentPage={1}
|
||||||
|
handlePageClick={() => Promise.resolve()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
container.firstChild && expect(container.firstChild.nodeName).toBe('UL')
|
||||||
|
})
|
||||||
|
})
|
72
admin/src/components/molecules/Pagination.tsx
Normal file
72
admin/src/components/molecules/Pagination.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import React, { PureComponent } from 'react'
|
||||||
|
import ReactPaginate from 'react-paginate'
|
||||||
|
import styles from './Pagination.module.scss'
|
||||||
|
|
||||||
|
interface PaginationProps {
|
||||||
|
totalPages: number
|
||||||
|
currentPage: number
|
||||||
|
handlePageClick(data: { selected: number }): Promise<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PaginationState {
|
||||||
|
smallViewport: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Pagination extends PureComponent<
|
||||||
|
PaginationProps,
|
||||||
|
PaginationState
|
||||||
|
> {
|
||||||
|
public state = { smallViewport: true }
|
||||||
|
private mq = window.matchMedia && window.matchMedia('(min-width: 600px)')
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
if (window.matchMedia) {
|
||||||
|
this.mq.addListener(this.viewportChange)
|
||||||
|
this.viewportChange(this.mq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
if (window.matchMedia) {
|
||||||
|
this.mq.removeListener(this.viewportChange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private viewportChange = (mq: { matches: boolean }) => {
|
||||||
|
if (mq.matches) {
|
||||||
|
this.setState({ smallViewport: false })
|
||||||
|
} else {
|
||||||
|
this.setState({ smallViewport: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { totalPages, currentPage, handlePageClick } = this.props
|
||||||
|
const { smallViewport } = this.state
|
||||||
|
|
||||||
|
return (
|
||||||
|
totalPages > 1 && (
|
||||||
|
<ReactPaginate
|
||||||
|
pageCount={totalPages}
|
||||||
|
// react-pagination starts counting at 0, we start at 1
|
||||||
|
initialPage={currentPage - 1}
|
||||||
|
// adapt based on media query match
|
||||||
|
marginPagesDisplayed={smallViewport ? 0 : 1}
|
||||||
|
pageRangeDisplayed={smallViewport ? 3 : 6}
|
||||||
|
onPageChange={data => handlePageClick(data)}
|
||||||
|
disableInitialCallback
|
||||||
|
previousLabel={'←'}
|
||||||
|
nextLabel={'→'}
|
||||||
|
breakLabel={'...'}
|
||||||
|
containerClassName={styles.pagination}
|
||||||
|
pageLinkClassName={styles.number}
|
||||||
|
activeLinkClassName={styles.current}
|
||||||
|
previousLinkClassName={styles.prev}
|
||||||
|
nextLinkClassName={styles.next}
|
||||||
|
disabledClassName={styles.prevNextDisabled}
|
||||||
|
breakLinkClassName={styles.break}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
composes: spinner, small from '../../atoms/Spinner.module.scss';
|
||||||
|
margin-right: $spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit {
|
||||||
|
margin-left: $spacer / 8;
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: $brand-grey-light;
|
||||||
|
font-size: $font-size-mini;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.network {
|
||||||
|
color: $brand-grey-light;
|
||||||
|
text-transform: capitalize;
|
||||||
|
margin-left: $spacer / 8;
|
||||||
|
font-size: $font-size-mini;
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import VersionNumber from './VersionNumber'
|
||||||
|
|
||||||
|
describe('VersionNumber', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(<VersionNumber name="Commons" />)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders with all props set', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<VersionNumber
|
||||||
|
name="Commons"
|
||||||
|
version="6.6.6"
|
||||||
|
network="Nile"
|
||||||
|
commit="xxxxxxxxxxx"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
expect(container.firstChild).toHaveTextContent('6.6.6')
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { OceanPlatformTechStatus } from '@oceanprotocol/squid'
|
||||||
|
import slugify from '@sindresorhus/slugify'
|
||||||
|
import Spinner from '../../atoms/Spinner'
|
||||||
|
import styles from './VersionNumber.module.scss'
|
||||||
|
|
||||||
|
const VersionNumber = ({
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
network,
|
||||||
|
status,
|
||||||
|
commit
|
||||||
|
}: {
|
||||||
|
name: string
|
||||||
|
version?: string
|
||||||
|
network?: string
|
||||||
|
status?: OceanPlatformTechStatus
|
||||||
|
commit?: string
|
||||||
|
}) =>
|
||||||
|
version ? (
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
href={`https://github.com/oceanprotocol/${slugify(
|
||||||
|
name
|
||||||
|
)}/releases/tag/v${version}`}
|
||||||
|
title="Go to release on GitHub"
|
||||||
|
>
|
||||||
|
<code>v{version}</code>
|
||||||
|
</a>
|
||||||
|
{commit && (
|
||||||
|
<a
|
||||||
|
href={`https://github.com/oceanprotocol/${slugify(
|
||||||
|
name
|
||||||
|
)}/commit/${commit}`}
|
||||||
|
className={styles.commit}
|
||||||
|
title={`Go to commit ${commit} on GitHub`}
|
||||||
|
>
|
||||||
|
<code>{commit.substring(0, 7)}</code>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{network && <span className={styles.network}>{` ${network}`}</span>}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
{status === OceanPlatformTechStatus.Loading ? (
|
||||||
|
<Spinner className={styles.spinner} small />
|
||||||
|
) : (
|
||||||
|
status || 'Could not get version'
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default VersionNumber
|
@ -0,0 +1,37 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.status {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: $spacer / 2;
|
||||||
|
padding-bottom: $spacer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.element {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: $spacer / 2;
|
||||||
|
margin-right: $spacer / 2;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator,
|
||||||
|
.indicatorActive {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: $spacer / 4;
|
||||||
|
margin-bottom: -.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
composes: statusIndicator from '../AccountStatus/Indicator.module.scss';
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicatorActive {
|
||||||
|
composes: statusIndicatorActive from '../AccountStatus/Indicator.module.scss';
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicatorLabel {
|
||||||
|
font-family: $font-family-title;
|
||||||
|
color: $brand-grey;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import VersionStatus from './VersionStatus'
|
||||||
|
|
||||||
|
describe('VersionStatus', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<VersionStatus
|
||||||
|
status={{ ok: false, contracts: false, network: false }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders true states', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<VersionStatus
|
||||||
|
status={{ ok: true, contracts: false, network: false }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styles from './VersionStatus.module.scss'
|
||||||
|
|
||||||
|
const statusInfo: { [key: string]: string } = {
|
||||||
|
ok: 'Shows if connection to all component endpoints can be established.',
|
||||||
|
network: 'Shows if all components are on the same network.',
|
||||||
|
contracts: 'Shows if contracts loaded by components are the same version.'
|
||||||
|
}
|
||||||
|
|
||||||
|
const VersionStatus = ({
|
||||||
|
status
|
||||||
|
}: {
|
||||||
|
status: { ok: boolean; network: boolean; contracts: boolean }
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.status}>
|
||||||
|
{Object.entries(status).map(([key, value]) => (
|
||||||
|
<div
|
||||||
|
className={styles.element}
|
||||||
|
key={key}
|
||||||
|
title={statusInfo[key]}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
value === true
|
||||||
|
? styles.indicatorActive
|
||||||
|
: styles.indicator
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
<span className={styles.indicatorLabel}>
|
||||||
|
{key === 'ok' ? 'components' : key}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VersionStatus
|
@ -0,0 +1,57 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.tableWrap {
|
||||||
|
// make 'em scrollable
|
||||||
|
overflow: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
border-top: 1px solid $brand-grey-lighter;
|
||||||
|
|
||||||
|
table {
|
||||||
|
margin-left: $spacer;
|
||||||
|
width: calc(100% - #{$spacer});
|
||||||
|
margin-bottom: -1px;
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: $spacer / 6 $spacer / 2;
|
||||||
|
|
||||||
|
// stylelint-disable-next-line selector-max-compound-selectors
|
||||||
|
&,
|
||||||
|
code {
|
||||||
|
font-size: $font-size-mini;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: $spacer / 4 $spacer / 2;
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
// stylelint-disable-next-line selector-no-qualifying-type
|
||||||
|
&[colspan] {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $brand-grey;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
&,
|
||||||
|
code {
|
||||||
|
color: $brand-pink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
min-width: 15rem;
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import { VersionTableContracts } from './VersionTable'
|
||||||
|
|
||||||
|
describe('VersionTableContracts', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<VersionTableContracts
|
||||||
|
contracts={{ hello: 'hello', hello2: 'hello2' }}
|
||||||
|
network="nile"
|
||||||
|
keeperVersion="6.6.6"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders correct Submarine links', () => {
|
||||||
|
const { container, rerender } = render(
|
||||||
|
<VersionTableContracts
|
||||||
|
contracts={{ hello: 'hello', hello2: 'hello2' }}
|
||||||
|
network="duero"
|
||||||
|
keeperVersion="6.6.6"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(container.querySelector('tr:last-child a').href).toMatch(
|
||||||
|
/submarine.duero.dev-ocean/
|
||||||
|
)
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<VersionTableContracts
|
||||||
|
contracts={{ hello: 'hello', hello2: 'hello2' }}
|
||||||
|
network="pacific"
|
||||||
|
keeperVersion="6.6.6"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(container.querySelector('tr:last-child a').href).toMatch(
|
||||||
|
/submarine.pacific.dev-ocean/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
114
admin/src/components/molecules/VersionNumbers/VersionTable.tsx
Normal file
114
admin/src/components/molecules/VersionNumbers/VersionTable.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { VersionNumbersState } from '.'
|
||||||
|
import VersionTableRow from './VersionTableRow'
|
||||||
|
import styles from './VersionTable.module.scss'
|
||||||
|
import VersionNumber from './VersionNumber'
|
||||||
|
|
||||||
|
import {
|
||||||
|
serviceUri,
|
||||||
|
nodeUri,
|
||||||
|
aquariusUri,
|
||||||
|
brizoUri,
|
||||||
|
brizoAddress,
|
||||||
|
secretStoreUri,
|
||||||
|
faucetUri
|
||||||
|
} from '../../../config'
|
||||||
|
|
||||||
|
const commonsConfig = {
|
||||||
|
serviceUri,
|
||||||
|
nodeUri,
|
||||||
|
aquariusUri,
|
||||||
|
brizoUri,
|
||||||
|
brizoAddress,
|
||||||
|
secretStoreUri,
|
||||||
|
faucetUri
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VersionTableContracts = ({
|
||||||
|
contracts,
|
||||||
|
network,
|
||||||
|
keeperVersion
|
||||||
|
}: {
|
||||||
|
contracts: {
|
||||||
|
[contractName: string]: string
|
||||||
|
}
|
||||||
|
network: string
|
||||||
|
keeperVersion?: string
|
||||||
|
}) => (
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Keeper Contracts</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<VersionNumber
|
||||||
|
name={'Keeper Contracts'}
|
||||||
|
version={keeperVersion}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{contracts &&
|
||||||
|
Object.keys(contracts)
|
||||||
|
// sort alphabetically
|
||||||
|
.sort((a, b) => a.localeCompare(b))
|
||||||
|
.map(key => {
|
||||||
|
const submarineLink = `https://submarine${
|
||||||
|
network === 'duero'
|
||||||
|
? '.duero'
|
||||||
|
: network === 'pacific'
|
||||||
|
? '.pacific'
|
||||||
|
: ''
|
||||||
|
}.dev-ocean.com/address/${contracts[key]}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={key}>
|
||||||
|
<td>
|
||||||
|
<code className={styles.label}>{key}</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href={submarineLink}>
|
||||||
|
<code>{contracts[key]}</code>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const VersionTableCommons = () => (
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{Object.entries(commonsConfig).map(([key, value]) => (
|
||||||
|
<tr key={key}>
|
||||||
|
<td>
|
||||||
|
<code className={styles.label}>{key}</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code>{value}</code>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)
|
||||||
|
|
||||||
|
const VersionTable = ({ data }: { data: VersionNumbersState }) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.tableWrap}>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<tbody>
|
||||||
|
{Object.entries(data)
|
||||||
|
.filter(([key]) => key !== 'status')
|
||||||
|
.map(([key, value]) => (
|
||||||
|
<VersionTableRow key={key} value={value} />
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VersionTable
|
@ -0,0 +1,15 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
display: inline-block;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
margin-left: -1rem;
|
||||||
|
margin-top: -.1rem;
|
||||||
|
padding-right: .5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $brand-grey-light;
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import useCollapse from 'react-collapsed'
|
||||||
|
import slugify from '@sindresorhus/slugify'
|
||||||
|
import styles from './VersionTableRow.module.scss'
|
||||||
|
import { VersionTableContracts, VersionTableCommons } from './VersionTable'
|
||||||
|
import VersionNumber from './VersionNumber'
|
||||||
|
|
||||||
|
const VersionTableRow = ({ value }: { value: any }) => {
|
||||||
|
const collapseStyles = {
|
||||||
|
transitionDuration: '0.01s'
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandStyles = {
|
||||||
|
transitionDuration: '0.01s',
|
||||||
|
transitionTimingFunction: 'ease-out'
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getCollapseProps, getToggleProps, isOpen } = useCollapse({
|
||||||
|
collapseStyles,
|
||||||
|
expandStyles
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{(value.name === 'Commons' || value.contracts) && (
|
||||||
|
<button className={styles.handle} {...getToggleProps()}>
|
||||||
|
{isOpen ? (
|
||||||
|
<span>▼</span>
|
||||||
|
) : (
|
||||||
|
<span>►</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
href={`https://github.com/oceanprotocol/${slugify(
|
||||||
|
value.name || value.software
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
<strong>{value.name || value.software}</strong>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<VersionNumber
|
||||||
|
name={value.name || value.software}
|
||||||
|
version={value.version}
|
||||||
|
status={value.status}
|
||||||
|
network={value.network}
|
||||||
|
commit={value.commit}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{value.name === 'Commons' && (
|
||||||
|
<tr {...getCollapseProps()}>
|
||||||
|
<td colSpan={2}>
|
||||||
|
<VersionTableCommons />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{value.contracts && (
|
||||||
|
<tr {...getCollapseProps()}>
|
||||||
|
<td colSpan={2}>
|
||||||
|
<VersionTableContracts
|
||||||
|
contracts={value.contracts}
|
||||||
|
network={value.network || ''}
|
||||||
|
keeperVersion={value.keeperVersion}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VersionTableRow
|
@ -0,0 +1,16 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.versions {
|
||||||
|
margin-top: $spacer * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versionsTitle {
|
||||||
|
font-size: $font-size-large;
|
||||||
|
margin-bottom: $spacer / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versionsMinimal {
|
||||||
|
font-family: $font-family-monospace;
|
||||||
|
font-size: $font-size-mini;
|
||||||
|
margin-top: $spacer;
|
||||||
|
}
|
85
admin/src/components/molecules/VersionNumbers/index.test.tsx
Normal file
85
admin/src/components/molecules/VersionNumbers/index.test.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import mockAxios from 'jest-mock-axios'
|
||||||
|
import { StateMock } from '@react-mock/state'
|
||||||
|
import VersionNumbers from '.'
|
||||||
|
|
||||||
|
import { User } from '../../../context'
|
||||||
|
import { userMockConnected } from '../../../../__mocks__/user-mock'
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockAxios.reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
const stateMockIncomplete = {
|
||||||
|
commons: {
|
||||||
|
name: 'Commons',
|
||||||
|
version: undefined
|
||||||
|
},
|
||||||
|
squid: {
|
||||||
|
name: 'Squid-js',
|
||||||
|
version: undefined
|
||||||
|
},
|
||||||
|
aquarius: {
|
||||||
|
name: 'Aquarius',
|
||||||
|
version: undefined
|
||||||
|
},
|
||||||
|
brizo: {
|
||||||
|
name: 'Brizo',
|
||||||
|
version: undefined,
|
||||||
|
contracts: undefined,
|
||||||
|
network: undefined,
|
||||||
|
keeperVersion: undefined,
|
||||||
|
keeperUrl: undefined
|
||||||
|
},
|
||||||
|
faucet: {
|
||||||
|
name: 'Faucet',
|
||||||
|
version: undefined
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
ok: false,
|
||||||
|
network: false,
|
||||||
|
contracts: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
data: {
|
||||||
|
software: 'Faucet',
|
||||||
|
version: '6.6.6'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockResponseFaulty = {
|
||||||
|
status: 404,
|
||||||
|
statusText: 'Not Found',
|
||||||
|
data: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('VersionNumbers', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={userMockConnected}>
|
||||||
|
<VersionNumbers />
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
mockAxios.mockResponse(mockResponse)
|
||||||
|
expect(mockAxios.get).toHaveBeenCalled()
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders without proper component response', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={userMockConnected}>
|
||||||
|
<StateMock state={stateMockIncomplete}>
|
||||||
|
<VersionNumbers />
|
||||||
|
</StateMock>
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
mockAxios.mockResponse(mockResponseFaulty)
|
||||||
|
expect(mockAxios.get).toHaveBeenCalled()
|
||||||
|
expect(container.querySelector('table')).toHaveTextContent(
|
||||||
|
'Could not get version'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
161
admin/src/components/molecules/VersionNumbers/index.tsx
Normal file
161
admin/src/components/molecules/VersionNumbers/index.tsx
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import React, { PureComponent } from 'react'
|
||||||
|
import {
|
||||||
|
OceanPlatformVersions,
|
||||||
|
OceanPlatformTechStatus,
|
||||||
|
Logger
|
||||||
|
} from '@oceanprotocol/squid'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { version } from '../../../../package.json'
|
||||||
|
import styles from './index.module.scss'
|
||||||
|
|
||||||
|
import { nodeUri, faucetUri } from '../../../config'
|
||||||
|
import { User } from '../../../context'
|
||||||
|
|
||||||
|
import VersionTable from './VersionTable'
|
||||||
|
import VersionStatus from './VersionStatus'
|
||||||
|
|
||||||
|
// construct values which are not part of any response
|
||||||
|
export const commonsVersion =
|
||||||
|
process.env.NODE_ENV === 'production' ? version : `${version}-dev`
|
||||||
|
const commonsNetwork = new URL(nodeUri).hostname.split('.')[0]
|
||||||
|
const faucetNetwork = new URL(faucetUri).hostname.split('.')[1]
|
||||||
|
|
||||||
|
interface VersionNumbersProps {
|
||||||
|
minimal?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VersionNumbersState extends OceanPlatformVersions {
|
||||||
|
commons: {
|
||||||
|
name: string
|
||||||
|
version: string
|
||||||
|
network: string
|
||||||
|
}
|
||||||
|
faucet: {
|
||||||
|
name: string
|
||||||
|
version: string
|
||||||
|
network: string
|
||||||
|
status: OceanPlatformTechStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class VersionNumbers extends PureComponent<
|
||||||
|
VersionNumbersProps,
|
||||||
|
VersionNumbersState
|
||||||
|
> {
|
||||||
|
public static contextType = User
|
||||||
|
|
||||||
|
// define a minimal default state to fill UI
|
||||||
|
public state: VersionNumbersState = {
|
||||||
|
commons: {
|
||||||
|
name: 'Commons',
|
||||||
|
network: commonsNetwork,
|
||||||
|
version: commonsVersion
|
||||||
|
},
|
||||||
|
squid: {
|
||||||
|
name: 'Squid-js',
|
||||||
|
status: OceanPlatformTechStatus.Loading
|
||||||
|
},
|
||||||
|
aquarius: {
|
||||||
|
name: 'Aquarius',
|
||||||
|
status: OceanPlatformTechStatus.Loading
|
||||||
|
},
|
||||||
|
brizo: {
|
||||||
|
name: 'Brizo',
|
||||||
|
status: OceanPlatformTechStatus.Loading
|
||||||
|
},
|
||||||
|
faucet: {
|
||||||
|
name: 'Faucet',
|
||||||
|
version: '',
|
||||||
|
network: faucetNetwork,
|
||||||
|
status: OceanPlatformTechStatus.Loading
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
ok: false,
|
||||||
|
network: false,
|
||||||
|
contracts: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for canceling axios requests
|
||||||
|
public signal = axios.CancelToken.source()
|
||||||
|
|
||||||
|
public async componentDidMount() {
|
||||||
|
this.getOceanVersions()
|
||||||
|
this.getFaucetVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
this.signal.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getOceanVersions() {
|
||||||
|
const { ocean } = this.context
|
||||||
|
// wait until ocean object is properly populated
|
||||||
|
if (ocean.versions === undefined) return
|
||||||
|
|
||||||
|
const response = await ocean.versions.get()
|
||||||
|
const { squid, brizo, aquarius, status } = response
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
squid,
|
||||||
|
brizo,
|
||||||
|
aquarius,
|
||||||
|
status
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getFaucetVersion() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(faucetUri, {
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
cancelToken: this.signal.token
|
||||||
|
})
|
||||||
|
|
||||||
|
// fail silently
|
||||||
|
if (response.status !== 200) return
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
faucet: {
|
||||||
|
...this.state.faucet,
|
||||||
|
version: response.data.version,
|
||||||
|
status: OceanPlatformTechStatus.Working
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
!axios.isCancel(error) && Logger.error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MinimalOutput = () => {
|
||||||
|
const { commons, squid, brizo, aquarius } = this.state
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p className={styles.versionsMinimal}>
|
||||||
|
<a
|
||||||
|
title={`${squid.name} v${squid.version}\n${brizo.name} v${brizo.version}\n${aquarius.name} v${aquarius.version}`}
|
||||||
|
href={'/about'}
|
||||||
|
>
|
||||||
|
v{commons.version} {squid.network && `(${squid.network})`}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { minimal } = this.props
|
||||||
|
|
||||||
|
return minimal ? (
|
||||||
|
<this.MinimalOutput />
|
||||||
|
) : (
|
||||||
|
<div className={styles.versions} id="#oceanversions">
|
||||||
|
<h2 className={styles.versionsTitle}>
|
||||||
|
Ocean Components Status
|
||||||
|
</h2>
|
||||||
|
<VersionStatus status={this.state.status} />
|
||||||
|
<VersionTable data={this.state} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
42
admin/src/components/organisms/AssetsLatest.module.scss
Normal file
42
admin/src/components/organisms/AssetsLatest.module.scss
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
.latestAssetsWrap {
|
||||||
|
// full width break out of container
|
||||||
|
margin-right: calc(-50vw + 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.latestAssets {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||||
|
display: grid;
|
||||||
|
grid-gap: $spacer;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
padding: $spacer / 2 $spacer;
|
||||||
|
border-left: 1px solid $brand-grey-lighter;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar,
|
||||||
|
&::-moz-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> article {
|
||||||
|
min-width: calc(18rem + #{$spacer});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: $font-size-h4;
|
||||||
|
text-align: center;
|
||||||
|
color: $brand-grey-light;
|
||||||
|
border-bottom: 1px solid $brand-grey-lighter;
|
||||||
|
padding-bottom: $spacer / 3;
|
||||||
|
margin-top: $spacer * 3;
|
||||||
|
margin-bottom: $spacer / 2;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--small) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
19
admin/src/components/organisms/AssetsLatest.test.tsx
Normal file
19
admin/src/components/organisms/AssetsLatest.test.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import AssetsLatest from './AssetsLatest'
|
||||||
|
import { User } from '../../context'
|
||||||
|
import { userMockConnected } from '../../../__mocks__/user-mock'
|
||||||
|
|
||||||
|
describe('AssetsLatest', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={userMockConnected}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<AssetsLatest />
|
||||||
|
</BrowserRouter>
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
79
admin/src/components/organisms/AssetsLatest.tsx
Normal file
79
admin/src/components/organisms/AssetsLatest.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import React, { PureComponent } from 'react'
|
||||||
|
import { Logger } from '@oceanprotocol/squid'
|
||||||
|
import { User } from '../../context'
|
||||||
|
import Spinner from '../atoms/Spinner'
|
||||||
|
import AssetTeaser from '../molecules/AssetTeaser'
|
||||||
|
import styles from './AssetsLatest.module.scss'
|
||||||
|
|
||||||
|
interface AssetsLatestState {
|
||||||
|
latestAssets?: any[]
|
||||||
|
isLoadingLatest?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AssetsLatest extends PureComponent<{}, AssetsLatestState> {
|
||||||
|
public state = { latestAssets: [], isLoadingLatest: true }
|
||||||
|
|
||||||
|
public _isMounted: boolean = false
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this._isMounted = true
|
||||||
|
this._isMounted && this.getLatestAssets()
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
this._isMounted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLatestAssets = async () => {
|
||||||
|
const { ocean } = this.context
|
||||||
|
|
||||||
|
const searchQuery = {
|
||||||
|
offset: 15,
|
||||||
|
page: 1,
|
||||||
|
query: {},
|
||||||
|
sort: {
|
||||||
|
created: -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const search = await ocean.aquarius.queryMetadata(searchQuery)
|
||||||
|
this.setState({
|
||||||
|
latestAssets: search.results,
|
||||||
|
isLoadingLatest: false
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(error.message)
|
||||||
|
this.setState({ isLoadingLatest: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { latestAssets, isLoadingLatest } = this.state
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2 className={styles.title}>Latest published assets</h2>
|
||||||
|
<div className={styles.latestAssetsWrap}>
|
||||||
|
{isLoadingLatest ? (
|
||||||
|
<Spinner message="Loading..." />
|
||||||
|
) : latestAssets && latestAssets.length ? (
|
||||||
|
<div className={styles.latestAssets}>
|
||||||
|
{latestAssets.map((asset: any) => (
|
||||||
|
<AssetTeaser
|
||||||
|
key={asset.id}
|
||||||
|
asset={asset}
|
||||||
|
minimal
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>No data sets found.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetsLatest.contextType = User
|
25
admin/src/components/organisms/AssetsUser.module.scss
Normal file
25
admin/src/components/organisms/AssetsUser.module.scss
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
.assetsUser {
|
||||||
|
margin-top: $spacer;
|
||||||
|
margin-bottom: $spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subTitle {
|
||||||
|
font-size: $font-size-h4;
|
||||||
|
color: $brand-grey-light;
|
||||||
|
border-bottom: 1px solid $brand-grey-lighter;
|
||||||
|
padding-bottom: $spacer / 2;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
display: block;
|
||||||
|
margin-top: $spacer / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: $spacer;
|
||||||
|
color: $brand-grey-light;
|
||||||
|
}
|
109
admin/src/components/organisms/AssetsUser.tsx
Normal file
109
admin/src/components/organisms/AssetsUser.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import React, { PureComponent } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { Logger } from '@oceanprotocol/squid'
|
||||||
|
import { User } from '../../context'
|
||||||
|
import Spinner from '../atoms/Spinner'
|
||||||
|
import AssetTeaser from '../molecules/AssetTeaser'
|
||||||
|
import styles from './AssetsUser.module.scss'
|
||||||
|
|
||||||
|
export default class AssetsUser extends PureComponent<
|
||||||
|
{ list?: boolean; recent?: number },
|
||||||
|
{ results: any[]; isLoading: boolean }
|
||||||
|
> {
|
||||||
|
public state = { results: [], isLoading: true }
|
||||||
|
|
||||||
|
public _isMounted: boolean = false
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this._isMounted = true
|
||||||
|
this._isMounted && this.searchOcean()
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
this._isMounted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private async searchOcean() {
|
||||||
|
const { account, ocean } = this.context
|
||||||
|
|
||||||
|
if (account) {
|
||||||
|
ocean.keeper.didRegistry.contract.getPastEvents(
|
||||||
|
'DIDAttributeRegistered',
|
||||||
|
{
|
||||||
|
filter: { _owner: account },
|
||||||
|
fromBlock: 0,
|
||||||
|
toBlock: 'latest'
|
||||||
|
},
|
||||||
|
async (error: any, events: any) => {
|
||||||
|
if (error) {
|
||||||
|
Logger.log('error retrieving', error)
|
||||||
|
this._isMounted && this.setState({ isLoading: false })
|
||||||
|
} else {
|
||||||
|
const results = []
|
||||||
|
for (const event of events) {
|
||||||
|
const ddo = await ocean.assets.resolve(
|
||||||
|
`did:op:${event.returnValues._did.substring(2)}`
|
||||||
|
)
|
||||||
|
results.push(ddo)
|
||||||
|
}
|
||||||
|
this._isMounted &&
|
||||||
|
this.setState({ results, isLoading: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.setState({ isLoading: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { account, isOceanNetwork } = this.context
|
||||||
|
|
||||||
|
return (
|
||||||
|
isOceanNetwork &&
|
||||||
|
account && (
|
||||||
|
<div className={styles.assetsUser}>
|
||||||
|
{this.props.recent && (
|
||||||
|
<h2 className={styles.subTitle}>
|
||||||
|
Your Latest Published Data Sets
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{this.state.isLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : this.state.results.length ? (
|
||||||
|
<>
|
||||||
|
{this.state.results
|
||||||
|
.slice(
|
||||||
|
0,
|
||||||
|
this.props.recent
|
||||||
|
? this.props.recent
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
.filter(asset => !!asset)
|
||||||
|
.map((asset: any) => (
|
||||||
|
<AssetTeaser
|
||||||
|
list={this.props.list}
|
||||||
|
key={asset.id}
|
||||||
|
asset={asset}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{this.props.recent && (
|
||||||
|
<Link className={styles.link} to={'/history'}>
|
||||||
|
All Data Sets
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className={styles.empty}>
|
||||||
|
<p>No Data Sets Yet.</p>
|
||||||
|
<Link to="/publish">+ Publish A Data Set</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetsUser.contextType = User
|
86
admin/src/components/organisms/ChannelTeaser.module.scss
Normal file
86
admin/src/components/organisms/ChannelTeaser.module.scss
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
.channel {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--medium) {
|
||||||
|
padding-top: $spacer * 2;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
&:first-child {
|
||||||
|
margin-bottom: $spacer;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--medium) {
|
||||||
|
margin-right: $spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $break-point--medium) {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
flex: 0 0 calc(18rem + #{$spacer * 2});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// style channel teaser following another one
|
||||||
|
+ .channel {
|
||||||
|
border-top: 1px solid $brand-grey-lighter;
|
||||||
|
margin-top: $spacer * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelTitle {
|
||||||
|
margin-top: $spacer * 4;
|
||||||
|
margin-bottom: $spacer / 4;
|
||||||
|
color: $brand-black;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--medium) {
|
||||||
|
margin-top: -($spacer / 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelHeader {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--small) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
transform: none;
|
||||||
|
|
||||||
|
// category image
|
||||||
|
// stylelint-disable-next-line
|
||||||
|
.channelTitle + div {
|
||||||
|
opacity: 1;
|
||||||
|
background-size: 105%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelTeaser {
|
||||||
|
color: $brand-grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelResults {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-gap: $spacer;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--small) {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
19
admin/src/components/organisms/ChannelTeaser.test.tsx
Normal file
19
admin/src/components/organisms/ChannelTeaser.test.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import ChannelTeaser from './ChannelTeaser'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { User } from '../../context'
|
||||||
|
import { userMockConnected } from '../../../__mocks__/user-mock'
|
||||||
|
|
||||||
|
describe('ChannelTeaser', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={userMockConnected}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<ChannelTeaser channel="ai-for-good" />
|
||||||
|
</BrowserRouter>
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
99
admin/src/components/organisms/ChannelTeaser.tsx
Normal file
99
admin/src/components/organisms/ChannelTeaser.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import React, { Component } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { User } from '../../context'
|
||||||
|
import { Logger } from '@oceanprotocol/squid'
|
||||||
|
import Spinner from '../atoms/Spinner'
|
||||||
|
import AssetTeaser from '../molecules/AssetTeaser'
|
||||||
|
import styles from './ChannelTeaser.module.scss'
|
||||||
|
import channels from '../../data/channels.json'
|
||||||
|
import CategoryImage from '../atoms/CategoryImage'
|
||||||
|
|
||||||
|
interface ChannelTeaserProps {
|
||||||
|
channel: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChannelTeaserState {
|
||||||
|
channelAssets?: any[]
|
||||||
|
isLoadingChannel?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ChannelTeaser extends Component<
|
||||||
|
ChannelTeaserProps,
|
||||||
|
ChannelTeaserState
|
||||||
|
> {
|
||||||
|
public static contextType = User
|
||||||
|
|
||||||
|
// Get channel content
|
||||||
|
public channel = channels.items
|
||||||
|
.filter(({ tag }) => tag === this.props.channel)
|
||||||
|
.map(channel => channel)[0]
|
||||||
|
|
||||||
|
public state = {
|
||||||
|
channelAssets: [],
|
||||||
|
isLoadingChannel: true
|
||||||
|
}
|
||||||
|
|
||||||
|
public async componentDidMount() {
|
||||||
|
this.getChannelAssets()
|
||||||
|
}
|
||||||
|
|
||||||
|
private getChannelAssets = async () => {
|
||||||
|
const { ocean } = this.context
|
||||||
|
|
||||||
|
const searchQuery = {
|
||||||
|
offset: 2,
|
||||||
|
page: 1,
|
||||||
|
query: {
|
||||||
|
tags: [this.channel.tag]
|
||||||
|
},
|
||||||
|
sort: {
|
||||||
|
created: -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const search = await ocean.aquarius.queryMetadata(searchQuery)
|
||||||
|
this.setState({
|
||||||
|
channelAssets: search.results,
|
||||||
|
isLoadingChannel: false
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(error.message)
|
||||||
|
this.setState({ isLoadingChannel: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { channelAssets, isLoadingChannel } = this.state
|
||||||
|
const { title, tag, teaser } = this.channel
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.channel}>
|
||||||
|
<div>
|
||||||
|
<header className={styles.channelHeader}>
|
||||||
|
<Link to={`/channels/${tag}`}>
|
||||||
|
<h2 className={styles.channelTitle}>{title}</h2>
|
||||||
|
<CategoryImage category={title} />
|
||||||
|
|
||||||
|
<p className={styles.channelTeaser}>{teaser}</p>
|
||||||
|
<p>Browse the channel →</p>
|
||||||
|
</Link>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{isLoadingChannel ? (
|
||||||
|
<Spinner message="Loading..." />
|
||||||
|
) : channelAssets && channelAssets.length ? (
|
||||||
|
<div className={styles.channelResults}>
|
||||||
|
{channelAssets.map((asset: any) => (
|
||||||
|
<AssetTeaser key={asset.id} asset={asset} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>No data sets found.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
93
admin/src/components/organisms/Footer.module.scss
Normal file
93
admin/src/components/organisms/Footer.module.scss
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
color: $brand-grey-light;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: $spacer;
|
||||||
|
padding-top: $spacer;
|
||||||
|
padding-bottom: $spacer;
|
||||||
|
align-self: flex-end;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
align-self: flex-end;
|
||||||
|
|
||||||
|
@media screen and (min-width: $break-point--small) {
|
||||||
|
text-align: left;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&,
|
||||||
|
small {
|
||||||
|
font-size: $font-size-mini;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: $brand-grey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: inline-block;
|
||||||
|
width: $font-size-large;
|
||||||
|
height: $font-size-large;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
margin-top: $spacer / 2;
|
||||||
|
|
||||||
|
@media screen and (min-width: $break-point--small) {
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
margin: 0 $spacer / 2;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: $spacer * $line-height;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: $spacer / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.aicommons {
|
||||||
|
svg {
|
||||||
|
width: 100px;
|
||||||
|
height: auto;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-top: -.05rem;
|
||||||
|
margin-left: $spacer / 6;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
svg {
|
||||||
|
fill: $brand-pink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
admin/src/components/organisms/Footer.tsx
Normal file
54
admin/src/components/organisms/Footer.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Market } from '../../context'
|
||||||
|
import Content from '../atoms/Content'
|
||||||
|
import { ReactComponent as AiCommons } from '../../img/aicommons.svg'
|
||||||
|
import styles from './Footer.module.scss'
|
||||||
|
|
||||||
|
import meta from '../../data/meta.json'
|
||||||
|
import VersionNumbers from '../molecules/VersionNumbers'
|
||||||
|
|
||||||
|
const Footer = () => (
|
||||||
|
<footer className={styles.footer}>
|
||||||
|
<aside className={styles.stats}>
|
||||||
|
<Content wide>
|
||||||
|
<p>
|
||||||
|
Online since March 2019.
|
||||||
|
<Market.Consumer>
|
||||||
|
{state =>
|
||||||
|
state.totalAssets > 0 &&
|
||||||
|
` With a total of ${state.totalAssets} registered assets.`
|
||||||
|
}
|
||||||
|
</Market.Consumer>
|
||||||
|
</p>
|
||||||
|
<p className={styles.aicommons}>
|
||||||
|
Proud supporter of{' '}
|
||||||
|
<a
|
||||||
|
href="https://aicommons.com/?utm_source=commons.oceanprotocol.com"
|
||||||
|
title="AI Commons"
|
||||||
|
>
|
||||||
|
<AiCommons />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<VersionNumbers minimal />
|
||||||
|
</Content>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<Content wide>
|
||||||
|
<small>
|
||||||
|
© {new Date().getFullYear()}{' '}
|
||||||
|
<a href={meta.social[0].url}>{meta.company}</a> — All
|
||||||
|
Rights Reserved
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<nav className={styles.links}>
|
||||||
|
{meta.social.map(site => (
|
||||||
|
<a key={site.title} href={site.url}>
|
||||||
|
{site.title}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</Content>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Footer
|
103
admin/src/components/organisms/Header.module.scss
Normal file
103
admin/src/components/organisms/Header.module.scss
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
.header {
|
||||||
|
width: 100%;
|
||||||
|
padding: $spacer / 2 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerContent {
|
||||||
|
composes: wide from '../atoms/Content.module.scss';
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerLogo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerLogoImage {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
fill: #fff;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerTitle {
|
||||||
|
font-size: $font-size-h3;
|
||||||
|
color: $brand-grey-light;
|
||||||
|
margin-left: $spacer / 2;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--medium) {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerMenu {
|
||||||
|
flex: 1;
|
||||||
|
justify-self: flex-end;
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-y: hidden;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
margin-right: -($spacer / 1.5);
|
||||||
|
padding-right: $spacer;
|
||||||
|
border-left: 1px solid $brand-grey-lighter;
|
||||||
|
margin-left: $spacer / 8;
|
||||||
|
|
||||||
|
@media (min-width: $break-point--medium) {
|
||||||
|
padding-right: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
border-left: 0;
|
||||||
|
overflow: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar,
|
||||||
|
&::-moz-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
transition: opacity .2s ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 $spacer / 2;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $brand-grey;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
color: $brand-pink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkActive {
|
||||||
|
composes: link;
|
||||||
|
color: $brand-pink;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accountStatus {
|
||||||
|
margin-left: $spacer / 2;
|
||||||
|
margin-bottom: -.5rem;
|
||||||
|
}
|
29
admin/src/components/organisms/Header.tsx
Normal file
29
admin/src/components/organisms/Header.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React, { PureComponent } from 'react'
|
||||||
|
import { NavLink } from 'react-router-dom'
|
||||||
|
import { ReactComponent as Logo } from '@oceanprotocol/art/logo/logo.svg'
|
||||||
|
import { User } from '../../context'
|
||||||
|
import AccountStatus from '../molecules/AccountStatus'
|
||||||
|
import styles from './Header.module.scss'
|
||||||
|
|
||||||
|
import meta from '../../data/meta.json'
|
||||||
|
|
||||||
|
export default class Header extends PureComponent {
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<header className={styles.header}>
|
||||||
|
<div className={styles.headerContent}>
|
||||||
|
<NavLink to={'/'} className={styles.headerLogo}>
|
||||||
|
<Logo className={styles.headerLogoImage} />
|
||||||
|
<h1 className={styles.headerTitle}>{meta.title}</h1>
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
<nav className={styles.headerMenu}>
|
||||||
|
<AccountStatus className={styles.accountStatus} />
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Header.contextType = User
|
22
admin/src/components/organisms/Web3message.module.scss
Normal file
22
admin/src/components/organisms/Web3message.module.scss
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
@import '../../styles/variables';
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-bottom: $spacer;
|
||||||
|
color: $brand-grey;
|
||||||
|
position: relative;
|
||||||
|
border-bottom: .1rem solid $brand-grey-lighter;
|
||||||
|
border-top: .1rem solid $brand-grey-lighter;
|
||||||
|
padding-top: $spacer / 2;
|
||||||
|
padding-bottom: $spacer / 2;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warnings {
|
||||||
|
padding-left: $spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin-left: -($spacer);
|
||||||
|
margin-right: $spacer / 2;
|
||||||
|
padding: 0;
|
||||||
|
}
|
64
admin/src/components/organisms/Web3message.test.tsx
Normal file
64
admin/src/components/organisms/Web3message.test.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render, fireEvent } from '@testing-library/react'
|
||||||
|
import Web3message from './Web3message'
|
||||||
|
import { User } from '../../context'
|
||||||
|
import { userMock, userMockConnected } from '../../../__mocks__/user-mock'
|
||||||
|
|
||||||
|
describe('Web3message', () => {
|
||||||
|
it('renders with noWeb3 message', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={{ ...userMock }}>
|
||||||
|
<Web3message />
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toHaveTextContent('Not a Web3 Browser')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders with wrongNetwork message', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={{ ...userMock, isWeb3: true }}>
|
||||||
|
<Web3message />
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toHaveTextContent(
|
||||||
|
'Not connected to Pacific network'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders with noAccount message', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider
|
||||||
|
value={{ ...userMock, isWeb3: true, isOceanNetwork: true }}
|
||||||
|
>
|
||||||
|
<Web3message />
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toHaveTextContent('No accounts detected')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders with hasAccount message', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<User.Provider value={userMockConnected}>
|
||||||
|
<Web3message />
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
expect(container.firstChild).toHaveTextContent('0xxxxxx')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('button click fires unlockAccounts', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<User.Provider
|
||||||
|
value={{
|
||||||
|
...userMock,
|
||||||
|
isWeb3: true,
|
||||||
|
isOceanNetwork: true
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Web3message />
|
||||||
|
</User.Provider>
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(getByText('Unlock Account'))
|
||||||
|
expect(userMock.unlockAccounts).toBeCalled()
|
||||||
|
})
|
||||||
|
})
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user