Pārlūkot izejas kodu

enhance(certificate): add copy functionality and update auto cert states

0xJacky 2 dienas atpakaļ
vecāks
revīzija
080ea59ef9

+ 1 - 1
app/package.json

@@ -88,4 +88,4 @@
     "vite-svg-loader": "^5.1.0",
     "vue-tsc": "^3.0.5"
   }
-}
+}

+ 2 - 1
app/src/constants/index.ts

@@ -7,8 +7,9 @@ export enum ConfigStatus {
 }
 
 export enum AutoCertState {
-  Disable = 0,
+  Disable = -1,
   Enable = 1,
+  Sync = 2,
 }
 
 export enum NotificationTypeT {

+ 2 - 6
app/src/views/certificate/CertificateEditor.vue

@@ -23,12 +23,8 @@ const id = computed(() => {
 
 const { data } = storeToRefs(certStore)
 
-const notShowInAutoCert = computed(() => {
-  return data.value.auto_cert !== AutoCertState.Enable
-})
-
 const isManaged = computed(() => {
-  return data.value.auto_cert === AutoCertState.Enable
+  return data.value.auto_cert === AutoCertState.Enable || data.value.auto_cert === AutoCertState.Sync
 })
 
 function init() {
@@ -111,7 +107,7 @@ const log = computed(() => {
           <CertificateContentEditor
             v-model:data="data"
             :errors="errors"
-            :readonly="!notShowInAutoCert"
+            :readonly="isManaged"
             class="max-w-600px"
           />
         </AForm>

+ 13 - 1
app/src/views/certificate/components/ACMEUserSelector.vue

@@ -45,13 +45,25 @@ const fieldNames = {
 function filterOption(input: string, option?: unknown) {
   return (option as AcmeUser)?.name?.toLowerCase().includes(input.toLowerCase()) ?? false
 }
+
+const value = computed({
+  set(value: number) {
+    data.value.acme_user_id = value
+  },
+  get() {
+    if (data.value.acme_user_id && data.value.acme_user_id > 0) {
+      return data.value.acme_user_id
+    }
+    return undefined
+  },
+})
 </script>
 
 <template>
   <AForm layout="vertical">
     <AFormItem :label="$gettext('ACME User')">
       <ASelect
-        v-model:value="data.acme_user_id"
+        v-model:value="value"
         :placeholder="$gettext('System Initial User')"
         :loading="loading"
         show-search

+ 120 - 21
app/src/views/certificate/components/CertificateBasicInfo.vue

@@ -1,5 +1,8 @@
 <script setup lang="ts">
 import type { Cert } from '@/api/cert'
+import { CopyOutlined } from '@ant-design/icons-vue'
+import { useClipboard } from '@vueuse/core'
+import { message } from 'ant-design-vue'
 import NodeSelector from '@/components/NodeSelector'
 
 interface Props {
@@ -12,6 +15,23 @@ defineProps<Props>()
 
 // Use defineModel for two-way binding
 const data = defineModel<Cert>('data', { required: true })
+
+const { copy } = useClipboard()
+
+async function copyToClipboard(text: string, label: string) {
+  if (!text) {
+    message.warning($gettext('Nothing to copy'))
+    return
+  }
+  try {
+    await copy(text)
+    message.success($gettext(`{label} copied to clipboard`).replace('{label}', label))
+  }
+  catch (error) {
+    console.error(error)
+    message.error($gettext('Failed to copy to clipboard'))
+  }
+}
 </script>
 
 <template>
@@ -26,13 +46,31 @@ const data = defineModel<Cert>('data', { required: true })
         ? $gettext('This field is required')
         : ''"
     >
-      <p v-if="isManaged">
-        {{ data.name }}
-      </p>
-      <AInput
-        v-else
-        v-model:value="data.name"
-      />
+      <div v-if="isManaged" class="copy-container">
+        <p class="copy-text">
+          {{ data.name }}
+        </p>
+        <AButton
+          v-if="data.name"
+          type="text"
+          size="small"
+          @click="copyToClipboard(data.name, $gettext('Name'))"
+        >
+          <CopyOutlined />
+        </AButton>
+      </div>
+      <div v-else class="input-with-copy">
+        <AInput v-model:value="data.name" />
+        <AButton
+          v-if="data.name"
+          type="text"
+          size="small"
+          class="copy-button"
+          @click="copyToClipboard(data.name, $gettext('Name'))"
+        >
+          <CopyOutlined />
+        </AButton>
+      </div>
     </AFormItem>
 
     <AFormItem
@@ -42,13 +80,31 @@ const data = defineModel<Cert>('data', { required: true })
         : errors.ssl_certificate_path === 'certificate_path'
           ? $gettext('The path exists, but the file is not a certificate') : ''"
     >
-      <p v-if="isManaged">
-        {{ data.ssl_certificate_path }}
-      </p>
-      <AInput
-        v-else
-        v-model:value="data.ssl_certificate_path"
-      />
+      <div v-if="isManaged" class="copy-container">
+        <p class="copy-text">
+          {{ data.ssl_certificate_path }}
+        </p>
+        <AButton
+          v-if="data.ssl_certificate_path"
+          type="text"
+          size="small"
+          @click="copyToClipboard(data.ssl_certificate_path, $gettext('SSL Certificate Path'))"
+        >
+          <CopyOutlined />
+        </AButton>
+      </div>
+      <div v-else class="input-with-copy">
+        <AInput v-model:value="data.ssl_certificate_path" />
+        <AButton
+          v-if="data.ssl_certificate_path"
+          type="text"
+          size="small"
+          class="copy-button"
+          @click="copyToClipboard(data.ssl_certificate_path, $gettext('SSL Certificate Path'))"
+        >
+          <CopyOutlined />
+        </AButton>
+      </div>
     </AFormItem>
 
     <AFormItem
@@ -58,13 +114,31 @@ const data = defineModel<Cert>('data', { required: true })
         : errors.ssl_certificate_key_path === 'privatekey_path'
           ? $gettext('The path exists, but the file is not a private key') : ''"
     >
-      <p v-if="isManaged">
-        {{ data.ssl_certificate_key_path }}
-      </p>
-      <AInput
-        v-else
-        v-model:value="data.ssl_certificate_key_path"
-      />
+      <div v-if="isManaged" class="copy-container">
+        <p class="copy-text">
+          {{ data.ssl_certificate_key_path }}
+        </p>
+        <AButton
+          v-if="data.ssl_certificate_key_path"
+          type="text"
+          size="small"
+          @click="copyToClipboard(data.ssl_certificate_key_path, $gettext('SSL Certificate Key Path'))"
+        >
+          <CopyOutlined />
+        </AButton>
+      </div>
+      <div v-else class="input-with-copy">
+        <AInput v-model:value="data.ssl_certificate_key_path" />
+        <AButton
+          v-if="data.ssl_certificate_key_path"
+          type="text"
+          size="small"
+          class="copy-button"
+          @click="copyToClipboard(data.ssl_certificate_key_path, $gettext('SSL Certificate Key Path'))"
+        >
+          <CopyOutlined />
+        </AButton>
+      </div>
     </AFormItem>
 
     <AFormItem :label="$gettext('Sync to')">
@@ -77,4 +151,29 @@ const data = defineModel<Cert>('data', { required: true })
 </template>
 
 <style scoped lang="less">
+.copy-container {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+
+  .copy-text {
+    margin: 0;
+    flex: 1;
+    word-break: break-all;
+  }
+}
+
+.input-with-copy {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+
+  .ant-input {
+    flex: 1;
+  }
+
+  .copy-button {
+    flex-shrink: 0;
+  }
+}
 </style>

+ 58 - 3
app/src/views/certificate/components/CertificateContentEditor.vue

@@ -1,6 +1,8 @@
 <script setup lang="ts">
 import type { Cert } from '@/api/cert'
-import { InboxOutlined } from '@ant-design/icons-vue'
+import { CopyOutlined, InboxOutlined } from '@ant-design/icons-vue'
+import { useClipboard } from '@vueuse/core'
+import { message } from 'ant-design-vue'
 import CodeEditor from '@/components/CodeEditor'
 import CertificateFileUpload from './CertificateFileUpload.vue'
 
@@ -15,6 +17,23 @@ defineProps<Props>()
 // Use defineModel for two-way binding
 const data = defineModel<Cert>('data', { required: true })
 
+const { copy } = useClipboard()
+
+async function copyToClipboard(text: string, label: string) {
+  if (!text) {
+    message.warning($gettext('Nothing to copy'))
+    return
+  }
+  try {
+    await copy(text)
+    message.success($gettext(`{label} copied to clipboard`).replace('{label}', label))
+  }
+  catch (error) {
+    console.error(error)
+    message.error($gettext('Failed to copy to clipboard'))
+  }
+}
+
 // Drag and drop state
 const isDragOverCert = ref(false)
 const isDragOverKey = ref(false)
@@ -90,11 +109,23 @@ function handleDrop(e: DragEvent, type: 'certificate' | 'key') {
   <div class="certificate-content-editor">
     <!-- SSL Certificate Content -->
     <AFormItem
-      :label="$gettext('SSL Certificate Content')"
       :validate-status="errors.ssl_certificate ? 'error' : ''"
       :help="errors.ssl_certificate === 'certificate'
         ? $gettext('The input is not a SSL Certificate') : ''"
     >
+      <template #label>
+        <div class="label-with-copy">
+          <span class="label-text">{{ $gettext('SSL Certificate Content') }}</span>
+          <AButton
+            v-if="data.ssl_certificate"
+            type="text"
+            size="small"
+            @click="copyToClipboard(data.ssl_certificate, $gettext('SSL Certificate Content'))"
+          >
+            <CopyOutlined />
+          </AButton>
+        </div>
+      </template>
       <!-- Certificate File Upload -->
       <CertificateFileUpload
         v-if="!readonly"
@@ -139,11 +170,23 @@ function handleDrop(e: DragEvent, type: 'certificate' | 'key') {
 
     <!-- SSL Certificate Key Content -->
     <AFormItem
-      :label="$gettext('SSL Certificate Key Content')"
       :validate-status="errors.ssl_certificate_key ? 'error' : ''"
       :help="errors.ssl_certificate_key === 'privatekey'
         ? $gettext('The input is not a SSL Certificate Key') : ''"
     >
+      <template #label>
+        <div class="label-with-copy">
+          <span class="label-text">{{ $gettext('SSL Certificate Key Content') }}</span>
+          <AButton
+            v-if="data.ssl_certificate_key"
+            type="text"
+            size="small"
+            @click="copyToClipboard(data.ssl_certificate_key, $gettext('SSL Certificate Key Content'))"
+          >
+            <CopyOutlined />
+          </AButton>
+        </div>
+      </template>
       <!-- Private Key File Upload -->
       <CertificateFileUpload
         v-if="!readonly"
@@ -190,6 +233,18 @@ function handleDrop(e: DragEvent, type: 'certificate' | 'key') {
 
 <style scoped lang="less">
 .certificate-content-editor {
+  .label-with-copy {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 100%;
+
+    .label-text {
+      font-weight: 500;
+      color: rgba(0, 0, 0, 0.85);
+    }
+  }
+
   .code-editor-container {
     position: relative;
 

+ 12 - 2
app/src/views/terminal/Terminal.vue

@@ -14,6 +14,7 @@ const router = useRouter()
 const websocket = shallowRef<ReconnectingWebSocket | WebSocket>()
 const lostConnection = ref(false)
 const insecureConnection = ref(false)
+const isWebSocketReady = ref(false)
 
 // Check if using HTTP in a non-localhost environment
 function checkSecureConnection() {
@@ -36,15 +37,19 @@ onMounted(() => {
     websocket.value = ws(`/api/pty?X-Secure-Session-ID=${secureSessionId}`, false)
 
     nextTick(() => {
-      initTerm()
       websocket.value!.onmessage = wsOnMessage
       websocket.value!.onopen = wsOnOpen
       websocket.value!.onerror = () => {
         lostConnection.value = true
+        isWebSocketReady.value = false
       }
       websocket.value!.onclose = () => {
         lostConnection.value = true
+        isWebSocketReady.value = false
       }
+
+      // Initialize terminal only after WebSocket is ready
+      initTerm()
     })
   }).catch(() => {
     if (window.history.length > 1)
@@ -84,6 +89,7 @@ function initTerm() {
   window.addEventListener('resize', fit)
   term.focus()
 
+  // Only set up event handlers, but don't send messages until WebSocket is ready
   term.onData(key => {
     const order: Message = {
       Data: key,
@@ -101,7 +107,10 @@ function initTerm() {
 }
 
 function sendMessage(data: Message) {
-  websocket.value?.send(JSON.stringify(data))
+  // Only send if WebSocket is ready
+  if (websocket.value && isWebSocketReady.value) {
+    websocket.value.send(JSON.stringify(data))
+  }
 }
 
 function wsOnMessage(msg: { data: string | Uint8Array }) {
@@ -109,6 +118,7 @@ function wsOnMessage(msg: { data: string | Uint8Array }) {
 }
 
 function wsOnOpen() {
+  isWebSocketReady.value = true
   ping = setInterval(() => {
     sendMessage({ Type: 3, Data: null })
   }, 30000)