mirror of
https://github.com/logos-storage/ethcc-demo.git
synced 2026-01-06 23:23:09 +00:00
add moderation controls
Moderation view of all requests: /moderate Allows request details page to be opened in two separate tabs: one with moderation enabled, and the other without (for the projector). Moderating the image has its state synced to the other (non-moderated) tab
This commit is contained in:
parent
3fb5a59069
commit
12702d4e59
@ -15,6 +15,7 @@
|
||||
"ethers": "^6.12.1",
|
||||
"flowbite": "^2.3.0",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0",
|
||||
"vue-unique-id": "^3.2.1"
|
||||
|
||||
14
src/App.vue
14
src/App.vue
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { onBeforeMount, onMounted, ref } from 'vue'
|
||||
import { onBeforeMount, onMounted, ref, onScopeDispose } from 'vue'
|
||||
import { useRequestsStore } from '@/stores/requests'
|
||||
import { RouterView } from 'vue-router'
|
||||
import Balance from '@/components/Balance.vue'
|
||||
@ -116,6 +116,18 @@ onMounted(async () => {
|
||||
onSlotFreed,
|
||||
onSlotFilled
|
||||
)
|
||||
|
||||
function handleStorageEvent(event) {
|
||||
if(event.key === 'requests') {
|
||||
requestsStore.$hydrate()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('storage', handleStorageEvent)
|
||||
|
||||
onScopeDispose(() => {
|
||||
window.removeEventListener('storage', handleStorageEvent)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -16,9 +16,20 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
alt: String
|
||||
alt: String,
|
||||
moderated: {
|
||||
type: [String],
|
||||
default() {
|
||||
return 'pending'
|
||||
},
|
||||
validator(value, props) {
|
||||
// The value must match one of these strings
|
||||
return ['pending', 'approved', 'banned'].includes(value)
|
||||
}
|
||||
}
|
||||
})
|
||||
const hidden = computed(() => props.cid === undefined)
|
||||
const blurred = computed(() => ['pending', 'banned'].includes(props.moderated))
|
||||
|
||||
onMounted(async () => {
|
||||
if (hidden.value) {
|
||||
@ -66,6 +77,19 @@ onMounted(async () => {
|
||||
<span class="sr-only">{{ error }}</span>
|
||||
</div>
|
||||
|
||||
<img v-else-if="imgSrc" :src="imgSrc" class="rounded-full" :alt="props.alt" />
|
||||
<img
|
||||
v-else-if="imgSrc"
|
||||
:src="imgSrc"
|
||||
class="rounded-lg"
|
||||
:alt="props.alt"
|
||||
:class="{ 'blur-xxl': blurred }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.blur-xxl {
|
||||
--tw-blur: blur(76px);
|
||||
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale)
|
||||
var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
<script setup>
|
||||
import { onMounted, computed } from 'vue'
|
||||
import { onMounted, onUnmounted, computed } from 'vue'
|
||||
import { initTooltips } from 'flowbite'
|
||||
import { getStateColour, price } from '@/utils/requests'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useRequestsStore } from '@/stores/requests'
|
||||
import { getStateColour, moderatedState, price } from '@/utils/requests'
|
||||
import { autoPluralize } from '@/utils/strings'
|
||||
|
||||
import CodexImage from '@/components/CodexImage.vue'
|
||||
@ -10,14 +12,15 @@ import RelativeTime from '@/components/RelativeTime.vue'
|
||||
import ShortenValue from '@/components/ShortenValue.vue'
|
||||
import Slots from './Slots.vue'
|
||||
|
||||
const request = defineModel()
|
||||
const props = defineProps({
|
||||
requestId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
request: {
|
||||
type: Object,
|
||||
required: true
|
||||
enableModeration: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
@ -25,20 +28,45 @@ onMounted(() => {
|
||||
initTooltips()
|
||||
})
|
||||
|
||||
const totalPrice = computed(() => price(props.request))
|
||||
const maxSlotLoss = computed(() => autoPluralize(props.request.ask.maxSlotLoss, 'slot'))
|
||||
const slots = computed(() => autoPluralize(props.request.ask.slots, 'slot'))
|
||||
const stateColour = computed(() => getStateColour(props.request.state))
|
||||
const totalPrice = computed(() => price(request.value))
|
||||
const maxSlotLoss = computed(() => autoPluralize(request.value.ask.maxSlotLoss, 'slot'))
|
||||
const slots = computed(() => autoPluralize(request.value.ask.slots, 'slot'))
|
||||
const stateColour = computed(() => getStateColour(request.value.state))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap">
|
||||
<CodexImage
|
||||
class="flex-initial mx-auto my-8 lg:my-16 min-w-sm max-w-md w-full rounded"
|
||||
:cid="props.request.content.cid"
|
||||
:cid="request.content.cid"
|
||||
:local-only="!['New', 'Fulfilled'].includes(request.state)"
|
||||
:moderated="enableModeration ? 'approved' : request.moderated"
|
||||
></CodexImage>
|
||||
<div class="py-8 px-4 ml-4 max-w-2xl lg:py-16 flex-1">
|
||||
<div v-if="enableModeration === true" class="mb-4 p-5 w-full border border-gray-300 rounded-lg b-1 bg-gray-100">
|
||||
<label for="moderation" class="block mb-2 text-lg font-medium text-gray-900 dark:text-white"
|
||||
>Moderation station</label
|
||||
>
|
||||
<div class="flex items-center justify-between space-x-4">
|
||||
<div class="flex-1">
|
||||
<select
|
||||
id="moderation"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
v-model="request.moderated"
|
||||
>
|
||||
<option v-for="(value, key) in moderatedState" :value="key" :key="key">
|
||||
{{ value.text }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<StateIndicator
|
||||
class="flex-none h-7 inline-block align-middle"
|
||||
:text="moderatedState[request.moderated].text"
|
||||
:color="moderatedState[request.moderated].color"
|
||||
size="lg"
|
||||
></StateIndicator>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h2 class="text-xl font-semibold leading-none text-gray-900 md:text-2xl dark:text-white">
|
||||
Request <ShortenValue :value="requestId" :chars="8"></ShortenValue>
|
||||
@ -51,7 +79,6 @@ const stateColour = computed(() => getStateColour(props.request.state))
|
||||
{{ totalPrice }} CDX
|
||||
</p>
|
||||
<dl>
|
||||
<!-- <dt class="mb-2 font-semibold leading-none text-gray-900 dark:text-white">Dataset CID</dt> -->
|
||||
<dd class="mb-4 font-light text-gray-500 sm:mb-5 dark:text-gray-400">
|
||||
<RelativeTime :timestamp="new Date(request.requestedAt * 1000)"></RelativeTime>
|
||||
</dd>
|
||||
@ -79,9 +106,7 @@ const stateColour = computed(() => getStateColour(props.request.state))
|
||||
class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 mx-auto"
|
||||
>
|
||||
<tbody>
|
||||
<tr
|
||||
class="bg-white dark:bg-transparent hover:bg-gray-50 dark:hover:bg-gray-600 text-base"
|
||||
>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600 text-base">
|
||||
<td
|
||||
class="flex items-center pr-1 py-2 font-medium text-gray-900 whitespace-nowrap dark:text-white border-r"
|
||||
>
|
||||
@ -89,9 +114,7 @@ const stateColour = computed(() => getStateColour(props.request.state))
|
||||
</td>
|
||||
<td class="px-6 py-2 font-light">{{ request.expiry }} seconds</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="bg-white dark:bg-transparent hover:bg-gray-50 dark:hover:bg-gray-600 text-base"
|
||||
>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600 text-base">
|
||||
<td
|
||||
class="flex items-center pr-1 py-2 font-medium text-gray-900 whitespace-nowrap dark:text-white border-r"
|
||||
>
|
||||
@ -99,9 +122,7 @@ const stateColour = computed(() => getStateColour(props.request.state))
|
||||
</td>
|
||||
<td class="px-6 py-2 font-light">{{ request.ask.duration }} seconds</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="bg-white dark:bg-transparent hover:bg-gray-50 dark:hover:bg-gray-600 text-base"
|
||||
>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600 text-base">
|
||||
<td
|
||||
class="flex items-center pr-1 py-2 font-medium text-gray-900 whitespace-nowrap dark:text-white border-r"
|
||||
>
|
||||
@ -109,9 +130,7 @@ const stateColour = computed(() => getStateColour(props.request.state))
|
||||
</td>
|
||||
<td class="px-6 py-2 font-light">{{ request.ask.slotSize }} bytes</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="bg-white dark:bg-transparent hover:bg-gray-50 dark:hover:bg-gray-600 text-base"
|
||||
>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600 text-base">
|
||||
<td
|
||||
class="flex items-center pr-1 py-2 font-medium text-gray-900 whitespace-nowrap dark:text-white border-r"
|
||||
>
|
||||
@ -119,9 +138,7 @@ const stateColour = computed(() => getStateColour(props.request.state))
|
||||
</td>
|
||||
<td class="px-6 py-2 font-light">{{ request.ask.proofProbability }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="bg-white dark:bg-transparent hover:bg-gray-50 dark:hover:bg-gray-600 text-base"
|
||||
>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600 text-base">
|
||||
<td
|
||||
class="flex items-center pr-1 py-2 font-medium text-gray-900 whitespace-nowrap dark:text-white border-r"
|
||||
>
|
||||
@ -129,9 +146,7 @@ const stateColour = computed(() => getStateColour(props.request.state))
|
||||
</td>
|
||||
<td class="px-6 py-2 font-light">{{ request.ask.reward }} CDX</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="bg-white dark:bg-transparent hover:bg-gray-50 dark:hover:tbg-gray-600 text-base"
|
||||
>
|
||||
<tr class="hover:bg-gray-50 dark:hover:tbg-gray-600 text-base">
|
||||
<td
|
||||
class="flex items-center pr-1 py-2 font-medium text-gray-900 whitespace-nowrap dark:text-white border-r"
|
||||
>
|
||||
@ -139,9 +154,7 @@ const stateColour = computed(() => getStateColour(props.request.state))
|
||||
</td>
|
||||
<td class="px-6 py-2 font-light">{{ request.ask.collateral }} CDX</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="bg-white dark:bg-transparent hover:bg-gray-50 dark:hover:tbg-gray-600 text-base"
|
||||
>
|
||||
<tr class="hover:bg-gray-50 dark:hover:tbg-gray-600 text-base">
|
||||
<td
|
||||
class="flex items-center pr-1 py-2 font-medium text-gray-900 whitespace-nowrap dark:text-white border-r"
|
||||
>
|
||||
@ -149,9 +162,7 @@ const stateColour = computed(() => getStateColour(props.request.state))
|
||||
</td>
|
||||
<td class="px-6 py-2 font-light">{{ slots }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="bg-white dark:bg-transparent hover:bg-gray-50 dark:hover:bg-gray-600 text-base"
|
||||
>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600 text-base">
|
||||
<td
|
||||
class="flex items-center pr-1 py-2 font-medium text-gray-900 whitespace-nowrap dark:text-white border-r"
|
||||
>
|
||||
|
||||
@ -7,17 +7,28 @@ import { useRequestsStore } from '@/stores/requests'
|
||||
import StateIndicator from '@/components/StateIndicator.vue'
|
||||
import RelativeTime from '@/components/RelativeTime.vue'
|
||||
import ShortenValue from '@/components/ShortenValue.vue'
|
||||
import { getStateColour } from '@/utils/requests'
|
||||
import {
|
||||
getStateColour,
|
||||
moderatedState
|
||||
} from '@/utils/requests'
|
||||
|
||||
const requestsStore = useRequestsStore()
|
||||
const { requests } = storeToRefs(requestsStore)
|
||||
const router = useRouter()
|
||||
const requestsOrdered = computed(() => {
|
||||
const sorted = [...requests.value.entries()].sort(
|
||||
const sorted = [...Object.entries(requests.value)].sort(
|
||||
([reqIdA, reqA], [reqIdB, reqB]) => reqB.requestedAt - reqA.requestedAt
|
||||
)
|
||||
return sorted
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
enableModeration: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initTooltips()
|
||||
})
|
||||
@ -133,16 +144,17 @@ onMounted(() => {
|
||||
>
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">RequestID</th>
|
||||
<th v-if="enableModeration" scope="col" class="px-6 py-3">Moderated</th>
|
||||
<th scope="col" class="px-6 py-3">State</th>
|
||||
<th scope="col" class="px-6 py-3">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="([requestId, { requestedAt, state }], idx) in requestsOrdered"
|
||||
v-for="([requestId, { requestedAt, moderated, state }], idx) in requestsOrdered"
|
||||
:key="{ requestId }"
|
||||
class="cursor-pointer bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-600"
|
||||
@click="router.push(`/request/${requestId}`)"
|
||||
class="cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-600"
|
||||
@click="router.push({ path: `/request/${requestId}`, query: { enableModeration } })"
|
||||
>
|
||||
<th
|
||||
scope="row"
|
||||
@ -157,6 +169,14 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td v-if="enableModeration" class="px-6 py-4">
|
||||
<div class="flex items-center">
|
||||
<StateIndicator
|
||||
:color="moderatedState[moderated].color"
|
||||
:text="moderatedState[moderated].text"
|
||||
></StateIndicator>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center">
|
||||
<StateIndicator :text="state" :color="getStateColour(state)"></StateIndicator>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
import uniqueId from '@/plugins/UniqueIdPlugin'
|
||||
|
||||
import App from '@/App.vue'
|
||||
@ -15,7 +16,10 @@ import './index.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
const pinia = createPinia()
|
||||
pinia.use(piniaPluginPersistedstate)
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
const environments = {
|
||||
localhost: {
|
||||
|
||||
@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
import RequestsView from '../views/RequestsView.vue'
|
||||
import RequestView from '../views/RequestView.vue'
|
||||
import NotFoundView from '../views/NotFoundView.vue'
|
||||
import ModerateView from '../views/ModerateView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
@ -29,6 +30,11 @@ const router = createRouter({
|
||||
path: '/404',
|
||||
name: 'NotFound',
|
||||
component: NotFoundView
|
||||
},
|
||||
{
|
||||
path: '/moderate',
|
||||
name: 'Moderate',
|
||||
component: ModerateView
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@ -4,286 +4,370 @@ import { slotId } from '../utils/ids'
|
||||
import { arrayToObject, requestState } from '@/utils/requests'
|
||||
import { slotState } from '@/utils/slots'
|
||||
|
||||
export const useRequestsStore = defineStore('request', () => {
|
||||
// let fetched = false
|
||||
const marketplace = inject('marketplace')
|
||||
const ethProvider = inject('ethProvider')
|
||||
let {
|
||||
StorageRequested,
|
||||
RequestFulfilled,
|
||||
RequestCancelled,
|
||||
RequestFailed,
|
||||
SlotFilled,
|
||||
SlotFreed
|
||||
} = marketplace.filters
|
||||
const requests = ref(new Map()) // key: requestId, val: {request, state, slots: [{slotId, slotIdx, state}]}
|
||||
// const slots = ref(new Map()) // key: slotId, val: {requestId, slotIdx, state}
|
||||
// const blockNumbers = ref(new Set()) // includes blocks that had events
|
||||
// const storageRequestedEvents = ref([]) // {blockNumber, requestId}
|
||||
// const slotFilledEvents = ref([]) // {blockNumber, requestId, slotIdx, slotId}
|
||||
// const slotFreedEvents = ref([]) // {blockNumber, requestId, slotIdx, slotId}
|
||||
// const requestFulfilledEvents = ref([]) // {blockNumber, requestId}
|
||||
// const requestCancelledEvents = ref([]) // {blockNumber, requestId}
|
||||
// const requestFailedEvents = ref([]) // {blockNumber, requestId}
|
||||
// const requestFinishedEvents = ref([]) // {blockNumber, requestId}
|
||||
const loading = ref(false)
|
||||
const blocks = ref(new Map())
|
||||
// const request = computed(() => count.value * 2)
|
||||
class RequestNotFoundError extends Error {
|
||||
constructor(requestId, ...params) {
|
||||
// Pass remaining arguments (including vendor specific ones) to parent constructor
|
||||
super(...params)
|
||||
|
||||
// onStorageRequested => add request to requests ref, along with slots
|
||||
// => add to storageRequestedEvents {blockNumber, requestId}
|
||||
// => add blockNumber to blockNumbers
|
||||
// onRequestFulfilled => update requests[requestId].state with marketplace.getRequestState(requestId)
|
||||
// => add to requestStartedEvents {blockNumber, requestId}
|
||||
// => add blockNumber to blockNumbers
|
||||
// onRequestCancelled => update requests[requestId].state with marketplace.getRequestState(requestId)
|
||||
// => add to requestCancelledEvents {blockNumber, requestId}
|
||||
// => add blockNumber to blockNumbers
|
||||
// onRequestFailed => update requests[requestId].state with marketplace.getRequestState(requestId)
|
||||
// => add to requestFailedEvents {blockNumber, requestId}
|
||||
// => add blockNumber to blockNumbers
|
||||
// onRequestFinished => update requests[requestId].state with marketplace.getRequestState(requestId)
|
||||
// => add to requestFinishedEvents {blockNumber, requestId}
|
||||
// => add blockNumber to blockNumbers
|
||||
// onSlotFilled => update request.slots[slotId].state with getSlotState
|
||||
// => add to slotFilledEvents {blockNumber, slotId, slotIdx}
|
||||
// => add blockNumber to blockNumbers
|
||||
// onSlotFreed => update slots[slotId].state with getSlotState
|
||||
// => add to slotFreedEvents {blockNumber, slotId, slotIdx}
|
||||
// => add blockNumber to blockNumbers
|
||||
|
||||
const getRequestState = async (requestId) => {
|
||||
let stateIdx = await marketplace.requestState(requestId)
|
||||
return requestState[stateIdx]
|
||||
}
|
||||
|
||||
const getSlotState = async (slotId) => {
|
||||
let stateIdx = await marketplace.slotState(slotId)
|
||||
return slotState[stateIdx]
|
||||
}
|
||||
|
||||
const getSlots = async (requestId, numSlots) => {
|
||||
console.log(`fetching ${numSlots} slots`)
|
||||
let start = Date.now()
|
||||
let slots = []
|
||||
for (let slotIdx = 0; slotIdx < numSlots; slotIdx++) {
|
||||
let id = slotId(requestId, slotIdx)
|
||||
const startSlotState = Date.now()
|
||||
let state = await getSlotState(id)
|
||||
console.log(`fetched slot state in ${(Date.now() - startSlotState) / 1000}s`)
|
||||
const startGetHost = Date.now()
|
||||
let provider = await marketplace.getHost(id)
|
||||
console.log(`fetched slot provider in ${(Date.now() - startGetHost) / 1000}s`)
|
||||
slots.push({ slotId: id, slotIdx, state, provider })
|
||||
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, RequestNotFoundError)
|
||||
}
|
||||
console.log(`fetched ${numSlots} slots in ${(Date.now() - start) / 1000}s`)
|
||||
return slots
|
||||
// blockNumbers.value.add(blockNumber)
|
||||
}
|
||||
|
||||
const getBlock = async (blockHash) => {
|
||||
if (blocks.value.has(blockHash)) {
|
||||
return blocks.value.get(blockHash)
|
||||
} else {
|
||||
let block = await ethProvider.getBlock(blockHash)
|
||||
blocks.value.set(blockHash, block)
|
||||
return block
|
||||
this.name = 'RequestNotFoundError'
|
||||
// Custom debugging information
|
||||
this.requestId = requestId
|
||||
}
|
||||
}
|
||||
|
||||
export const useRequestsStore = defineStore(
|
||||
'requests',
|
||||
() => {
|
||||
// let fetched = false
|
||||
const marketplace = inject('marketplace')
|
||||
const ethProvider = inject('ethProvider')
|
||||
let {
|
||||
StorageRequested,
|
||||
RequestFulfilled,
|
||||
RequestCancelled,
|
||||
RequestFailed,
|
||||
SlotFilled,
|
||||
SlotFreed
|
||||
} = marketplace.filters
|
||||
const requests = ref({}) // key: requestId, val: {request, state, slots: [{slotId, slotIdx, state}]}
|
||||
// const slots = ref(new Map()) // key: slotId, val: {requestId, slotIdx, state}
|
||||
// const blockNumbers = ref(new Set()) // includes blocks that had events
|
||||
// const storageRequestedEvents = ref([]) // {blockNumber, requestId}
|
||||
// const slotFilledEvents = ref([]) // {blockNumber, requestId, slotIdx, slotId}
|
||||
// const slotFreedEvents = ref([]) // {blockNumber, requestId, slotIdx, slotId}
|
||||
// const requestFulfilledEvents = ref([]) // {blockNumber, requestId}
|
||||
// const requestCancelledEvents = ref([]) // {blockNumber, requestId}
|
||||
// const requestFailedEvents = ref([]) // {blockNumber, requestId}
|
||||
// const requestFinishedEvents = ref([]) // {blockNumber, requestId}
|
||||
const loading = ref(false)
|
||||
const fetched = ref(false) // indicates if past events were fetched
|
||||
const blocks = ref({})
|
||||
|
||||
// const request = computed(() => count.value * 2)
|
||||
|
||||
// onStorageRequested => add request to requests ref, along with slots
|
||||
// => add to storageRequestedEvents {blockNumber, requestId}
|
||||
// => add blockNumber to blockNumbers
|
||||
// onRequestFulfilled => update requests[requestId].state with marketplace.getRequestState(requestId)
|
||||
// => add to requestStartedEvents {blockNumber, requestId}
|
||||
// => add blockNumber to blockNumbers
|
||||
// onRequestCancelled => update requests[requestId].state with marketplace.getRequestState(requestId)
|
||||
// => add to requestCancelledEvents {blockNumber, requestId}
|
||||
// => add blockNumber to blockNumbers
|
||||
// onRequestFailed => update requests[requestId].state with marketplace.getRequestState(requestId)
|
||||
// => add to requestFailedEvents {blockNumber, requestId}
|
||||
// => add blockNumber to blockNumbers
|
||||
// onRequestFinished => update requests[requestId].state with marketplace.getRequestState(requestId)
|
||||
// => add to requestFinishedEvents {blockNumber, requestId}
|
||||
// => add blockNumber to blockNumbers
|
||||
// onSlotFilled => update request.slots[slotId].state with getSlotState
|
||||
// => add to slotFilledEvents {blockNumber, slotId, slotIdx}
|
||||
// => add blockNumber to blockNumbers
|
||||
// onSlotFreed => update slots[slotId].state with getSlotState
|
||||
// => add to slotFreedEvents {blockNumber, slotId, slotIdx}
|
||||
// => add blockNumber to blockNumbers
|
||||
|
||||
const getRequestState = async (requestId) => {
|
||||
let stateIdx = await marketplace.requestState(requestId)
|
||||
return requestState[stateIdx]
|
||||
}
|
||||
}
|
||||
|
||||
async function addRequest(requestId, blockHash) {
|
||||
let state = await getRequestState(requestId)
|
||||
let block = await getBlock(blockHash)
|
||||
let reqExisting = requests.value.get(requestId) // just in case it already exists
|
||||
let request = {
|
||||
...reqExisting,
|
||||
state,
|
||||
requestedAt: block.timestamp,
|
||||
requestFinishedId: null,
|
||||
detailsFetched: false
|
||||
const getSlotState = async (slotId) => {
|
||||
let stateIdx = await marketplace.slotState(slotId)
|
||||
return slotState[stateIdx]
|
||||
}
|
||||
requests.value.set(requestId, request)
|
||||
return request
|
||||
}
|
||||
|
||||
async function fetchPastRequests() {
|
||||
// query past events
|
||||
console.log('fetching past requests')
|
||||
loading.value = true
|
||||
try {
|
||||
let events = await marketplace.queryFilter(StorageRequested)
|
||||
console.log('got ', events.length, ' StorageRequested events')
|
||||
events.forEach(async (event, i) => {
|
||||
console.log('getting details for StorageRequested event ', i)
|
||||
let start = Date.now()
|
||||
let { requestId, ask, expiry } = event.args
|
||||
let { blockHash, blockNumber } = event
|
||||
await addRequest(requestId, blockHash)
|
||||
console.log(`got details for ${i} in ${(Date.now() - start) / 1000} seconds`)
|
||||
if (i === events.length - 1) {
|
||||
const getSlots = async (requestId, numSlots) => {
|
||||
console.log(`fetching ${numSlots} slots`)
|
||||
let start = Date.now()
|
||||
let slots = []
|
||||
for (let slotIdx = 0; slotIdx < numSlots; slotIdx++) {
|
||||
let id = slotId(requestId, slotIdx)
|
||||
const startSlotState = Date.now()
|
||||
let state = await getSlotState(id)
|
||||
console.log(`fetched slot state in ${(Date.now() - startSlotState) / 1000}s`)
|
||||
const startGetHost = Date.now()
|
||||
let provider = await marketplace.getHost(id)
|
||||
console.log(`fetched slot provider in ${(Date.now() - startGetHost) / 1000}s`)
|
||||
slots.push({ slotId: id, slotIdx, state, provider })
|
||||
}
|
||||
console.log(`fetched ${numSlots} slots in ${(Date.now() - start) / 1000}s`)
|
||||
return slots
|
||||
// blockNumbers.value.add(blockNumber)
|
||||
}
|
||||
|
||||
const getBlock = async (blockHash) => {
|
||||
if (Object.keys(blocks.value).includes(blockHash)) {
|
||||
return blocks.value[blockHash]
|
||||
} else {
|
||||
let block = await ethProvider.getBlock(blockHash)
|
||||
blocks.value[blockHash] = block
|
||||
return block
|
||||
}
|
||||
}
|
||||
|
||||
async function addRequest(requestId, blockHash) {
|
||||
let state = await getRequestState(requestId)
|
||||
let block = await getBlock(blockHash)
|
||||
let reqExisting = requests.value[requestId] || {} // just in case it already exists
|
||||
let request = {
|
||||
...reqExisting,
|
||||
state,
|
||||
requestedAt: block.timestamp,
|
||||
requestFinishedId: null,
|
||||
detailsFetched: false,
|
||||
moderated: 'pending'
|
||||
}
|
||||
requests.value[requestId] = request
|
||||
return request
|
||||
}
|
||||
|
||||
async function fetchPastRequests() {
|
||||
// query past events
|
||||
if (fetched.value) {
|
||||
console.log('skipping fetching past requests, already fetched')
|
||||
return
|
||||
}
|
||||
console.log('fetching past requests')
|
||||
loading.value = true
|
||||
try {
|
||||
let events = await marketplace.queryFilter(StorageRequested)
|
||||
console.log('got ', events.length, ' StorageRequested events')
|
||||
events.forEach(async (event, i) => {
|
||||
console.log('getting details for StorageRequested event ', i)
|
||||
let start = Date.now()
|
||||
let { requestId, ask, expiry } = event.args
|
||||
let { blockHash, blockNumber } = event
|
||||
await addRequest(requestId, blockHash)
|
||||
console.log(`got details for ${i} in ${(Date.now() - start) / 1000} seconds`)
|
||||
if (i === events.length - 1) {
|
||||
loading.value = false
|
||||
fetched.value = true
|
||||
}
|
||||
})
|
||||
if (events.length === 0) {
|
||||
loading.value = false
|
||||
fetched.value = true
|
||||
}
|
||||
})
|
||||
if (events.length === 0) {
|
||||
} catch (error) {
|
||||
console.error(`failed to load past contract events: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRequest(requestId) {
|
||||
let start = Date.now()
|
||||
console.log('fetching request ', requestId)
|
||||
const preFetched = requests.value[requestId] || {}
|
||||
if (preFetched?.detailsFetched) {
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
let arrRequest = await marketplace.getRequest(requestId)
|
||||
let request = arrayToObject(arrRequest)
|
||||
let slots = await getSlots(requestId, request.ask.slots)
|
||||
const reqExisting = requests.value[requestId] || {}
|
||||
requests.value[requestId] = {
|
||||
...reqExisting, // state, requestedAt, requestFinishedId (null)
|
||||
...request,
|
||||
slots,
|
||||
detailsFetched: true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`failed to load slots for request ${requestId}: ${error}`)
|
||||
throw error
|
||||
} finally {
|
||||
console.log(`fetched request in ${(Date.now() - start) / 1000}s`)
|
||||
loading.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`failed to load past contract events: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRequest(requestId) {
|
||||
let start = Date.now()
|
||||
console.log('fetching request ', requestId)
|
||||
const preFetched = requests.value.get(requestId)
|
||||
if (preFetched?.detailsFetched) {
|
||||
return
|
||||
function updateRequestState(requestId, newState) {
|
||||
if (!Object.keys(requests.value).includes(requestId)) {
|
||||
throw new RequestNotFoundError(requestId, `Request not found`)
|
||||
}
|
||||
let { state, ...rest } = requests.value[requestId]
|
||||
state = newState
|
||||
requests.value[requestId] = { state, ...rest }
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
let arrRequest = await marketplace.getRequest(requestId)
|
||||
let request = arrayToObject(arrRequest)
|
||||
let slots = await getSlots(requestId, request.ask.slots)
|
||||
const reqExisting = requests.value.get(requestId)
|
||||
requests.value.set(requestId, {
|
||||
...reqExisting, // state, requestedAt, requestFinishedId (null)
|
||||
...request,
|
||||
slots,
|
||||
detailsFetched: true
|
||||
|
||||
function updateRequestFinishedId(requestId, newRequestFinishedId) {
|
||||
if (!Object.keys(requests.value).includes(requestId)) {
|
||||
throw new RequestNotFoundError(requestId, `Request not found`)
|
||||
}
|
||||
let { requestFinishedId, ...rest } = requests.value[requestId]
|
||||
requestFinishedId = newRequestFinishedId
|
||||
requests.value[requestId] = { requestFinishedId, ...rest }
|
||||
}
|
||||
|
||||
function updateRequestSlotState(requestId, slotIdx, newState) {
|
||||
if (!Object.keys(requests.value).includes(requestId)) {
|
||||
throw new RequestNotFoundError(requestId, `Request not found`)
|
||||
}
|
||||
let { slots, ...rest } = requests.value[requestId]
|
||||
slots = slots.map((slot) => {
|
||||
if (slot.slotIdx == slotIdx) {
|
||||
slot.state = newState
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`failed to load slots for request ${requestId}: ${error}`)
|
||||
throw error
|
||||
} finally {
|
||||
console.log(`fetched request in ${(Date.now() - start) / 1000}s`)
|
||||
loading.value = false
|
||||
requests.value[requestId] = { slots, ...rest }
|
||||
}
|
||||
|
||||
function waitForRequestFinished(requestId, duration, onRequestFinished) {
|
||||
return setTimeout(async () => {
|
||||
try {
|
||||
updateRequestState(requestId, 'Finished')
|
||||
updateRequestFinishedId(requestId, null)
|
||||
} catch (error) {
|
||||
if (error instanceof RequestNotFoundError) {
|
||||
await fetchRequest(requestId)
|
||||
}
|
||||
}
|
||||
if (onRequestFinished) {
|
||||
let blockNumber = await ethProvider.getBlockNumber()
|
||||
onRequestFinished(blockNumber, requestId)
|
||||
}
|
||||
}, duration)
|
||||
}
|
||||
|
||||
function cancelWaitForRequestFinished(requestId) {
|
||||
if (!Object.keys(requests.value).includes(requestId)) {
|
||||
throw new RequestNotFoundError(requestId, `Request not found`)
|
||||
}
|
||||
let { requestFinishedId } = requests.value[requestId]
|
||||
if (requestFinishedId) {
|
||||
clearTimeout(requestFinishedId)
|
||||
}
|
||||
}
|
||||
|
||||
async function listenForNewEvents(
|
||||
onStorageRequested,
|
||||
onRequestFulfilled,
|
||||
onRequestCancelled,
|
||||
onRequestFailed,
|
||||
onRequestFinished,
|
||||
onSlotFreed,
|
||||
onSlotFilled
|
||||
) {
|
||||
marketplace.on(StorageRequested, async (requestId, ask, expiry, event) => {
|
||||
let { blockNumber, blockHash } = event.log
|
||||
const request = addRequest(requestId, blockHash)
|
||||
|
||||
// callback
|
||||
if (onStorageRequested) {
|
||||
onStorageRequested(blockNumber, requestId, request.state)
|
||||
}
|
||||
})
|
||||
|
||||
marketplace.on(RequestFulfilled, async (requestId, event) => {
|
||||
let requestOnChain = await marketplace.getRequest(requestId)
|
||||
let ask = requestOnChain[1]
|
||||
let duration = ask[1]
|
||||
// set request state to finished at the end of the request -- there's no
|
||||
// other way to know when a request finishes
|
||||
console.log('request ', requestOnChain)
|
||||
let requestFinishedId = waitForRequestFinished(requestId, duration, onRequestFinished)
|
||||
updateRequestState(requestId, 'Fulfilled')
|
||||
updateRequestFinishedId(requestId, requestFinishedId)
|
||||
|
||||
let { blockNumber } = event.log
|
||||
if (onRequestFulfilled) {
|
||||
onRequestFulfilled(blockNumber, requestId)
|
||||
}
|
||||
})
|
||||
marketplace.on(RequestCancelled, async (requestId, event) => {
|
||||
try {
|
||||
updateRequestState(requestId, 'Cancelled')
|
||||
} catch (error) {
|
||||
if (error instanceof RequestNotFoundError) {
|
||||
await fetchRequest(requestId)
|
||||
}
|
||||
}
|
||||
|
||||
let { blockNumber } = event.log
|
||||
if (onRequestCancelled) {
|
||||
onRequestCancelled(blockNumber, requestId)
|
||||
}
|
||||
})
|
||||
marketplace.on(RequestFailed, async (requestId, event) => {
|
||||
try {
|
||||
updateRequestState(requestId, 'Failed')
|
||||
cancelWaitForRequestFinished(requestId)
|
||||
} catch (error) {
|
||||
if (error instanceof RequestNotFoundError) {
|
||||
await fetchRequest(requestId)
|
||||
}
|
||||
}
|
||||
|
||||
let { blockNumber } = event.log
|
||||
if (onRequestFailed) {
|
||||
onRequestFailed(blockNumber, requestId)
|
||||
}
|
||||
})
|
||||
marketplace.on(SlotFreed, async (requestId, slotIdx, event) => {
|
||||
try {
|
||||
updateRequestSlotState(requestId, slotIdx, 'Freed')
|
||||
} catch (error) {
|
||||
if (error instanceof RequestNotFoundError) {
|
||||
await fetchRequest(requestId)
|
||||
}
|
||||
}
|
||||
|
||||
let { blockNumber } = event.log
|
||||
if (onSlotFreed) {
|
||||
onSlotFreed(blockNumber, requestId, slotIdx)
|
||||
}
|
||||
})
|
||||
marketplace.on(SlotFilled, async (requestId, slotIdx, event) => {
|
||||
try {
|
||||
updateRequestSlotState(requestId, slotIdx, 'Filled')
|
||||
} catch (error) {
|
||||
if (error instanceof RequestNotFoundError) {
|
||||
await fetchRequest(requestId)
|
||||
}
|
||||
}
|
||||
|
||||
let { blockNumber } = event.log
|
||||
if (onSlotFilled) {
|
||||
onSlotFilled(blockNumber, requestId, slotIdx)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
requests,
|
||||
// slots,
|
||||
// blockNumbers,
|
||||
// storageRequestedEvents,
|
||||
// slotFilledEvents,
|
||||
// slotFreedEvents,
|
||||
// requestStartedEvents,
|
||||
// requestCancelledEvents,
|
||||
// requestFailedEvents,
|
||||
// requestFinishedEvents,
|
||||
fetchPastRequests,
|
||||
fetchRequest,
|
||||
listenForNewEvents,
|
||||
loading,
|
||||
fetched
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
serializer: {
|
||||
serialize: (state) => {
|
||||
return JSON.stringify(state, (_, v) => (typeof v === 'bigint' ? v.toString() : v))
|
||||
},
|
||||
deserialize: (serialized) => {
|
||||
// TODO: deserialize bigints properly
|
||||
return JSON.parse(serialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateRequestState(requestId, newState) {
|
||||
let { state, ...rest } = requests.value.get(requestId)
|
||||
state = newState
|
||||
requests.value.set(requestId, { state, ...rest })
|
||||
}
|
||||
|
||||
function updateRequestFinishedId(requestId, newRequestFinishedId) {
|
||||
let { requestFinishedId, ...rest } = requests.value.get(requestId)
|
||||
requestFinishedId = newRequestFinishedId
|
||||
requests.value.set(requestId, { requestFinishedId, ...rest })
|
||||
}
|
||||
|
||||
function updateRequestSlotState(requestId, slotIdx, newState) {
|
||||
let { slots, ...rest } = requests.value.get(requestId)
|
||||
slots = slots.map((slot) => {
|
||||
if (slot.slotIdx == slotIdx) {
|
||||
slot.state = newState
|
||||
}
|
||||
})
|
||||
requests.value.set(requestId, { slots, ...rest })
|
||||
}
|
||||
|
||||
function waitForRequestFinished(requestId, duration, onRequestFinished) {
|
||||
return setTimeout(async () => {
|
||||
updateRequestState(requestId, 'Finished')
|
||||
updateRequestFinishedId(requestId, null)
|
||||
if (onRequestFinished) {
|
||||
let blockNumber = await ethProvider.getBlockNumber()
|
||||
onRequestFinished(blockNumber, requestId)
|
||||
}
|
||||
}, duration)
|
||||
}
|
||||
|
||||
function cancelWaitForRequestFinished(requestId) {
|
||||
let { requestFinishedId } = requests.value.get(requestId)
|
||||
if (requestFinishedId) {
|
||||
clearTimeout(requestFinishedId)
|
||||
}
|
||||
}
|
||||
|
||||
async function listenForNewEvents(
|
||||
onStorageRequested,
|
||||
onRequestFulfilled,
|
||||
onRequestCancelled,
|
||||
onRequestFailed,
|
||||
onRequestFinished,
|
||||
onSlotFreed,
|
||||
onSlotFilled
|
||||
) {
|
||||
marketplace.on(StorageRequested, async (requestId, ask, expiry, event) => {
|
||||
let { blockNumber, blockHash } = event.log
|
||||
const request = addRequest(requestId, blockHash)
|
||||
|
||||
// callback
|
||||
if (onStorageRequested) {
|
||||
onStorageRequested(blockNumber, requestId, request.state)
|
||||
}
|
||||
})
|
||||
|
||||
marketplace.on(RequestFulfilled, async (requestId, event) => {
|
||||
let requestOnChain = await marketplace.getRequest(requestId)
|
||||
let ask = requestOnChain[1]
|
||||
let duration = ask[1]
|
||||
// set request state to finished at the end of the request -- there's no
|
||||
// other way to know when a request finishes
|
||||
console.log('request ', requestOnChain)
|
||||
let requestFinishedId = waitForRequestFinished(requestId, duration, onRequestFinished)
|
||||
updateRequestState(requestId, 'Fulfilled')
|
||||
updateRequestFinishedId(requestId, requestFinishedId)
|
||||
|
||||
let { blockNumber } = event.log
|
||||
if (onRequestFulfilled) {
|
||||
onRequestFulfilled(blockNumber, requestId)
|
||||
}
|
||||
})
|
||||
marketplace.on(RequestCancelled, async (requestId, event) => {
|
||||
updateRequestState(requestId, 'Cancelled')
|
||||
|
||||
let { blockNumber } = event.log
|
||||
if (onRequestCancelled) {
|
||||
onRequestCancelled(blockNumber, requestId)
|
||||
}
|
||||
})
|
||||
marketplace.on(RequestFailed, async (requestId, event) => {
|
||||
updateRequestState(requestId, 'Failed')
|
||||
cancelWaitForRequestFinished(requestId)
|
||||
|
||||
let { blockNumber } = event.log
|
||||
if (onRequestFailed) {
|
||||
onRequestFailed(blockNumber, requestId)
|
||||
}
|
||||
})
|
||||
marketplace.on(SlotFreed, async (requestId, slotIdx, event) => {
|
||||
updateRequestSlotState(requestId, slotIdx, 'Freed')
|
||||
|
||||
let { blockNumber } = event.log
|
||||
if (onSlotFreed) {
|
||||
onSlotFreed(blockNumber, requestId, slotIdx)
|
||||
}
|
||||
})
|
||||
marketplace.on(SlotFilled, async (requestId, slotIdx, event) => {
|
||||
updateRequestSlotState(requestId, slotIdx, 'Filled')
|
||||
|
||||
let { blockNumber } = event.log
|
||||
if (onSlotFilled) {
|
||||
onSlotFilled(blockNumber, requestId, slotIdx)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
requests,
|
||||
// slots,
|
||||
// blockNumbers,
|
||||
// storageRequestedEvents,
|
||||
// slotFilledEvents,
|
||||
// slotFreedEvents,
|
||||
// requestStartedEvents,
|
||||
// requestCancelledEvents,
|
||||
// requestFailedEvents,
|
||||
// requestFinishedEvents,
|
||||
fetchPastRequests,
|
||||
fetchRequest,
|
||||
listenForNewEvents,
|
||||
loading
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
@ -41,6 +41,12 @@ export function getStateColour(state) {
|
||||
}
|
||||
}
|
||||
|
||||
export const moderatedState = {
|
||||
pending: { text: 'Pending 🤷♂️', color: 'gray' },
|
||||
approved: { text: 'Approved ✅', color: 'green' },
|
||||
banned: { text: 'NSFW! 🫣', color: 'red' }
|
||||
}
|
||||
|
||||
export const requestState = [
|
||||
'New', // [default] waiting to fill slots
|
||||
'Fulfilled', // all slots filled, accepting regular proofs
|
||||
|
||||
16
src/views/ModerateView.vue
Normal file
16
src/views/ModerateView.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useRequestsStore } from '@/stores/requests'
|
||||
import StorageRequests from '@/components/StorageRequests.vue'
|
||||
import SkeletonLoading from '@/components/SkeletonLoading.vue'
|
||||
|
||||
const requestsStore = useRequestsStore()
|
||||
const { loading } = storeToRefs(requestsStore)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<SkeletonLoading v-if="loading" type="text" />
|
||||
<StorageRequests :enable-moderation="true" v-else />
|
||||
</div>
|
||||
</template>
|
||||
@ -20,7 +20,7 @@ async function fetch(requestId) {
|
||||
router.push({ name: 'NotFound' })
|
||||
}
|
||||
}
|
||||
request.value = requests.value.get(requestId)
|
||||
request.value = requests.value[requestId]
|
||||
}
|
||||
|
||||
const hasRequest = computed(() => {
|
||||
@ -47,6 +47,11 @@ if (loading.value) {
|
||||
<template>
|
||||
<div>
|
||||
<SkeletonLoading v-if="loading" type="image" />
|
||||
<StorageRequest v-else-if="hasRequest" :requestId="route.params.requestId" :request="request" />
|
||||
<StorageRequest
|
||||
v-else-if="hasRequest"
|
||||
:requestId="route.params.requestId"
|
||||
v-model="request"
|
||||
:enableModeration="route.query.enableModeration === 'true'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -2076,6 +2076,11 @@ pify@^2.3.0:
|
||||
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
||||
integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
|
||||
|
||||
pinia-plugin-persistedstate@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.1.tgz#66780602aecd6c7b152dd7e3ddc249a1f7a13fe5"
|
||||
integrity sha512-MK++8LRUsGF7r45PjBFES82ISnPzyO6IZx3CH5vyPseFLZCk1g2kgx6l/nW8pEBKxxd4do0P6bJw+mUSZIEZUQ==
|
||||
|
||||
pinia@^2.1.7:
|
||||
version "2.1.7"
|
||||
resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.1.7.tgz#4cf5420d9324ca00b7b4984d3fbf693222115bbc"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user