Skip to content

Commit bc50011

Browse files
Antoine de Chevignéclaude
andcommitted
Refactor OpBatchDetail to match OrbitBatchDetail structure
- Add tabbed interface with Overview and Transactions tabs - Create OpBatchOverview component with v-list layout - Create OpBatchTransactions wrapper component - Add skeleton loader for loading state - Support URL hash navigation between tabs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent da7b59f commit bc50011

File tree

3 files changed

+273
-144
lines changed

3 files changed

+273
-144
lines changed

src/components/OpBatchDetail.vue

Lines changed: 85 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,51 @@
11
<template>
2-
<v-container>
3-
<v-row>
4-
<v-col cols="12">
5-
<h2 class="text-h6 font-weight-medium">Batch #{{ batchIndex }}</h2>
6-
<v-divider class="my-4"></v-divider>
7-
</v-col>
8-
</v-row>
9-
10-
<v-row v-if="loading">
11-
<v-col cols="12" class="text-center">
12-
<v-progress-circular indeterminate color="primary"></v-progress-circular>
13-
</v-col>
14-
</v-row>
15-
16-
<v-row v-else-if="batch">
17-
<v-col cols="12" md="6">
18-
<v-card>
19-
<v-card-title>Batch Information</v-card-title>
20-
<v-card-text>
21-
<v-table density="compact">
22-
<tbody>
23-
<tr>
24-
<td class="font-weight-medium">Batch Index</td>
25-
<td>#{{ batch.batchIndex.toLocaleString() }}</td>
26-
</tr>
27-
<tr>
28-
<td class="font-weight-medium">L1 Block</td>
29-
<td>
30-
<a :href="l1BlockUrl" target="_blank" rel="noopener noreferrer" class="text-decoration-none">
31-
{{ batch.l1BlockNumber.toLocaleString() }}
32-
<v-icon size="x-small" class="ml-1">mdi-open-in-new</v-icon>
33-
</a>
34-
</td>
35-
</tr>
36-
<tr>
37-
<td class="font-weight-medium">L1 Transaction</td>
38-
<td>
39-
<a :href="l1TransactionUrl" target="_blank" rel="noopener noreferrer" class="text-decoration-none text-truncate" style="max-width: 200px; display: inline-block; font-family: monospace;">
40-
{{ batch.l1TransactionHash }}
41-
<v-icon size="x-small" class="ml-1">mdi-open-in-new</v-icon>
42-
</a>
43-
</td>
44-
</tr>
45-
<tr>
46-
<td class="font-weight-medium">L2 Block Range</td>
47-
<td v-if="batch.l2BlockStart !== null">
48-
{{ batch.l2BlockStart.toLocaleString() }} - {{ batch.l2BlockEnd.toLocaleString() }}
49-
</td>
50-
<td v-else class="text-medium-emphasis">Pending</td>
51-
</tr>
52-
<tr>
53-
<td class="font-weight-medium">Timestamp</td>
54-
<td>{{ $dt.shortDate(batch.timestamp) }} ({{ $dt.fromNow(batch.timestamp) }})</td>
55-
</tr>
56-
<tr>
57-
<td class="font-weight-medium">Status</td>
58-
<td>
59-
<v-chip :color="statusColors[batch.status]">
60-
{{ statusLabels[batch.status] }}
61-
</v-chip>
62-
</td>
63-
</tr>
64-
<tr v-if="batch.blobHash">
65-
<td class="font-weight-medium">Blob Hash</td>
66-
<td style="font-family: monospace;">
67-
<a :href="blobViewerUrl" target="_blank" rel="noopener noreferrer" class="text-decoration-none">
68-
{{ batch.blobHash }}
69-
<v-icon size="x-small" class="ml-1">mdi-open-in-new</v-icon>
70-
</a>
71-
</td>
72-
</tr>
73-
<tr v-if="batch.dataContainer">
74-
<td class="font-weight-medium">Data Container</td>
75-
<td>
76-
<v-chip size="small" :color="batch.dataContainer === 'in_blob4844' ? 'primary' : 'secondary'">
77-
{{ dataContainerLabels[batch.dataContainer] }}
78-
</v-chip>
79-
</td>
80-
</tr>
81-
<tr v-if="batch.txCount">
82-
<td class="font-weight-medium">Transaction Count</td>
83-
<td>{{ batch.txCount.toLocaleString() }}</td>
84-
</tr>
85-
</tbody>
86-
</v-table>
87-
</v-card-text>
88-
</v-card>
89-
</v-col>
90-
</v-row>
91-
92-
<v-row v-if="batch && batch.l2BlockStart !== null">
93-
<v-col cols="12">
94-
<v-card class="mt-4">
95-
<v-card-title>Transactions in Batch</v-card-title>
96-
<v-card-text>
97-
<TransactionsList :opBatchIndex="parseInt(batchIndex)" :withCount="true" />
98-
</v-card-text>
99-
</v-card>
100-
</v-col>
101-
</v-row>
2+
<v-container fluid>
3+
<h2 class="text-h6 font-weight-medium">
4+
Batch <span class="text-grey-darken-1">#{{ $route.params.batchIndex }}</span>
5+
</h2>
6+
<v-divider class="my-4"></v-divider>
7+
8+
<template v-if="loading">
9+
<v-card>
10+
<v-card-text>
11+
<v-skeleton-loader type="list-item-three-line"></v-skeleton-loader>
12+
<v-skeleton-loader type="list-item-three-line"></v-skeleton-loader>
13+
<v-skeleton-loader type="list-item-three-line"></v-skeleton-loader>
14+
</v-card-text>
15+
</v-card>
16+
</template>
17+
<template v-else-if="batch && !loading">
18+
<BaseChipGroup v-model="selectedTab" mandatory>
19+
<v-chip label size="small" value="overview">Overview</v-chip>
20+
<v-chip label size="small" value="transactions">Transactions</v-chip>
21+
</BaseChipGroup>
22+
23+
<OpBatchOverview
24+
v-if="selectedTab === 'overview'"
25+
:batch="batch"
26+
/>
27+
28+
<OpBatchTransactions
29+
v-if="selectedTab === 'transactions'"
30+
:batchIndex="Number(batch.batchIndex)"
31+
/>
32+
</template>
33+
<template v-else>
34+
<v-card>
35+
<v-card-text>
36+
<p>Couldn't find batch #{{ $route.params.batchIndex }}</p>
37+
</v-card-text>
38+
</v-card>
39+
</template>
10240
</v-container>
10341
</template>
10442

10543
<script setup>
106-
import { ref, computed, onMounted, inject } from 'vue';
107-
import TransactionsList from '@/components/TransactionsList.vue';
44+
import { ref, onMounted, inject, watch } from 'vue';
45+
import { useRouter } from 'vue-router';
46+
import BaseChipGroup from './base/BaseChipGroup.vue';
47+
import OpBatchOverview from './OpBatchOverview.vue';
48+
import OpBatchTransactions from './OpBatchTransactions.vue';
10849
10950
const props = defineProps({
11051
batchIndex: {
@@ -114,59 +55,59 @@ const props = defineProps({
11455
});
11556
11657
const $server = inject('$server');
117-
const $dt = inject('$dt');
58+
const router = useRouter();
11859
119-
const loading = ref(true);
120-
const batch = ref(null);
60+
const selectedTab = ref('overview');
12161
122-
const blobViewerUrl = computed(() => {
123-
if (!batch.value?.blobHash) return '';
124-
const explorer = batch.value.parentChainExplorer || 'https://etherscan.io';
125-
return `${explorer}/blob/${batch.value.blobHash}`;
126-
});
62+
// Reactive data
63+
const loading = ref(false);
64+
const batch = ref(null);
65+
const error = ref(null);
12766
128-
const l1TransactionUrl = computed(() => {
129-
if (!batch.value?.l1TransactionHash) return '';
130-
const explorer = batch.value.parentChainExplorer || 'https://etherscan.io';
131-
return `${explorer}/tx/${batch.value.l1TransactionHash}`;
132-
});
67+
// Methods
68+
function loadBatch() {
69+
loading.value = true;
70+
error.value = null;
13371
134-
const l1BlockUrl = computed(() => {
135-
if (!batch.value?.l1BlockNumber) return '';
136-
const explorer = batch.value.parentChainExplorer || 'https://etherscan.io';
137-
return `${explorer}/block/${batch.value.l1BlockNumber}`;
138-
});
72+
$server.getOpBatchDetail(props.batchIndex)
73+
.then(response => batch.value = response.data)
74+
.catch(console.log)
75+
.finally(() => loading.value = false);
76+
}
13977
140-
const statusColors = {
141-
pending: 'warning',
142-
confirmed: 'info',
143-
finalized: 'success'
78+
const checkUrlHash = () => {
79+
if (window.location.hash === '#transactions') {
80+
selectedTab.value = 'transactions';
81+
} else {
82+
selectedTab.value = 'overview';
83+
}
14484
};
14585
146-
const statusLabels = {
147-
pending: 'Pending',
148-
confirmed: 'Confirmed',
149-
finalized: 'Finalized'
150-
};
86+
// Watch with optimization
87+
watch(() => props.batchIndex, (batchIndex) => {
88+
// Reset state when hash changes
89+
if (batchIndex !== props.batchIndex) {
90+
batch.value = null;
91+
}
15192
152-
const dataContainerLabels = {
153-
in_blob4844: 'EIP-4844 Blob',
154-
in_calldata: 'Calldata'
155-
};
93+
loadBatch(batchIndex);
94+
}, { immediate: true });
15695
157-
async function loadBatch() {
158-
loading.value = true;
159-
try {
160-
const { data } = await $server.getOpBatchDetail(props.batchIndex);
161-
batch.value = data;
162-
} catch (error) {
163-
console.error('Error loading batch:', error);
164-
} finally {
165-
loading.value = false;
96+
watch(() => selectedTab.value, (newTab) => {
97+
const currentPath = router.currentRoute.value.fullPath.split('#')[0];
98+
let hash = '';
99+
100+
if (newTab === 'transactions') {
101+
hash = '#transactions';
166102
}
167-
}
103+
104+
router.replace(currentPath + hash);
105+
});
168106
169107
onMounted(() => {
170-
loadBatch();
108+
checkUrlHash();
109+
router.afterEach(() => {
110+
checkUrlHash();
111+
});
171112
});
172113
</script>

0 commit comments

Comments
 (0)