-
Notifications
You must be signed in to change notification settings - Fork 49
feat: add post-deployment success panel with explorer link #1613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| <script setup lang="ts"> | ||
| import { computed } from 'vue'; | ||
| import { getExplorerUrl } from '@/utils/explorerUrl'; | ||
| import CopyTextButton from '@/components/global/CopyTextButton.vue'; | ||
| import { ExternalLink, CheckCircle2, Zap, X } from 'lucide-vue-next'; | ||
|
|
||
| const props = defineProps<{ | ||
| contractAddress: string; | ||
| txHash: string; | ||
| }>(); | ||
|
|
||
| const emit = defineEmits(['close', 'interact']); | ||
|
|
||
| const explorerUrl = computed(() => getExplorerUrl()); | ||
| </script> | ||
|
|
||
| <template> | ||
| <div | ||
| class="relative mb-4 flex flex-col overflow-hidden rounded-xl border border-emerald-200 bg-gradient-to-b from-emerald-50/50 to-white shadow-sm dark:border-emerald-900/30 dark:from-emerald-950/20 dark:to-zinc-900" | ||
| data-testid="deployment-success-panel" | ||
| > | ||
| <button | ||
| @click="emit('close')" | ||
| class="absolute right-3 top-3 text-emerald-600/50 hover:text-emerald-600 dark:text-emerald-400/50 dark:hover:text-emerald-400" | ||
| aria-label="Close success panel" | ||
| > | ||
| <X class="h-4 w-4" /> | ||
| </button> | ||
|
|
||
| <div class="flex items-center gap-2 border-b border-emerald-100 bg-emerald-50 px-4 py-3 dark:border-emerald-900/30 dark:bg-emerald-900/10"> | ||
| <CheckCircle2 class="h-5 w-5 text-emerald-500 dark:text-emerald-400" /> | ||
| <h3 class="font-semibold text-emerald-800 dark:text-emerald-300">Contract Deployed Successfully</h3> | ||
| </div> | ||
|
|
||
| <div class="flex flex-col gap-3 p-4"> | ||
| <div class="flex flex-col gap-1"> | ||
| <span class="text-[10px] font-medium uppercase tracking-wider text-slate-500 dark:text-zinc-400">Contract Address</span> | ||
| <div class="flex items-center justify-between rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 dark:border-zinc-700 dark:bg-zinc-800/50"> | ||
| <span class="font-mono text-xs text-slate-700 break-all dark:text-zinc-300">{{ contractAddress }}</span> | ||
| <CopyTextButton :text="contractAddress" class="ml-2" /> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div class="flex flex-col gap-1"> | ||
| <span class="text-[10px] font-medium uppercase tracking-wider text-slate-500 dark:text-zinc-400">Transaction Hash</span> | ||
| <div class="flex items-center justify-between rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 dark:border-zinc-700 dark:bg-zinc-800/50"> | ||
| <span class="font-mono text-xs text-slate-700 break-all dark:text-zinc-300">{{ txHash }}</span> | ||
| <CopyTextButton :text="txHash" class="ml-2" /> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div class="mt-2 flex flex-row flex-wrap gap-2"> | ||
| <a | ||
| v-if="explorerUrl" | ||
| :href="`${explorerUrl}/tx/${txHash}`" | ||
| target="_blank" | ||
| class="inline-flex items-center gap-1.5 rounded-lg bg-white px-4 py-2 text-xs font-medium text-slate-700 shadow-sm ring-1 ring-inset ring-slate-300 transition-colors hover:bg-slate-50 dark:bg-zinc-800 dark:text-zinc-200 dark:ring-zinc-600 dark:hover:bg-zinc-700" | ||
| > | ||
| <ExternalLink class="h-3.5 w-3.5" /> | ||
| View on Explorer | ||
| </a> | ||
|
Comment on lines
+53
to
+61
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard the explorer link against an empty If 🤖 Prompt for AI Agents |
||
| <button | ||
| @click="emit('interact')" | ||
| class="inline-flex items-center gap-1.5 rounded-lg bg-emerald-500 px-4 py-2 text-xs font-medium text-white shadow-sm transition-colors hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-500" | ||
| > | ||
| <Zap class="h-3.5 w-3.5" /> | ||
| Interact Now | ||
| </button> | ||
| </div> | ||
|
|
||
| <div class="mt-1 text-[11px] text-slate-500 dark:text-zinc-400"> | ||
| ℹ Save your contract address — you'll need it to call this contract from outside the Studio. | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </template> | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,7 @@ import ConstructorParameters from '@/components/Simulator/ConstructorParameters. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ContractReadMethods from '@/components/Simulator/ContractReadMethods.vue'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ContractWriteMethods from '@/components/Simulator/ContractWriteMethods.vue'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import TransactionsList from '@/components/Simulator/TransactionsList.vue'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import DeploymentSuccessPanel from '@/components/Simulator/DeploymentSuccessPanel.vue'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useContractQueries } from '@/hooks'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import MainTitle from '@/components/Simulator/MainTitle.vue'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ref, watch, computed } from 'vue'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -59,6 +60,33 @@ function isFinalityWindowValid(value: number) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const consensusMaxRotations = computed(() => consensusStore.maxRotations); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const showDeploymentSuccessPanel = ref(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const newlyDeployedTxHash = ref(''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| watch( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [() => isDeployed.value, () => contract.value?.id], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ([newIsDeployed, newId], [oldIsDeployed, oldId]) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (newIsDeployed && !oldIsDeployed && newId === oldId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| showDeploymentSuccessPanel.value = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const deployedContract = contractsStore.deployedContracts.find( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (c) => c.contractId === newId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| newlyDeployedTxHash.value = deployedContract?.deployTxHash || ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (newId !== oldId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| showDeploymentSuccessPanel.value = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+67
to
+80
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Watcher swallows the missing-hash case behind an empty-string fallback.
Consider not opening the panel when no hash is available, or at least logging the lookup miss so it surfaces during QA: 🛡️ Defensive variant- if (newIsDeployed && !oldIsDeployed && newId === oldId) {
- showDeploymentSuccessPanel.value = true;
- const deployedContract = contractsStore.deployedContracts.find(
- (c) => c.contractId === newId,
- );
- newlyDeployedTxHash.value = deployedContract?.deployTxHash || '';
- } else if (newId !== oldId) {
+ if (newIsDeployed && !oldIsDeployed && newId === oldId) {
+ const deployedContract = contractsStore.deployedContracts.find(
+ (c) => c.contractId === newId,
+ );
+ if (!deployedContract?.deployTxHash) {
+ // Avoid surfacing a panel with an empty tx hash / broken explorer link.
+ return;
+ }
+ newlyDeployedTxHash.value = deployedContract.deployTxHash;
+ showDeploymentSuccessPanel.value = true;
+ } else if (newId !== oldId) {
showDeploymentSuccessPanel.value = false;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const scrollToInteract = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const el = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.getElementById('tutorial-read-methods') || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.getElementById('tutorial-write-methods'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (el) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| el.scrollIntoView({ behavior: 'smooth', block: 'center' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <template> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -113,6 +141,16 @@ const consensusMaxRotations = computed(() => consensusStore.maxRotations); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @openDeployment="isDeploymentOpen = true" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="px-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <DeploymentSuccessPanel | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| v-if="showDeploymentSuccessPanel && isDeployed" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :contractAddress="address" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :txHash="newlyDeployedTxHash" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @close="showDeploymentSuccessPanel = false" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @interact="scrollToInteract" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <template v-if="nodeStore.hasAtLeastOneValidator || uiStore.showTutorial"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ConstructorParameters | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id="tutorial-how-to-deploy" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: genlayerlabs/genlayer-studio
Length of output: 917
🏁 Script executed:
Repository: genlayerlabs/genlayer-studio
Length of output: 4149
🏁 Script executed:
fd -t f 'runtimeConfig.ts' frontend/src -x cat {}Repository: genlayerlabs/genlayer-studio
Length of output: 2093
🏁 Script executed:
rg -n 'network|Network' frontend/src/stores -t ts --max-count=20Repository: genlayerlabs/genlayer-studio
Length of output: 54
🏁 Script executed:
Repository: genlayerlabs/genlayer-studio
Length of output: 54
🏁 Script executed:
Repository: genlayerlabs/genlayer-studio
Length of output: 100
🏁 Script executed:
rg -n 'VITE_GENLAYER_NETWORK' frontend/src --type ts -C3Repository: genlayerlabs/genlayer-studio
Length of output: 1204
🏁 Script executed:
rg -n 'local.*network|studionet' frontend/src --type ts --type vue -i -C2Repository: genlayerlabs/genlayer-studio
Length of output: 100
🏁 Script executed:
cat -n frontend/src/hooks/useGenlayer.ts | head -60Repository: genlayerlabs/genlayer-studio
Length of output: 2142
🏁 Script executed:
rg -n 'chains\s*=' frontend/src/hooks -A 15Repository: genlayerlabs/genlayer-studio
Length of output: 54
🏁 Script executed:
rg -n 'localnet|studionet|explorer.*not.*available' frontend/src -iRepository: genlayerlabs/genlayer-studio
Length of output: 1289
🏁 Script executed:
rg -n 'github.com|gitlab.com|issue|#[0-9]+' frontend/src/components/Simulator/DeploymentSuccessPanel.vueRepository: genlayerlabs/genlayer-studio
Length of output: 54
🏁 Script executed:
Repository: genlayerlabs/genlayer-studio
Length of output: 139
🏁 Script executed:
rg -n 'isStudio|is_studio|isstudio' frontend/src --type ts -C2Repository: genlayerlabs/genlayer-studio
Length of output: 1042
Major: explorer link is shown for local networks, contradicting the intended design.
getExplorerUrl()always returns a non-empty string — it falls back tohttp://localhost:3001when neitherVITE_EXPLORER_URLnor a*.genlayer.comhost is present. Thereforev-if="explorerUrl"is always truthy, and on local/studionet the "View on Explorer" button will appear and link tohttp://localhost:3001/tx/....The intended behavior (per component design) requires:
The current implementation doesn't consult the active network at all. Although
VITE_GENLAYER_NETWORKis available (providing 'localnet', 'studionet', 'testnetAsimov'),getExplorerUrl()derives the explorer purely fromwindow.location.hostname, so a Studio session onstudio.genlayer.comconnected to local/studionet would still expose the wrong link.Suggested fix:
getExplorerUrl()to accept or access the active network identifier and returnnull/''for local/studionet.v-if="explorerUrl && txHash"to also handle empty txHash cases.Template sketch
Also applies to: 52-61
🤖 Prompt for AI Agents