diff --git a/app/page.tsx b/app/page.tsx index 176ad97..2bf610e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,7 @@ import '@root/global.scss'; -import Application from '@components/Application'; +import Main from '@components/Main'; export default async function Page(props) { - return ; + return
; } diff --git a/common/navigation.ts b/common/navigation.ts index 8774944..1a4ca76 100644 --- a/common/navigation.ts +++ b/common/navigation.ts @@ -12,4 +12,9 @@ export const tooltipStates = { addWallet: 4, addReplication: 5, associateWallet: 6, -}; \ No newline at end of file +}; + +export const apps = { + ddm: 'ddm', + ptolemy: 'ptolemy', +} \ No newline at end of file diff --git a/components/Application.tsx b/components/Application.tsx deleted file mode 100644 index f83f87c..0000000 --- a/components/Application.tsx +++ /dev/null @@ -1,221 +0,0 @@ -'use client'; - -import * as React from 'react'; -import PackageJSON from '@root/package.json'; -import { navigationStates, tooltipStates } from '@common/navigation'; -import { getCookie, setCookie } from '@modules/cookies'; -import { associateWallet, checkAuth, getDatasets, getHealth, getProviders, getReplications, getWallets } from '@data/api'; - -import styles from '@components/Application.module.scss'; - -import DefaultLayout from '@components/DefaultLayout'; - -import SceneDatasets from '@components/scenes/SceneDatasets'; -import SceneProviders from '@components/scenes/SceneProviders'; -import SceneReplications from '@components/scenes/SceneReplications'; -import SceneWallets from '@components/scenes/SceneWallets'; -import SceneAuth from '@components/scenes/SceneAuth'; - -import FormUploadData from '@components/forms/FormUploadData'; -import FormAddWallet from '@components/forms/FormAddWallet'; -import FormAddProvider from '@components/forms/FormAddProvider'; -import FormAddReplication from '@components/forms/FormAddReplication'; -import FormNewDataset from '@components/forms/FormNewDataset'; -import FormAssociateWallet from '@components/forms/FormAssociateWallet'; -import { readFileSync } from 'fs'; - - -export default function Application(props) { - const [appNavigationState, setAppNavigationState] = React.useState(1); - const [datasetSearch, setDatasetSearch] = React.useState(''); - const [providerSearch, setProviderSearch] = React.useState(''); - const [replicationSearch, setReplicationSearch] = React.useState(''); - const [selectedProvider, setSelectedProvider] = React.useState(''); - const [selectedDataset, setSelectedDataset] = React.useState(''); - const [selectedWallet, setSelectedWallet] = React.useState(''); - const [state, setState] = React.useState({ - datasets: undefined, - providers: undefined, - replications: undefined, - wallets: undefined - }); - const [health, setHealth] = React.useState(undefined); - const [commitHash, setCommitHash] = React.useState(undefined); - const [appTooltipState, setAppTooltipState] = React.useState(0); - const [authToken, setAuthTokenEphemeral] = React.useState(''); - const setAuthToken = authToken => { - setAuthTokenEphemeral(authToken); - setCookie('auth', authToken); - }; - const setDDMAddress = ddmAddress => { - setCookie('ddm-address', ddmAddress); - }; - - async function updateState() { - setState({ - datasets: await getDatasets(), - providers: await getProviders(), - replications: await getReplications(), - wallets: await getWallets(), - }); - } - - async function updateHealth() { - setHealth(await getHealth()); - } - - async function updateCommitHash() { - setCommitHash((await (await fetch('/api')).json()).commit_hash) - } - - function dismissTooltip() { - setAppTooltipState(0); - } - - React.useEffect(() => { - (async () => { - setAuthTokenEphemeral(getCookie('auth')); - - try { - if (!await checkAuth()) { - throw new Error(); - } - } catch { - setAuthTokenEphemeral(''); - return; - } - })(); - }, []); - - React.useEffect(() => { - if (authToken) { - updateState(); - updateHealth(); - updateCommitHash(); - } - }, [authToken]); - - if (!authToken) { - return ( - - ) - } - - return ( - setAppNavigationState(navigationStates.datasets)} - onClickProviders={() => setAppNavigationState(navigationStates.providers)} - onClickReplications={() => setAppNavigationState(navigationStates.replications)} - onNewDataset={() => setAppTooltipState(tooltipStates.newDataset)} - onAddProviders={() => setAppTooltipState(tooltipStates.addProvider)} - onAddReplication={() => setAppTooltipState(tooltipStates.addReplication)} - onClickWallets={() => setAppNavigationState(navigationStates.wallets)} - onAddWallet={() => setAppTooltipState(tooltipStates.addWallet)} - > - {appNavigationState === navigationStates.datasets && ( - setDatasetSearch(e.target.value)} - searchLabel="Search datasets" - placeholder="(example: university-bird-sounds)" - state={state} - onAttachContent={() => setAppTooltipState(tooltipStates.attachContent)} - selectedDataset={selectedDataset} - setSelectedDataset={setSelectedDataset} - /> - )} - {appNavigationState === navigationStates.providers && ( - setProviderSearch(e.target.value)} - providerLabel="Search providers" - placeholder="(example: f0123456)" - state={state} - updateState={updateState} - /> - )} - {appNavigationState === navigationStates.replications && ( - setReplicationSearch(e.target.value)} - searchLabel='Search replications' - placeholder='search any field (replication filtering is w.i.p.)' - selectedProvider={selectedProvider} - setSelectedProvider={() => { - alert('test'); - }} - selectedDataset={selectedDataset} - setSelectedDataset={() => { }} - state={state} - /> - )} - {appNavigationState === navigationStates.wallets && ( - setAppTooltipState(tooltipStates.associateWallet)} - setSelectedWallet={setSelectedWallet} - /> - )} - - {appTooltipState === tooltipStates.newDataset && ( - - )} - {appTooltipState === tooltipStates.addProvider && ( - - )} - {appTooltipState === tooltipStates.addWallet && ( - - )} - {appTooltipState === tooltipStates.attachContent && ( - - )} - {appTooltipState === tooltipStates.addReplication && ( - - )} - {appTooltipState === tooltipStates.associateWallet && ( - - )} - - ); -} diff --git a/components/DatasetSelect.tsx b/components/DatasetSelect.tsx index ebd75ec..fa19a3d 100644 --- a/components/DatasetSelect.tsx +++ b/components/DatasetSelect.tsx @@ -1,8 +1,8 @@ -import Select from '@components/Select'; +import Select from '@components/basic/Select'; export default function ProviderSelect(props) { return ( - {props.datasets?.map((dataset, i) => { return ( diff --git a/components/DefaultLayout.module.scss b/components/DefaultLayout.module.scss index 9dc288c..b38344b 100644 --- a/components/DefaultLayout.module.scss +++ b/components/DefaultLayout.module.scss @@ -34,7 +34,6 @@ width: 100%; min-width: 10%; border-left: 1px solid var(--color-border); - position: relative; } .right { @@ -42,7 +41,6 @@ min-width: 10%; min-height: calc(100vh - 56px); border-left: 1px solid var(--color-border); - position: relative; } .appTitle { diff --git a/components/DefaultLayout.tsx b/components/DefaultLayout.tsx index 9cfdf63..78f3f6e 100644 --- a/components/DefaultLayout.tsx +++ b/components/DefaultLayout.tsx @@ -3,48 +3,68 @@ import { navigationStates } from '@root/common/navigation'; import * as React from 'react'; -export default function DefaultLayout(props) { +export default function DefaultLayout(props: { + apps: string[], + activeApp: string, + onSwitchApp: (app: string) => void, + children: any, +}) { + let title = props.children?.find(child => child.type === AppTitle); + let version = props.children?.find(child => child.type === AppVersion); + let nav = props.children?.find(child => child.type === AppNav); + let body = props.children?.find(child => child.type === AppBody); + + // NOTE(@elijaharita): this works right now because there are only 2 apps + // (ptolemy and ddm). It will need to be changed in the future if more apps + // are added. + function toggleApp() { + let otherApp = props.apps.filter(app => app != props.activeApp)[0]; + props.onSwitchApp(otherApp); + console.log('switching to app ' + otherApp); + } + return (
-
{props.appTitle}
+
+ toggleApp()}>{title} +
-
{props.appVersion}
+
{version}
- +
-
{props.children}
+
{body}
); } + +export function AppTitle(props) { + return <>{props.children}; +} + +export function AppVersion(props) { + return <>{props.children}; +} + +export function AppNav(props) { + return
{props.children}
; +} + +export function AppNavItem(props) { + return {props.children}; +} + +export function AppNavSubItem(props) { + return {props.children}; +} + +export function AppBody(props) { + return
{props.children}
; +} \ No newline at end of file diff --git a/components/FileList.tsx b/components/FileList.tsx index 6ac81d7..a1261f5 100644 --- a/components/FileList.tsx +++ b/components/FileList.tsx @@ -5,6 +5,8 @@ import styles from './FileList.module.scss'; export default function FileList(props) { const [files, setFiles] = React.useState([]); + const [error, setError] = React.useState(null); + const [loading, setLoading] = React.useState(true); React.useEffect(() => { (async () => { @@ -28,11 +30,33 @@ export default function FileList(props) { setFiles(files); } catch (e) { console.log(e); - alert(e.toString()); + setError('Failed to get directory'); + } finally { + setLoading(false); } })() }, [props.root]); + if (loading) { + return ( +
+
+ Loading... +
+
+ ) + } + + if (error != null) { + return ( +
+
+ {error} +
+
+ ) + } + return (
{files.map((file) => ( diff --git a/components/FileSelect.tsx b/components/FileSelect.tsx index ce8bfb0..ce78f85 100644 --- a/components/FileSelect.tsx +++ b/components/FileSelect.tsx @@ -2,8 +2,13 @@ import * as React from 'react'; import FileList from '@components/FileList'; -export default function FileSelect(props) { - const [root, setRoot] = React.useState('/home/elijah'); +export default function FileSelect(props: { + label?: string, + root?: string, + onSelectFile?: (path: string) => void, + onChangeRoot?: (path: string) => void, +}) { + const [root, setRoot] = React.useState('/'); return ( diff --git a/components/forms/Form.module.scss b/components/Form.module.scss similarity index 100% rename from components/forms/Form.module.scss rename to components/Form.module.scss diff --git a/components/Main.module.scss b/components/Main.module.scss new file mode 100644 index 0000000..1fd8b0e --- /dev/null +++ b/components/Main.module.scss @@ -0,0 +1,34 @@ +.body { + display: flex; + flex-direction: column; +} + +.top { + width: 100%; + height: 4rem; + display: flex; + flex-direction: row; + + border-bottom: 1px solid var(--color-border); + margin-bottom: -1px; +} + +.bottom { + display: flex; + + flex-grow: 1; + min-height: 100vh; +} + +.left { + display: flex; + + width: 16rem; + height: inherit; + border-right: 1px solid var(--color-border); + margin-right: -1px; +} + +.right { + height: inherit; +} \ No newline at end of file diff --git a/components/Main.tsx b/components/Main.tsx new file mode 100644 index 0000000..7da61e9 --- /dev/null +++ b/components/Main.tsx @@ -0,0 +1,36 @@ +'use client'; + +import React from 'react'; +import { apps } from '@root/common/navigation'; + +import styles from './Main.module.scss'; + +import AppDDM from '@components/apps/ddm/DDM'; +import AppPtolemy from '@components/apps/ptolemy/Ptolemy'; +import { getCookie, setCookie } from '@root/modules/cookies'; + +export default function Main(props) { + const [activeApp, _setActiveApp] = React.useState(apps.ddm); + function setActiveApp(app: string) { + // TODO: app switching temporarily disabled + return; + + _setActiveApp(app); + setCookie('active-app', app); + } + const appNames = Object.keys(apps); + + React.useEffect(() => { + let savedState = getCookie('active-app'); + if (savedState) { + _setActiveApp(savedState); + } + }) + + switch (activeApp) { + case apps.ddm: + return + case apps.ptolemy: + return + } +} \ No newline at end of file diff --git a/components/Modal.module.scss b/components/Modal.module.scss new file mode 100644 index 0000000..6fe6e09 --- /dev/null +++ b/components/Modal.module.scss @@ -0,0 +1,34 @@ +.body { + position: absolute; + border: 1px solid var(--color-border); + background: var(--color-background); + border-radius: 0.5rem; + transform: translateY(-3rem); + margin-left: 2rem; + box-shadow: 0px 0px 6px 4px #0004; + z-index: 100; + opacity: 100%; + transition: 0.15s; + padding: 2rem; + + &::before { + content: ""; + margin-top: 1rem; + display: block; + position: fixed; + width: 8px; + height: 8px; + left: 0; + transform: translate(-50%, -50%) rotate(45deg); + background: var(--color-background); + border-left: 1px solid var(--color-border); + border-bottom: 1px solid var(--color-border); + } +} + +.bodyHidden { + @extend .body; + + opacity: 0%; + margin-left: 1rem; +} \ No newline at end of file diff --git a/components/Modal.tsx b/components/Modal.tsx new file mode 100644 index 0000000..53378eb --- /dev/null +++ b/components/Modal.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; + +import styles from './Modal.module.scss'; + +// The `onClose` function will pass the provided `id` back to the caller so it +// can be compared to avoid the case of one modal closing causing another one to +// close. +export default function Modal(props: { + modalID?: any, + anchor: React.RefObject, + children: React.ReactNode, + onClose: (modalID?: any) => void, +}) { + const fadeSeconds = 0.15; + + const ref = React.useRef(null); + + const [modalPos, setModalPos] = React.useState(null); + const [hidden, setHidden] = React.useState(true); + + function updateModalPos(node) { + let anchorRect = node.getBoundingClientRect(); + let anchorOffsetParentRect = node.offsetParent.getBoundingClientRect(); + let left = anchorRect.left - anchorOffsetParentRect.left + anchorRect.width; + let top = anchorRect.top - anchorOffsetParentRect.top + anchorRect.height * 0.5; + setModalPos({ x: left, y: top }); + } + + React.useEffect(() => { + function handleClickOutside(event) { + console.log('handle click'); + + if (ref.current && !ref.current.contains(event.target)) { + event.stopPropagation(); + + setHidden(true); + + setTimeout(() => { + props.onClose(props.modalID); + }, fadeSeconds * 1000); + } + } + + updateModalPos(props.anchor.current); + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [ref]); + + React.useEffect(() => { + setTimeout(e => setHidden(false), 10); + }, []); + + if (modalPos == null) { + return null; + } + + return ( +
+ {props.children} +
+ ); +} \ No newline at end of file diff --git a/components/ProviderSelect.tsx b/components/ProviderSelect.tsx index f1afa79..16cf5f6 100644 --- a/components/ProviderSelect.tsx +++ b/components/ProviderSelect.tsx @@ -1,15 +1,13 @@ -import Select from '@components/Select'; +import Select from '@components/basic/Select'; export default function ProviderSelect(props) { return ( - + {props.providers?.map((provider, i) => + + )} ); } \ No newline at end of file diff --git a/components/Select.tsx b/components/Select.tsx deleted file mode 100644 index dda10f2..0000000 --- a/components/Select.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import styles from './Select.module.scss'; - -export default function Select(props) { - return ( -
- - -
- ) -} \ No newline at end of file diff --git a/components/TagSelect.tsx b/components/TagSelect.tsx index db13c3b..63cbb1a 100644 --- a/components/TagSelect.tsx +++ b/components/TagSelect.tsx @@ -48,6 +48,7 @@ export default function TagSelect(props: { deselect(option))} + key={i} >{option} ) })} diff --git a/components/apps/ddm/DDM.tsx b/components/apps/ddm/DDM.tsx new file mode 100644 index 0000000..ffe0dd5 --- /dev/null +++ b/components/apps/ddm/DDM.tsx @@ -0,0 +1,200 @@ +'use client'; + +import * as React from 'react'; +import PackageJSON from '@root/package.json'; +import { navigationStates, tooltipStates } from '@common/navigation'; +import { getCookie, setCookie } from '@modules/cookies'; +import { associateWallet, checkAuth, getDatasets, getHealth, getProviders, getReplications, GetReplicationsConfig, getWallets } from '@data/api'; + +import DefaultLayout, { AppBody, AppNav, AppNavItem, AppNavSubItem, AppTitle, AppVersion } from '@components/DefaultLayout'; +import Modal from '@root/components/Modal'; + +import Datasets from '@root/components/apps/ddm/scenes/Datasets'; +import Providers from '@root/components/apps/ddm/scenes/Providers'; +import Replications from '@root/components/apps/ddm/scenes/Replications'; +import Wallets from '@components/apps/ddm/scenes/Wallets'; +import Auth from '@root/components/apps/ddm/scenes/Auth'; + +import FormUploadData from '@root/components/apps/ddm/forms/FormUploadData'; +import FormAddWallet from '@root/components/apps/ddm/forms/FormAddWallet'; +import FormAddProvider from '@root/components/apps/ddm/forms/FormAddProvider'; +import FormAddReplication from '@root/components/apps/ddm/forms/FormAddReplication'; +import FormNewDataset from '@root/components/apps/ddm/forms/FormNewDataset'; + +export default function DDM(props) { + const [appNavigationState, setAppNavigationState] = React.useState(1); + const [datasetSearch, setDatasetSearch] = React.useState(''); + const [providerSearch, setProviderSearch] = React.useState(''); + const [replicationSearch, setReplicationSearch] = React.useState(''); + const [selectedProvider, setSelectedProvider] = React.useState(''); + const [selectedDataset, setSelectedDataset] = React.useState(''); + const [selectedWallet, setSelectedWallet] = React.useState(''); + const [datasets, setDatasets] = React.useState(undefined); + const [providers, setProviders] = React.useState(undefined); + const [replications, setReplications] = React.useState(undefined); + const [wallets, setWallets] = React.useState(undefined); + const [health, setHealth] = React.useState(undefined); + const [commitHash, setCommitHash] = React.useState(undefined); + const [appTooltipState, setAppTooltipState] = React.useState(0); + const [authToken, setAuthTokenEphemeral] = React.useState(''); + const setAuthToken = (authToken) => { + setAuthTokenEphemeral(authToken); + setCookie('auth', authToken); + }; + const setDDMAddress = (ddmAddress) => { + setCookie('ddm-address', ddmAddress); + }; + + const getReplicationsConfig = React.useRef(); + const setGetReplicationsConfig = (cfg: GetReplicationsConfig) => (getReplicationsConfig.current = cfg); + + const updateDatasets = async () => { setDatasets(undefined); setDatasets(await getDatasets()) }; + const updateProviders = async () => { setProviders(undefined); setProviders(await getProviders()) }; + const updateReplications = async () => { setReplications(undefined); setReplications(await getReplications(getReplicationsConfig.current)) }; + const updateWallets = async () => { setWallets(undefined); setWallets(await getWallets()) }; + + const newDatasetButton = React.useRef(null); + const addProviderButton = React.useRef(null); + const addReplicationButton = React.useRef(null); + const addWalletButton = React.useRef(null); + + async function updateHealth() { + setHealth(await getHealth()); + } + + async function updateCommitHash() { + setCommitHash((await (await fetch('/api')).json()).commit_hash); + } + + function dismissTooltip(id) { + setAppTooltipState((prev) => (prev === id ? 0 : prev)); + } + + React.useEffect(() => { + (async () => { + setAuthTokenEphemeral(getCookie('auth')); + + try { + if (!(await checkAuth())) { + throw new Error(); + } + } catch { + setAuthTokenEphemeral(''); + return; + } + })(); + }, []); + + React.useEffect(() => { + if (authToken) { + updateDatasets(); + updateProviders(); + // updateReplications(); + updateWallets(); + + updateHealth(); + updateCommitHash(); + } + }, [authToken]); + + if (!authToken) { + return ; + } + + return ( + + Delta DM + {` + UUID: ${health?.uuid || 'not set'} + | + DDM Version: ${health?.ddm_info.version} (${health?.ddm_info.commit}) + | + Delta Version: ${health?.delta_info.version} (${health?.delta_info.commit}) + | + UI Version: ${PackageJSON.version} (${commitHash}) + `} + + setAppNavigationState(navigationStates.datasets)}>Datasets {appNavigationState === navigationStates.datasets && '➝'} + setAppTooltipState(tooltipStates.newDataset)}> + + New dataset + + setAppNavigationState(navigationStates.providers)}>Providers {appNavigationState === navigationStates.providers && '➝'} + setAppTooltipState(tooltipStates.addProvider)}> + + Add provider + + setAppNavigationState(navigationStates.replications)}>Replications {appNavigationState === navigationStates.replications && '➝'} + setAppTooltipState(tooltipStates.addReplication)}> + + Add replication + + setAppNavigationState(navigationStates.wallets)}>Wallets {appNavigationState === navigationStates.wallets && '➝'} + setAppTooltipState(tooltipStates.addWallet)}> + + Add wallet + + + + {appNavigationState === navigationStates.datasets && ( + setDatasetSearch(e.target.value)} + searchLabel="Search datasets" + placeholder="(example: university-bird-sounds)" + datasets={datasets} + onAttachContent={() => setAppTooltipState(tooltipStates.attachContent)} + // selectedDataset={selectedDataset} + setSelectedDataset={setSelectedDataset} + /> + )} + {appNavigationState === navigationStates.providers && ( + setProviderSearch(e.target.value)} + providerLabel="Search providers" + placeholder="(example: f0123456)" + /> + )} + {appNavigationState === navigationStates.replications && ( + + )} + {appNavigationState === navigationStates.wallets && ( + + )} + + {appTooltipState === tooltipStates.newDataset && ( + + + + )} + {appTooltipState === tooltipStates.addProvider && ( + + + + )} + {appTooltipState === tooltipStates.addWallet && ( + + + + )} + {appTooltipState === tooltipStates.attachContent && } + {appTooltipState === tooltipStates.addReplication && ( + + + + )} + + + ); +} diff --git a/components/apps/ddm/forms/FormAddProvider.module.scss b/components/apps/ddm/forms/FormAddProvider.module.scss new file mode 100644 index 0000000..d94c873 --- /dev/null +++ b/components/apps/ddm/forms/FormAddProvider.module.scss @@ -0,0 +1,5 @@ +@import '@components/Form.module.scss'; + +.body { + top: 170px; +} \ No newline at end of file diff --git a/components/apps/ddm/forms/FormAddProvider.tsx b/components/apps/ddm/forms/FormAddProvider.tsx new file mode 100644 index 0000000..feda88c --- /dev/null +++ b/components/apps/ddm/forms/FormAddProvider.tsx @@ -0,0 +1,70 @@ +import * as React from 'react'; +import { addProvider } from '@data/api'; + +import styles from './FormAddProvider.module.scss'; + +import Dismissible from '@components/Dismissible'; +import Button from '@components/Button'; +import Input from '@components/basic/Input'; +import Feedback from '@components/Feedback'; + +export default function FormAddProvider(props: { + updateProviders: () => void, +}) { + + const [providerID, setProviderID] = React.useState(''); + const [providerName, setProviderName] = React.useState(''); + + const [loading, setLoading] = React.useState(false); + const [feedback, setFeedback] = React.useState(); + + async function onSubmit(e) { + e.preventDefault(); + + try { + setFeedback(undefined); + setLoading(true); + + await addProvider(providerID, providerName); + props.updateProviders(); + + setFeedback(); + } catch (e) { + setFeedback({e.toString()}); + } finally { + setLoading(false); + } + } + + return ( +
+

Add provider

+
+
+ setProviderID(e.target.value)} + autoFocus + autoComplete='new-password' + placeholder='example: f012345' + spellCheck='false' + /> +
+
+ setProviderName(e.target.value)} + placeholder="a friendly name" + /> +
+
+ +
+
+ {feedback} +
+ ) +} \ No newline at end of file diff --git a/components/apps/ddm/forms/FormAddReplication.module.scss b/components/apps/ddm/forms/FormAddReplication.module.scss new file mode 100644 index 0000000..9cc69b5 --- /dev/null +++ b/components/apps/ddm/forms/FormAddReplication.module.scss @@ -0,0 +1 @@ +@import '@components/Form.module.scss'; \ No newline at end of file diff --git a/components/forms/FormAddReplication.tsx b/components/apps/ddm/forms/FormAddReplication.tsx similarity index 83% rename from components/forms/FormAddReplication.tsx rename to components/apps/ddm/forms/FormAddReplication.tsx index 420a5f7..50feb69 100644 --- a/components/forms/FormAddReplication.tsx +++ b/components/apps/ddm/forms/FormAddReplication.tsx @@ -7,12 +7,16 @@ import styles from './FormAddReplication.module.scss'; import Dismissible from '@components/Dismissible'; import Button from '@components/Button'; -import Input from 'components/Input'; +import Input from 'components/basic/Input'; import ProviderSelect from '@components/ProviderSelect'; import DatasetSelect from '@components/DatasetSelect'; import Feedback from '@components/Feedback'; -export default function FormAddReplication(props) { +export default function FormAddReplication(props: { + providers: any[], + datasets: any[], + updateReplications: () => void, +}) { const [providerID, setProviderID] = React.useState(''); const [datasetName, setDatasetName] = React.useState(''); const [numDeals, setNumDeals] = React.useState(1); @@ -31,11 +35,9 @@ export default function FormAddReplication(props) { setLoading(true); await addReplication(providerID, datasetName, numDeals, delayStartDays); - - await props.updateState(); + props.updateReplications(); setFeedback(); - setTimeout(props.onOutsideClick, 2500); } catch (e) { setFeedback({e.toString()}); } finally { @@ -44,14 +46,14 @@ export default function FormAddReplication(props) { } return ( - +

Add replication

setProviderID(e.target.value)} required autoFocus />
- setDatasetName(e.target.value)} /> + setDatasetName(e.target.value)} />
{feedback} - +
) } \ No newline at end of file diff --git a/components/forms/FormAddWallet.module.scss b/components/apps/ddm/forms/FormAddWallet.module.scss similarity index 80% rename from components/forms/FormAddWallet.module.scss rename to components/apps/ddm/forms/FormAddWallet.module.scss index 546372e..ee10736 100644 --- a/components/forms/FormAddWallet.module.scss +++ b/components/apps/ddm/forms/FormAddWallet.module.scss @@ -1,4 +1,4 @@ -@import 'Form.module.scss'; +@import '@components/Form.module.scss'; .body { top: 270px; diff --git a/components/forms/FormAddWallet.tsx b/components/apps/ddm/forms/FormAddWallet.tsx similarity index 84% rename from components/forms/FormAddWallet.tsx rename to components/apps/ddm/forms/FormAddWallet.tsx index e490a45..e5aa37a 100644 --- a/components/forms/FormAddWallet.tsx +++ b/components/apps/ddm/forms/FormAddWallet.tsx @@ -9,10 +9,10 @@ import Dismissible from '@components/Dismissible'; export default function FormAddWallet(props) { return ( - +

Add wallet

For security reasons, please use the CLI to add a wallet:

./delta-dm wallet import --hex $(lotus wallet export <wallet address>) - +
); } diff --git a/components/apps/ddm/forms/FormNewDataset.module.scss b/components/apps/ddm/forms/FormNewDataset.module.scss new file mode 100644 index 0000000..9cc69b5 --- /dev/null +++ b/components/apps/ddm/forms/FormNewDataset.module.scss @@ -0,0 +1 @@ +@import '@components/Form.module.scss'; \ No newline at end of file diff --git a/components/forms/FormNewDataset.tsx b/components/apps/ddm/forms/FormNewDataset.tsx similarity index 92% rename from components/forms/FormNewDataset.tsx rename to components/apps/ddm/forms/FormNewDataset.tsx index 5ae4e21..35bd1e9 100644 --- a/components/forms/FormNewDataset.tsx +++ b/components/apps/ddm/forms/FormNewDataset.tsx @@ -7,11 +7,13 @@ import { createSlug } from '@root/common/utilities'; import styles from './FormNewDataset.module.scss'; import Dismissible from '@components/Dismissible'; -import Input from '@components/Input'; +import Input from '@components/basic/Input'; import Button from '@components/Button'; import Feedback from '@components/Feedback'; -export default function FormNewDataset(props) { +export default function FormNewDataset(props: { + updateDatasets: () => void, +}) { let [name, setName] = React.useState(''); let [replications, setReplications] = React.useState(6); let [duration, setDuration] = React.useState(540); @@ -36,10 +38,9 @@ export default function FormNewDataset(props) { unsealed, indexed ); - props.updateState(); + props.updateDatasets(); setFeedback() - setTimeout(props.onOutsideClick, 2500); } catch (e) { setFeedback({e.toString()}); } finally { @@ -68,7 +69,7 @@ export default function FormNewDataset(props) { } return ( - +

New dataset

@@ -91,6 +92,6 @@ export default function FormNewDataset(props) {
{feedback} - +
); } \ No newline at end of file diff --git a/components/forms/FormUploadData.module.scss b/components/apps/ddm/forms/FormUploadData.module.scss similarity index 95% rename from components/forms/FormUploadData.module.scss rename to components/apps/ddm/forms/FormUploadData.module.scss index 844f3a5..89a537b 100644 --- a/components/forms/FormUploadData.module.scss +++ b/components/apps/ddm/forms/FormUploadData.module.scss @@ -1,4 +1,4 @@ -@import 'Form.module.scss'; +@import '@components/Form.module.scss'; .upload { // border: 2px dotted var(--color-border); diff --git a/components/forms/FormUploadData.tsx b/components/apps/ddm/forms/FormUploadData.tsx similarity index 98% rename from components/forms/FormUploadData.tsx rename to components/apps/ddm/forms/FormUploadData.tsx index 7c976bd..b83c3bd 100644 --- a/components/forms/FormUploadData.tsx +++ b/components/apps/ddm/forms/FormUploadData.tsx @@ -14,7 +14,7 @@ import { pluralize, truncCid } from '@root/common/utilities'; export default function FormUploadData(props: { selectedDataset: string, onOutsideClick: React.MouseEventHandler, - updateState: () => void, + updateDatasets: () => void, }) { const [datasetName, setDatasetName] = React.useState(props.selectedDataset || ''); const [file, setFile] = React.useState(null); @@ -48,7 +48,7 @@ export default function FormUploadData(props: { JSON.parse(fileContents); let res = await addContents(props.selectedDataset, fileContents); - props.updateState(); + props.updateDatasets(); setFeedback( diff --git a/components/scenes/SceneAuth.module.scss b/components/apps/ddm/scenes/Auth.module.scss similarity index 100% rename from components/scenes/SceneAuth.module.scss rename to components/apps/ddm/scenes/Auth.module.scss diff --git a/components/scenes/SceneAuth.tsx b/components/apps/ddm/scenes/Auth.tsx similarity index 92% rename from components/scenes/SceneAuth.tsx rename to components/apps/ddm/scenes/Auth.tsx index 260e76e..2edab6d 100644 --- a/components/scenes/SceneAuth.tsx +++ b/components/apps/ddm/scenes/Auth.tsx @@ -3,12 +3,12 @@ import * as React from 'react'; import { checkAuth, checkAuthFormat } from '@data/api'; -import styles from './SceneAuth.module.scss'; +import styles from './Auth.module.scss'; import Button from '@components/Button'; -import Input from '@components/Input'; +import Input from '@components/basic/Input'; -export default function SceneAuth(props) { +export default function Auth(props) { // Store a tmp auth token in the component so the main application auth token // state doesn't update until the user submits the form const [tmpAuthToken, setTmpAuthToken] = React.useState(props.authToken || ''); diff --git a/components/Application.module.scss b/components/apps/ddm/scenes/Datasets.module.scss similarity index 100% rename from components/Application.module.scss rename to components/apps/ddm/scenes/Datasets.module.scss diff --git a/components/scenes/SceneDatasets.tsx b/components/apps/ddm/scenes/Datasets.tsx similarity index 87% rename from components/scenes/SceneDatasets.tsx rename to components/apps/ddm/scenes/Datasets.tsx index 14464a0..b24daa8 100644 --- a/components/scenes/SceneDatasets.tsx +++ b/components/apps/ddm/scenes/Datasets.tsx @@ -3,16 +3,24 @@ import * as React from 'react'; import * as Utilities from '@common/utilities'; -import styles from './SceneDatasets.module.scss'; +import styles from './Datasets.module.scss'; import tableStyles from '@components/Table.module.scss'; -import Input from '@components/Input'; +import Input from '@components/basic/Input'; import LoadingIndicator from '@components/LoadingIndicator'; import WalletRef from '@components/WalletRef'; -export default function SceneDatasets(props) { +export default function Datasets(props: { + datasets: any[], + searchLabel: string, + search: string, + onSearchChange: (string) => void, + placeholder: string, + setSelectedDataset: (string) => void, + onAttachContent: () => void, +}) { return (
- {props.state.datasets && + {props.datasets &&
Indexed Wallets
- {props.state.datasets + {props.datasets .filter((dataset, i) => !props.search || dataset.name.includes(props.search)) .map((dataset, i) => { let progress = dataset.bytes_replicated.padded / dataset.bytes_total.padded / dataset.replication_quota; @@ -66,6 +74,6 @@ export default function SceneDatasets(props) { }
} - {props.state.datasets === undefined && } + {props.datasets === undefined && }
); } diff --git a/components/scenes/SceneProviders.module.scss b/components/apps/ddm/scenes/Providers.module.scss similarity index 100% rename from components/scenes/SceneProviders.module.scss rename to components/apps/ddm/scenes/Providers.module.scss diff --git a/components/scenes/SceneProviders.tsx b/components/apps/ddm/scenes/Providers.tsx similarity index 51% rename from components/scenes/SceneProviders.tsx rename to components/apps/ddm/scenes/Providers.tsx index b081744..bb0ec59 100644 --- a/components/scenes/SceneProviders.tsx +++ b/components/apps/ddm/scenes/Providers.tsx @@ -4,19 +4,27 @@ import * as React from 'react'; import * as Utilities from '@common/utilities'; import { updateProvider } from '@root/data/api'; -import styles from './SceneProviders.module.scss'; +import styles from './Providers.module.scss'; import tableStyles from '@components/Table.module.scss'; -import Input from '@components/Input'; +import Input from '@components/basic/Input'; import LoadingIndicator from '@components/LoadingIndicator'; import ProviderRef from '@components/ProviderRef'; import Button from '@components/Button'; import TagSelect from '@components/TagSelect'; -export default function SceneProviders(props) { +export default function Providers(props: { + providers: any[], + datasets: any[], + updateDatasets: () => void, + providerLabel: string, + placeholder: string, + search: string, + onSearchChange: (string) => void, +}) { return (
- {props.state.providers && + {props.providers && (
Provider Key Allowed Datasets
- {props.state.providers - .filter( - (provider, i) => !props.search || provider.actor_id.includes(props.search) - ) - .map( - (provider, i) => { - return - } - ) - } + {props.providers + .filter((provider, i) => !props.search || provider.actor_id.includes(props.search)) + .map((provider, i) => { + return ; + })}
- } - {props.state.providers === undefined && } + )} + {props.providers === undefined && }
); } -function ProviderCard(props) { +function ProviderCard(props: { provider: any, datasets: any[], updateDatasets: () => void }) { let provider = props.provider; let [editing, setEditing] = React.useState(false); @@ -60,7 +63,7 @@ function ProviderCard(props) { let [saving, setSaving] = React.useState(false); let [allowedDatasets, setAllowedDatasets] = React.useState(provider.allowed_datasets?.map((dataset, i) => dataset.name) || []); - let datasetNames = props.state.datasets?.map((dataset, i) => dataset.name) || []; + let datasetNames = props.datasets?.map((dataset, i) => dataset.name) || []; function cancelEdit() { setAllowSelfService(provider.allow_self_service); @@ -82,40 +85,47 @@ function ProviderCard(props) { setSaving(false); } - props.updateState(); + props.updateDatasets(); } - if (editing) return ( -
- - Rename {provider.actor_id}} - value={name} - onChange={e => setName(e.target.value)} - /> - - -
{Utilities.bytesToSize(provider.bytes_replicated.padded)} (padded)
-
{Utilities.bytesToSize(provider.bytes_replicated.raw)} (unpadded)
-
- - setAllowSelfService(e.target.checked)} /> - - - - - - - -
- ); + if (editing) + return ( +
+ + + Rename {provider.actor_id} + + } + value={name} + onChange={(e) => setName(e.target.value)} + /> + + +
{Utilities.bytesToSize(provider.bytes_replicated.padded)} (padded)
+
{Utilities.bytesToSize(provider.bytes_replicated.raw)} (unpadded)
+
+ + setAllowSelfService(e.target.checked)} /> + + + + + + + + + +
+ ); return (
@@ -128,26 +138,24 @@ function ProviderCard(props) {
{Utilities.bytesToSize(provider.bytes_replicated.raw)} (unpadded)
- + + + + - - + -
); } function ProviderKey(props) { - const [copied, setCopied] = React.useState(false); function copy() { @@ -160,7 +168,9 @@ function ProviderKey(props) { return ( {props.providerKey} - {copied ? 'copied ✓' : 'copy 📋'} + + {copied ? 'copied ✓' : 'copy 📋'} + ); -} \ No newline at end of file +} diff --git a/components/apps/ddm/scenes/Replications.module.scss b/components/apps/ddm/scenes/Replications.module.scss new file mode 100644 index 0000000..9a93546 --- /dev/null +++ b/components/apps/ddm/scenes/Replications.module.scss @@ -0,0 +1,55 @@ +.body { +} + +.filterMenu { + border-bottom: 1px solid var(--color-border); + padding: 1.5rem; +} + +.filterMenuBody { + display: flex; + flex-direction: column; + margin: 0 -1rem -1.5rem; +} + +.filterMenuColumn { + flex-grow: 1; + flex-basis: 0; + margin: 0 1rem; +} + +.filterMenuRow { + display: flex; + flex-direction: row; + margin-bottom: 1.5rem; +} + +.filterMenuButtonColumn { + @extend .filterMenuColumn; + + flex-grow: 0; + min-width: 10rem; +} + +.indexButton { + // text-decoration: underline; + color: var(--color-primary); + margin: 0 0.5rem; + + &:hover { + cursor: pointer; + } +} + +.pageIndex { + margin: 1.5rem; +} + +.indexButtonActive { + @extend .indexButton; + + // color: var(--color-text); + font-size: 1.2em; + font-weight: bold; + text-decoration: underline; +} \ No newline at end of file diff --git a/components/apps/ddm/scenes/Replications.tsx b/components/apps/ddm/scenes/Replications.tsx new file mode 100644 index 0000000..dbf13d4 --- /dev/null +++ b/components/apps/ddm/scenes/Replications.tsx @@ -0,0 +1,169 @@ +'use client'; + +import * as React from 'react'; +import * as Utilities from '@common/utilities'; + +import styles from './Replications.module.scss'; +import tableStyles from '@components/Table.module.scss'; + +import Input from '@components/basic/Input'; +import LoadingIndicator from '@components/LoadingIndicator'; +import ProviderRef from '@components/ProviderRef'; +import Button from '@components/Button'; +import { GetReplicationsConfig, updateProvider } from '@root/data/api'; +import Select from '@root/components/basic/Select'; + +export default function Replications(props: { + replications: any, + updateReplications: () => void, + getReplicationsConfig: GetReplicationsConfig, + setGetReplicationsConfig: (cfg: GetReplicationsConfig) => void, +}) { + const [searchDatasets, setSearchDatasets] = React.useState(''); + const [searchProviders, setSearchProviders] = React.useState(''); + const [searchTimeMin, setSearchTimeMin] = React.useState(''); + const [searchTimeMax, setSearchTimeMax] = React.useState(''); + const [searchSelfService, setSearchSelfService] = React.useState(''); + const [searchProposalCID, setSearchProposalCID] = React.useState(''); + const [searchPieceCID, setSearchPieceCID] = React.useState(''); + const [searchMessage, setSearchMessage] = React.useState(''); + const [offset, setOffset] = React.useState(0); + const limit = 100; + + React.useEffect(() => { + applySearch(); + props.updateReplications(); + }, [offset]); + + function applySearch() { + props.setGetReplicationsConfig({ + offset: offset, + limit: limit, + datasets: searchDatasets.split(',').map((dataset) => dataset.trim()), + providers: searchProviders.split(',').map((provider) => provider.trim()), + timeMin: searchTimeMin && new Date(searchTimeMin), + timeMax: searchTimeMax && new Date(searchTimeMax), + selfService: !!searchSelfService, + proposalCID: searchProposalCID.trim(), + pieceCID: searchPieceCID.trim(), + message: searchMessage.trim(), + }); + props.updateReplications(); + } + + return ( +
+ { +
+
+
e.preventDefault()}> +
+
+ setSearchDatasets(e.target.value)} /> +
+
+ setSearchProviders(e.target.value)} /> +
+
+ setSearchTimeMin(e.target.value)} /> +
+
+ setSearchTimeMax(e.target.value)} /> +
+
+ +
+
+
+
+ setSearchProposalCID(e.target.value)} /> +
+
+ setSearchPieceCID(e.target.value)} /> +
+
+
+
+ setSearchMessage(e.target.value)} /> +
+
+ +
+
+
+
+
+
Dataset
+
Status
+
Provider ID
+
Self Service
+
Deal Time
+
Proposal CID
+
Piece CID (CommP)
+
Message
+
+ {props.replications?.data?.map((replication, i) => { + return ( +
+
+
{replication.content.dataset_name}
+
{replication.status}
+
+ +
+
{replication.is_self_service ? 'true' : 'false'}
+
{new Date(Date.parse(replication.deal_time)).toUTCString()}
+
{replication.proposal_cid}
+
{replication.content_commp}
+
{replication.delta_message}
+
+
+ ); + })} +
+ } + { + setOffset(offset); + }} + limit={limit} + total={props.replications?.totalCount} + /> + {props.replications === undefined && } +
+ ); +} + +function PageIndex(props: { offset: number, onChangeOffset: (number) => void, limit: number, total: number }) { + const pageCount = Math.ceil(props.total / props.limit); + const currPage = Math.floor(props.offset / props.limit); + + const itemCount = Math.min(props.limit, props.total - props.offset); + const firstItem = props.offset + 1; + const lastItem = props.offset + itemCount; + + return ( +
+ + Showing {firstItem} - {lastItem} of {props.total} results + + props.onChangeOffset(0)}> + << + + {...Array(pageCount || 1) + .fill(0) + .map((_, i) => ( + props.onChangeOffset(i * props.limit)}> + {i + 1} + + ))} + props.onChangeOffset((pageCount - 1) * props.limit)}> + >> + +
+ ); +} diff --git a/components/scenes/SceneWallets.module.scss b/components/apps/ddm/scenes/Wallets.module.scss similarity index 100% rename from components/scenes/SceneWallets.module.scss rename to components/apps/ddm/scenes/Wallets.module.scss diff --git a/components/scenes/SceneWallets.tsx b/components/apps/ddm/scenes/Wallets.tsx similarity index 64% rename from components/scenes/SceneWallets.tsx rename to components/apps/ddm/scenes/Wallets.tsx index 9f8c38b..5c8427d 100644 --- a/components/scenes/SceneWallets.tsx +++ b/components/apps/ddm/scenes/Wallets.tsx @@ -1,24 +1,23 @@ 'use client'; -import styles from './SceneWallets.module.scss'; +import styles from './Wallets.module.scss'; import tableStyles from '@components/Table.module.scss'; import * as React from 'react'; import * as Utilities from '@common/utilities'; +import apiIndex from '@root/pages/api'; +import { associateWallet } from '@root/data/api'; -import Input from '@components/Input'; +import Input from '@components/basic/Input'; import LoadingIndicator from '@components/LoadingIndicator'; import WalletRef from '@components/WalletRef'; import TagSelect from '@components/TagSelect'; -import apiIndex from '@root/pages/api'; -import { associateWallet } from '@root/data/api'; -import Button from '../Button'; +import Button from '@components/Button'; -export default function SceneWallets(props) { - +export default function Wallets(props: { wallets: any[], updateWallets: () => void, datasets: any[], updateDatasets: () => void }) { return (
- {props.state.wallets && + {props.wallets && (
Address @@ -26,30 +25,23 @@ export default function SceneWallets(props) { Datacap Balance Datasets
- {props.state.wallets.map((wallet, i) => { + {props.wallets.map((wallet, i) => { return (
- dataset.name)} - updateState={props.updateState} /> + dataset.name)} updateDatasets={props.updateDatasets} />
); })}
- } - {props.state.wallets === undefined && } -
- ) + )} + {props.wallets === undefined && } + + ); } -function WalletCard(props: { - wallet: any, - datasets: string[], - updateState: CallableFunction -}) { +function WalletCard(props: { wallet: any, datasets: string[], updateDatasets: () => void }) { let selectedDefault = () => props.wallet.datasets.map((dataset, i) => dataset.name); - + const [selected, setSelected] = React.useState(selectedDefault()); const [editing, setEditing] = React.useState(false); @@ -74,7 +66,7 @@ function WalletCard(props: { setSaving(false); } - props.updateState(); + props.updateDatasets(); } return ( @@ -87,16 +79,22 @@ function WalletCard(props: { - { - editing - ? <> - - - - : <> - - - } + {editing ? ( + <> + + + + ) : ( + <> + + + )} ); -} \ No newline at end of file +} diff --git a/components/apps/ptolemy/Ptolemy.tsx b/components/apps/ptolemy/Ptolemy.tsx new file mode 100644 index 0000000..6605edc --- /dev/null +++ b/components/apps/ptolemy/Ptolemy.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; + +import styles from '@components/Form.module.scss'; + +import DefaultLayout, { AppBody, AppNav, AppNavItem, AppNavSubItem, AppTitle, AppVersion } from '@components/DefaultLayout'; +import Modal from '@components/Modal'; +import Input from '@components/basic/Input'; +import FileSelect from '@components/FileSelect'; +import Select from '@components/basic/Select'; +import Slider from '@components/basic/Slider'; + +const formStates = { + none: 0, + createJob: 1, +} + +export default function Ptolemy(props) { + const [formState, setFormState] = React.useState(formStates.none); + + const createJobButton = React.useRef(null); + + function onClose(modalID) { + if (modalID === formState) { + setFormState(formStates.none); + } + } + + return ( + + Ptolemy + Vesion Placeholder + + Jobs + setFormState(formStates.createJob)}> + + Create job + + {formState === formStates.createJob && +

Create Job

+
+ +
+
+ +
+
+ +
+
} +
+ + +
+ ) +} \ No newline at end of file diff --git a/components/Input.module.scss b/components/basic/Input.module.scss similarity index 93% rename from components/Input.module.scss rename to components/basic/Input.module.scss index 35fb2c3..21e9a16 100644 --- a/components/Input.module.scss +++ b/components/basic/Input.module.scss @@ -1,3 +1,5 @@ +@import './Label.module.scss'; + .body { position: relative; } @@ -70,11 +72,4 @@ color: var(--color-primary); } } -} - -.label { - font-size: 0.8rem; - text-transform: uppercase; - display: block; - color: gray; } \ No newline at end of file diff --git a/components/Input.tsx b/components/basic/Input.tsx similarity index 57% rename from components/Input.tsx rename to components/basic/Input.tsx index d25d9ad..0a7f5a7 100644 --- a/components/Input.tsx +++ b/components/basic/Input.tsx @@ -1,9 +1,9 @@ 'use client'; -import styles from '@components/Input.module.scss'; +import * as React from 'react'; import { createSlug } from '@root/common/utilities'; -import * as React from 'react'; +import styles from './Input.module.scss'; export default function Input(props: { label?: any, @@ -12,12 +12,18 @@ export default function Input(props: { } & React.ComponentPropsWithRef<'input'>) { let id = (props.id ? props.id + '-' : '') + createSlug(props.label); + // Find custom props that should be forwarded to the input element by removing custom props + const filteredProps = Object.assign({}, props); + delete filteredProps.label; + delete filteredProps.inputClassName; + delete filteredProps.labelClassName; + return (
- +
); } diff --git a/components/basic/Label.module.scss b/components/basic/Label.module.scss new file mode 100644 index 0000000..a59a359 --- /dev/null +++ b/components/basic/Label.module.scss @@ -0,0 +1,6 @@ +.label { + font-size: 0.8rem; + text-transform: uppercase; + display: block; + color: gray; +} \ No newline at end of file diff --git a/components/Select.module.scss b/components/basic/Select.module.scss similarity index 82% rename from components/Select.module.scss rename to components/basic/Select.module.scss index 91c7ff5..52ee1ea 100644 --- a/components/Select.module.scss +++ b/components/basic/Select.module.scss @@ -1,14 +1,9 @@ +@import './Label.module.scss'; + .body { position: relative; } -.label { - font-size: 0.8em; - text-transform: uppercase; - display: block; - color: gray; -} - .select { background: var(--color-background); border: none; diff --git a/components/basic/Select.tsx b/components/basic/Select.tsx new file mode 100644 index 0000000..6f42120 --- /dev/null +++ b/components/basic/Select.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; + +import styles from './Select.module.scss'; + +export default function Select(props: { + id?: string, + placeholder?: string, + label?: string, + onChange?: React.ChangeEventHandler, + value?: string, + required?: boolean, + disabled?: boolean + children?: React.ReactElement[], +}) { + const id = props.id || 'select-' + Math.floor(Math.random() * 100000); + + return ( +
+ + +
+ ) +} \ No newline at end of file diff --git a/components/basic/Slider.module.scss b/components/basic/Slider.module.scss new file mode 100644 index 0000000..31337d0 --- /dev/null +++ b/components/basic/Slider.module.scss @@ -0,0 +1,29 @@ +@import 'Label.module.scss'; + +.slider { + transition: 0.25s; + width: 100%; + display: block; + appearance: none; + background: var(--color-border); + height: 2px; + padding: 0; + margin: 0.75rem 0; + + &::-webkit-slider-thumb { + background: var(--color-primary); + appearance: none; + display: block; + width: 1rem; + height: 1rem; + border-radius: 0.5rem + } + + &:focus { + background: var(--color-text); + } + + &:focus { + outline: none; + } +} \ No newline at end of file diff --git a/components/basic/Slider.tsx b/components/basic/Slider.tsx new file mode 100644 index 0000000..efda1ad --- /dev/null +++ b/components/basic/Slider.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; + +import styles from './Slider.module.scss'; + +export default function Slider(props: { + id?: string, + min: number, + max: number, + step?: number, + initialValue?: number + label?: string, + onChange?: React.ChangeEventHandler, + value?: number, + required?: boolean +}) { + const id = props.id || 'slider-' + Math.floor(Math.random() * 100000).toString(); + + const [value, setValue] = React.useState(props.initialValue || props.min); + + return ( +
+ + { + setValue(Number.parseFloat(e.target.value)); + props.onChange && props.onChange(e); + }} + value={value} + step={props.step} + required={props.required} + /> +
+ ) +} \ No newline at end of file diff --git a/components/forms/FormAddProvider.module.scss b/components/forms/FormAddProvider.module.scss deleted file mode 100644 index 868044b..0000000 --- a/components/forms/FormAddProvider.module.scss +++ /dev/null @@ -1,5 +0,0 @@ -@import 'Form.module.scss'; - -.body { - top: 170px; -} \ No newline at end of file diff --git a/components/forms/FormAddProvider.tsx b/components/forms/FormAddProvider.tsx deleted file mode 100644 index 0cad91d..0000000 --- a/components/forms/FormAddProvider.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import * as React from 'react'; -import { addProvider } from '@data/api'; - -import styles from './FormAddProvider.module.scss'; - -import Dismissible from '@components/Dismissible'; -import Button from '@components/Button'; -import Input from '@components/Input'; -import Feedback from '@components/Feedback'; - -export default function FormAddProvider(props) { - - let [providerID, setProviderID] = React.useState(''); - let [providerName, setProviderName] = React.useState(''); - - let [loading, setLoading] = React.useState(false); - let [feedback, setFeedback] = React.useState(); - - async function onSubmit(e) { - e.preventDefault(); - - try { - setFeedback(undefined); - setLoading(true); - - await addProvider(providerID, providerName); - props.updateState(); - - setFeedback(); - setTimeout(props.onOutsideClick, 2500); - } catch (e) { - setFeedback({e.toString()}); - } finally { - setLoading(false); - } - } - - return ( -

Add provider

-
-
- setProviderID(e.target.value)} - autoFocus - autoComplete='new-password' - placeholder='example: f012345' - spellCheck='false' - /> -
-
- setProviderName(e.target.value)} - placeholder="a friendly name" - /> -
-
- -
-
- {feedback} -
) -} \ No newline at end of file diff --git a/components/forms/FormAddReplication.module.scss b/components/forms/FormAddReplication.module.scss deleted file mode 100644 index 2c8ec02..0000000 --- a/components/forms/FormAddReplication.module.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'Form.module.scss'; \ No newline at end of file diff --git a/components/forms/FormAssociateWallet.module.scss b/components/forms/FormAssociateWallet.module.scss deleted file mode 100644 index ebe5f2d..0000000 --- a/components/forms/FormAssociateWallet.module.scss +++ /dev/null @@ -1,5 +0,0 @@ -@import 'Form.module.scss'; - -.body { - width: 350px; -} \ No newline at end of file diff --git a/components/forms/FormAssociateWallet.tsx b/components/forms/FormAssociateWallet.tsx deleted file mode 100644 index be26fdd..0000000 --- a/components/forms/FormAssociateWallet.tsx +++ /dev/null @@ -1,56 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { associateWallet } from '@data/api'; - -import styles from './FormAssociateWallet.module.scss'; - -import Button from '@components/Button'; -import Dismissible from '@components/Dismissible'; -import DatasetSelect from '@components/DatasetSelect'; -import TagSelect from '@components/TagSelect'; -import Feedback from '@components/Feedback'; - -export default function FormAssociateWallet(props) { - const [datasets, setDatasets] = React.useState([]); - - const [loading, setLoading] = React.useState(false); - const [feedback, setFeedback] = React.useState(); - - async function onSubmit(e) { - e.preventDefault(); - - try { - setLoading(true); - - await associateWallet(props.selectedWallet, datasets); - props.updateState(); - - setTimeout(props.onOutsideClick, 2500); - } catch (e) { - setFeedback({e.toString()}); - } finally { - setLoading(false); - } - } - - function isFormValid() { - return datasets.length !== 0; - } - - return ( - -

Associate wallet

-

{props.selectedWallet}

-
-
- dataset.name)} /> -
-
- -
-
- {feedback &&
{feedback}
} -
- ) -} \ No newline at end of file diff --git a/components/forms/FormNewDataset.module.scss b/components/forms/FormNewDataset.module.scss deleted file mode 100644 index 2c8ec02..0000000 --- a/components/forms/FormNewDataset.module.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'Form.module.scss'; \ No newline at end of file diff --git a/components/scenes/SceneDatasets.module.scss b/components/scenes/SceneDatasets.module.scss deleted file mode 100644 index 6d86a06..0000000 --- a/components/scenes/SceneDatasets.module.scss +++ /dev/null @@ -1,2 +0,0 @@ -.body { -} diff --git a/components/scenes/SceneReplications.module.scss b/components/scenes/SceneReplications.module.scss deleted file mode 100644 index 6d86a06..0000000 --- a/components/scenes/SceneReplications.module.scss +++ /dev/null @@ -1,2 +0,0 @@ -.body { -} diff --git a/components/scenes/SceneReplications.tsx b/components/scenes/SceneReplications.tsx deleted file mode 100644 index d4a6dd2..0000000 --- a/components/scenes/SceneReplications.tsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as Utilities from '@common/utilities'; - -import styles from './SceneReplications.module.scss'; -import tableStyles from '@components/Table.module.scss'; - -import Input from '@components/Input'; -import LoadingIndicator from '@components/LoadingIndicator'; -import ProviderRef from '@components/ProviderRef'; - -export default function SceneReplications(props) { - return ( -
- {props.state.replications && -
- -
-
Dataset
-
Status
-
Provider ID
-
Self Service
-
Deal Time
-
Proposal CID
-
Piece CID (CommP)
-
Message
-
- {props.state.replications - .filter((replication, i) => { - return !props.search - || replication.status?.toLowerCase().includes(props.search.toLowerCase()) - || replication.deal_time?.toString().toLowerCase().includes(props.search.toLowerCase()) - || replication.provider_actor_id?.toLowerCase().includes(props.search.toLowerCase()) - || replication.proposal_cid?.toLowerCase().includes(props.search.toLowerCase()) - || replication.content_commp?.toLowerCase().includes(props.search.toLowerCase()) - || replication.delta_message?.toLowerCase().includes(props.search.toLowerCase()); - }) - .map((replication, i) => { - return ( -
-
-
{replication.content.dataset_name}
-
{replication.status}
-
-
{replication.is_self_service ? "true" : "false"}
-
{new Date(Date.parse(replication.deal_time)).toUTCString()}
-
{replication.proposal_cid}
-
{replication.content_commp}
-
{replication.delta_message}
-
-
- ) - }) - } -
- } - {props.state.replications === undefined && } -
- ); -} diff --git a/data/api.ts b/data/api.ts index eb49944..2a2c6cf 100644 --- a/data/api.ts +++ b/data/api.ts @@ -7,9 +7,9 @@ function apiURL() { function defaultHeaders() { return { 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + getCookie('auth'), - } -}; + Authorization: 'Bearer ' + getCookie('auth'), + }; +} // Checks only whether an auth key is in a valid format, without contacting // delta-dm API. Optionally accepts an auth key parameter - if not provided, the @@ -38,7 +38,7 @@ export async function checkAuth(auth?: string, ddmAddress?: string): Promise