Skip to content

Commit eed717b

Browse files
Antoine de Chevignéclaude
andcommitted
Refactor OpOutputDetail to match OpBatchDetail design
- Create OpOutputOverview component with v-list layout - Update OpOutputDetail to use tabbed interface with BaseChipGroup - Add parentChainExplorer to getOpOutput API response - Add game type labels for dispute games (Cannon, Permissioned Cannon, etc) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 70bbdef commit eed717b

File tree

3 files changed

+266
-128
lines changed

3 files changed

+266
-128
lines changed

run/lib/firebase.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,15 @@ const getOpOutput = async (workspaceId, outputIndex) => {
679679
if (!output)
680680
throw new Error('Could not find output');
681681

682-
return output.toJSON();
682+
// Get the OpChainConfig to include parentChainExplorer for L1 links
683+
const opConfig = await OpChainConfig.findOne({
684+
where: { workspaceId },
685+
attributes: ['parentChainExplorer']
686+
});
687+
688+
const result = output.toJSON();
689+
result.parentChainExplorer = opConfig?.parentChainExplorer || 'https://eth.blockscout.com';
690+
return result;
683691
};
684692

685693
/**

src/components/OpOutputDetail.vue

Lines changed: 58 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,44 @@
11
<template>
2-
<v-container>
3-
<v-row>
4-
<v-col cols="12">
5-
<h2 class="text-h6 font-weight-medium">State Output #{{ outputIndex }}</h2>
6-
<v-divider class="my-4"></v-divider>
7-
</v-col>
8-
</v-row>
2+
<v-container fluid>
3+
<h2 class="text-h6 font-weight-medium">
4+
State Output <span class="text-grey-darken-1">#{{ $route.params.outputIndex }}</span>
5+
</h2>
6+
<v-divider class="my-4"></v-divider>
97

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>
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="output && !loading">
18+
<BaseChipGroup v-model="selectedTab" mandatory>
19+
<v-chip label size="small" value="overview">Overview</v-chip>
20+
</BaseChipGroup>
1521

16-
<v-row v-else-if="output">
17-
<v-col cols="12" md="6">
18-
<v-card>
19-
<v-card-title>Output Information</v-card-title>
20-
<v-card-text>
21-
<v-table density="compact">
22-
<tbody>
23-
<tr>
24-
<td class="font-weight-medium">Output Index</td>
25-
<td>#{{ output.outputIndex.toLocaleString() }}</td>
26-
</tr>
27-
<tr>
28-
<td class="font-weight-medium">Output Root</td>
29-
<td style="font-family: monospace; word-break: break-all;">{{ output.outputRoot }}</td>
30-
</tr>
31-
<tr>
32-
<td class="font-weight-medium">L2 Block</td>
33-
<td v-if="output.l2BlockNumber">
34-
<HashLink :type="'block'" :hash="output.l2BlockNumber" />
35-
</td>
36-
<td v-else class="text-medium-emphasis">-</td>
37-
</tr>
38-
<tr>
39-
<td class="font-weight-medium">L1 Block</td>
40-
<td>{{ output.l1BlockNumber.toLocaleString() }}</td>
41-
</tr>
42-
<tr>
43-
<td class="font-weight-medium">L1 Transaction</td>
44-
<td style="font-family: monospace;">
45-
{{ output.l1TransactionHash ? `${output.l1TransactionHash.slice(0, 20)}...${output.l1TransactionHash.slice(-16)}` : '-' }}
46-
</td>
47-
</tr>
48-
<tr>
49-
<td class="font-weight-medium">Proposer</td>
50-
<td>
51-
<HashLink :type="'address'" :hash="output.proposer" :withTokenName="true" />
52-
</td>
53-
</tr>
54-
<tr>
55-
<td class="font-weight-medium">Proposed At</td>
56-
<td>{{ $dt.shortDate(output.timestamp) }} ({{ $dt.fromNow(output.timestamp) }})</td>
57-
</tr>
58-
<tr>
59-
<td class="font-weight-medium">Challenge Period Ends</td>
60-
<td>
61-
{{ $dt.shortDate(output.challengePeriodEnds) }}
62-
<span v-if="new Date(output.challengePeriodEnds) > new Date()" class="text-warning">
63-
({{ $dt.fromNow(output.challengePeriodEnds) }})
64-
</span>
65-
<span v-else class="text-success">(Ended)</span>
66-
</td>
67-
</tr>
68-
<tr>
69-
<td class="font-weight-medium">Status</td>
70-
<td>
71-
<v-chip :color="statusColors[output.status]">
72-
{{ statusLabels[output.status] }}
73-
</v-chip>
74-
</td>
75-
</tr>
76-
</tbody>
77-
</v-table>
78-
</v-card-text>
79-
</v-card>
80-
</v-col>
81-
82-
<v-col cols="12" md="6" v-if="output.disputeGameAddress">
83-
<v-card>
84-
<v-card-title>Dispute Game</v-card-title>
85-
<v-card-text>
86-
<v-table density="compact">
87-
<tbody>
88-
<tr>
89-
<td class="font-weight-medium">Game Address</td>
90-
<td>
91-
<HashLink :type="'address'" :hash="output.disputeGameAddress" />
92-
</td>
93-
</tr>
94-
<tr v-if="output.gameType !== null">
95-
<td class="font-weight-medium">Game Type</td>
96-
<td>{{ output.gameType }}</td>
97-
</tr>
98-
</tbody>
99-
</v-table>
100-
</v-card-text>
101-
</v-card>
102-
</v-col>
103-
</v-row>
22+
<OpOutputOverview
23+
v-if="selectedTab === 'overview'"
24+
:output="output"
25+
/>
26+
</template>
27+
<template v-else>
28+
<v-card>
29+
<v-card-text>
30+
<p>Couldn't find state output #{{ $route.params.outputIndex }}</p>
31+
</v-card-text>
32+
</v-card>
33+
</template>
10434
</v-container>
10535
</template>
10636

10737
<script setup>
108-
import { ref, onMounted, inject } from 'vue';
109-
import HashLink from '@/components/HashLink.vue';
38+
import { ref, onMounted, inject, watch } from 'vue';
39+
import { useRouter } from 'vue-router';
40+
import BaseChipGroup from './base/BaseChipGroup.vue';
41+
import OpOutputOverview from './OpOutputOverview.vue';
11042
11143
const props = defineProps({
11244
outputIndex: {
@@ -116,38 +48,37 @@ const props = defineProps({
11648
});
11749
11850
const $server = inject('$server');
119-
const $dt = inject('$dt');
51+
const router = useRouter();
52+
53+
const selectedTab = ref('overview');
12054
121-
const loading = ref(true);
55+
// Reactive data
56+
const loading = ref(false);
12257
const output = ref(null);
58+
const error = ref(null);
12359
124-
const statusColors = {
125-
proposed: 'info',
126-
challenged: 'warning',
127-
resolved: 'primary',
128-
finalized: 'success'
129-
};
60+
// Methods
61+
function loadOutput() {
62+
loading.value = true;
63+
error.value = null;
13064
131-
const statusLabels = {
132-
proposed: 'Proposed',
133-
challenged: 'Challenged',
134-
resolved: 'Resolved',
135-
finalized: 'Finalized'
136-
};
65+
$server.getOpOutputDetail(props.outputIndex)
66+
.then(response => output.value = response.data)
67+
.catch(console.log)
68+
.finally(() => loading.value = false);
69+
}
13770
138-
async function loadOutput() {
139-
loading.value = true;
140-
try {
141-
const { data } = await $server.getOpOutputDetail(props.outputIndex);
142-
output.value = data;
143-
} catch (error) {
144-
console.error('Error loading output:', error);
145-
} finally {
146-
loading.value = false;
71+
// Watch with optimization
72+
watch(() => props.outputIndex, (outputIndex) => {
73+
// Reset state when index changes
74+
if (outputIndex !== props.outputIndex) {
75+
output.value = null;
14776
}
148-
}
77+
78+
loadOutput(outputIndex);
79+
}, { immediate: true });
14980
15081
onMounted(() => {
151-
loadOutput();
82+
// Future: could add hash-based tab switching if more tabs are added
15283
});
15384
</script>

0 commit comments

Comments
 (0)