mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2026-05-06 22:12:23 +08:00
refactor: refresh editor ui
This commit is contained in:
9
.cursor/mcp.json
Normal file
9
.cursor/mcp.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"eslint": {
|
||||
"command": "npx",
|
||||
"args": ["@eslint/mcp@latest"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
app/components.d.ts
vendored
2
app/components.d.ts
vendored
@@ -68,6 +68,7 @@ declare module 'vue' {
|
||||
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
|
||||
AutoCertFormAutoCertForm: typeof import('./src/components/AutoCertForm/AutoCertForm.vue')['default']
|
||||
AutoCertFormDNSChallenge: typeof import('./src/components/AutoCertForm/DNSChallenge.vue')['default']
|
||||
BaseEditorBaseEditor: typeof import('./src/components/BaseEditor/BaseEditor.vue')['default']
|
||||
BreadcrumbBreadcrumb: typeof import('./src/components/Breadcrumb/Breadcrumb.vue')['default']
|
||||
CertInfoCertInfo: typeof import('./src/components/CertInfo/CertInfo.vue')['default']
|
||||
ChartAreaChart: typeof import('./src/components/Chart/AreaChart.vue')['default']
|
||||
@@ -78,6 +79,7 @@ declare module 'vue' {
|
||||
ChatGPTChatMessageInput: typeof import('./src/components/ChatGPT/ChatMessageInput.vue')['default']
|
||||
ChatGPTChatMessageList: typeof import('./src/components/ChatGPT/ChatMessageList.vue')['default']
|
||||
CodeEditorCodeEditor: typeof import('./src/components/CodeEditor/CodeEditor.vue')['default']
|
||||
CommonEditorCommonEditor: typeof import('./src/components/CommonEditor/CommonEditor.vue')['default']
|
||||
ConfigHistoryConfigHistory: typeof import('./src/components/ConfigHistory/ConfigHistory.vue')['default']
|
||||
ConfigHistoryDiffViewer: typeof import('./src/components/ConfigHistory/DiffViewer.vue')['default']
|
||||
EnvGroupTabsEnvGroupTabs: typeof import('./src/components/EnvGroupTabs/EnvGroupTabs.vue')['default']
|
||||
|
||||
87
app/src/components/BaseEditor/BaseEditor.vue
Normal file
87
app/src/components/BaseEditor/BaseEditor.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { LoadingOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
// Generic editor layout with left and right panels
|
||||
interface BaseEditorProps {
|
||||
colRightClass?: string
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<BaseEditorProps>(), {
|
||||
colRightClass: 'col-right',
|
||||
})
|
||||
|
||||
const indicator = h(LoadingOutlined, {
|
||||
style: {
|
||||
fontSize: '32px',
|
||||
},
|
||||
spin: true,
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const loading = computed(() =>
|
||||
props.loading || (import.meta.env.DEV && route.query.loading === 'true'),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ASpin class="h-full base-editor-spin" :spinning="loading" :indicator="indicator">
|
||||
<ARow :gutter="{ xs: 0, sm: 16 }">
|
||||
<ACol
|
||||
:xs="24"
|
||||
:sm="24"
|
||||
:md="24"
|
||||
:lg="16"
|
||||
:xl="17"
|
||||
>
|
||||
<!-- Left panel content (main editor) -->
|
||||
<slot name="left" />
|
||||
</ACol>
|
||||
|
||||
<ACol
|
||||
:class="props.colRightClass"
|
||||
:xs="24"
|
||||
:sm="24"
|
||||
:md="24"
|
||||
:lg="8"
|
||||
:xl="7"
|
||||
>
|
||||
<!-- Right panel content (settings/configuration) -->
|
||||
<slot name="right" />
|
||||
</ACol>
|
||||
</ARow>
|
||||
</ASpin>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.col-right {
|
||||
position: sticky;
|
||||
top: 78px;
|
||||
}
|
||||
|
||||
:deep(.ant-card) {
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
:deep(.card-body) {
|
||||
max-height: calc(100vh - 260px);
|
||||
overflow-y: scroll;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.ant-spin) {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
max-height: 100% !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.dark {
|
||||
.base-editor-spin {
|
||||
background: rgba(30, 30, 30, 0.8);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
4
app/src/components/BaseEditor/index.ts
Normal file
4
app/src/components/BaseEditor/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import BaseEditor from './BaseEditor.vue'
|
||||
|
||||
export { BaseEditor }
|
||||
export default BaseEditor
|
||||
@@ -44,5 +44,7 @@ const isValid = computed(() => dayjs().isAfter(props.cert?.not_before) && dayjs(
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 12px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import Icon from '@ant-design/icons-vue'
|
||||
import { useElementVisibility } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import ChatGPT_logo from '@/assets/svg/ChatGPT_logo.svg?component'
|
||||
import { useSettingsStore } from '@/pinia'
|
||||
@@ -16,7 +17,7 @@ const { language: current } = storeToRefs(useSettingsStore())
|
||||
|
||||
// Use ChatGPT store
|
||||
const chatGPTStore = useChatGPTStore()
|
||||
const { messageListRef, loading, shouldShowStartButton } = storeToRefs(chatGPTStore)
|
||||
const { messageContainerRef, loading, shouldShowStartButton } = storeToRefs(chatGPTStore)
|
||||
|
||||
// Initialize messages when path changes
|
||||
watch(() => props.path, async () => {
|
||||
@@ -28,6 +29,14 @@ watch(() => props.path, async () => {
|
||||
async function handleSend() {
|
||||
await chatGPTStore.send(props.content, current.value)
|
||||
}
|
||||
|
||||
const isVisible = useElementVisibility(messageContainerRef)
|
||||
|
||||
watch(isVisible, visible => {
|
||||
if (visible) {
|
||||
chatGPTStore.scrollToBottom()
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -49,17 +58,20 @@ async function handleSend() {
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="chatgpt-container"
|
||||
ref="messageContainerRef"
|
||||
class="message-container"
|
||||
>
|
||||
<ChatMessageList ref="messageListRef" />
|
||||
<ChatMessageList />
|
||||
|
||||
<ChatMessageInput />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.chatgpt-container {
|
||||
.message-container {
|
||||
margin: 0 auto;
|
||||
max-width: 800px;
|
||||
max-height: calc(100vh - 260px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -59,7 +59,7 @@ const messagesLength = computed(() => messages.value?.length ?? 0)
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
padding: 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 0 0 8px 8px;
|
||||
|
||||
.control-btn {
|
||||
display: flex;
|
||||
@@ -76,7 +76,6 @@ const messagesLength = computed(() => messages.value?.length ?? 0)
|
||||
.dark {
|
||||
.input-msg {
|
||||
background: rgba(30, 30, 30, 0.8);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,57 +6,6 @@ import ChatMessage from './ChatMessage.vue'
|
||||
const chatGPTStore = useChatGPTStore()
|
||||
const { messages, editingIdx, editValue, loading } = storeToRefs(chatGPTStore)
|
||||
|
||||
const messageListRef = useTemplateRef('messageList')
|
||||
let scrollTimeoutId: number | null = null
|
||||
|
||||
function scrollToBottom() {
|
||||
// Debounce scroll operations for better performance
|
||||
if (scrollTimeoutId) {
|
||||
clearTimeout(scrollTimeoutId)
|
||||
}
|
||||
|
||||
scrollTimeoutId = window.setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (messageListRef.value) {
|
||||
let element = messageListRef.value.parentElement
|
||||
while (element) {
|
||||
const style = window.getComputedStyle(element)
|
||||
if (style.overflowY === 'auto' || style.overflowY === 'scroll') {
|
||||
element.scrollTo({
|
||||
top: element.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
return
|
||||
}
|
||||
element = element.parentElement
|
||||
}
|
||||
}
|
||||
})
|
||||
}, 50) // 50ms debounce
|
||||
}
|
||||
|
||||
// Watch for messages changes and auto scroll - with debouncing
|
||||
watch(() => messages.value, () => {
|
||||
scrollToBottom()
|
||||
}, { deep: true, flush: 'post' })
|
||||
|
||||
// Auto scroll when messages are loaded
|
||||
onMounted(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
// Clean up on unmount
|
||||
onUnmounted(() => {
|
||||
if (scrollTimeoutId) {
|
||||
clearTimeout(scrollTimeoutId)
|
||||
}
|
||||
})
|
||||
|
||||
// Expose scroll function for parent component
|
||||
defineExpose({
|
||||
scrollToBottom,
|
||||
})
|
||||
|
||||
function handleEdit(index: number) {
|
||||
chatGPTStore.startEdit(index)
|
||||
}
|
||||
@@ -77,7 +26,7 @@ async function handleRegenerate(index: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="messageList" class="message-list-container">
|
||||
<div class="message-list-container">
|
||||
<AList
|
||||
class="chatgpt-log"
|
||||
item-layout="horizontal"
|
||||
@@ -102,6 +51,9 @@ async function handleRegenerate(index: number) {
|
||||
|
||||
<style lang="less" scoped>
|
||||
.message-list-container {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
|
||||
.chatgpt-log {
|
||||
:deep(.ant-list-item) {
|
||||
padding: 0 12px;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { ChatMessageList } from '.'
|
||||
import type { ChatComplicationMessage } from '@/api/openai'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
@@ -9,7 +8,7 @@ export const useChatGPTStore = defineStore('chatgpt', () => {
|
||||
// State
|
||||
const path = ref<string>('') // Path to the chat record file
|
||||
const messages = ref<ChatComplicationMessage[]>([])
|
||||
const messageListRef = ref<InstanceType<typeof ChatMessageList>>()
|
||||
const messageContainerRef = ref<HTMLDivElement>()
|
||||
const loading = ref(false)
|
||||
const editingIdx = ref(-1)
|
||||
const editValue = ref('')
|
||||
@@ -154,7 +153,10 @@ export const useChatGPTStore = defineStore('chatgpt', () => {
|
||||
|
||||
// scroll to bottom
|
||||
function scrollToBottom() {
|
||||
messageListRef.value?.scrollToBottom()
|
||||
messageContainerRef.value?.scrollTo({
|
||||
top: messageContainerRef.value.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
|
||||
// Set streaming message index
|
||||
@@ -234,6 +236,9 @@ export const useChatGPTStore = defineStore('chatgpt', () => {
|
||||
await request()
|
||||
}
|
||||
|
||||
watch(messages, () => {
|
||||
scrollToBottom()
|
||||
}, { immediate: true })
|
||||
// Return all state, getters, and actions
|
||||
return {
|
||||
// State
|
||||
@@ -242,7 +247,7 @@ export const useChatGPTStore = defineStore('chatgpt', () => {
|
||||
editingIdx,
|
||||
editValue,
|
||||
askBuffer,
|
||||
messageListRef,
|
||||
messageContainerRef,
|
||||
streamingMessageIndex,
|
||||
|
||||
// Getters
|
||||
@@ -271,5 +276,6 @@ export const useChatGPTStore = defineStore('chatgpt', () => {
|
||||
request,
|
||||
send,
|
||||
regenerate,
|
||||
scrollToBottom,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -45,24 +45,19 @@ const activeKey = ref(['3'])
|
||||
>
|
||||
<NgxUpstream />
|
||||
</ACollapsePanel>
|
||||
<ACollapsePanel
|
||||
key="3"
|
||||
header="Server"
|
||||
>
|
||||
<NgxServer :context>
|
||||
<template
|
||||
v-for="(_, key) in $slots"
|
||||
:key="key"
|
||||
#[key]="slotProps"
|
||||
>
|
||||
<slot
|
||||
:name="key"
|
||||
v-bind="slotProps"
|
||||
/>
|
||||
</template>
|
||||
</NgxServer>
|
||||
</ACollapsePanel>
|
||||
</ACollapse>
|
||||
<NgxServer :context>
|
||||
<template
|
||||
v-for="(_, key) in $slots"
|
||||
:key="key"
|
||||
#[key]="slotProps"
|
||||
>
|
||||
<slot
|
||||
:name="key"
|
||||
v-bind="slotProps"
|
||||
/>
|
||||
</template>
|
||||
</NgxServer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -22,6 +22,10 @@ const serversLength = computed(() => {
|
||||
return ngxConfig.value.servers?.length ?? 0
|
||||
})
|
||||
|
||||
const hasServers = computed(() => {
|
||||
return serversLength.value > 0
|
||||
})
|
||||
|
||||
watch(serversLength, () => {
|
||||
if (curServerIdx.value >= serversLength.value)
|
||||
curServerIdx.value = serversLength.value - 1
|
||||
@@ -38,6 +42,9 @@ watch(curServerIdx, () => {
|
||||
})
|
||||
|
||||
function addServer() {
|
||||
if (!ngxConfig.value.servers)
|
||||
ngxConfig.value.servers = []
|
||||
|
||||
ngxConfig.value.servers.push({
|
||||
comments: '',
|
||||
locations: [],
|
||||
@@ -63,7 +70,32 @@ function removeServer(index: number) {
|
||||
<template>
|
||||
<div>
|
||||
<ContextHolder />
|
||||
<ATabs v-model:active-key="curServerIdx">
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="!hasServers" class="empty-state">
|
||||
<AEmpty
|
||||
:description="$gettext('No servers configured')"
|
||||
class="mb-6"
|
||||
>
|
||||
<template #image>
|
||||
<div class="text-6xl mb-4 text-gray-300">
|
||||
🖥️
|
||||
</div>
|
||||
</template>
|
||||
</AEmpty>
|
||||
<div class="text-center">
|
||||
<AButton
|
||||
type="primary"
|
||||
@click="addServer"
|
||||
>
|
||||
<PlusOutlined />
|
||||
{{ $gettext('Add Server') }}
|
||||
</AButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server Tabs -->
|
||||
<ATabs v-else v-model:active-key="curServerIdx">
|
||||
<ATabPane
|
||||
v-for="(v, k) in ngxConfig.servers"
|
||||
:key="k"
|
||||
@@ -117,5 +149,12 @@ function removeServer(index: number) {
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
.empty-state {
|
||||
@apply px-8 text-center;
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -167,14 +167,24 @@ function getAvailabilityResult(directive: NgxDirective) {
|
||||
</AButton>
|
||||
</template>
|
||||
</ATabs>
|
||||
<div v-else>
|
||||
<AEmpty />
|
||||
<div class="flex justify-center">
|
||||
<div v-else class="empty-state">
|
||||
<AEmpty
|
||||
:description="$gettext('No upstreams configured')"
|
||||
class="mb-6"
|
||||
>
|
||||
<template #image>
|
||||
<div class="text-6xl mb-4 text-gray-300">
|
||||
⚖️
|
||||
</div>
|
||||
</template>
|
||||
</AEmpty>
|
||||
<div class="text-center">
|
||||
<AButton
|
||||
type="primary"
|
||||
@click="addUpstream"
|
||||
>
|
||||
{{ $gettext('Create') }}
|
||||
<PlusOutlined />
|
||||
{{ $gettext('Add Upstream') }}
|
||||
</AButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,5 +205,12 @@ function getAvailabilityResult(directive: NgxDirective) {
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
.empty-state {
|
||||
@apply px-8 text-center;
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":"2.1.0","build_id":1,"total_build":427}
|
||||
{"version":"2.1.0","build_id":2,"total_build":428}
|
||||
@@ -1,380 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import type { Config } from '@/api/config'
|
||||
import { HistoryOutlined, InfoCircleOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { trim, trimEnd } from 'lodash'
|
||||
import config from '@/api/config'
|
||||
import ngx from '@/api/ngx'
|
||||
import ChatGPT from '@/components/ChatGPT'
|
||||
import CodeEditor from '@/components/CodeEditor'
|
||||
import { ConfigHistory } from '@/components/ConfigHistory'
|
||||
import FooterToolBar from '@/components/FooterToolbar'
|
||||
import NodeSelector from '@/components/NodeSelector'
|
||||
import { useBreadcrumbs } from '@/composables/useBreadcrumbs'
|
||||
import { formatDateTime } from '@/lib/helper'
|
||||
import { useSettingsStore } from '@/pinia'
|
||||
import ConfigName from '@/views/config/components/ConfigName.vue'
|
||||
import InspectConfig from '@/views/config/InspectConfig.vue'
|
||||
import BaseEditor from '@/components/BaseEditor'
|
||||
import ConfigLeftPanel from '@/views/config/components/ConfigLeftPanel.vue'
|
||||
import ConfigRightPanel from '@/views/config/components/ConfigRightPanel.vue'
|
||||
|
||||
const settings = useSettingsStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// eslint-disable-next-line vue/require-typed-ref
|
||||
const refForm = ref()
|
||||
const origName = ref('')
|
||||
const addMode = computed(() => !route.params.name)
|
||||
|
||||
const showHistory = ref(false)
|
||||
const basePath = computed(() => {
|
||||
if (route.query.basePath)
|
||||
return trim(route?.query?.basePath?.toString(), '/')
|
||||
else if (typeof route.params.name === 'object')
|
||||
return (route.params.name as string[]).slice(0, -1).join('/')
|
||||
else
|
||||
return ''
|
||||
})
|
||||
|
||||
const data = ref({
|
||||
name: '',
|
||||
content: '',
|
||||
filepath: '',
|
||||
sync_node_ids: [] as number[],
|
||||
sync_overwrite: false,
|
||||
} as Config)
|
||||
|
||||
const activeKey = ref(['basic', 'deploy', 'chatgpt'])
|
||||
const modifiedAt = ref('')
|
||||
const nginxConfigBase = ref('')
|
||||
|
||||
const newPath = computed(() => {
|
||||
// Decode and display after combining paths
|
||||
const path = [nginxConfigBase.value, basePath.value, data.value.name]
|
||||
.filter(v => v)
|
||||
.join('/')
|
||||
return path
|
||||
})
|
||||
|
||||
const relativePath = computed(() => (basePath.value ? `${basePath.value}/${route.params.name}` : route.params.name) as string)
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
// Use Vue 3.4+ useTemplateRef for InspectConfig component
|
||||
const inspectConfigRef = useTemplateRef<InstanceType<typeof InspectConfig>>('inspectConfig')
|
||||
|
||||
async function init() {
|
||||
const { name } = route.params
|
||||
|
||||
data.value.name = name?.[name?.length - 1] ?? ''
|
||||
origName.value = data.value.name
|
||||
if (!addMode.value) {
|
||||
config.getItem(relativePath.value).then(r => {
|
||||
data.value = r
|
||||
modifiedAt.value = r.modified_at
|
||||
|
||||
const filteredPath = trimEnd(data.value.filepath
|
||||
.replaceAll(`${nginxConfigBase.value}/`, ''), data.value.name)
|
||||
.split('/')
|
||||
.filter(v => v)
|
||||
|
||||
// Build accumulated path to maintain original encoding state
|
||||
let accumulatedPath = ''
|
||||
const path = filteredPath.map((segment, index) => {
|
||||
// Decode for display
|
||||
const decodedSegment = decodeURIComponent(segment)
|
||||
|
||||
// Accumulated path keeps original encoding state
|
||||
if (index === 0) {
|
||||
accumulatedPath = segment
|
||||
}
|
||||
else {
|
||||
accumulatedPath = `${accumulatedPath}/${segment}`
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'Manage Configs',
|
||||
translatedName: () => decodedSegment,
|
||||
path: '/config',
|
||||
query: {
|
||||
dir: accumulatedPath,
|
||||
},
|
||||
hasChildren: false,
|
||||
}
|
||||
})
|
||||
|
||||
breadcrumbs.value = [{
|
||||
name: 'Dashboard',
|
||||
translatedName: () => $gettext('Dashboard'),
|
||||
path: '/dashboard',
|
||||
hasChildren: false,
|
||||
}, {
|
||||
name: 'Manage Configs',
|
||||
translatedName: () => $gettext('Manage Configs'),
|
||||
path: '/config',
|
||||
hasChildren: false,
|
||||
}, ...path, {
|
||||
name: 'Edit Config',
|
||||
translatedName: () => origName.value,
|
||||
hasChildren: false,
|
||||
}]
|
||||
})
|
||||
}
|
||||
else {
|
||||
data.value.content = ''
|
||||
data.value.filepath = ''
|
||||
|
||||
const pathSegments = basePath.value
|
||||
.split('/')
|
||||
.filter(v => v)
|
||||
|
||||
// Build accumulated path
|
||||
let accumulatedPath = ''
|
||||
const path = pathSegments.map((segment, index) => {
|
||||
// Decode for display
|
||||
const decodedSegment = decodeURIComponent(segment)
|
||||
|
||||
// Accumulated path keeps original encoding state
|
||||
if (index === 0) {
|
||||
accumulatedPath = segment
|
||||
}
|
||||
else {
|
||||
accumulatedPath = `${accumulatedPath}/${segment}`
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'Manage Configs',
|
||||
translatedName: () => decodedSegment,
|
||||
path: '/config',
|
||||
query: {
|
||||
dir: accumulatedPath,
|
||||
},
|
||||
hasChildren: false,
|
||||
}
|
||||
})
|
||||
|
||||
breadcrumbs.value = [{
|
||||
name: 'Dashboard',
|
||||
translatedName: () => $gettext('Dashboard'),
|
||||
path: '/dashboard',
|
||||
hasChildren: false,
|
||||
}, {
|
||||
name: 'Manage Configs',
|
||||
translatedName: () => $gettext('Manage Configs'),
|
||||
path: '/config',
|
||||
hasChildren: false,
|
||||
}, ...path, {
|
||||
name: 'Add Config',
|
||||
translatedName: () => $gettext('Add Configuration'),
|
||||
hasChildren: false,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await config.get_base_path().then(r => {
|
||||
nginxConfigBase.value = r.base_path
|
||||
})
|
||||
await init()
|
||||
})
|
||||
|
||||
function save() {
|
||||
refForm.value?.validate().then(() => {
|
||||
const payload = {
|
||||
name: addMode.value ? data.value.name : undefined,
|
||||
base_dir: addMode.value ? basePath.value : undefined,
|
||||
content: data.value.content,
|
||||
sync_node_ids: data.value.sync_node_ids,
|
||||
sync_overwrite: data.value.sync_overwrite,
|
||||
}
|
||||
|
||||
const api = addMode.value
|
||||
? config.createItem(payload)
|
||||
: config.updateItem(relativePath.value, payload)
|
||||
|
||||
api.then(r => {
|
||||
data.value.content = r.content
|
||||
message.success($gettext('Saved successfully'))
|
||||
|
||||
if (addMode.value) {
|
||||
router.push({
|
||||
path: `/config/${data.value.name}/edit`,
|
||||
query: {
|
||||
basePath: basePath.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
else {
|
||||
data.value = r
|
||||
// Run test after saving to verify configuration
|
||||
inspectConfigRef.value?.test()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function formatCode() {
|
||||
ngx.format_code(data.value.content).then(r => {
|
||||
data.value.content = r.content
|
||||
message.success($gettext('Format successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
// Keep original path with encoding state
|
||||
const encodedPath = basePath.value || ''
|
||||
|
||||
router.push({
|
||||
path: '/config',
|
||||
query: {
|
||||
dir: encodedPath || undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function openHistory() {
|
||||
showHistory.value = true
|
||||
}
|
||||
// Use Vue 3.4+ useTemplateRef to get reference to left panel
|
||||
const leftPanelRef = useTemplateRef<InstanceType<typeof ConfigLeftPanel>>('leftPanel')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ARow :gutter="16">
|
||||
<ACol
|
||||
:xs="24"
|
||||
:sm="24"
|
||||
:md="18"
|
||||
>
|
||||
<ACard :title="addMode ? $gettext('Add Configuration') : $gettext('Edit Configuration')">
|
||||
<template #extra>
|
||||
<AButton
|
||||
v-if="!addMode && data.filepath"
|
||||
type="link"
|
||||
@click="openHistory"
|
||||
>
|
||||
<template #icon>
|
||||
<HistoryOutlined />
|
||||
</template>
|
||||
{{ $gettext('History') }}
|
||||
</AButton>
|
||||
</template>
|
||||
<BaseEditor :loading="leftPanelRef?.loading">
|
||||
<template #left>
|
||||
<ConfigLeftPanel ref="leftPanel" />
|
||||
</template>
|
||||
|
||||
<InspectConfig
|
||||
v-show="!addMode"
|
||||
ref="inspectConfig"
|
||||
/>
|
||||
<CodeEditor v-model:content="data.content" />
|
||||
<FooterToolBar>
|
||||
<ASpace>
|
||||
<AButton @click="goBack">
|
||||
{{ $gettext('Back') }}
|
||||
</AButton>
|
||||
<AButton @click="formatCode">
|
||||
{{ $gettext('Format Code') }}
|
||||
</AButton>
|
||||
<AButton
|
||||
type="primary"
|
||||
@click="save"
|
||||
>
|
||||
{{ $gettext('Save') }}
|
||||
</AButton>
|
||||
</ASpace>
|
||||
</FooterToolBar>
|
||||
</ACard>
|
||||
</ACol>
|
||||
|
||||
<ACol
|
||||
:xs="24"
|
||||
:sm="24"
|
||||
:md="6"
|
||||
>
|
||||
<ACard class="col-right">
|
||||
<ACollapse
|
||||
v-model:active-key="activeKey"
|
||||
ghost
|
||||
>
|
||||
<ACollapsePanel
|
||||
key="basic"
|
||||
:header="$gettext('Basic')"
|
||||
>
|
||||
<AForm
|
||||
ref="refForm"
|
||||
layout="vertical"
|
||||
:model="data"
|
||||
:rules="{
|
||||
name: [
|
||||
{ required: true, message: $gettext('Please input a filename') },
|
||||
{ pattern: /^[^\\/]+$/, message: $gettext('Invalid filename') },
|
||||
],
|
||||
}"
|
||||
>
|
||||
<AFormItem
|
||||
name="name"
|
||||
:label="$gettext('Name')"
|
||||
>
|
||||
<AInput v-if="addMode" v-model:value="data.name" />
|
||||
<ConfigName v-else :name="data.name" :dir="data.dir" />
|
||||
</AFormItem>
|
||||
<AFormItem
|
||||
v-if="!addMode"
|
||||
:label="$gettext('Path')"
|
||||
>
|
||||
{{ decodeURIComponent(data.filepath) }}
|
||||
</AFormItem>
|
||||
<AFormItem
|
||||
v-show="data.name !== origName"
|
||||
:label="addMode ? $gettext('New Path') : $gettext('Changed Path')"
|
||||
required
|
||||
>
|
||||
{{ decodeURIComponent(newPath) }}
|
||||
</AFormItem>
|
||||
<AFormItem
|
||||
v-if="!addMode"
|
||||
:label="$gettext('Updated at')"
|
||||
>
|
||||
{{ formatDateTime(modifiedAt) }}
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
</ACollapsePanel>
|
||||
<ACollapsePanel
|
||||
v-if="!settings.is_remote"
|
||||
key="deploy"
|
||||
:header="$gettext('Deploy')"
|
||||
>
|
||||
<NodeSelector
|
||||
v-model:target="data.sync_node_ids"
|
||||
hidden-local
|
||||
/>
|
||||
<div class="node-deploy-control">
|
||||
<div class="overwrite">
|
||||
<ACheckbox v-model:checked="data.sync_overwrite">
|
||||
{{ $gettext('Overwrite') }}
|
||||
</ACheckbox>
|
||||
<ATooltip placement="bottom">
|
||||
<template #title>
|
||||
{{ $gettext('Overwrite exist file') }}
|
||||
</template>
|
||||
<InfoCircleOutlined />
|
||||
</ATooltip>
|
||||
</div>
|
||||
</div>
|
||||
</ACollapsePanel>
|
||||
<ACollapsePanel
|
||||
key="chatgpt"
|
||||
header="ChatGPT"
|
||||
>
|
||||
<ChatGPT
|
||||
:content="data.content"
|
||||
:path="data.filepath"
|
||||
/>
|
||||
</ACollapsePanel>
|
||||
</ACollapse>
|
||||
</ACard>
|
||||
</ACol>
|
||||
|
||||
<ConfigHistory
|
||||
v-model:visible="showHistory"
|
||||
v-model:current-content="data.content"
|
||||
:filepath="data.filepath"
|
||||
/>
|
||||
</ARow>
|
||||
<template #right>
|
||||
<ConfigRightPanel
|
||||
v-if="leftPanelRef"
|
||||
v-model:data="leftPanelRef.data"
|
||||
:add-mode="leftPanelRef.addMode || false"
|
||||
:new-path="leftPanelRef.newPath || ''"
|
||||
:modified-at="leftPanelRef.modifiedAt || ''"
|
||||
:orig-name="leftPanelRef.origName || ''"
|
||||
/>
|
||||
</template>
|
||||
</BaseEditor>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@@ -410,4 +59,8 @@ function openHistory() {
|
||||
margin-top: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
import ngx from '@/api/ngx'
|
||||
import { logLevel } from '@/views/config/constants'
|
||||
|
||||
defineProps<{
|
||||
banner?: boolean
|
||||
}>()
|
||||
|
||||
const data = ref({
|
||||
level: 0,
|
||||
message: '',
|
||||
@@ -24,6 +28,7 @@ defineExpose({
|
||||
<div class="inspect-container">
|
||||
<AAlert
|
||||
v-if="data?.level <= logLevel.Info"
|
||||
:banner
|
||||
:message="$gettext('Configuration file is test successful')"
|
||||
type="success"
|
||||
show-icon
|
||||
|
||||
306
app/src/views/config/components/ConfigLeftPanel.vue
Normal file
306
app/src/views/config/components/ConfigLeftPanel.vue
Normal file
@@ -0,0 +1,306 @@
|
||||
<script setup lang="ts">
|
||||
import type { Config } from '@/api/config'
|
||||
import { HistoryOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { trim, trimEnd } from 'lodash'
|
||||
import config from '@/api/config'
|
||||
import ngx from '@/api/ngx'
|
||||
import CodeEditor from '@/components/CodeEditor'
|
||||
import { ConfigHistory } from '@/components/ConfigHistory'
|
||||
import FooterToolbar from '@/components/FooterToolbar'
|
||||
import { useBreadcrumbs } from '@/composables/useBreadcrumbs'
|
||||
import InspectConfig from '@/views/config/InspectConfig.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// eslint-disable-next-line vue/require-typed-ref
|
||||
const refForm = ref()
|
||||
const origName = ref('')
|
||||
const addMode = computed(() => !route.params.name)
|
||||
const showHistory = ref(false)
|
||||
|
||||
const basePath = computed(() => {
|
||||
if (route.query.basePath)
|
||||
return trim(route?.query?.basePath?.toString(), '/')
|
||||
else if (typeof route.params.name === 'object')
|
||||
return (route.params.name as string[]).slice(0, -1).join('/')
|
||||
else
|
||||
return ''
|
||||
})
|
||||
|
||||
const data = ref({
|
||||
name: '',
|
||||
content: '',
|
||||
filepath: '',
|
||||
sync_node_ids: [] as number[],
|
||||
sync_overwrite: false,
|
||||
} as Config)
|
||||
|
||||
const modifiedAt = ref('')
|
||||
const nginxConfigBase = ref('')
|
||||
const loading = ref(true)
|
||||
|
||||
const newPath = computed(() => {
|
||||
// Decode and display after combining paths
|
||||
const path = [nginxConfigBase.value, basePath.value, data.value.name]
|
||||
.filter(v => v)
|
||||
.join('/')
|
||||
return path
|
||||
})
|
||||
|
||||
const relativePath = computed(() => (basePath.value ? `${basePath.value}/${route.params.name}` : route.params.name) as string)
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
// Use Vue 3.4+ useTemplateRef for InspectConfig component
|
||||
const inspectConfigRef = useTemplateRef<InstanceType<typeof InspectConfig>>('inspectConfig')
|
||||
|
||||
// Expose data for right panel
|
||||
defineExpose({
|
||||
data,
|
||||
refForm,
|
||||
addMode,
|
||||
newPath,
|
||||
modifiedAt,
|
||||
origName,
|
||||
loading,
|
||||
})
|
||||
|
||||
async function init() {
|
||||
const { name } = route.params
|
||||
|
||||
data.value.name = name?.[name?.length - 1] ?? ''
|
||||
origName.value = data.value.name
|
||||
|
||||
if (!addMode.value) {
|
||||
config.getItem(relativePath.value).then(r => {
|
||||
data.value = r
|
||||
modifiedAt.value = r.modified_at
|
||||
|
||||
const filteredPath = trimEnd(data.value.filepath
|
||||
.replaceAll(`${nginxConfigBase.value}/`, ''), data.value.name)
|
||||
.split('/')
|
||||
.filter(v => v)
|
||||
|
||||
// Build accumulated path to maintain original encoding state
|
||||
let accumulatedPath = ''
|
||||
const path = filteredPath.map((segment, index) => {
|
||||
// Decode for display
|
||||
const decodedSegment = decodeURIComponent(segment)
|
||||
|
||||
// Accumulated path keeps original encoding state
|
||||
if (index === 0) {
|
||||
accumulatedPath = segment
|
||||
}
|
||||
else {
|
||||
accumulatedPath = `${accumulatedPath}/${segment}`
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'Manage Configs',
|
||||
translatedName: () => decodedSegment,
|
||||
path: '/config',
|
||||
query: {
|
||||
dir: accumulatedPath,
|
||||
},
|
||||
hasChildren: false,
|
||||
}
|
||||
})
|
||||
|
||||
breadcrumbs.value = [{
|
||||
name: 'Dashboard',
|
||||
translatedName: () => $gettext('Dashboard'),
|
||||
path: '/dashboard',
|
||||
hasChildren: false,
|
||||
}, {
|
||||
name: 'Manage Configs',
|
||||
translatedName: () => $gettext('Manage Configs'),
|
||||
path: '/config',
|
||||
hasChildren: false,
|
||||
}, ...path, {
|
||||
name: 'Edit Config',
|
||||
translatedName: () => origName.value,
|
||||
hasChildren: false,
|
||||
}]
|
||||
})
|
||||
}
|
||||
else {
|
||||
data.value.content = ''
|
||||
data.value.filepath = ''
|
||||
|
||||
const pathSegments = basePath.value
|
||||
.split('/')
|
||||
.filter(v => v)
|
||||
|
||||
// Build accumulated path
|
||||
let accumulatedPath = ''
|
||||
const path = pathSegments.map((segment, index) => {
|
||||
// Decode for display
|
||||
const decodedSegment = decodeURIComponent(segment)
|
||||
|
||||
// Accumulated path keeps original encoding state
|
||||
if (index === 0) {
|
||||
accumulatedPath = segment
|
||||
}
|
||||
else {
|
||||
accumulatedPath = `${accumulatedPath}/${segment}`
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'Manage Configs',
|
||||
translatedName: () => decodedSegment,
|
||||
path: '/config',
|
||||
query: {
|
||||
dir: accumulatedPath,
|
||||
},
|
||||
hasChildren: false,
|
||||
}
|
||||
})
|
||||
|
||||
breadcrumbs.value = [{
|
||||
name: 'Dashboard',
|
||||
translatedName: () => $gettext('Dashboard'),
|
||||
path: '/dashboard',
|
||||
hasChildren: false,
|
||||
}, {
|
||||
name: 'Manage Configs',
|
||||
translatedName: () => $gettext('Manage Configs'),
|
||||
path: '/config',
|
||||
hasChildren: false,
|
||||
}, ...path, {
|
||||
name: 'Add Config',
|
||||
translatedName: () => $gettext('Add Configuration'),
|
||||
hasChildren: false,
|
||||
}]
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await config.get_base_path().then(r => {
|
||||
nginxConfigBase.value = r.base_path
|
||||
})
|
||||
await init()
|
||||
})
|
||||
|
||||
function save() {
|
||||
refForm.value?.validate().then(() => {
|
||||
const payload = {
|
||||
name: addMode.value ? data.value.name : undefined,
|
||||
base_dir: addMode.value ? basePath.value : undefined,
|
||||
content: data.value.content,
|
||||
sync_node_ids: data.value.sync_node_ids,
|
||||
sync_overwrite: data.value.sync_overwrite,
|
||||
}
|
||||
|
||||
const api = addMode.value
|
||||
? config.createItem(payload)
|
||||
: config.updateItem(relativePath.value, payload)
|
||||
|
||||
api.then(r => {
|
||||
data.value.content = r.content
|
||||
message.success($gettext('Saved successfully'))
|
||||
|
||||
if (addMode.value) {
|
||||
router.push({
|
||||
path: `/config/${data.value.name}/edit`,
|
||||
query: {
|
||||
basePath: basePath.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
else {
|
||||
data.value = r
|
||||
// Run test after saving to verify configuration
|
||||
inspectConfigRef.value?.test()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function formatCode() {
|
||||
ngx.format_code(data.value.content).then(r => {
|
||||
data.value.content = r.content
|
||||
message.success($gettext('Format successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
// Keep orignal path with encoding state
|
||||
const encodedPath = basePath.value || ''
|
||||
|
||||
router.push({
|
||||
path: '/config',
|
||||
query: {
|
||||
dir: encodedPath || undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function openHistory() {
|
||||
showHistory.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard
|
||||
:title="addMode ? $gettext('Add Configuration') : $gettext('Edit Configuration')"
|
||||
:bordered="false" :loading
|
||||
>
|
||||
<template #extra>
|
||||
<AButton
|
||||
v-if="!addMode && data.filepath"
|
||||
type="link"
|
||||
@click="openHistory"
|
||||
>
|
||||
<template #icon>
|
||||
<HistoryOutlined />
|
||||
</template>
|
||||
{{ $gettext('History') }}
|
||||
</AButton>
|
||||
</template>
|
||||
|
||||
<InspectConfig
|
||||
v-show="!addMode"
|
||||
ref="inspectConfig"
|
||||
class="mb-0!"
|
||||
banner
|
||||
/>
|
||||
|
||||
<CodeEditor
|
||||
v-model:content="data.content"
|
||||
no-border-radius
|
||||
/>
|
||||
|
||||
<FooterToolbar>
|
||||
<ASpace>
|
||||
<AButton @click="goBack">
|
||||
{{ $gettext('Back') }}
|
||||
</AButton>
|
||||
<AButton @click="formatCode">
|
||||
{{ $gettext('Format Code') }}
|
||||
</AButton>
|
||||
<AButton
|
||||
type="primary"
|
||||
@click="save"
|
||||
>
|
||||
{{ $gettext('Save') }}
|
||||
</AButton>
|
||||
</ASpace>
|
||||
</FooterToolbar>
|
||||
|
||||
<ConfigHistory
|
||||
v-model:visible="showHistory"
|
||||
v-model:current-content="data.content"
|
||||
:filepath="data.filepath"
|
||||
/>
|
||||
</ACard>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.ant-card-body) {
|
||||
max-height: calc(100vh - 260px);
|
||||
overflow-y: scroll;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
24
app/src/views/config/components/ConfigRightPanel.vue
Normal file
24
app/src/views/config/components/ConfigRightPanel.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { Config } from '@/api/config'
|
||||
import ConfigRightPanel from './ConfigRightPanel/ConfigRightPanel.vue'
|
||||
|
||||
interface ConfigRightPanelProps {
|
||||
addMode: boolean
|
||||
newPath: string
|
||||
modifiedAt: string
|
||||
origName: string
|
||||
}
|
||||
|
||||
const props = defineProps<ConfigRightPanelProps>()
|
||||
const data = defineModel<Config>('data', { required: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigRightPanel
|
||||
v-model:data="data"
|
||||
:add-mode="props.addMode"
|
||||
:new-path="props.newPath"
|
||||
:modified-at="props.modifiedAt"
|
||||
:orig-name="props.origName"
|
||||
/>
|
||||
</template>
|
||||
66
app/src/views/config/components/ConfigRightPanel/Basic.vue
Normal file
66
app/src/views/config/components/ConfigRightPanel/Basic.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import type { Config } from '@/api/config'
|
||||
import { formatDateTime } from '@/lib/helper'
|
||||
import { useSettingsStore } from '@/pinia'
|
||||
import ConfigName from '@/views/config/components/ConfigName.vue'
|
||||
import Deploy from './Deploy.vue'
|
||||
|
||||
interface BasicProps {
|
||||
addMode: boolean
|
||||
newPath: string
|
||||
modifiedAt: string
|
||||
origName: string
|
||||
}
|
||||
|
||||
const props = defineProps<BasicProps>()
|
||||
const data = defineModel<Config>('data', { required: true })
|
||||
const settings = useSettingsStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-6">
|
||||
<AForm
|
||||
layout="vertical"
|
||||
:model="data"
|
||||
:rules="{
|
||||
name: [
|
||||
{ required: true, message: $gettext('Please input a filename') },
|
||||
{ pattern: /^[^\\/]+$/, message: $gettext('Invalid filename') },
|
||||
],
|
||||
}"
|
||||
>
|
||||
<AFormItem
|
||||
name="name"
|
||||
:label="$gettext('Name')"
|
||||
>
|
||||
<AInput v-if="props.addMode" v-model:value="data.name" />
|
||||
<ConfigName v-else :name="data.name" :dir="data.dir" />
|
||||
</AFormItem>
|
||||
<AFormItem
|
||||
v-if="!props.addMode"
|
||||
:label="$gettext('Path')"
|
||||
>
|
||||
{{ decodeURIComponent(data.filepath) }}
|
||||
</AFormItem>
|
||||
<AFormItem
|
||||
v-show="data.name !== props.origName"
|
||||
:label="props.addMode ? $gettext('New Path') : $gettext('Changed Path')"
|
||||
required
|
||||
>
|
||||
{{ decodeURIComponent(props.newPath) }}
|
||||
</AFormItem>
|
||||
<AFormItem
|
||||
v-if="!props.addMode"
|
||||
:label="$gettext('Updated at')"
|
||||
>
|
||||
{{ formatDateTime(props.modifiedAt) }}
|
||||
</AFormItem>
|
||||
<AFormItem
|
||||
v-if="!settings.is_remote"
|
||||
:label="$gettext('Deploy')"
|
||||
>
|
||||
<Deploy v-model:data="data" />
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
</div>
|
||||
</template>
|
||||
15
app/src/views/config/components/ConfigRightPanel/Chat.vue
Normal file
15
app/src/views/config/components/ConfigRightPanel/Chat.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { Config } from '@/api/config'
|
||||
import ChatGPT from '@/components/ChatGPT'
|
||||
|
||||
const data = defineModel<Config>('data', { required: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt--6">
|
||||
<ChatGPT
|
||||
:content="data.content"
|
||||
:path="data.filepath"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import type { Config } from '@/api/config'
|
||||
import Basic from './Basic.vue'
|
||||
import Chat from './Chat.vue'
|
||||
|
||||
interface ConfigRightPanelProps {
|
||||
addMode: boolean
|
||||
newPath: string
|
||||
modifiedAt: string
|
||||
origName: string
|
||||
}
|
||||
|
||||
const props = defineProps<ConfigRightPanelProps>()
|
||||
const data = defineModel<Config>('data', { required: true })
|
||||
|
||||
const activeKey = ref('basic')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="right-settings-container">
|
||||
<ACard
|
||||
class="right-settings"
|
||||
:bordered="false"
|
||||
>
|
||||
<ATabs
|
||||
v-model:active-key="activeKey"
|
||||
size="small"
|
||||
>
|
||||
<ATabPane key="basic" :tab="$gettext('Basic')">
|
||||
<Basic
|
||||
v-model:data="data"
|
||||
:add-mode="props.addMode"
|
||||
:new-path="props.newPath"
|
||||
:modified-at="props.modifiedAt"
|
||||
:orig-name="props.origName"
|
||||
/>
|
||||
</ATabPane>
|
||||
<ATabPane key="chat" :tab="$gettext('Chat')">
|
||||
<Chat v-model:data="data" />
|
||||
</ATabPane>
|
||||
</ATabs>
|
||||
</ACard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.right-settings-container {
|
||||
position: relative;
|
||||
|
||||
.right-settings {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-nav) {
|
||||
margin: 0;
|
||||
height: 55px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-content) {
|
||||
padding-top: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:deep(.ant-card) {
|
||||
box-shadow: unset;
|
||||
|
||||
.ant-tabs-content {
|
||||
max-height: calc(100vh - 260px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
46
app/src/views/config/components/ConfigRightPanel/Deploy.vue
Normal file
46
app/src/views/config/components/ConfigRightPanel/Deploy.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import type { Config } from '@/api/config'
|
||||
import { InfoCircleOutlined } from '@ant-design/icons-vue'
|
||||
import NodeSelector from '@/components/NodeSelector'
|
||||
|
||||
const data = defineModel<Config>('data', { required: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<NodeSelector
|
||||
v-model:target="data.sync_node_ids"
|
||||
hidden-local
|
||||
/>
|
||||
<div class="node-deploy-control">
|
||||
<div class="overwrite">
|
||||
<ACheckbox v-model:checked="data.sync_overwrite">
|
||||
{{ $gettext('Overwrite') }}
|
||||
</ACheckbox>
|
||||
<ATooltip placement="bottom">
|
||||
<template #title>
|
||||
{{ $gettext('Overwrite exist file') }}
|
||||
</template>
|
||||
<InfoCircleOutlined />
|
||||
</ATooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.overwrite {
|
||||
margin-right: 15px;
|
||||
|
||||
span {
|
||||
color: #9b9b9b;
|
||||
}
|
||||
}
|
||||
|
||||
.node-deploy-control {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
import ConfigRightPanel from './ConfigRightPanel.vue'
|
||||
|
||||
export default ConfigRightPanel
|
||||
4
app/src/views/config/components/index.ts
Normal file
4
app/src/views/config/components/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import ConfigLeftPanel from './ConfigLeftPanel.vue'
|
||||
import ConfigRightPanel from './ConfigRightPanel.vue'
|
||||
|
||||
export { ConfigLeftPanel, ConfigRightPanel }
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { CustomRenderArgs, StdTableColumn } from '@uozi-admin/curd'
|
||||
import { FileFilled, FolderFilled } from '@ant-design/icons-vue'
|
||||
import { datetimeRender } from '@uozi-admin/curd'
|
||||
|
||||
const configColumns: StdTableColumn[] = [{
|
||||
@@ -15,8 +14,8 @@ const configColumns: StdTableColumn[] = [{
|
||||
return (
|
||||
<div class="mr-2 text-truegray-5">
|
||||
{isDir
|
||||
? <FolderFilled />
|
||||
: <FileFilled />}
|
||||
? <div class="i-tabler-folder-filled" />
|
||||
: <div class="i-tabler-file" />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,34 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import BaseEditor from '@/components/BaseEditor'
|
||||
import RightSettings from '@/views/site/site_edit/components/RightPanel/RightPanel.vue'
|
||||
import SiteEditor from '@/views/site/site_edit/components/SiteEditor'
|
||||
import { useSiteEditorStore } from './components/SiteEditor/store'
|
||||
|
||||
const editorStore = useSiteEditorStore()
|
||||
const { loading } = storeToRefs(editorStore)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="site-container">
|
||||
<ARow :gutter="{ xs: 0, sm: 16 }">
|
||||
<ACol
|
||||
:xs="24"
|
||||
:sm="24"
|
||||
:md="24"
|
||||
:lg="16"
|
||||
:xl="17"
|
||||
>
|
||||
<div>
|
||||
<SiteEditor />
|
||||
</div>
|
||||
</ACol>
|
||||
<BaseEditor :loading>
|
||||
<template #left>
|
||||
<SiteEditor />
|
||||
</template>
|
||||
|
||||
<ACol
|
||||
class="col-right"
|
||||
:xs="24"
|
||||
:sm="24"
|
||||
:md="24"
|
||||
:lg="8"
|
||||
:xl="7"
|
||||
>
|
||||
<template #right>
|
||||
<RightSettings />
|
||||
</ACol>
|
||||
</ARow>
|
||||
</template>
|
||||
</BaseEditor>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -36,7 +26,7 @@ import SiteEditor from '@/views/site/site_edit/components/SiteEditor'
|
||||
:deep(.ant-card) {
|
||||
box-shadow: unset;
|
||||
|
||||
.card-body, .ant-tabs-content {
|
||||
.ant-tabs-content {
|
||||
max-height: calc(100vh - 260px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useSiteEditorStore } from '@/views/site/site_edit/components/SiteEditor
|
||||
const [modal, ContextHolder] = Modal.useModal()
|
||||
|
||||
const editorStore = useSiteEditorStore()
|
||||
const { ngxConfig, curServerIdx, curDirectivesMap } = storeToRefs(editorStore)
|
||||
const { ngxConfig, curServerIdx, curDirectivesMap, hasServers } = storeToRefs(editorStore)
|
||||
|
||||
function confirmChangeTLS(status: CheckedType) {
|
||||
modal.confirm({
|
||||
@@ -107,7 +107,7 @@ const supportSSL = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="hasServers">
|
||||
<ContextHolder />
|
||||
|
||||
<AFormItem
|
||||
|
||||
@@ -10,7 +10,7 @@ const {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt--4">
|
||||
<div class="mt--6">
|
||||
<ChatGPT
|
||||
:content="configText"
|
||||
:path="filepath"
|
||||
|
||||
@@ -8,7 +8,7 @@ import ConfigTemplate from './ConfigTemplate.vue'
|
||||
const activeKey = ref('basic')
|
||||
|
||||
const editorStore = useSiteEditorStore()
|
||||
const { advanceMode } = storeToRefs(editorStore)
|
||||
const { advanceMode, loading } = storeToRefs(editorStore)
|
||||
|
||||
watch(advanceMode, val => {
|
||||
if (val) {
|
||||
@@ -22,10 +22,10 @@ watch(advanceMode, val => {
|
||||
<ACard
|
||||
class="right-settings"
|
||||
:bordered="false"
|
||||
:loading
|
||||
>
|
||||
<ATabs
|
||||
v-model:active-key="activeKey"
|
||||
class="mb-24px"
|
||||
size="small"
|
||||
>
|
||||
<ATabPane key="basic" :tab="$gettext('Basic')">
|
||||
@@ -54,31 +54,22 @@ watch(advanceMode, val => {
|
||||
position: relative;
|
||||
|
||||
.right-settings {
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-nav) {
|
||||
margin: 0;
|
||||
padding: 0 24px;
|
||||
height: 55px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-tabs) {
|
||||
margin-bottom: 0;
|
||||
|
||||
.ant-tabs-nav-wrap {
|
||||
height: 55px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.ant-tabs-content {
|
||||
padding-top: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
:deep(.ant-tabs-content) {
|
||||
padding-top: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -46,7 +46,7 @@ async function save() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard class="mb-4 site-edit-container" :bordered="false">
|
||||
<ACard class="site-edit-container" :bordered="false">
|
||||
<template #title>
|
||||
<span style="margin-right: 10px">{{ $gettext('Edit %{n}', { n: name }) }}</span>
|
||||
<ATag
|
||||
@@ -111,6 +111,7 @@ async function save() {
|
||||
class="parse-error-alert-wrapper"
|
||||
>
|
||||
<AAlert
|
||||
banner
|
||||
:message="$gettext('Nginx Configuration Parse Error')"
|
||||
:description="parseErrorMessage"
|
||||
type="error"
|
||||
@@ -189,10 +190,6 @@ async function save() {
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.parse-error-alert-wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.site-edit-container {
|
||||
height: 100%;
|
||||
:deep(.ant-card-body) {
|
||||
@@ -219,4 +216,8 @@ async function save() {
|
||||
transform: translateX(10px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:deep(.tab-content) {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -31,6 +31,10 @@ export const useSiteEditorStore = defineStore('siteEditor', () => {
|
||||
},
|
||||
})
|
||||
|
||||
const hasServers = computed(() => {
|
||||
return ngxConfig.value.servers && ngxConfig.value.servers.length > 0
|
||||
})
|
||||
|
||||
async function init(_name: string) {
|
||||
loading.value = true
|
||||
await nextTick()
|
||||
@@ -168,6 +172,7 @@ export const useSiteEditorStore = defineStore('siteEditor', () => {
|
||||
configText,
|
||||
issuingCert,
|
||||
curSupportSSL,
|
||||
hasServers,
|
||||
init,
|
||||
save,
|
||||
handleModeChange,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import BaseEditor from '@/components/BaseEditor'
|
||||
import RightSettings from '@/views/stream/components/RightPanel'
|
||||
import StreamEditor from '@/views/stream/components/StreamEditor.vue'
|
||||
import { useStreamEditorStore } from '@/views/stream/store'
|
||||
@@ -8,6 +9,7 @@ const route = useRoute()
|
||||
const name = computed(() => decodeURIComponent(route.params?.name?.toString() ?? ''))
|
||||
|
||||
const store = useStreamEditorStore()
|
||||
const { loading } = storeToRefs(store)
|
||||
|
||||
onMounted(() => {
|
||||
store.init(name.value)
|
||||
@@ -15,42 +17,19 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ARow :gutter="{ xs: 0, sm: 16 }">
|
||||
<ACol
|
||||
:xs="24"
|
||||
:sm="24"
|
||||
:md="24"
|
||||
:lg="16"
|
||||
:xl="17"
|
||||
>
|
||||
<div>
|
||||
<StreamEditor />
|
||||
</div>
|
||||
</ACol>
|
||||
<BaseEditor :loading>
|
||||
<template #left>
|
||||
<StreamEditor />
|
||||
</template>
|
||||
|
||||
<ACol
|
||||
class="col-right"
|
||||
:xs="24"
|
||||
:sm="24"
|
||||
:md="24"
|
||||
:lg="8"
|
||||
:xl="7"
|
||||
>
|
||||
<template #right>
|
||||
<RightSettings />
|
||||
</ACol>
|
||||
</ARow>
|
||||
</template>
|
||||
</BaseEditor>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.col-right {
|
||||
position: sticky;
|
||||
top: 78px;
|
||||
}
|
||||
|
||||
:deep(.ant-card) {
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
// Animation styles for mode switching
|
||||
.slide-fade-enter-active {
|
||||
transition: all .3s ease-in-out;
|
||||
}
|
||||
@@ -59,17 +38,19 @@ onMounted(() => {
|
||||
transition: all .3s cubic-bezier(1.0, 0.5, 0.8, 1.0);
|
||||
}
|
||||
|
||||
.slide-fade-enter-from, .slide-fade-enter-to, .slide-fade-leave-to
|
||||
/* .slide-fade-leave-active for below version 2.1.8 */ {
|
||||
.slide-fade-enter-from, .slide-fade-enter-to, .slide-fade-leave-to {
|
||||
transform: translateX(10px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// Stream-specific styles
|
||||
.directive-params-wrapper {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 10px;
|
||||
:deep(.ant-card-body) {
|
||||
max-height: 100%;
|
||||
overflow-y: scroll;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { SiteStatus } from '@/api/site'
|
||||
import { InfoCircleOutlined } from '@ant-design/icons-vue'
|
||||
import { StdSelector } from '@uozi-admin/curd'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import envGroup from '@/api/env_group'
|
||||
import stream from '@/api/stream'
|
||||
import NodeSelector from '@/components/NodeSelector'
|
||||
import { ConfigStatus } from '@/constants'
|
||||
import { formatDateTime } from '@/lib/helper'
|
||||
import { useSettingsStore } from '@/pinia'
|
||||
import envGroupColumns from '@/views/environments/group/columns'
|
||||
@@ -19,53 +15,15 @@ const settings = useSettingsStore()
|
||||
const store = useStreamEditorStore()
|
||||
const { name, status, data } = storeToRefs(store)
|
||||
|
||||
const [modal, ContextHolder] = Modal.useModal()
|
||||
const showSync = computed(() => !settings.is_remote)
|
||||
|
||||
function enable() {
|
||||
stream.enable(name.value).then(() => {
|
||||
message.success($gettext('Enabled successfully'))
|
||||
status.value = ConfigStatus.Enabled
|
||||
}).catch(r => {
|
||||
message.error($gettext('Failed to enable %{msg}', { msg: r.message ?? '' }), 10)
|
||||
})
|
||||
}
|
||||
|
||||
function disable() {
|
||||
stream.disable(name.value).then(() => {
|
||||
message.success($gettext('Disabled successfully'))
|
||||
status.value = ConfigStatus.Disabled
|
||||
}).catch(r => {
|
||||
message.error($gettext('Failed to disable %{msg}', { msg: r.message ?? '' }))
|
||||
})
|
||||
}
|
||||
|
||||
function onChangeEnabled({ status }: { status: SiteStatus }) {
|
||||
modal.confirm({
|
||||
title: status === ConfigStatus.Enabled ? $gettext('Do you want to enable this stream?') : $gettext('Do you want to disable this stream?'),
|
||||
mask: false,
|
||||
centered: true,
|
||||
okText: $gettext('OK'),
|
||||
cancelText: $gettext('Cancel'),
|
||||
async onOk() {
|
||||
if (status === ConfigStatus.Enabled)
|
||||
enable()
|
||||
else
|
||||
disable()
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ContextHolder />
|
||||
|
||||
<div class="px-6">
|
||||
<AFormItem :label="$gettext('Enabled')">
|
||||
<StreamStatusSelect
|
||||
v-model:status="status"
|
||||
:stream-name="name"
|
||||
@status-changed="onChangeEnabled"
|
||||
/>
|
||||
</AFormItem>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const { configText, filepath } = storeToRefs(store)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="mt--6">
|
||||
<ChatGPT
|
||||
:content="configText"
|
||||
:path="filepath"
|
||||
|
||||
@@ -14,7 +14,6 @@ const activeKey = ref('basic')
|
||||
>
|
||||
<ATabs
|
||||
v-model:active-key="activeKey"
|
||||
class="mb-24px"
|
||||
size="small"
|
||||
>
|
||||
<ATabPane key="basic" :tab="$gettext('Basic')">
|
||||
@@ -36,23 +35,18 @@ const activeKey = ref('basic')
|
||||
position: relative;
|
||||
|
||||
.right-settings {
|
||||
max-height: calc(100vh - 323px);
|
||||
overflow-y: scroll;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 19.5px 24px;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-nav) {
|
||||
margin: 0;
|
||||
height: 55px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-content) {
|
||||
padding-top: 24px;
|
||||
max-height: calc(100vh - 425px);
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { HistoryOutlined } from '@ant-design/icons-vue'
|
||||
import { HistoryOutlined, LoadingOutlined } from '@ant-design/icons-vue'
|
||||
import CodeEditor from '@/components/CodeEditor'
|
||||
import ConfigHistory from '@/components/ConfigHistory'
|
||||
import FooterToolBar from '@/components/FooterToolbar'
|
||||
@@ -10,112 +10,117 @@ import { useStreamEditorStore } from '../store'
|
||||
const router = useRouter()
|
||||
|
||||
const store = useStreamEditorStore()
|
||||
const { name, status, configText, filepath, saving, parseErrorStatus, parseErrorMessage, advanceMode } = storeToRefs(store)
|
||||
const { name, status, configText, filepath, saving, parseErrorStatus, parseErrorMessage, advanceMode, loading } = storeToRefs(store)
|
||||
const showHistory = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard class="mb-4" :bordered="false">
|
||||
<template #title>
|
||||
<span style="margin-right: 10px">{{ $gettext('Edit %{n}', { n: name }) }}</span>
|
||||
<ATag
|
||||
v-if="status === ConfigStatus.Enabled"
|
||||
color="blue"
|
||||
>
|
||||
{{ $gettext('Enabled') }}
|
||||
</ATag>
|
||||
<ATag
|
||||
v-else
|
||||
color="orange"
|
||||
>
|
||||
{{ $gettext('Disabled') }}
|
||||
</ATag>
|
||||
</template>
|
||||
<template #extra>
|
||||
<ASpace>
|
||||
<AButton
|
||||
v-if="filepath"
|
||||
type="link"
|
||||
@click="showHistory = true"
|
||||
<ASpin :spinning="loading" :indicator="LoadingOutlined">
|
||||
<ACard class="mb-4" :bordered="false">
|
||||
<template #title>
|
||||
<span style="margin-right: 10px">{{ $gettext('Edit %{n}', { n: name }) }}</span>
|
||||
<ATag
|
||||
v-if="status === ConfigStatus.Enabled"
|
||||
color="blue"
|
||||
>
|
||||
<template #icon>
|
||||
<HistoryOutlined />
|
||||
</template>
|
||||
{{ $gettext('History') }}
|
||||
</AButton>
|
||||
<div class="mode-switch">
|
||||
<div class="switch">
|
||||
<ASwitch
|
||||
size="small"
|
||||
:disabled="parseErrorStatus"
|
||||
:checked="advanceMode"
|
||||
@change="store.handleModeChange"
|
||||
{{ $gettext('Enabled') }}
|
||||
</ATag>
|
||||
<ATag
|
||||
v-else
|
||||
color="orange"
|
||||
>
|
||||
{{ $gettext('Disabled') }}
|
||||
</ATag>
|
||||
</template>
|
||||
<template #extra>
|
||||
<ASpace>
|
||||
<AButton
|
||||
v-if="filepath"
|
||||
type="link"
|
||||
@click="showHistory = true"
|
||||
>
|
||||
<template #icon>
|
||||
<HistoryOutlined />
|
||||
</template>
|
||||
{{ $gettext('History') }}
|
||||
</AButton>
|
||||
<div class="mode-switch">
|
||||
<div class="switch">
|
||||
<ASwitch
|
||||
size="small"
|
||||
:disabled="parseErrorStatus"
|
||||
:checked="advanceMode"
|
||||
@change="store.handleModeChange"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="advanceMode">
|
||||
<div>{{ $gettext('Advance Mode') }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div>{{ $gettext('Basic Mode') }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</ASpace>
|
||||
</template>
|
||||
|
||||
<div class="card-body">
|
||||
<Transition name="slide-fade">
|
||||
<div
|
||||
v-if="advanceMode"
|
||||
key="advance"
|
||||
>
|
||||
<div v-if="parseErrorStatus">
|
||||
<AAlert
|
||||
banner
|
||||
:message="$gettext('Nginx Configuration Parse Error')"
|
||||
:description="parseErrorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<CodeEditor
|
||||
v-model:content="configText"
|
||||
no-border-radius
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
key="basic"
|
||||
class="domain-edit-container"
|
||||
>
|
||||
<NgxConfigEditor
|
||||
:enabled="status === ConfigStatus.Enabled"
|
||||
context="stream"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="advanceMode">
|
||||
<div>{{ $gettext('Advance Mode') }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div>{{ $gettext('Basic Mode') }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</ASpace>
|
||||
</template>
|
||||
|
||||
<Transition name="slide-fade">
|
||||
<div
|
||||
v-if="advanceMode"
|
||||
key="advance"
|
||||
>
|
||||
<div
|
||||
v-if="parseErrorStatus"
|
||||
class="mb-4"
|
||||
>
|
||||
<AAlert
|
||||
:message="$gettext('Nginx Configuration Parse Error')"
|
||||
:description="parseErrorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<CodeEditor v-model:content="configText" />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
key="basic"
|
||||
class="domain-edit-container"
|
||||
>
|
||||
<NgxConfigEditor
|
||||
:enabled="status === ConfigStatus.Enabled"
|
||||
context="stream"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
<ConfigHistory
|
||||
v-model:visible="showHistory"
|
||||
v-model:current-content="configText"
|
||||
:filepath="filepath"
|
||||
/>
|
||||
|
||||
<ConfigHistory
|
||||
v-model:visible="showHistory"
|
||||
v-model:current-content="configText"
|
||||
:filepath="filepath"
|
||||
/>
|
||||
|
||||
<FooterToolBar>
|
||||
<ASpace>
|
||||
<AButton @click="router.push('/streams')">
|
||||
{{ $gettext('Back') }}
|
||||
</AButton>
|
||||
<AButton
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
@click="store.save"
|
||||
>
|
||||
{{ $gettext('Save') }}
|
||||
</AButton>
|
||||
</ASpace>
|
||||
</FooterToolBar>
|
||||
</ACard>
|
||||
<FooterToolBar>
|
||||
<ASpace>
|
||||
<AButton @click="router.push('/streams')">
|
||||
{{ $gettext('Back') }}
|
||||
</AButton>
|
||||
<AButton
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
@click="store.save"
|
||||
>
|
||||
{{ $gettext('Save') }}
|
||||
</AButton>
|
||||
</ASpace>
|
||||
</FooterToolBar>
|
||||
</ACard>
|
||||
</ASpin>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
@@ -133,4 +138,8 @@ const showHistory = ref(false)
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
:deep(.tab-content) {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -80,9 +80,7 @@ function onChangeStatus(checked: CheckedType) {
|
||||
<ContextHolder />
|
||||
<div class="status-display">
|
||||
<ASwitch
|
||||
:checked="status === 'enabled'"
|
||||
:checked-children="$gettext('Enabled')"
|
||||
:un-checked-children="$gettext('Disabled')"
|
||||
:checked="status === ConfigStatus.Enabled"
|
||||
@change="onChangeStatus"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { CertificateInfo } from '@/api/cert'
|
||||
import type { Stream } from '@/api/stream'
|
||||
import type { CheckedType } from '@/types'
|
||||
import { message } from 'ant-design-vue'
|
||||
import config from '@/api/config'
|
||||
import ngx from '@/api/ngx'
|
||||
import stream from '@/api/stream'
|
||||
@@ -65,6 +66,8 @@ export const useStreamEditorStore = defineStore('streamEditor', () => {
|
||||
})
|
||||
|
||||
handleResponse(response)
|
||||
|
||||
message.success($gettext('Saved successfully'))
|
||||
}
|
||||
catch (error) {
|
||||
handleParseError(error as { error?: string, message: string })
|
||||
|
||||
Reference in New Issue
Block a user