1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-11-15 01:34:57 +01:00

Set, edit, and display timeout (#324)

* added timeout to publish asset

* add timeout to edit asses(wip)

* added timout to edit metadata form

* fixed wrong constant name

* fix options autosorting

Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro>

* Fixed autosorting in edit form

* Added "1 day" to timeout options

* Changed ternary operators to switch

* Feature/asset timeout (#325)

* Compute asset timeout

* Code styled

* Deleted unused import

* Display timeout for buy/download

* Switch case for timeout values

* Moved mapping function to /utils/metadata

* display timeout option not matching defined ones, map seconds to string

* handle update with no predefined timeout value, add weeks to map method

* Display timeout on button

* consume button text logic change

* whoops, revert wrong change

* small millisecondsToStr refactor

* copy tweaks

* template literal logic restore

* keep tweaking help text logic

* abstract into method
* change whole condition logic
* tweak hasDatatoken/hasPreviousOrder combination condition

* Unified seconds to string conversion methods

* getHelpText tweaks, small refactor

* copy editing, limit hardcoded timeout list

* fix mixup of map & filter

* use Timeout as label and be done with it

Co-authored-by: mihaisc <mihai.scarlat@smartcontrol.ro>
Co-authored-by: claudiaHash <49017601+claudiaHash@users.noreply.github.com>
Co-authored-by: Matthias Kretschmann <m@kretschmann.io>
Co-authored-by: Claudia Holhos <clawww1996@gmail.com>
This commit is contained in:
Norby 2021-01-22 18:12:48 +02:00 committed by GitHub
parent 7b854e09dd
commit a2fe2fdee0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 234 additions and 32 deletions

View File

@ -19,6 +19,15 @@
"type": "textarea", "type": "textarea",
"rows": 10, "rows": 10,
"required": true "required": true
},
{
"name": "timeout",
"label": "Timeout",
"help": "Define how long buyers should be able to download the data set again after the initial purchase.",
"type": "select",
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
"sortOptions": false,
"required": true
} }
] ]
} }

View File

@ -42,6 +42,15 @@
"options": ["Download"], "options": ["Download"],
"required": true "required": true
}, },
{
"name": "timeout",
"label": "Timeout",
"help": "Define how long buyers should be able to download the data set again after the initial purchase.",
"type": "select",
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
"sortOptions": false,
"required": true
},
{ {
"name": "dataTokenOptions", "name": "dataTokenOptions",
"label": "Datatoken Name & Symbol", "label": "Datatoken Name & Symbol",

View File

@ -3,6 +3,7 @@ export interface FormFieldProps {
name: string name: string
type?: string type?: string
options?: string[] options?: string[]
sortOptions?: boolean
required?: boolean required?: boolean
help?: string help?: string
placeholder?: string placeholder?: string

View File

@ -30,6 +30,7 @@ export interface MetadataPublishForm {
description: string description: string
files: string | File[] files: string | File[]
author: string author: string
timeout: string
dataTokenOptions: DataTokenOptions dataTokenOptions: DataTokenOptions
access: 'Download' | 'Compute' | string access: 'Download' | 'Compute' | string
termsAndConditions: boolean termsAndConditions: boolean

View File

@ -21,6 +21,10 @@
composes: help from './index.module.css'; composes: help from './index.module.css';
} }
.help:not(:empty) {
margin-top: calc(var(--spacer) / 2);
}
.feedback { .feedback {
width: 100%; width: 100%;
} }

View File

@ -11,6 +11,27 @@ import { useOcean, useConsume, usePricing } from '@oceanprotocol/react'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata' import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
import checkPreviousOrder from '../../../utils/checkPreviousOrder' import checkPreviousOrder from '../../../utils/checkPreviousOrder'
import { useAsset } from '../../../providers/Asset' import { useAsset } from '../../../providers/Asset'
import { secondsToString } from '../../../utils/metadata'
function getHelpText(
token: {
dtBalance: string
dtSymbol: string
},
hasDatatoken: boolean,
hasPreviousOrder: boolean,
timeout: string
) {
const { dtBalance, dtSymbol } = token
const assetTimeout = timeout === 'Forever' ? '' : ` for ${timeout}`
const text = hasPreviousOrder
? `You bought this data set already allowing you to download it without paying again${assetTimeout}.`
: hasDatatoken
? `You own ${dtBalance} ${dtSymbol} allowing you to use this data set by spending 1 ${dtSymbol}, but without paying OCEAN again.`
: `For using this data set, you will buy 1 ${dtSymbol} and immediately spend it back to the publisher and pool.`
return text
}
export default function Consume({ export default function Consume({
ddo, ddo,
@ -35,6 +56,12 @@ export default function Consume({
const [isDisabled, setIsDisabled] = useState(true) const [isDisabled, setIsDisabled] = useState(true)
const [hasDatatoken, setHasDatatoken] = useState(false) const [hasDatatoken, setHasDatatoken] = useState(false)
const [isConsumable, setIsConsumable] = useState(true) const [isConsumable, setIsConsumable] = useState(true)
const [assetTimeout, setAssetTimeout] = useState('')
useEffect(() => {
const { timeout } = ddo.findServiceByType('access').attributes.main
setAssetTimeout(secondsToString(timeout))
}, [ddo])
useEffect(() => { useEffect(() => {
if (!price) return if (!price) return
@ -72,6 +99,7 @@ export default function Consume({
if (!ocean || !accountId) return if (!ocean || !accountId) return
async function checkOrders() { async function checkOrders() {
// HEADS UP! checkPreviousOrder() also checks for expiration of possible set timeout.
const orderId = await checkPreviousOrder(ocean, accountId, ddo, 'access') const orderId = await checkPreviousOrder(ocean, accountId, ddo, 'access')
setPreviousOrderId(orderId) setPreviousOrderId(orderId)
setHasPreviousOrder(!!orderId) setHasPreviousOrder(!!orderId)
@ -104,20 +132,20 @@ export default function Consume({
) : ( ) : (
<> <>
<Button style="primary" onClick={handleConsume} disabled={isDisabled}> <Button style="primary" onClick={handleConsume} disabled={isDisabled}>
{hasDatatoken || hasPreviousOrder ? 'Download' : 'Buy'} {hasPreviousOrder
? 'Download'
: `Buy ${
assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`
}`}
</Button> </Button>
{hasDatatoken && ( <div className={styles.help}>
<div className={styles.help}> {getHelpText(
You own {dtBalance} {ddo.dataTokenInfo.symbol} allowing you to use { dtBalance, dtSymbol: ddo.dataTokenInfo.symbol },
this data set without paying again. hasDatatoken,
</div> hasPreviousOrder,
)} assetTimeout
{(!hasDatatoken || !hasPreviousOrder) && ( )}
<div className={styles.help}> </div>
For using this data set, you will buy 1 {ddo.dataTokenInfo.symbol}{' '}
and immediately spend it back to the publisher and pool.
</div>
)}
</> </>
)} )}
</div> </div>

View File

@ -6,13 +6,52 @@ import Input from '../../../atoms/Input'
import { useOcean } from '@oceanprotocol/react' import { useOcean } from '@oceanprotocol/react'
import { FormFieldProps } from '../../../../@types/Form' import { FormFieldProps } from '../../../../@types/Form'
import { MetadataPublishForm } from '../../../../@types/MetaData' import { MetadataPublishForm } from '../../../../@types/MetaData'
import { checkIfTimeoutInPredefinedValues } from '../../../../utils/metadata'
function handleTimeoutCustomOption(
data: FormFieldProps[],
values: Partial<MetadataPublishForm>
) {
const timeoutFieldContent = data.filter(
(field) => field.name === 'timeout'
)[0]
const timeoutInputIndex = data.findIndex(
(element) => element.name === 'timeout'
)
if (
data[timeoutInputIndex].options.length < 6 &&
!checkIfTimeoutInPredefinedValues(
values.timeout,
timeoutFieldContent.options
)
) {
data[timeoutInputIndex].options.push(values.timeout)
} else if (
data[timeoutInputIndex].options.length === 6 &&
checkIfTimeoutInPredefinedValues(
values.timeout,
timeoutFieldContent.options
)
) {
data[timeoutInputIndex].options.pop()
} else if (
data[timeoutInputIndex].options.length === 6 &&
data[timeoutInputIndex].options[5] !== values.timeout
) {
data[timeoutInputIndex].options[5] = values.timeout
}
}
export default function FormEditMetadata({ export default function FormEditMetadata({
data, data,
setShowEdit setShowEdit,
setTimeoutStringValue,
values
}: { }: {
data: FormFieldProps[] data: FormFieldProps[]
setShowEdit: (show: boolean) => void setShowEdit: (show: boolean) => void
setTimeoutStringValue: (value: string) => void
values: Partial<MetadataPublishForm>
}): ReactElement { }): ReactElement {
const { ocean, accountId } = useOcean() const { ocean, accountId } = useOcean()
const { const {
@ -31,6 +70,11 @@ export default function FormEditMetadata({
setFieldValue(field.name, e.target.value) setFieldValue(field.name, e.target.value)
} }
// This component is handled by Formik so it's not rendered like a "normal" react component,
// so handleTimeoutCustomOption is called only once.
// https://github.com/oceanprotocol/market/pull/324#discussion_r561132310
if (data && values) handleTimeoutCustomOption(data, values)
return ( return (
<Form className={styles.form}> <Form className={styles.form}>
{data.map((field: FormFieldProps) => ( {data.map((field: FormFieldProps) => (
@ -45,7 +89,11 @@ export default function FormEditMetadata({
))} ))}
<footer className={styles.actions}> <footer className={styles.actions}>
<Button style="primary" disabled={!ocean || !accountId || !isValid}> <Button
style="primary"
disabled={!ocean || !accountId || !isValid}
onClick={() => setTimeoutStringValue(values.timeout)}
>
Submit Submit
</Button> </Button>
<Button style="text" onClick={() => setShowEdit(false)}> <Button style="text" onClick={() => setShowEdit(false)}>

View File

@ -12,6 +12,7 @@ import MetadataPreview from '../../../molecules/MetadataPreview'
import Debug from './Debug' import Debug from './Debug'
import Web3Feedback from '../../../molecules/Wallet/Feedback' import Web3Feedback from '../../../molecules/Wallet/Feedback'
import FormEditMetadata from './FormEditMetadata' import FormEditMetadata from './FormEditMetadata'
import { mapTimeoutStringToSeconds } from '../../../../utils/metadata'
import styles from './index.module.css' import styles from './index.module.css'
import { Logger } from '@oceanprotocol/lib' import { Logger } from '@oceanprotocol/lib'
import MetadataFeedback from '../../../molecules/MetadataFeedback' import MetadataFeedback from '../../../molecules/MetadataFeedback'
@ -35,6 +36,7 @@ const contentQuery = graphql`
help help
type type
required required
sortOptions
options options
rows rows
} }
@ -59,6 +61,7 @@ export default function Edit({
const { metadata, ddo, refreshDdo } = useAsset() const { metadata, ddo, refreshDdo } = useAsset()
const [success, setSuccess] = useState<string>() const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>() const [error, setError] = useState<string>()
const [timeoutStringValue, setTimeoutStringValue] = useState<string>()
const hasFeedback = error || success const hasFeedback = error || success
@ -68,24 +71,46 @@ export default function Edit({
) { ) {
try { try {
// Construct new DDO with new values // Construct new DDO with new values
const newDdo = await ocean.assets.editMetadata(ddo, { const ddoEditedMetdata = await ocean.assets.editMetadata(ddo, {
title: values.name, title: values.name,
description: values.description description: values.description
}) })
// Update DDO on-chain if (!ddoEditedMetdata) {
const tx = await ocean.assets.updateMetadata(newDdo, accountId) setError(content.form.error)
Logger.error(content.form.error)
return
}
let ddoEditedTimeout = ddoEditedMetdata
if (timeoutStringValue !== values.timeout) {
const service = ddoEditedMetdata.findServiceByType('access')
const timeout = mapTimeoutStringToSeconds(values.timeout)
ddoEditedTimeout = await ocean.assets.editServiceTimeout(
ddoEditedMetdata,
service.index,
timeout
)
}
// Edit failed if (!ddoEditedTimeout) {
if (!newDdo || !tx) {
setError(content.form.error) setError(content.form.error)
Logger.error(content.form.error) Logger.error(content.form.error)
return return
} }
// Edit succeeded const storedddo = await ocean.assets.updateMetadata(
setSuccess(content.form.success) ddoEditedTimeout,
resetForm() accountId
)
if (!storedddo) {
setError(content.form.error)
Logger.error(content.form.error)
return
} else {
// Edit succeeded
setSuccess(content.form.success)
resetForm()
}
} catch (error) { } catch (error) {
Logger.error(error.message) Logger.error(error.message)
setError(error.message) setError(error.message)
@ -94,7 +119,10 @@ export default function Edit({
return ( return (
<Formik <Formik
initialValues={getInitialValues(metadata)} initialValues={getInitialValues(
metadata,
ddo.findServiceByType('access').attributes.main.timeout
)}
validationSchema={validationSchema} validationSchema={validationSchema}
onSubmit={async (values, { resetForm }) => { onSubmit={async (values, { resetForm }) => {
// move user's focus to top of screen // move user's focus to top of screen
@ -103,7 +131,7 @@ export default function Edit({
await handleSubmit(values, resetForm) await handleSubmit(values, resetForm)
}} }}
> >
{({ isSubmitting, values }) => {({ isSubmitting, values, initialValues }) =>
isSubmitting || hasFeedback ? ( isSubmitting || hasFeedback ? (
<MetadataFeedback <MetadataFeedback
title="Updating Data Set" title="Updating Data Set"
@ -125,6 +153,8 @@ export default function Edit({
<FormEditMetadata <FormEditMetadata
data={content.form.data} data={content.form.data}
setShowEdit={setShowEdit} setShowEdit={setShowEdit}
setTimeoutStringValue={setTimeoutStringValue}
values={initialValues}
/> />
<aside> <aside>

View File

@ -6,7 +6,10 @@ import FormPublish from './FormPublish'
import Web3Feedback from '../../molecules/Wallet/Feedback' import Web3Feedback from '../../molecules/Wallet/Feedback'
import { FormContent } from '../../../@types/Form' import { FormContent } from '../../../@types/Form'
import { initialValues, validationSchema } from '../../../models/FormPublish' import { initialValues, validationSchema } from '../../../models/FormPublish'
import { transformPublishFormToMetadata } from '../../../utils/metadata' import {
transformPublishFormToMetadata,
mapTimeoutStringToSeconds
} from '../../../utils/metadata'
import MetadataPreview from '../../molecules/MetadataPreview' import MetadataPreview from '../../molecules/MetadataPreview'
import { MetadataPublishForm } from '../../../@types/MetaData' import { MetadataPublishForm } from '../../../@types/MetaData'
import { useUserPreferences } from '../../../providers/UserPreferences' import { useUserPreferences } from '../../../providers/UserPreferences'
@ -37,6 +40,8 @@ export default function PublishPage({
resetForm: () => void resetForm: () => void
): Promise<void> { ): Promise<void> {
const metadata = transformPublishFormToMetadata(values) const metadata = transformPublishFormToMetadata(values)
const timeout = mapTimeoutStringToSeconds(values.timeout)
const serviceType = values.access === 'Download' ? 'access' : 'compute' const serviceType = values.access === 'Download' ? 'access' : 'compute'
try { try {
@ -50,7 +55,8 @@ export default function PublishPage({
const ddo = await publish( const ddo = await publish(
(metadata as unknown) as Metadata, (metadata as unknown) as Metadata,
serviceType, serviceType,
values.dataTokenOptions values.dataTokenOptions,
timeout
) )
// Publish failed // Publish failed

View File

@ -31,6 +31,7 @@
--border-color: #e2e2e2; --border-color: #e2e2e2;
--box-shadow-color: rgba(0, 0, 0, 0.05); --box-shadow-color: rgba(0, 0, 0, 0.05);
--font-family-base: 'Sharp Sans', -apple-system, BlinkMacSystemFont, --font-family-base: 'Sharp Sans', -apple-system, BlinkMacSystemFont,
'Segoe UI', Helvetica, Arial, sans-serif; 'Segoe UI', Helvetica, Arial, sans-serif;
--font-family-heading: 'Sharp Sans Display', -apple-system, BlinkMacSystemFont, --font-family-heading: 'Sharp Sans Display', -apple-system, BlinkMacSystemFont,

View File

@ -1,18 +1,22 @@
import { MetadataMarket, MetadataPublishForm } from '../@types/MetaData' import { MetadataMarket, MetadataPublishForm } from '../@types/MetaData'
import { secondsToString } from '../utils/metadata'
import * as Yup from 'yup' import * as Yup from 'yup'
export const validationSchema = Yup.object().shape({ export const validationSchema = Yup.object().shape({
name: Yup.string() name: Yup.string()
.min(4, (param) => `Title must be at least ${param.min} characters`) .min(4, (param) => `Title must be at least ${param.min} characters`)
.required('Required'), .required('Required'),
description: Yup.string().required('Required').min(10) description: Yup.string().required('Required').min(10),
timeout: Yup.string().required('Required')
}) })
export function getInitialValues( export function getInitialValues(
metadata: MetadataMarket metadata: MetadataMarket,
timeout: number
): Partial<MetadataPublishForm> { ): Partial<MetadataPublishForm> {
return { return {
name: metadata.main.name, name: metadata.main.name,
description: metadata.additionalInformation?.description description: metadata.additionalInformation.description,
timeout: secondsToString(timeout)
} }
} }

View File

@ -17,6 +17,7 @@ export const validationSchema: Yup.SchemaOf<MetadataPublishForm> = Yup.object()
.required('Required'), .required('Required'),
files: Yup.array<FileMetadata>().required('Required').nullable(), files: Yup.array<FileMetadata>().required('Required').nullable(),
description: Yup.string().min(10).required('Required'), description: Yup.string().min(10).required('Required'),
timeout: Yup.string().required('Required'),
access: Yup.string() access: Yup.string()
.matches(/Compute|Download/g, { excludeEmptyString: true }) .matches(/Compute|Download/g, { excludeEmptyString: true })
.required('Required'), .required('Required'),
@ -37,6 +38,8 @@ export const initialValues: Partial<MetadataPublishForm> = {
}, },
files: '', files: '',
description: '', description: '',
timeout: 'Forever',
access: '', access: '',
termsAndConditions: false termsAndConditions: false,
tags: ''
} }

View File

@ -32,6 +32,7 @@ export const contentQuery = graphql`
help help
type type
required required
sortOptions
options options
} }
success success

View File

@ -5,7 +5,7 @@ export default async function checkPreviousOrder(
accountId: string, accountId: string,
ddo: DDO, ddo: DDO,
serviceType: ServiceType serviceType: ServiceType
) { ): Promise<string> {
if (!ocean) return if (!ocean) return
const service = ddo.findServiceByType(serviceType) const service = ddo.findServiceByType(serviceType)

View File

@ -10,6 +10,62 @@ export function transformTags(value: string): string[] {
return transformedTags return transformedTags
} }
export function mapTimeoutStringToSeconds(timeout: string): number {
switch (timeout) {
case 'Forever':
return 0
case '1 day':
return 86400
case '1 week':
return 604800
case '1 month':
return 2630000
case '1 year':
return 31556952
default:
return 0
}
}
function numberEnding(number: number): string {
return number > 1 ? 's' : ''
}
export function secondsToString(numberOfSeconds: number): string {
if (numberOfSeconds === 0) return 'Forever'
const years = Math.floor(numberOfSeconds / 31536000)
const weeks = Math.floor((numberOfSeconds %= 31536000) / 604800)
const days = Math.floor((numberOfSeconds %= 604800) / 86400)
const hours = Math.floor((numberOfSeconds %= 86400) / 3600)
const minutes = Math.floor((numberOfSeconds %= 3600) / 60)
const seconds = numberOfSeconds % 60
return years
? `${years} year${numberEnding(years)}`
: weeks
? `${weeks} week${numberEnding(weeks)}`
: days
? `${days} day${numberEnding(days)}`
: hours
? `${hours} hour${numberEnding(hours)}`
: minutes
? `${minutes} minute${numberEnding(minutes)}`
: seconds
? `${seconds} second${numberEnding(seconds)}`
: 'less than a second'
}
export function checkIfTimeoutInPredefinedValues(
timeout: string,
timeoutOptions: string[]
): boolean {
if (timeoutOptions.indexOf(timeout) > -1) {
return true
}
return false
}
export function transformPublishFormToMetadata( export function transformPublishFormToMetadata(
{ {
name, name,

View File

@ -8,6 +8,7 @@ const testFormData: MetadataPublishForm = {
symbol: '' symbol: ''
}, },
name: '', name: '',
timeout: '',
description: 'description', description: 'description',
termsAndConditions: true, termsAndConditions: true,
access: 'Download' access: 'Download'