ConfigEditor.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  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 { ConfigHistory } from '@/components/ConfigHistory'
  10. import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
  11. import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
  12. import { useBreadcrumbs } from '@/composables/useBreadcrumbs'
  13. import { formatDateTime } from '@/lib/helper'
  14. import { useSettingsStore } from '@/pinia'
  15. import ConfigName from '@/views/config/components/ConfigName.vue'
  16. import InspectConfig from '@/views/config/InspectConfig.vue'
  17. import { HistoryOutlined, InfoCircleOutlined } from '@ant-design/icons-vue'
  18. import { message } from 'ant-design-vue'
  19. import _ from 'lodash'
  20. const settings = useSettingsStore()
  21. const route = useRoute()
  22. const router = useRouter()
  23. // eslint-disable-next-line vue/require-typed-ref
  24. const refForm = ref()
  25. const refInspectConfig = useTemplateRef('refInspectConfig')
  26. const origName = ref('')
  27. const addMode = computed(() => !route.params.name)
  28. const errors = ref({})
  29. const showHistory = ref(false)
  30. const basePath = computed(() => {
  31. if (route.query.basePath)
  32. return _.trim(route?.query?.basePath?.toString(), '/')
  33. else if (typeof route.params.name === 'object')
  34. return (route.params.name as string[]).slice(0, -1).join('/')
  35. else
  36. return ''
  37. })
  38. const data = ref({
  39. name: '',
  40. content: '',
  41. filepath: '',
  42. sync_node_ids: [] as number[],
  43. sync_overwrite: false,
  44. } as Config)
  45. const historyChatgptRecord = ref([]) as Ref<ChatComplicationMessage[]>
  46. const activeKey = ref(['basic', 'deploy', 'chatgpt'])
  47. const modifiedAt = ref('')
  48. const nginxConfigBase = ref('')
  49. const newPath = computed(() => [nginxConfigBase.value, basePath.value, data.value.name]
  50. .filter(v => v)
  51. .join('/'))
  52. const relativePath = computed(() => (route.params.name as string[]).join('/'))
  53. const breadcrumbs = useBreadcrumbs()
  54. async function init() {
  55. const { name } = route.params
  56. data.value.name = name?.[name?.length - 1] ?? ''
  57. origName.value = data.value.name
  58. if (!addMode.value) {
  59. config.get(relativePath.value).then(r => {
  60. data.value = r
  61. historyChatgptRecord.value = r.chatgpt_messages
  62. modifiedAt.value = r.modified_at
  63. const filteredPath = _.trimEnd(data.value.filepath
  64. .replaceAll(`${nginxConfigBase.value}/`, ''), data.value.name)
  65. .split('/')
  66. .filter(v => v)
  67. const path = filteredPath.map((v, k) => {
  68. let dir = v
  69. if (k > 0) {
  70. dir = filteredPath.slice(0, k).join('/')
  71. dir += `/${v}`
  72. }
  73. return {
  74. name: 'Manage Configs',
  75. translatedName: () => v,
  76. path: '/config',
  77. query: {
  78. dir,
  79. },
  80. hasChildren: false,
  81. }
  82. })
  83. breadcrumbs.value = [{
  84. name: 'Dashboard',
  85. translatedName: () => $gettext('Dashboard'),
  86. path: '/dashboard',
  87. hasChildren: false,
  88. }, {
  89. name: 'Manage Configs',
  90. translatedName: () => $gettext('Manage Configs'),
  91. path: '/config',
  92. hasChildren: false,
  93. }, ...path, {
  94. name: 'Edit Config',
  95. translatedName: () => origName.value,
  96. hasChildren: false,
  97. }]
  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. function openHistory() {
  178. showHistory.value = true
  179. }
  180. </script>
  181. <template>
  182. <ARow :gutter="16">
  183. <ACol
  184. :xs="24"
  185. :sm="24"
  186. :md="18"
  187. >
  188. <ACard :title="addMode ? $gettext('Add Configuration') : $gettext('Edit Configuration')">
  189. <template #extra>
  190. <AButton
  191. v-if="!addMode && data.filepath"
  192. type="link"
  193. @click="openHistory"
  194. >
  195. <template #icon>
  196. <HistoryOutlined />
  197. </template>
  198. {{ $gettext('History') }}
  199. </AButton>
  200. </template>
  201. <InspectConfig
  202. v-show="!addMode"
  203. ref="refInspectConfig"
  204. />
  205. <CodeEditor v-model:content="data.content" />
  206. <FooterToolBar>
  207. <ASpace>
  208. <AButton @click="goBack">
  209. {{ $gettext('Back') }}
  210. </AButton>
  211. <AButton @click="formatCode">
  212. {{ $gettext('Format Code') }}
  213. </AButton>
  214. <AButton
  215. type="primary"
  216. @click="save"
  217. >
  218. {{ $gettext('Save') }}
  219. </AButton>
  220. </ASpace>
  221. </FooterToolBar>
  222. </ACard>
  223. </ACol>
  224. <ACol
  225. :xs="24"
  226. :sm="24"
  227. :md="6"
  228. >
  229. <ACard class="col-right">
  230. <ACollapse
  231. v-model:active-key="activeKey"
  232. ghost
  233. >
  234. <ACollapsePanel
  235. key="basic"
  236. :header="$gettext('Basic')"
  237. >
  238. <AForm
  239. ref="refForm"
  240. layout="vertical"
  241. :model="data"
  242. :rules="{
  243. name: [
  244. { required: true, message: $gettext('Please input a filename') },
  245. { pattern: /^[^\\/]+$/, message: $gettext('Invalid filename') },
  246. ],
  247. }"
  248. >
  249. <AFormItem
  250. name="name"
  251. :label="$gettext('Name')"
  252. >
  253. <AInput v-if="addMode" v-model:value="data.name" />
  254. <ConfigName v-else :name="data.name" :dir="data.dir" />
  255. </AFormItem>
  256. <AFormItem
  257. v-if="!addMode"
  258. :label="$gettext('Path')"
  259. >
  260. {{ data.filepath }}
  261. </AFormItem>
  262. <AFormItem
  263. v-show="data.name !== origName"
  264. :label="addMode ? $gettext('New Path') : $gettext('Changed Path')"
  265. required
  266. >
  267. {{ newPath }}
  268. </AFormItem>
  269. <AFormItem
  270. v-if="!addMode"
  271. :label="$gettext('Updated at')"
  272. >
  273. {{ formatDateTime(modifiedAt) }}
  274. </AFormItem>
  275. </AForm>
  276. </ACollapsePanel>
  277. <ACollapsePanel
  278. v-if="!settings.is_remote"
  279. key="deploy"
  280. :header="$gettext('Deploy')"
  281. >
  282. <NodeSelector
  283. v-model:target="data.sync_node_ids"
  284. hidden-local
  285. />
  286. <div class="node-deploy-control">
  287. <div class="overwrite">
  288. <ACheckbox v-model:checked="data.sync_overwrite">
  289. {{ $gettext('Overwrite') }}
  290. </ACheckbox>
  291. <ATooltip placement="bottom">
  292. <template #title>
  293. {{ $gettext('Overwrite exist file') }}
  294. </template>
  295. <InfoCircleOutlined />
  296. </ATooltip>
  297. </div>
  298. </div>
  299. </ACollapsePanel>
  300. <ACollapsePanel
  301. key="chatgpt"
  302. header="ChatGPT"
  303. >
  304. <ChatGPT
  305. v-model:history-messages="historyChatgptRecord"
  306. :content="data.content"
  307. :path="data.filepath"
  308. />
  309. </ACollapsePanel>
  310. </ACollapse>
  311. </ACard>
  312. </ACol>
  313. <ConfigHistory
  314. v-model:visible="showHistory"
  315. v-model:current-content="data.content"
  316. :filepath="data.filepath"
  317. />
  318. </ARow>
  319. </template>
  320. <style lang="less" scoped>
  321. .col-right {
  322. position: sticky;
  323. top: 78px;
  324. :deep(.ant-card-body) {
  325. max-height: 100vh;
  326. overflow-y: scroll;
  327. }
  328. }
  329. :deep(.ant-collapse-ghost > .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box) {
  330. padding: 0;
  331. }
  332. :deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header) {
  333. padding: 0 0 10px 0;
  334. }
  335. .overwrite {
  336. margin-right: 15px;
  337. span {
  338. color: #9b9b9b;
  339. }
  340. }
  341. .node-deploy-control {
  342. display: flex;
  343. justify-content: flex-end;
  344. margin-top: 10px;
  345. align-items: center;
  346. }
  347. </style>