@meddleware/walrus-storage-ui 0.1.64 → 0.1.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/package.json +22 -12
- package/src/App.vue +20 -41
- package/src/components/BlobGroupRows.vue +133 -0
- package/src/components/ExtendControl.vue +102 -0
- package/src/components/MyBlobs.vue +108 -345
- package/src/components/UploadResultCard.vue +35 -0
- package/src/components/WalrusStorageView.vue +94 -310
- package/src/composables/useExtendQuotes.ts +3 -3
- package/src/composables/useOwnedBlobs.ts +8 -9
- package/src/composables/useStorageUpload.ts +111 -0
- package/src/config.ts +3 -16
- package/src/e2e-data.ts +11 -0
- package/src/e2e.ts +46 -0
- package/src/main.ts +3 -1
- package/src/wallet.ts +5 -44
- package/src/walrus.ts +29 -0
- package/vite.config.ts +13 -7
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
// The connected wallet's Walrus blobs, one row per blob id, with Extend (add storage) and Certify (finish an upload from its stored
|
|
3
|
+
// certificate). This component owns the data and runs the transactions; the rows are `BlobGroupRows`, the amount control `ExtendControl`.
|
|
4
|
+
import { computed, nextTick, onMounted, ref, shallowRef, watch } from 'vue'
|
|
5
|
+
import { UiButton, UiDataTable, UiEmptyState, UiLiveStatus, UiNotice, UiPanel } from '@meddleware/ui'
|
|
6
|
+
import { errorMessage } from '@meddleware/web-utils'
|
|
7
|
+
import type { Transaction } from '@mysten/sui/transactions'
|
|
4
8
|
import type { OwnedBlob } from '@meddleware/walrus-storage-client'
|
|
5
|
-
import { MAX_SINGLE_RESERVATION_EPOCHS
|
|
6
|
-
import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
|
|
7
|
-
import type { Executor } from '../wallet.js'
|
|
8
|
-
import { walrusNetwork, walruscanBlobUrl } from '../config.js'
|
|
9
|
-
import { useOwnedBlobs } from '../composables/useOwnedBlobs.js'
|
|
10
|
-
import { useExtendQuotes } from '../composables/useExtendQuotes.js'
|
|
11
|
-
import { groupBlobs, expiryLabel, maxExtendableEpochs, type BlobGroup } from '../blob-groups.js'
|
|
9
|
+
import { MAX_SINGLE_RESERVATION_EPOCHS } from '@meddleware/walrus-storage-kit'
|
|
12
10
|
import {
|
|
13
|
-
|
|
14
|
-
loadPendingCertifies,
|
|
11
|
+
browserStorage,
|
|
15
12
|
clearPendingCertify,
|
|
13
|
+
loadPendingCertifies,
|
|
14
|
+
pendingCertifyKey,
|
|
16
15
|
type PendingCertify,
|
|
17
|
-
browserStorage,
|
|
18
16
|
} from '@meddleware/walrus-storage-client/flow'
|
|
17
|
+
import type { Executor } from '../wallet.js'
|
|
18
|
+
import { walrusNetwork } from '../config.js'
|
|
19
|
+
import { walrusClient, walrusLib } from '../walrus.js'
|
|
20
|
+
import { useOwnedBlobs } from '../composables/useOwnedBlobs.js'
|
|
21
|
+
import { groupBlobs } from '../blob-groups.js'
|
|
22
|
+
import BlobGroupRows from './BlobGroupRows.vue'
|
|
19
23
|
|
|
20
24
|
// Browser storage that never throws (blocked site data falls back to memory for this page).
|
|
21
25
|
const storage = browserStorage()
|
|
@@ -41,50 +45,25 @@ const { blobs, currentEpoch, loading, error, load } = useOwnedBlobs()
|
|
|
41
45
|
|
|
42
46
|
// One row per blobId; re-uploads of the same content are collapsed into a group's `copies`.
|
|
43
47
|
const groups = computed(() => groupBlobs(blobs.value))
|
|
44
|
-
const expanded =
|
|
48
|
+
const expanded = shallowRef<ReadonlySet<string>>(new Set())
|
|
45
49
|
|
|
46
50
|
// Pending certifications (uploaded-but-not-certified copies we still hold a certificate for).
|
|
47
|
-
const pending =
|
|
51
|
+
const pending = shallowRef<Record<string, PendingCertify>>({})
|
|
48
52
|
|
|
49
53
|
// Per-object action state (keyed by Blob objectId), shared by Extend and Certify.
|
|
50
54
|
const busy = ref<string | null>(null)
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
const MAX = MAX_SINGLE_RESERVATION_EPOCHS
|
|
55
|
-
|
|
56
|
-
function msg(e: unknown): string {
|
|
57
|
-
return e instanceof Error ? e.message : String(e)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function maxAddable(endEpoch: number): number {
|
|
61
|
-
return maxExtendableEpochs(endEpoch, currentEpoch.value, MAX)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** The stored certificate for an uncertified copy, if we can certify it here. */
|
|
65
|
-
function pendingFor(objectId: string): PendingCertify | null {
|
|
66
|
-
return pending.value[objectId] ?? null
|
|
67
|
-
}
|
|
68
|
-
/** The first pending (certifiable) copy in a group, if any. */
|
|
69
|
-
function groupPendingCopy(g: BlobGroup): OwnedBlob | null {
|
|
70
|
-
return g.copies.find((c) => !c.certified && pending.value[c.objectId]) ?? null
|
|
71
|
-
}
|
|
72
|
-
/** Status shown in the Certified column for a group. */
|
|
73
|
-
function groupCertStatus(g: BlobGroup): 'certified' | 'pending' | 'none' {
|
|
74
|
-
if (g.anyCertified) return 'certified'
|
|
75
|
-
return groupPendingCopy(g) ? 'pending' : 'none'
|
|
55
|
+
const statuses = shallowRef<Record<string, string>>({})
|
|
56
|
+
const say = (objectId: string, text: string): void => {
|
|
57
|
+
statuses.value = { ...statuses.value, [objectId]: text }
|
|
76
58
|
}
|
|
77
59
|
|
|
78
60
|
function toggleExpand(blobId: string): void {
|
|
79
61
|
const next = new Set(expanded.value)
|
|
80
|
-
if (next.
|
|
81
|
-
else next.add(blobId)
|
|
62
|
+
if (!next.delete(blobId)) next.add(blobId)
|
|
82
63
|
expanded.value = next
|
|
83
64
|
}
|
|
84
65
|
|
|
85
|
-
|
|
86
|
-
return load(props.address, { force: true })
|
|
87
|
-
}
|
|
66
|
+
const refresh = (): Promise<void> => load(props.address, { force: true })
|
|
88
67
|
|
|
89
68
|
/** Reload the pending map from storage, dropping entries whose blob is already certified. */
|
|
90
69
|
function refreshPending(): void {
|
|
@@ -103,110 +82,70 @@ function refreshPending(): void {
|
|
|
103
82
|
pending.value = map
|
|
104
83
|
}
|
|
105
84
|
|
|
106
|
-
// ── Extend ──────────────────────────────────────────────────────────────────
|
|
107
85
|
// Extend adds storage only (no one-time write cost), so the quote prices the `storageCost` component.
|
|
108
86
|
async function estimateExtendCost(size: number, epochs: number): Promise<bigint | null> {
|
|
109
|
-
const {
|
|
110
|
-
|
|
111
|
-
return (await estimateStorageCost(walrusClient, size, epochs)).storageCost
|
|
112
|
-
}
|
|
113
|
-
const extendQuotes = useExtendQuotes(estimateExtendCost)
|
|
114
|
-
|
|
115
|
-
// User picked an amount → clamp and (re)price it. Only the picked amount is priced here, not every
|
|
116
|
-
// row on load, to avoid spinning up a Walrus client per blob; a row that was never touched is priced
|
|
117
|
-
// when Extend is first pressed (see `extendBlob`), before the wallet opens.
|
|
118
|
-
function setExtendAmount(blob: OwnedBlob, value: number | string): void {
|
|
119
|
-
const max = maxAddable(blob.endEpoch)
|
|
120
|
-
const v = Math.min(max, Math.max(1, Math.floor(Number(value) || 1)))
|
|
121
|
-
extendAmount.value = { ...extendAmount.value, [blob.objectId]: v }
|
|
122
|
-
void extendQuotes.price(blob.objectId, blob.size, v)
|
|
87
|
+
const [{ estimateStorageCost }, client] = await Promise.all([walrusLib(), walrusClient(currentNetwork())])
|
|
88
|
+
return (await estimateStorageCost(client, size, epochs)).storageCost
|
|
123
89
|
}
|
|
124
90
|
|
|
125
|
-
/**
|
|
126
|
-
function
|
|
127
|
-
const epochs = extendAmount.value[blob.objectId]
|
|
128
|
-
const q = extendQuotes.quoteFor(blob.objectId, epochs)
|
|
129
|
-
if (q?.state === 'ready') return `≈${formatCoinAmount(q.costFrost, 'WAL')} for +${q.epochs} epochs`
|
|
130
|
-
if (q?.state === 'unavailable') return 'price unavailable'
|
|
131
|
-
return extendQuotes.pricing.value[blob.objectId] === epochs ? 'pricing…' : ''
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function extendBlob(blob: OwnedBlob): Promise<void> {
|
|
135
|
-
const epochs = extendAmount.value[blob.objectId]
|
|
136
|
-
if (!epochs || busy.value) return
|
|
137
|
-
// The wallet opens only for an amount whose cost the user has been shown. The seeded default has
|
|
138
|
-
// no quote yet: price it now, show it, and let the user press Extend again to confirm.
|
|
139
|
-
if (!(await extendQuotes.shownBeforeSigning(blob.objectId, blob.size, epochs))) return
|
|
91
|
+
/** Build, sign and run one transaction for a blob, reporting each stage on its row and refreshing the list after. */
|
|
92
|
+
async function act(blob: OwnedBlob, done: string, build: () => Promise<Transaction>, after?: () => void): Promise<void> {
|
|
140
93
|
busy.value = blob.objectId
|
|
141
|
-
|
|
94
|
+
say(blob.objectId, 'Building transaction…')
|
|
142
95
|
try {
|
|
143
|
-
const
|
|
144
|
-
const walrusClient = createWalrusClient({ network: currentNetwork(), wasmUrl: walrusWasmUrl })
|
|
145
|
-
const tx = await extendBlobLifetimeTransaction(walrusClient, blob.objectId, { epochs })
|
|
96
|
+
const tx = await build()
|
|
146
97
|
const executor = await props.buildExecutor()
|
|
147
|
-
|
|
98
|
+
say(blob.objectId, 'Approve in wallet…')
|
|
148
99
|
const { digest } = await executor.signAndExecute(tx)
|
|
149
100
|
await executor.waitForTransaction(digest)
|
|
150
|
-
|
|
151
|
-
|
|
101
|
+
after?.()
|
|
102
|
+
say(blob.objectId, `${done} (${digest.slice(0, 8)}…)`)
|
|
152
103
|
await refresh()
|
|
153
104
|
} catch (e) {
|
|
154
|
-
|
|
105
|
+
say(blob.objectId, `Failed: ${errorMessage(e)}`)
|
|
155
106
|
} finally {
|
|
156
107
|
busy.value = null
|
|
157
108
|
}
|
|
158
109
|
}
|
|
159
110
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
watch([() => props.address, walrusNetwork], () =>
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
watch(
|
|
198
|
-
[groups, currentEpoch],
|
|
199
|
-
() => {
|
|
200
|
-
for (const g of groups.value) {
|
|
201
|
-
const b = g.representative
|
|
202
|
-
const max = maxAddable(b.endEpoch)
|
|
203
|
-
if (max > 0 && extendAmount.value[b.objectId] === undefined) {
|
|
204
|
-
extendAmount.value = { ...extendAmount.value, [b.objectId]: Math.min(10, max) }
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
},
|
|
208
|
-
{ immediate: true },
|
|
209
|
-
)
|
|
111
|
+
function extendBlob(blob: OwnedBlob, epochs: number): Promise<void> {
|
|
112
|
+
if (busy.value) return Promise.resolve()
|
|
113
|
+
return act(blob, 'Extended', async () => {
|
|
114
|
+
const [{ extendBlobLifetimeTransaction }, client] = await Promise.all([walrusLib(), walrusClient(currentNetwork())])
|
|
115
|
+
return extendBlobLifetimeTransaction(client, blob.objectId, { epochs })
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function certifyBlob(blob: OwnedBlob): Promise<void> {
|
|
120
|
+
const entry = pending.value[blob.objectId]
|
|
121
|
+
const address = props.address
|
|
122
|
+
if (!entry || !address || busy.value) return Promise.resolve()
|
|
123
|
+
return act(
|
|
124
|
+
blob,
|
|
125
|
+
'Certified',
|
|
126
|
+
async () => {
|
|
127
|
+
const [{ certifyBlobTransaction }, client] = await Promise.all([walrusLib(), walrusClient(currentNetwork())])
|
|
128
|
+
return certifyBlobTransaction(client, {
|
|
129
|
+
blobId: entry.blobId,
|
|
130
|
+
blobObjectId: entry.blobObjectId,
|
|
131
|
+
certificate: entry.certificate,
|
|
132
|
+
deletable: entry.deletable,
|
|
133
|
+
})
|
|
134
|
+
},
|
|
135
|
+
() => clearPendingCertify(storage, pendingCertifyKey(currentNetwork(), address), blob.objectId),
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Load on first open and whenever the address or network changes; re-derive pending on list/address changes.
|
|
140
|
+
onMounted(() => {
|
|
141
|
+
void load(props.address)
|
|
142
|
+
refreshPending() // the cached list may already be there (a re-mount), so it will not change
|
|
143
|
+
})
|
|
144
|
+
watch([() => props.address, walrusNetwork], ([addr]) => {
|
|
145
|
+
void load(addr)
|
|
146
|
+
refreshPending()
|
|
147
|
+
})
|
|
148
|
+
watch(blobs, refreshPending)
|
|
210
149
|
|
|
211
150
|
// Highlight + expand the group routed from the duplicate-upload dialog.
|
|
212
151
|
watch(
|
|
@@ -222,221 +161,45 @@ watch(
|
|
|
222
161
|
</script>
|
|
223
162
|
|
|
224
163
|
<template>
|
|
225
|
-
<
|
|
226
|
-
<
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
<
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
<
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
class="copies-toggle"
|
|
261
|
-
@click="toggleExpand(g.blobId)"
|
|
262
|
-
>
|
|
263
|
-
{{ expanded.has(g.blobId) ? '▾' : '▸' }} {{ g.copies.length }} copies
|
|
264
|
-
</button>
|
|
265
|
-
</td>
|
|
266
|
-
<td>{{ (g.size / 1024).toFixed(1) }} KB</td>
|
|
267
|
-
<td>{{ expiryLabel(g.maxEndEpoch, currentEpoch) }}</td>
|
|
268
|
-
<td>
|
|
269
|
-
<span v-if="groupCertStatus(g) === 'certified'">✓</span>
|
|
270
|
-
<span v-else-if="groupCertStatus(g) === 'pending'" class="pending-badge" title="Uploaded but not certified">pending</span>
|
|
271
|
-
<span v-else>—</span>
|
|
272
|
-
</td>
|
|
273
|
-
<td class="actions">
|
|
274
|
-
<span v-if="actionStatus[g.representative.objectId]" class="act-status">
|
|
275
|
-
{{ actionStatus[g.representative.objectId] }}
|
|
276
|
-
</span>
|
|
277
|
-
<template v-else>
|
|
278
|
-
<button
|
|
279
|
-
v-if="groupPendingCopy(g)"
|
|
280
|
-
type="button"
|
|
281
|
-
class="certify-btn"
|
|
282
|
-
:disabled="!!busy"
|
|
283
|
-
@click="certifyBlob(groupPendingCopy(g)!)"
|
|
284
|
-
>
|
|
285
|
-
Certify
|
|
286
|
-
</button>
|
|
287
|
-
<span v-if="maxAddable(g.representative.endEpoch) === 0" class="at-max">at max lifetime</span>
|
|
288
|
-
<span v-else class="extend">
|
|
289
|
-
<input
|
|
290
|
-
type="number"
|
|
291
|
-
min="1"
|
|
292
|
-
:max="maxAddable(g.representative.endEpoch)"
|
|
293
|
-
:value="extendAmount[g.representative.objectId]"
|
|
294
|
-
aria-label="Epochs to add"
|
|
295
|
-
@input="setExtendAmount(g.representative, ($event.target as HTMLInputElement).value)"
|
|
296
|
-
/>
|
|
297
|
-
<button type="button" class="preset" @click="setExtendAmount(g.representative, 10)">+10</button>
|
|
298
|
-
<button type="button" class="preset" @click="setExtendAmount(g.representative, 25)">+25</button>
|
|
299
|
-
<button
|
|
300
|
-
type="button"
|
|
301
|
-
class="preset"
|
|
302
|
-
@click="setExtendAmount(g.representative, maxAddable(g.representative.endEpoch))"
|
|
303
|
-
>
|
|
304
|
-
Max
|
|
305
|
-
</button>
|
|
306
|
-
<button type="button" :disabled="!!busy" @click="extendBlob(g.representative)">
|
|
307
|
-
{{ extendQuotes.quoteFor(g.representative.objectId, extendAmount[g.representative.objectId]) ? 'Extend' : 'Show cost' }}
|
|
308
|
-
</button>
|
|
309
|
-
<span class="est" aria-live="polite">{{ extendQuoteLabel(g.representative) }}</span>
|
|
310
|
-
</span>
|
|
311
|
-
</template>
|
|
312
|
-
</td>
|
|
313
|
-
</tr>
|
|
314
|
-
|
|
315
|
-
<!-- Per-copy detail for grouped duplicates: certify a specific pending copy. -->
|
|
316
|
-
<tr v-for="c in (expanded.has(g.blobId) ? g.copies : [])" :key="c.objectId" class="copy-row">
|
|
317
|
-
<td class="copy-obj">
|
|
318
|
-
<CopyableAddress :address="c.objectId" label="Copy object ID">
|
|
319
|
-
<span class="mono">{{ c.objectId.slice(0, 10) }}…</span>
|
|
320
|
-
</CopyableAddress>
|
|
321
|
-
</td>
|
|
322
|
-
<td></td>
|
|
323
|
-
<td>{{ expiryLabel(c.endEpoch, currentEpoch) }}</td>
|
|
324
|
-
<td>
|
|
325
|
-
<span v-if="c.certified">✓</span>
|
|
326
|
-
<span v-else-if="pendingFor(c.objectId)" class="pending-badge">pending</span>
|
|
327
|
-
<span v-else>—</span>
|
|
328
|
-
</td>
|
|
329
|
-
<td class="actions">
|
|
330
|
-
<span v-if="actionStatus[c.objectId]" class="act-status">{{ actionStatus[c.objectId] }}</span>
|
|
331
|
-
<button
|
|
332
|
-
v-else-if="pendingFor(c.objectId)"
|
|
333
|
-
type="button"
|
|
334
|
-
class="certify-btn"
|
|
335
|
-
:disabled="!!busy"
|
|
336
|
-
@click="certifyBlob(c)"
|
|
337
|
-
>
|
|
338
|
-
Certify
|
|
339
|
-
</button>
|
|
340
|
-
</td>
|
|
341
|
-
</tr>
|
|
342
|
-
</template>
|
|
343
|
-
</tbody>
|
|
344
|
-
</table>
|
|
345
|
-
</section>
|
|
164
|
+
<UiPanel title="My Blobs" :level="3">
|
|
165
|
+
<p class="bar">
|
|
166
|
+
<UiButton size="sm" variant="secondary" :disabled="!address" :busy="loading" @click="refresh">Refresh</UiButton>
|
|
167
|
+
</p>
|
|
168
|
+
<UiLiveStatus label="Blobs" :message="loading ? 'Loading your blobs…' : ''" />
|
|
169
|
+
|
|
170
|
+
<UiEmptyState v-if="!address">Connect your wallet to list your Walrus blobs.</UiEmptyState>
|
|
171
|
+
<UiNotice v-else-if="error" type="error" :message="error" />
|
|
172
|
+
<UiDataTable v-else-if="walrusNetwork && (groups.length || !loading)" caption="Your Walrus blobs" empty="No Walrus blobs found for this address." :rows="groups.length">
|
|
173
|
+
<template #head>
|
|
174
|
+
<th scope="col">Blob ID</th>
|
|
175
|
+
<th scope="col">Size</th>
|
|
176
|
+
<th scope="col">Expires</th>
|
|
177
|
+
<th scope="col">Certified</th>
|
|
178
|
+
<th scope="col">Actions</th>
|
|
179
|
+
</template>
|
|
180
|
+
<BlobGroupRows
|
|
181
|
+
v-for="g in groups"
|
|
182
|
+
:key="g.blobId"
|
|
183
|
+
:group="g"
|
|
184
|
+
:network="walrusNetwork"
|
|
185
|
+
:current-epoch="currentEpoch"
|
|
186
|
+
:max-reservation="MAX_SINGLE_RESERVATION_EPOCHS"
|
|
187
|
+
:highlighted="g.blobId === highlightBlobId"
|
|
188
|
+
:expanded="expanded.has(g.blobId)"
|
|
189
|
+
:pending="pending"
|
|
190
|
+
:busy="busy"
|
|
191
|
+
:statuses="statuses"
|
|
192
|
+
:estimate="estimateExtendCost"
|
|
193
|
+
@toggle="toggleExpand(g.blobId)"
|
|
194
|
+
@extend="extendBlob"
|
|
195
|
+
@certify="certifyBlob"
|
|
196
|
+
/>
|
|
197
|
+
</UiDataTable>
|
|
198
|
+
</UiPanel>
|
|
346
199
|
</template>
|
|
347
200
|
|
|
348
201
|
<style scoped>
|
|
349
|
-
.
|
|
350
|
-
margin
|
|
351
|
-
}
|
|
352
|
-
.toolbar {
|
|
353
|
-
display: flex;
|
|
354
|
-
align-items: center;
|
|
355
|
-
gap: 1rem;
|
|
356
|
-
margin-bottom: 0.75rem;
|
|
357
|
-
}
|
|
358
|
-
.toolbar h2 {
|
|
359
|
-
margin: 0;
|
|
360
|
-
font-size: 1.2rem;
|
|
361
|
-
}
|
|
362
|
-
.hint {
|
|
363
|
-
color: var(--muted);
|
|
364
|
-
font-size: 0.9rem;
|
|
365
|
-
}
|
|
366
|
-
.err {
|
|
367
|
-
color: var(--danger);
|
|
368
|
-
font-size: 0.9rem;
|
|
369
|
-
}
|
|
370
|
-
.blob-table {
|
|
371
|
-
width: 100%;
|
|
372
|
-
border-collapse: collapse;
|
|
373
|
-
font-size: 0.88rem;
|
|
374
|
-
}
|
|
375
|
-
.blob-table th,
|
|
376
|
-
.blob-table td {
|
|
377
|
-
text-align: left;
|
|
378
|
-
padding: 0.4rem 0.6rem;
|
|
379
|
-
border-bottom: 1px solid var(--border);
|
|
380
|
-
vertical-align: top;
|
|
381
|
-
}
|
|
382
|
-
.blob-table th {
|
|
383
|
-
font-weight: 600;
|
|
384
|
-
color: var(--muted);
|
|
385
|
-
}
|
|
386
|
-
.blob-table tr.warn td {
|
|
387
|
-
background: color-mix(in srgb, var(--warning) 8%, transparent);
|
|
388
|
-
}
|
|
389
|
-
.blob-table tr.highlight td {
|
|
390
|
-
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
|
391
|
-
}
|
|
392
|
-
.copy-row td {
|
|
393
|
-
background: color-mix(in srgb, var(--muted) 6%, transparent);
|
|
394
|
-
font-size: 0.82rem;
|
|
395
|
-
}
|
|
396
|
-
.copies-toggle {
|
|
397
|
-
display: inline-block;
|
|
398
|
-
margin-top: 0.25rem;
|
|
399
|
-
font-size: 0.78rem;
|
|
400
|
-
padding: 0.05rem 0.4rem;
|
|
401
|
-
}
|
|
402
|
-
.mono {
|
|
403
|
-
font-family: monospace;
|
|
404
|
-
}
|
|
405
|
-
.actions {
|
|
406
|
-
white-space: nowrap;
|
|
407
|
-
}
|
|
408
|
-
.extend {
|
|
409
|
-
display: inline-flex;
|
|
410
|
-
align-items: center;
|
|
411
|
-
gap: 0.3rem;
|
|
412
|
-
flex-wrap: wrap;
|
|
413
|
-
}
|
|
414
|
-
.extend input {
|
|
415
|
-
width: 4rem;
|
|
416
|
-
}
|
|
417
|
-
.preset {
|
|
418
|
-
font-size: 0.75rem;
|
|
419
|
-
padding: 0.1rem 0.4rem;
|
|
420
|
-
}
|
|
421
|
-
.est {
|
|
422
|
-
font-size: 0.78rem;
|
|
423
|
-
color: var(--muted);
|
|
424
|
-
}
|
|
425
|
-
.at-max {
|
|
426
|
-
font-size: 0.8rem;
|
|
427
|
-
color: var(--muted);
|
|
428
|
-
}
|
|
429
|
-
.act-status {
|
|
430
|
-
font-size: 0.85rem;
|
|
431
|
-
color: var(--muted);
|
|
432
|
-
}
|
|
433
|
-
.certify-btn {
|
|
434
|
-
border-color: var(--accent);
|
|
435
|
-
color: var(--accent);
|
|
436
|
-
}
|
|
437
|
-
.pending-badge {
|
|
438
|
-
font-size: 0.78rem;
|
|
439
|
-
color: var(--accent);
|
|
440
|
-
font-weight: 600;
|
|
202
|
+
.bar {
|
|
203
|
+
margin: 0 0 var(--space-xs);
|
|
441
204
|
}
|
|
442
205
|
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// What an upload produced: the blob id and its aggregator URL, and the certify transaction when there was one.
|
|
3
|
+
import { CopyableAddress, ExplorerLink, UiCard, UiStatGrid, UiStatRow } from '@meddleware/ui'
|
|
4
|
+
import { safeHref, suiExplorerUrl, walruscanBlobUrl } from '@meddleware/web-utils'
|
|
5
|
+
import type { WalrusNetwork } from '@meddleware/walrus-storage-kit'
|
|
6
|
+
import type { UploadResult } from '../composables/useStorageUpload.js'
|
|
7
|
+
|
|
8
|
+
defineProps<{ result: UploadResult; network: WalrusNetwork }>()
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<UiCard title="Uploaded" :level="3">
|
|
13
|
+
<UiStatGrid>
|
|
14
|
+
<UiStatRow label="Blob ID" align="left">
|
|
15
|
+
<CopyableAddress :address="result.blobId" label="Copy blob ID">
|
|
16
|
+
<ExplorerLink :href="walruscanBlobUrl(network, result.blobId)" :value="result.blobId" :chars="[8, 6]" />
|
|
17
|
+
</CopyableAddress>
|
|
18
|
+
</UiStatRow>
|
|
19
|
+
<UiStatRow label="URL" align="left">
|
|
20
|
+
<a class="url" :href="safeHref(result.url)" target="_blank" rel="noopener noreferrer">{{ result.url }}</a>
|
|
21
|
+
</UiStatRow>
|
|
22
|
+
<UiStatRow v-if="result.digest" label="Certify tx" align="left">
|
|
23
|
+
<CopyableAddress :address="result.digest" label="Copy transaction digest">
|
|
24
|
+
<ExplorerLink :href="suiExplorerUrl('txblock', result.digest, network)" :value="result.digest" :chars="[8, 6]" />
|
|
25
|
+
</CopyableAddress>
|
|
26
|
+
</UiStatRow>
|
|
27
|
+
</UiStatGrid>
|
|
28
|
+
</UiCard>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<style scoped>
|
|
32
|
+
.url {
|
|
33
|
+
overflow-wrap: anywhere;
|
|
34
|
+
}
|
|
35
|
+
</style>
|