Skip to content

Commit fe81bd3

Browse files
github-actions[bot]rjpowerclaude
committed
Extract copy-IP button into shared CopyButton.vue component
Move the duplicated copy-to-clipboard logic and SVG icons from EndpointsTab, FleetTab, and WorkerDetail into a reusable CopyButton component in components/shared/. Co-authored-by: Russell Power <rjpower@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 927793a commit fe81bd3

File tree

4 files changed

+44
-82
lines changed

4 files changed

+44
-82
lines changed

lib/iris/dashboard/src/components/controller/EndpointsTab.vue

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useControllerRpc } from '@/composables/useRpc'
55
import { useAutoRefresh } from '@/composables/useAutoRefresh'
66
import type { EndpointInfo, ListEndpointsResponse } from '@/types/rpc'
77
import EmptyState from '@/components/shared/EmptyState.vue'
8+
import CopyButton from '@/components/shared/CopyButton.vue'
89
910
const SHOW_ALL_THRESHOLD = 100
1011
@@ -53,15 +54,6 @@ function metadataString(metadata?: Record<string, string>): string {
5354
return entries.map(([k, v]) => `${k}=${v}`).join(', ')
5455
}
5556
56-
const copiedAddress = ref<string | null>(null)
57-
58-
async function copyAddress(addr: string) {
59-
const ip = addr.replace(/^https?:\/\//, '').replace(/:\d+$/, '')
60-
await navigator.clipboard.writeText(ip)
61-
copiedAddress.value = addr
62-
setTimeout(() => { copiedAddress.value = null }, 1500)
63-
}
64-
6557
function jobIdFromTaskId(taskId?: string): string | null {
6658
if (!taskId) return null
6759
// taskId format: jobId/taskIndex or jobId
@@ -155,19 +147,7 @@ function jobIdFromTaskId(taskId?: string): string | null {
155147
<td class="px-3 py-2 text-[13px] font-mono text-text-secondary">
156148
<span v-if="ep.address" class="group/addr inline-flex items-center gap-1">
157149
{{ ep.address }}
158-
<button
159-
class="text-text-muted hover:text-text opacity-0 group-hover/addr:opacity-100 transition-opacity"
160-
title="Copy IP"
161-
@click="copyAddress(ep.address)"
162-
>
163-
<svg v-if="copiedAddress === ep.address" class="w-3.5 h-3.5 text-status-success" viewBox="0 0 20 20" fill="currentColor">
164-
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
165-
</svg>
166-
<svg v-else class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
167-
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
168-
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
169-
</svg>
170-
</button>
150+
<CopyButton :value="ep.address" />
171151
</span>
172152
<span v-else>-</span>
173153
</td>

lib/iris/dashboard/src/components/controller/FleetTab.vue

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { ref, computed, onMounted } from 'vue'
2+
import { computed, onMounted } from 'vue'
33
import { RouterLink } from 'vue-router'
44
import { useControllerRpc } from '@/composables/useRpc'
55
import { useAutoRefresh } from '@/composables/useAutoRefresh'
@@ -8,6 +8,7 @@ import { timestampMs, formatRelativeTime, formatBytes, formatWorkerDevice } from
88
99
import DataTable, { type Column } from '@/components/shared/DataTable.vue'
1010
import EmptyState from '@/components/shared/EmptyState.vue'
11+
import CopyButton from '@/components/shared/CopyButton.vue'
1112
1213
const { data, loading, error, refresh } = useControllerRpc<ListWorkersResponse>('ListWorkers')
1314
@@ -16,15 +17,6 @@ onMounted(refresh)
1617
1718
const workers = computed<WorkerHealthStatus[]>(() => data.value?.workers ?? [])
1819
19-
const copiedAddress = ref<string | null>(null)
20-
21-
async function copyAddress(addr: string) {
22-
const ip = addr.replace(/^https?:\/\//, '').replace(/:\d+$/, '')
23-
await navigator.clipboard.writeText(ip)
24-
copiedAddress.value = addr
25-
setTimeout(() => { copiedAddress.value = null }, 1500)
26-
}
27-
2820
const columns: Column[] = [
2921
{ key: 'workerId', label: 'Worker ID', mono: true },
3022
{ key: 'address', label: 'Address', mono: true },
@@ -81,19 +73,7 @@ const columns: Column[] = [
8173
<template #cell-address="{ row }">
8274
<span v-if="(row as WorkerHealthStatus).address" class="group/addr inline-flex items-center gap-1">
8375
{{ (row as WorkerHealthStatus).address }}
84-
<button
85-
class="text-text-muted hover:text-text opacity-0 group-hover/addr:opacity-100 transition-opacity"
86-
title="Copy IP"
87-
@click="copyAddress((row as WorkerHealthStatus).address!)"
88-
>
89-
<svg v-if="copiedAddress === (row as WorkerHealthStatus).address" class="w-3.5 h-3.5 text-status-success" viewBox="0 0 20 20" fill="currentColor">
90-
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
91-
</svg>
92-
<svg v-else class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
93-
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
94-
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
95-
</svg>
96-
</button>
76+
<CopyButton :value="(row as WorkerHealthStatus).address!" />
9777
</span>
9878
<span v-else>-</span>
9979
</template>

lib/iris/dashboard/src/components/controller/WorkerDetail.vue

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { ref, computed, onMounted } from 'vue'
2+
import { computed, onMounted } from 'vue'
33
import { RouterLink } from 'vue-router'
44
import { useControllerRpc } from '@/composables/useRpc'
55
import { useAutoRefresh } from '@/composables/useAutoRefresh'
@@ -19,6 +19,7 @@ import InfoRow from '@/components/shared/InfoRow.vue'
1919
import MetricCard from '@/components/shared/MetricCard.vue'
2020
import Sparkline from '@/components/shared/Sparkline.vue'
2121
import DataTable, { type Column } from '@/components/shared/DataTable.vue'
22+
import CopyButton from '@/components/shared/CopyButton.vue'
2223
2324
const props = defineProps<{
2425
workerId: string
@@ -73,16 +74,6 @@ const taskColumns: Column[] = [
7374
useAutoRefresh(fetchWorker, 5_000)
7475
onMounted(fetchWorker)
7576
76-
const copiedAddress = ref(false)
77-
78-
async function copyAddress(addr: string) {
79-
if (!addr) return
80-
const ip = addr.replace(/^https?:\/\//, '').replace(/:\d+$/, '')
81-
await navigator.clipboard.writeText(ip)
82-
copiedAddress.value = true
83-
setTimeout(() => { copiedAddress.value = false }, 1500)
84-
}
85-
8677
function attributeDisplay(val: { stringValue?: string; intValue?: string; floatValue?: string }): string {
8778
if (val.stringValue !== undefined) return val.stringValue
8879
if (val.intValue !== undefined) return val.intValue
@@ -130,19 +121,7 @@ function attributeDisplay(val: { stringValue?: string; intValue?: string; floatV
130121
</span>
131122
<span v-if="worker?.address" class="group/addr text-sm text-text-muted font-mono inline-flex items-center gap-1">
132123
{{ worker.address }}
133-
<button
134-
class="text-text-muted hover:text-text opacity-0 group-hover/addr:opacity-100 transition-opacity"
135-
title="Copy IP"
136-
@click="copyAddress(worker.address)"
137-
>
138-
<svg v-if="copiedAddress" class="w-3.5 h-3.5 text-status-success" viewBox="0 0 20 20" fill="currentColor">
139-
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
140-
</svg>
141-
<svg v-else class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
142-
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
143-
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
144-
</svg>
145-
</button>
124+
<CopyButton :value="worker.address" />
146125
</span>
147126
<button
148127
class="ml-auto px-3 py-1.5 text-xs border border-surface-border rounded hover:bg-surface-sunken"
@@ -172,19 +151,7 @@ function attributeDisplay(val: { stringValue?: string; intValue?: string; floatV
172151
</InfoRow>
173152
<InfoRow label="Address">
174153
<span v-if="worker?.address" class="group/addr inline-flex items-center gap-1">
175-
<button
176-
class="text-text-muted hover:text-text opacity-0 group-hover/addr:opacity-100 transition-opacity"
177-
title="Copy IP"
178-
@click="copyAddress(worker.address)"
179-
>
180-
<svg v-if="copiedAddress" class="w-3.5 h-3.5 text-status-success" viewBox="0 0 20 20" fill="currentColor">
181-
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
182-
</svg>
183-
<svg v-else class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
184-
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
185-
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
186-
</svg>
187-
</button>
154+
<CopyButton :value="worker.address" />
188155
<span class="font-mono">{{ worker.address }}</span>
189156
</span>
190157
<span v-else class="font-mono">-</span>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
4+
const props = defineProps<{
5+
/** The raw value to copy (e.g. "https://10.0.0.1:8080"). Protocol and port are stripped before copying. */
6+
value: string
7+
/** Tooltip text shown on hover. */
8+
title?: string
9+
}>()
10+
11+
const copied = ref(false)
12+
13+
async function copy() {
14+
const ip = props.value.replace(/^https?:\/\//, '').replace(/:\d+$/, '')
15+
await navigator.clipboard.writeText(ip)
16+
copied.value = true
17+
setTimeout(() => { copied.value = false }, 1500)
18+
}
19+
</script>
20+
21+
<template>
22+
<button
23+
class="text-text-muted hover:text-text opacity-0 group-hover/addr:opacity-100 transition-opacity"
24+
:title="title ?? 'Copy IP'"
25+
@click="copy"
26+
>
27+
<svg v-if="copied" class="w-3.5 h-3.5 text-status-success" viewBox="0 0 20 20" fill="currentColor">
28+
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
29+
</svg>
30+
<svg v-else class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
31+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
32+
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
33+
</svg>
34+
</button>
35+
</template>

0 commit comments

Comments
 (0)