ConfigEditor.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. <script setup lang="ts">
  2. import type { Config } from '@/api/config'
  3. import type { ChatComplicationMessage } from '@/api/openai'
  4. import type { Ref } from 'vue'
  5. import config from '@/api/config'
  6. import ngx from '@/api/ngx'
  7. import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
  8. import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
  9. import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
  10. import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
  11. import { useBreadcrumbs } from '@/composables/useBreadcrumbs'
  12. import { formatDateTime } from '@/lib/helper'
  13. import { useSettingsStore } from '@/pinia'
  14. import ConfigName from '@/views/config/components/ConfigName.vue'
  15. import InspectConfig from '@/views/config/InspectConfig.vue'
  16. import { InfoCircleOutlined } from '@ant-design/icons-vue'
  17. import { message } from 'ant-design-vue'
  18. import _ from 'lodash'
  19. const settings = useSettingsStore()
  20. const route = useRoute()
  21. const router = useRouter()
  22. // eslint-disable-next-line vue/require-typed-ref
  23. const refForm = ref()
  24. const refInspectConfig = useTemplateRef('refInspectConfig')
  25. const origName = ref('')
  26. const addMode = computed(() => !route.params.name)
  27. const errors = ref({})
  28. const basePath = computed(() => {
  29. if (route.query.basePath)
  30. return _.trim(route?.query?.basePath?.toString(), '/')
  31. else if (typeof route.params.name === 'object')
  32. return (route.params.name as string[]).slice(0, -1).join('/')
  33. else
  34. return ''
  35. })
  36. const data = ref({
  37. name: '',
  38. content: '',
  39. filepath: '',
  40. sync_node_ids: [] as number[],
  41. sync_overwrite: false,
  42. } as Config)
  43. const historyChatgptRecord = ref([]) as Ref<ChatComplicationMessage[]>
  44. const activeKey = ref(['basic', 'deploy', 'chatgpt'])
  45. const modifiedAt = ref('')
  46. const nginxConfigBase = ref('')
  47. const newPath = computed(() => [nginxConfigBase.value, basePath.value, data.value.name]
  48. .filter(v => v)
  49. .join('/'))
  50. const relativePath = computed(() => (route.params.name as string[]).join('/'))
  51. const breadcrumbs = useBreadcrumbs()
  52. async function init() {
  53. const { name } = route.params
  54. data.value.name = name?.[name?.length - 1] ?? ''
  55. origName.value = data.value.name
  56. if (!addMode.value) {
  57. config.get(relativePath.value).then(r => {
  58. data.value = r
  59. historyChatgptRecord.value = r.chatgpt_messages
  60. modifiedAt.value = r.modified_at
  61. const filteredPath = _.trimEnd(data.value.filepath
  62. .replaceAll(`${nginxConfigBase.value}/`, ''), data.value.name)
  63. .split('/')
  64. .filter(v => v)
  65. const path = filteredPath.map((v, k) => {
  66. let dir = v
  67. if (k > 0) {
  68. dir = filteredPath.slice(0, k).join('/')
  69. dir += `/${v}`
  70. }
  71. return {
  72. name: 'Manage Configs',
  73. translatedName: () => v,
  74. path: '/config',
  75. query: {
  76. dir,
  77. },
  78. hasChildren: false,
  79. }
  80. })
  81. breadcrumbs.value = [{
  82. name: 'Dashboard',
  83. translatedName: () => $gettext('Dashboard'),
  84. path: '/dashboard',
  85. hasChildren: false,
  86. }, {
  87. name: 'Manage Configs',
  88. translatedName: () => $gettext('Manage Configs'),
  89. path: '/config',
  90. hasChildren: false,
  91. }, ...path, {
  92. name: 'Edit Config',
  93. translatedName: () => origName.value,
  94. hasChildren: false,
  95. }]
  96. }).catch(r => {
  97. message.error(r.message ?? $gettext('Server error'))
  98. })
  99. }
  100. else {
  101. data.value.content = ''
  102. historyChatgptRecord.value = []
  103. data.value.filepath = ''
  104. const path = basePath.value
  105. .split('/')
  106. .filter(v => v)
  107. .map(v => {
  108. return {
  109. name: 'Manage Configs',
  110. translatedName: () => v,
  111. path: '/config',
  112. query: {
  113. dir: v,
  114. },
  115. hasChildren: false,
  116. }
  117. })
  118. breadcrumbs.value = [{
  119. name: 'Dashboard',
  120. translatedName: () => $gettext('Dashboard'),
  121. path: '/dashboard',
  122. hasChildren: false,
  123. }, {
  124. name: 'Manage Configs',
  125. translatedName: () => $gettext('Manage Configs'),
  126. path: '/config',
  127. hasChildren: false,
  128. }, ...path, {
  129. name: 'Add Config',
  130. translatedName: () => $gettext('Add Configuration'),
  131. hasChildren: false,
  132. }]
  133. }
  134. }
  135. onMounted(async () => {
  136. await config.get_base_path().then(r => {
  137. nginxConfigBase.value = r.base_path
  138. })
  139. await init()
  140. })
  141. function save() {
  142. refForm.value?.validate().then(() => {
  143. config.save(addMode.value ? undefined : relativePath.value, {
  144. name: addMode.value ? data.value.name : undefined,
  145. base_dir: addMode.value ? basePath.value : undefined,
  146. content: data.value.content,
  147. sync_node_ids: data.value.sync_node_ids,
  148. sync_overwrite: data.value.sync_overwrite,
  149. }).then(r => {
  150. data.value.content = r.content
  151. message.success($gettext('Saved successfully'))
  152. router.push(`/config/${r.filepath.replaceAll(`${nginxConfigBase.value}/`, '')}/edit`)
  153. }).catch(e => {
  154. errors.value = e.errors
  155. message.error($gettext('Save error %{msg}', { msg: e.message ?? '' }))
  156. }).finally(() => {
  157. refInspectConfig.value?.test()
  158. })
  159. })
  160. }
  161. function formatCode() {
  162. ngx.format_code(data.value.content).then(r => {
  163. data.value.content = r.content
  164. message.success($gettext('Format successfully'))
  165. }).catch(r => {
  166. message.error($gettext('Format error %{msg}', { msg: r.message ?? '' }))
  167. })
  168. }
  169. function goBack() {
  170. router.push({
  171. path: '/config',
  172. query: {
  173. dir: basePath.value || undefined,
  174. },
  175. })
  176. }
  177. </script>
  178. <template>
  179. <ARow :gutter="16">
  180. <ACol
  181. :xs="24"
  182. :sm="24"
  183. :md="18"
  184. >
  185. <ACard :title="addMode ? $gettext('Add Configuration') : $gettext('Edit Configuration')">
  186. <InspectConfig
  187. v-show="!addMode"
  188. ref="refInspectConfig"
  189. />
  190. <CodeEditor v-model:content="data.content" />
  191. <FooterToolBar>
  192. <ASpace>
  193. <AButton @click="goBack">
  194. {{ $gettext('Back') }}
  195. </AButton>
  196. <AButton @click="formatCode">
  197. {{ $gettext('Format Code') }}
  198. </AButton>
  199. <AButton
  200. type="primary"
  201. @click="save"
  202. >
  203. {{ $gettext('Save') }}
  204. </AButton>
  205. </ASpace>
  206. </FooterToolBar>
  207. </ACard>
  208. </ACol>
  209. <ACol
  210. :xs="24"
  211. :sm="24"
  212. :md="6"
  213. >
  214. <ACard class="col-right">
  215. <ACollapse
  216. v-model:active-key="activeKey"
  217. ghost
  218. >
  219. <ACollapsePanel
  220. key="basic"
  221. :header="$gettext('Basic')"
  222. >
  223. <AForm
  224. ref="refForm"
  225. layout="vertical"
  226. :model="data"
  227. :rules="{
  228. name: [
  229. { required: true, message: $gettext('Please input a filename') },
  230. { pattern: /^[^\\/]+$/, message: $gettext('Invalid filename') },
  231. ],
  232. }"
  233. >
  234. <AFormItem
  235. name="name"
  236. :label="$gettext('Name')"
  237. >
  238. <AInput v-if="addMode" v-model:value="data.name" />
  239. <ConfigName v-else :name="data.name" :dir="data.dir" />
  240. </AFormItem>
  241. <AFormItem
  242. v-if="!addMode"
  243. :label="$gettext('Path')"
  244. >
  245. {{ data.filepath }}
  246. </AFormItem>
  247. <AFormItem
  248. v-show="data.name !== origName"
  249. :label="addMode ? $gettext('New Path') : $gettext('Changed Path')"
  250. required
  251. >
  252. {{ newPath }}
  253. </AFormItem>
  254. <AFormItem
  255. v-if="!addMode"
  256. :label="$gettext('Updated at')"
  257. >
  258. {{ formatDateTime(modifiedAt) }}
  259. </AFormItem>
  260. </AForm>
  261. </ACollapsePanel>
  262. <ACollapsePanel
  263. v-if="!settings.is_remote"
  264. key="deploy"
  265. :header="$gettext('Deploy')"
  266. >
  267. <NodeSelector
  268. v-model:target="data.sync_node_ids"
  269. hidden-local
  270. />
  271. <div class="node-deploy-control">
  272. <div class="overwrite">
  273. <ACheckbox v-model:checked="data.sync_overwrite">
  274. {{ $gettext('Overwrite') }}
  275. </ACheckbox>
  276. <ATooltip placement="bottom">
  277. <template #title>
  278. {{ $gettext('Overwrite exist file') }}
  279. </template>
  280. <InfoCircleOutlined />
  281. </ATooltip>
  282. </div>
  283. </div>
  284. </ACollapsePanel>
  285. <ACollapsePanel
  286. key="chatgpt"
  287. header="ChatGPT"
  288. >
  289. <ChatGPT
  290. v-model:history-messages="historyChatgptRecord"
  291. :content="data.content"
  292. :path="data.filepath"
  293. />
  294. </ACollapsePanel>
  295. </ACollapse>
  296. </ACard>
  297. </ACol>
  298. </ARow>
  299. </template>
  300. <style lang="less" scoped>
  301. .col-right {
  302. position: sticky;
  303. top: 78px;
  304. :deep(.ant-card-body) {
  305. max-height: 100vh;
  306. overflow-y: scroll;
  307. }
  308. }
  309. :deep(.ant-collapse-ghost > .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box) {
  310. padding: 0;
  311. }
  312. :deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header) {
  313. padding: 0 0 10px 0;
  314. }
  315. .overwrite {
  316. margin-right: 15px;
  317. span {
  318. color: #9b9b9b;
  319. }
  320. }
  321. .node-deploy-control {
  322. display: flex;
  323. justify-content: flex-end;
  324. margin-top: 10px;
  325. align-items: center;
  326. }
  327. </style>