Skip to content

Commit 58c32d7

Browse files
committed
enhance(certificate): add copy functionality and update auto cert states
1 parent 6c4849f commit 58c32d7

File tree

6 files changed

+197
-34
lines changed

6 files changed

+197
-34
lines changed

app/src/constants/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ export enum ConfigStatus {
77
}
88

99
export enum AutoCertState {
10-
Disable = 0,
10+
Disable = -1,
1111
Enable = 1,
12+
Sync = 2,
1213
}
1314

1415
export enum NotificationTypeT {

app/src/views/certificate/CertificateEditor.vue

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,8 @@ const id = computed(() => {
2323
2424
const { data } = storeToRefs(certStore)
2525
26-
const notShowInAutoCert = computed(() => {
27-
return data.value.auto_cert !== AutoCertState.Enable
28-
})
29-
3026
const isManaged = computed(() => {
31-
return data.value.auto_cert === AutoCertState.Enable
27+
return data.value.auto_cert === AutoCertState.Enable || data.value.auto_cert === AutoCertState.Sync
3228
})
3329
3430
function init() {
@@ -111,7 +107,7 @@ const log = computed(() => {
111107
<CertificateContentEditor
112108
v-model:data="data"
113109
:errors="errors"
114-
:readonly="!notShowInAutoCert"
110+
:readonly="isManaged"
115111
class="max-w-600px"
116112
/>
117113
</AForm>

app/src/views/certificate/components/ACMEUserSelector.vue

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,25 @@ const fieldNames = {
4545
function filterOption(input: string, option?: unknown) {
4646
return (option as AcmeUser)?.name?.toLowerCase().includes(input.toLowerCase()) ?? false
4747
}
48+
49+
const value = computed({
50+
set(value: number) {
51+
data.value.acme_user_id = value
52+
},
53+
get() {
54+
if (data.value.acme_user_id && data.value.acme_user_id > 0) {
55+
return data.value.acme_user_id
56+
}
57+
return undefined
58+
},
59+
})
4860
</script>
4961

5062
<template>
5163
<AForm layout="vertical">
5264
<AFormItem :label="$gettext('ACME User')">
5365
<ASelect
54-
v-model:value="data.acme_user_id"
66+
v-model:value="value"
5567
:placeholder="$gettext('System Initial User')"
5668
:loading="loading"
5769
show-search
Lines changed: 112 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<script setup lang="ts">
22
import type { Cert } from '@/api/cert'
3+
import { CopyOutlined } from '@ant-design/icons-vue'
4+
import { message } from 'ant-design-vue'
5+
import { useClipboard } from '@vueuse/core'
36
import NodeSelector from '@/components/NodeSelector'
47
58
interface Props {
@@ -12,6 +15,21 @@ defineProps<Props>()
1215
1316
// Use defineModel for two-way binding
1417
const data = defineModel<Cert>('data', { required: true })
18+
19+
const { copy } = useClipboard()
20+
21+
async function copyToClipboard(text: string, label: string) {
22+
if (!text) {
23+
message.warning($gettext('Nothing to copy'))
24+
return
25+
}
26+
try {
27+
await copy(text)
28+
message.success($gettext(`{label} copied to clipboard`).replace('{label}', label))
29+
} catch (error) {
30+
message.error($gettext('Failed to copy to clipboard'))
31+
}
32+
}
1533
</script>
1634

1735
<template>
@@ -26,13 +44,29 @@ const data = defineModel<Cert>('data', { required: true })
2644
? $gettext('This field is required')
2745
: ''"
2846
>
29-
<p v-if="isManaged">
30-
{{ data.name }}
31-
</p>
32-
<AInput
33-
v-else
34-
v-model:value="data.name"
35-
/>
47+
<div v-if="isManaged" class="copy-container">
48+
<p class="copy-text">{{ data.name }}</p>
49+
<AButton
50+
v-if="data.name"
51+
type="text"
52+
size="small"
53+
@click="copyToClipboard(data.name, $gettext('Name'))"
54+
>
55+
<CopyOutlined />
56+
</AButton>
57+
</div>
58+
<div v-else class="input-with-copy">
59+
<AInput v-model:value="data.name" />
60+
<AButton
61+
v-if="data.name"
62+
type="text"
63+
size="small"
64+
class="copy-button"
65+
@click="copyToClipboard(data.name, $gettext('Name'))"
66+
>
67+
<CopyOutlined />
68+
</AButton>
69+
</div>
3670
</AFormItem>
3771

3872
<AFormItem
@@ -42,13 +76,29 @@ const data = defineModel<Cert>('data', { required: true })
4276
: errors.ssl_certificate_path === 'certificate_path'
4377
? $gettext('The path exists, but the file is not a certificate') : ''"
4478
>
45-
<p v-if="isManaged">
46-
{{ data.ssl_certificate_path }}
47-
</p>
48-
<AInput
49-
v-else
50-
v-model:value="data.ssl_certificate_path"
51-
/>
79+
<div v-if="isManaged" class="copy-container">
80+
<p class="copy-text">{{ data.ssl_certificate_path }}</p>
81+
<AButton
82+
v-if="data.ssl_certificate_path"
83+
type="text"
84+
size="small"
85+
@click="copyToClipboard(data.ssl_certificate_path, $gettext('SSL Certificate Path'))"
86+
>
87+
<CopyOutlined />
88+
</AButton>
89+
</div>
90+
<div v-else class="input-with-copy">
91+
<AInput v-model:value="data.ssl_certificate_path" />
92+
<AButton
93+
v-if="data.ssl_certificate_path"
94+
type="text"
95+
size="small"
96+
class="copy-button"
97+
@click="copyToClipboard(data.ssl_certificate_path, $gettext('SSL Certificate Path'))"
98+
>
99+
<CopyOutlined />
100+
</AButton>
101+
</div>
52102
</AFormItem>
53103

54104
<AFormItem
@@ -58,13 +108,29 @@ const data = defineModel<Cert>('data', { required: true })
58108
: errors.ssl_certificate_key_path === 'privatekey_path'
59109
? $gettext('The path exists, but the file is not a private key') : ''"
60110
>
61-
<p v-if="isManaged">
62-
{{ data.ssl_certificate_key_path }}
63-
</p>
64-
<AInput
65-
v-else
66-
v-model:value="data.ssl_certificate_key_path"
67-
/>
111+
<div v-if="isManaged" class="copy-container">
112+
<p class="copy-text">{{ data.ssl_certificate_key_path }}</p>
113+
<AButton
114+
v-if="data.ssl_certificate_key_path"
115+
type="text"
116+
size="small"
117+
@click="copyToClipboard(data.ssl_certificate_key_path, $gettext('SSL Certificate Key Path'))"
118+
>
119+
<CopyOutlined />
120+
</AButton>
121+
</div>
122+
<div v-else class="input-with-copy">
123+
<AInput v-model:value="data.ssl_certificate_key_path" />
124+
<AButton
125+
v-if="data.ssl_certificate_key_path"
126+
type="text"
127+
size="small"
128+
class="copy-button"
129+
@click="copyToClipboard(data.ssl_certificate_key_path, $gettext('SSL Certificate Key Path'))"
130+
>
131+
<CopyOutlined />
132+
</AButton>
133+
</div>
68134
</AFormItem>
69135

70136
<AFormItem :label="$gettext('Sync to')">
@@ -77,4 +143,29 @@ const data = defineModel<Cert>('data', { required: true })
77143
</template>
78144

79145
<style scoped lang="less">
146+
.copy-container {
147+
display: flex;
148+
align-items: center;
149+
gap: 8px;
150+
151+
.copy-text {
152+
margin: 0;
153+
flex: 1;
154+
word-break: break-all;
155+
}
156+
}
157+
158+
.input-with-copy {
159+
display: flex;
160+
align-items: center;
161+
gap: 8px;
162+
163+
.ant-input {
164+
flex: 1;
165+
}
166+
167+
.copy-button {
168+
flex-shrink: 0;
169+
}
170+
}
80171
</style>

app/src/views/certificate/components/CertificateContentEditor.vue

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<script setup lang="ts">
22
import type { Cert } from '@/api/cert'
3-
import { InboxOutlined } from '@ant-design/icons-vue'
3+
import { CopyOutlined, InboxOutlined } from '@ant-design/icons-vue'
4+
import { message } from 'ant-design-vue'
5+
import { useClipboard } from '@vueuse/core'
46
import CodeEditor from '@/components/CodeEditor'
57
import CertificateFileUpload from './CertificateFileUpload.vue'
68
@@ -15,6 +17,21 @@ defineProps<Props>()
1517
// Use defineModel for two-way binding
1618
const data = defineModel<Cert>('data', { required: true })
1719
20+
const { copy } = useClipboard()
21+
22+
async function copyToClipboard(text: string, label: string) {
23+
if (!text) {
24+
message.warning($gettext('Nothing to copy'))
25+
return
26+
}
27+
try {
28+
await copy(text)
29+
message.success($gettext(`{label} copied to clipboard`).replace('{label}', label))
30+
} catch (error) {
31+
message.error($gettext('Failed to copy to clipboard'))
32+
}
33+
}
34+
1835
// Drag and drop state
1936
const isDragOverCert = ref(false)
2037
const isDragOverKey = ref(false)
@@ -90,11 +107,23 @@ function handleDrop(e: DragEvent, type: 'certificate' | 'key') {
90107
<div class="certificate-content-editor">
91108
<!-- SSL Certificate Content -->
92109
<AFormItem
93-
:label="$gettext('SSL Certificate Content')"
94110
:validate-status="errors.ssl_certificate ? 'error' : ''"
95111
:help="errors.ssl_certificate === 'certificate'
96112
? $gettext('The input is not a SSL Certificate') : ''"
97113
>
114+
<template #label>
115+
<div class="label-with-copy">
116+
<span class="label-text">{{ $gettext('SSL Certificate Content') }}</span>
117+
<AButton
118+
v-if="data.ssl_certificate"
119+
type="text"
120+
size="small"
121+
@click="copyToClipboard(data.ssl_certificate, $gettext('SSL Certificate Content'))"
122+
>
123+
<CopyOutlined />
124+
</AButton>
125+
</div>
126+
</template>
98127
<!-- Certificate File Upload -->
99128
<CertificateFileUpload
100129
v-if="!readonly"
@@ -139,11 +168,23 @@ function handleDrop(e: DragEvent, type: 'certificate' | 'key') {
139168

140169
<!-- SSL Certificate Key Content -->
141170
<AFormItem
142-
:label="$gettext('SSL Certificate Key Content')"
143171
:validate-status="errors.ssl_certificate_key ? 'error' : ''"
144172
:help="errors.ssl_certificate_key === 'privatekey'
145173
? $gettext('The input is not a SSL Certificate Key') : ''"
146174
>
175+
<template #label>
176+
<div class="label-with-copy">
177+
<span class="label-text">{{ $gettext('SSL Certificate Key Content') }}</span>
178+
<AButton
179+
v-if="data.ssl_certificate_key"
180+
type="text"
181+
size="small"
182+
@click="copyToClipboard(data.ssl_certificate_key, $gettext('SSL Certificate Key Content'))"
183+
>
184+
<CopyOutlined />
185+
</AButton>
186+
</div>
187+
</template>
147188
<!-- Private Key File Upload -->
148189
<CertificateFileUpload
149190
v-if="!readonly"
@@ -190,6 +231,18 @@ function handleDrop(e: DragEvent, type: 'certificate' | 'key') {
190231

191232
<style scoped lang="less">
192233
.certificate-content-editor {
234+
.label-with-copy {
235+
display: flex;
236+
align-items: center;
237+
justify-content: space-between;
238+
width: 100%;
239+
240+
.label-text {
241+
font-weight: 500;
242+
color: rgba(0, 0, 0, 0.85);
243+
}
244+
}
245+
193246
.code-editor-container {
194247
position: relative;
195248

app/src/views/terminal/Terminal.vue

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const router = useRouter()
1414
const websocket = shallowRef<ReconnectingWebSocket | WebSocket>()
1515
const lostConnection = ref(false)
1616
const insecureConnection = ref(false)
17+
const isWebSocketReady = ref(false)
1718
1819
// Check if using HTTP in a non-localhost environment
1920
function checkSecureConnection() {
@@ -36,15 +37,19 @@ onMounted(() => {
3637
websocket.value = ws(`/api/pty?X-Secure-Session-ID=${secureSessionId}`, false)
3738
3839
nextTick(() => {
39-
initTerm()
4040
websocket.value!.onmessage = wsOnMessage
4141
websocket.value!.onopen = wsOnOpen
4242
websocket.value!.onerror = () => {
4343
lostConnection.value = true
44+
isWebSocketReady.value = false
4445
}
4546
websocket.value!.onclose = () => {
4647
lostConnection.value = true
48+
isWebSocketReady.value = false
4749
}
50+
51+
// Initialize terminal only after WebSocket is ready
52+
initTerm()
4853
})
4954
}).catch(() => {
5055
if (window.history.length > 1)
@@ -84,6 +89,7 @@ function initTerm() {
8489
window.addEventListener('resize', fit)
8590
term.focus()
8691
92+
// Only set up event handlers, but don't send messages until WebSocket is ready
8793
term.onData(key => {
8894
const order: Message = {
8995
Data: key,
@@ -101,14 +107,18 @@ function initTerm() {
101107
}
102108
103109
function sendMessage(data: Message) {
104-
websocket.value?.send(JSON.stringify(data))
110+
// Only send if WebSocket is ready
111+
if (websocket.value && isWebSocketReady.value) {
112+
websocket.value.send(JSON.stringify(data))
113+
}
105114
}
106115
107116
function wsOnMessage(msg: { data: string | Uint8Array }) {
108117
term!.write(msg.data)
109118
}
110119
111120
function wsOnOpen() {
121+
isWebSocketReady.value = true
112122
ping = setInterval(() => {
113123
sendMessage({ Type: 3, Data: null })
114124
}, 30000)

0 commit comments

Comments
 (0)