Merge pull request #4427 from yan9651688/feat/channel-monitor-one-click-copy

feat(channel-monitor): add safe one-click duplication
This commit is contained in:
Wesley Liddick
2026-07-16 16:54:22 +08:00
committed by GitHub
19 changed files with 1512 additions and 29 deletions
@@ -0,0 +1,77 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import type { ChannelMonitor } from '@/api/admin/channelMonitor'
import MonitorActionsCell from '@/components/admin/monitor/MonitorActionsCell.vue'
vi.mock('vue-i18n', () => ({
useI18n: () => ({ t: (key: string) => key }),
}))
function makeMonitor(overrides: Partial<ChannelMonitor> = {}): ChannelMonitor {
return {
id: 42,
name: 'primary',
provider: 'openai',
api_mode: 'chat_completions',
endpoint: 'https://api.example.com',
api_key_masked: 'sk-t***',
primary_model: 'gpt-4o-mini',
extra_models: [],
group_name: '',
enabled: true,
interval_seconds: 60,
jitter_seconds: 0,
last_checked_at: null,
created_by: 1,
created_at: '2026-07-16T00:00:00Z',
updated_at: '2026-07-16T00:00:00Z',
primary_status: '',
primary_latency_ms: null,
availability_7d: 0,
extra_models_status: [],
template_id: null,
extra_headers: {},
body_override_mode: 'off',
body_override: null,
...overrides,
}
}
describe('MonitorActionsCell duplicate action', () => {
it('emits the selected monitor when duplicate is clicked', async () => {
const row = makeMonitor()
const wrapper = mount(MonitorActionsCell, {
props: { row, running: false, duplicating: false },
})
await wrapper.get('[data-testid="monitor-duplicate"]').trigger('click')
expect(wrapper.emitted('duplicate')).toEqual([[row]])
})
it('disables the action while the same monitor is being duplicated', () => {
const wrapper = mount(MonitorActionsCell, {
props: { row: makeMonitor(), running: false, duplicating: true },
})
const button = wrapper.get('[data-testid="monitor-duplicate"]')
expect(button.attributes('disabled')).toBeDefined()
expect(button.attributes('title')).toBe('admin.channelMonitor.duplicating')
expect(button.text()).toContain('admin.channelMonitor.duplicating')
})
it('disables the action when the stored API key cannot be decrypted', () => {
const wrapper = mount(MonitorActionsCell, {
props: {
row: makeMonitor({ api_key_decrypt_failed: true }),
running: false,
duplicating: false,
},
})
const button = wrapper.get('[data-testid="monitor-duplicate"]')
expect(button.attributes('disabled')).toBeDefined()
expect(button.attributes('title')).toBe('admin.channelMonitor.duplicateKeyUnavailable')
})
})
@@ -8,6 +8,18 @@
<Icon name="refresh" size="sm" :class="running ? 'animate-spin' : ''" />
<span class="text-xs">{{ t('admin.channelMonitor.runNow') }}</span>
</button>
<button
data-testid="monitor-duplicate"
:title="duplicateTitle"
:disabled="duplicating || Boolean(row.api_key_decrypt_failed)"
@click="$emit('duplicate', row)"
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-primary-600 disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-dark-700 dark:hover:text-primary-400"
>
<Icon name="copy" size="sm" />
<span class="text-xs">
{{ duplicating ? t('admin.channelMonitor.duplicating') : t('admin.channelMonitor.duplicate') }}
</span>
</button>
<button
@click="$emit('edit', row)"
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-primary-600 dark:hover:bg-dark-700 dark:hover:text-primary-400"
@@ -26,20 +38,28 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { ChannelMonitor } from '@/api/admin/channelMonitor'
import Icon from '@/components/icons/Icon.vue'
defineProps<{
const props = defineProps<{
row: ChannelMonitor
running: boolean
duplicating: boolean
}>()
defineEmits<{
(e: 'run', row: ChannelMonitor): void
(e: 'duplicate', row: ChannelMonitor): void
(e: 'edit', row: ChannelMonitor): void
(e: 'delete', row: ChannelMonitor): void
}>()
const { t } = useI18n()
const duplicateTitle = computed(() => {
if (props.row.api_key_decrypt_failed) return t('admin.channelMonitor.duplicateKeyUnavailable')
if (props.duplicating) return t('admin.channelMonitor.duplicating')
return t('admin.channelMonitor.duplicate')
})
</script>