Begin large refactor of Vue to move it into a proper development environment:

No more CNDs. All software is now bundled and served from a proper node-based development server.
Switch to using proper Vue files, so we can have proper IDE integration.
Open new port for the dev server mentioned above.
Split status and mod info requests in the back end.
Bypass CORS while developing.
This commit is contained in:
Daniel Ceregatti 2023-06-05 11:14:24 -07:00
parent cf47b7fe1f
commit fd1774cf1c
20 changed files with 1742 additions and 1 deletions

View file

@ -30,6 +30,7 @@ services:
- ./web/bin/dz:/usr/local/bin/dz
- ./web:/web
ports:
- "8001:8001/tcp"
- "8000:8000/tcp"
restart: no
environment:

13
web/docroot/index.html Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DayZ Docker Server</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="src/main.js"></script>
</body>
</html>

1231
web/docroot/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

21
web/docroot/package.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "docroot",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite --host",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.0",
"bootstrap-icons": "^1.10.5",
"pinia": "^2.1.3",
"vue": "^3.3.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"vite": "^4.3.5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

13
web/docroot/src/App.vue Normal file
View file

@ -0,0 +1,13 @@
<script setup>
import Body from '@/components/Body.vue'
import Error from '@/components/Error.vue'
import Header from '@/components/Header.vue'
</script>
<template>
<Error />
<div class="container-fluid min-vh-100 d-flex flex-column bg-light">
<Header />
<Body />
</div>
</template>

View file

@ -0,0 +1,13 @@
<script setup>
import Mods from "@/components/Mods.vue"
import Modinfo from "@/components/Modinfo.vue";
</script>
<template>
<div class="row flex-grow-1">
<div class="col-md-3 border">
<Mods />
<Modinfo />
</div>
</div>
</template>

View file

@ -0,0 +1,39 @@
<script setup>
import { onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useErrorStore } from '@/stores/error'
const { errorText } = storeToRefs(useErrorStore())
import { Modal } from 'bootstrap'
let modal = {}
onMounted(() => {
modal = new Modal('#errorModal', {})
// modal.show()
})
</script>
<template>
<div
class="modal"
id="errorModal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="errorModalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="errorModalLabel">Error</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{{ errorText }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,35 @@
<script setup>
import Search from '@/components/Search.vue'
import { useFetch } from '@/fetch.js'
const { data, error } = useFetch('/status')
</script>
<template>
<div v-if="! error && data" class="row">
<div class="col-3 text-center">
<h1>DayZ Docker Server</h1>
</div>
<div class="col-5">
<button
@click="installbase"
:class="'btn ' + (data.installed ? 'btn-danger' : 'btn-success')"
>
Install Server Files
</button>
<button @click="updatebase" class="btn btn-success">Update Server Files</button>
<button @click="updatemods" class="btn btn-success">Update Mods</button>
<button @click="servers" class="btn btn-primary">Servers</button>
<button @click="listmods" class="btn btn-primary">Mods</button>
</div>
<Search />
<div class="col">
<div>
Server files installed:
<span class="bi bi-check h2 text-success" v-if="data.installed"></span>
<span class="bi bi-x h2 danger text-danger" v-else></span>
</div>
<div v-if="data.version !== ''">
Version: <span class="text-success font-weight-bold">{{ data.version }}</span>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,44 @@
<script setup>
import { ref, computed } from 'vue'
import xmltree from '@/components/XmlTree.vue'
import { useFetch} from '@/fetch'
const modId = ref(null)
const modInfo = null
const xmlInfo = null
const url = computed(() => baseUrl + '/mod/' + modId.value)
const { data } = useFetch(url)
</script>
<template>
<div class="col-md-9 border">
<div class="d-flex" v-if="data">
<div>
<div>
<strong>{{ data.name }}</strong>
</div>
<div>
ID: {{ data.id }}
</div>
<div>
Size: {{ data.size.toLocaleString("en-US") }}
</div>
<div v-if="data.customXML.length > 0">
Custom XML files:
<ul>
<li v-for="info in data.customXML">
<a
:class="'simulink xmlfile ' + info.name"
@click="modInfo=nfo.name"
>
{{ info.name }}
</a>
</li>
</ul>
</div>
</div>
<div class="col-1"></div>
<div>
<xmltree v-if="xmlInfo" :xmlData="xmlInfo" />
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,34 @@
<script setup>
import { useFetch} from '@/fetch'
const { data } = useFetch('/mods')
const modId = null
</script>
<template>
<div v-if="data">
<h4 class="text-center">Installed Mods</h4>
<table>
<tr>
<th>Steam Link</th>
<th>Mod Info</th>
</tr>
<template
v-for="mod in data.mods"
>
<tr>
<td>
<a
target="_blank"
:href="steamUrl + mod.id"
>
{{ mod.id }}
</a>
</td>
<td>
<a class="simulink" @click="modId=mod.id">{{ mod.name }}</a>
</td>
</tr>
</template>
</table>
</div>
</template>

View file

@ -0,0 +1,52 @@
<!--<script setup>-->
<!--import { ref, computed } from 'vue'-->
<!--import { useFetch} from '@/fetch'-->
<!--const baseUrl = 'http://bubba:8000/search/'-->
<!--const searchTerm = ref('')-->
<!--const url = computed(() => baseUrl + searchTerm.value)-->
<!--const { data, error } = useFetch(url)-->
<!--</script>-->
<template>
<div class="col form-control-lg text-center">
<form @submit.prevent="searchTerm=this">
<input name="search" placeholder="Search mods..." autofocus>
</form>
</div>
</template>
<script>
export default {
name: "Search",
methods: {
handleSubmit(e) {
e.preventDefault()
fetch(this.apihost + '/search/' + e.target.search.value)
.then(response => response.json())
.then(response => {
this.modInfo = ""
this.XMLInfo = ""
// const sortField = "time_updated"
const sortField = "lifetime_subscriptions"
response.response.publishedfiledetails.sort((a, b) =>
a[sortField] < b[sortField] ? 1 : -1
)
this.searchResults = response.response.publishedfiledetails
})
.then(() => {
// Enable all tooltips
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
// Enable all alerts
// $('.alert').alert()
})
.catch((error) => {
console.error(error)
this.fetchError = error.message
})
},
}
}
</script>

View file

@ -0,0 +1,36 @@
<script setup>
const searchResults = null
</script>
<template>
<div v-if="searchResults" class="d-flex">
<table>
<tr>
<th>Steam Link</th>
<th>Title</th>
<th>Size</th>
<th>Last Updated</th>
<th>Subscriptions</th>
<th></th>
</tr>
<tr v-for="result in searchResults">
<td>
<a
target="_blank"
:href="steamURL + result.publishedfileid"
>
<img :alt="result.short_description" data-bs-toggle="tooltip" data-bs-placement="left" :title="result.short_description" width="160" height="90" :src="result.preview_url">
</a>
</td>
<td>{{ result.title }}</td>
<td>{{ BKMG(result.file_size) }}</td>
<td>{{ new Date(result.time_updated * 1000).toLocaleDateString("en-us") }}</td>
<td>{{ result.lifetime_subscriptions }}</td>
<td>
<button v-if="mods.find(o => o.id === result.publishedfileid)" @click="removeMod(result.publishedfileid)" type="button" class="btn btn-danger">Remove</button>
<button v-else @click="installMod(result.publishedfileid)" type="button" class="btn btn-success">Install</button>
</td>
</tr>
</table>
</div>
</template>

View file

@ -0,0 +1,104 @@
<template>
<div
v-if="elem.nodeType === 1 && isText"
:style="'padding-left: ' + (depth * 10) + 'px'"
@click="collapse"
>
<span class="xml-tree-tag">&lt;{{elem.nodeName}}</span>
<span v-if="elem.hasAttributes()" v-for="attribute in elem.attributes">
<span class="xml-tree-attr">&nbsp;{{attribute.name}}</span>
<span>=</span>
<span class="xml-tree-attr">"{{attribute.value}}"</span>
</span>
<span class="xml-tree-tag">></span>
<span>{{this.children[0].data.trim()}}</span>
<span class="xml-tree-tag">&lt;/{{elem.nodeName}}></span>
</div>
<div v-else :style="'padding-left: ' + (depth * 10) + 'px'">
<span v-if="elem.nodeType === 1" class="d-flex">
<span
v-if="elem.children.length > 0"
class="bi-dash simulink text-center"
@click="collapse"
/>
<span v-else></span>
<span class="xml-tree-tag">&lt;{{elem.nodeName}}</span>
<span v-if="elem.hasAttributes()" v-for="attribute in elem.attributes">
<span class="xml-tree-attr">&nbsp;{{attribute.name}}</span>
<span>=</span>
<span class="xml-tree-attr">"{{attribute.value}}"</span>
</span>
<span v-if="elem.children.length === 0" class="xml-tree-tag">&nbsp;/></span>
<span v-else class="xml-tree-tag">></span>
</span>
<span v-if="elem.nodeType === 3">{{elem.data.trim()}}</span>
<div v-for="child in children">
<xmltree v-if="child.nodeType !== 8" :element="child" :d="depth" />
</div>
<span
v-if="elem.nodeType === 1 && elem.children.length > 0"
style="padding-left: -10px"
>
<span style="padding-left: 20px" class="xml-tree-tag">&lt;/{{elem.nodeName}}></span>
</span>
</div>
</template>
<script>
export default {
name: "xmltree",
props: {
d: {
type: Number,
default: 0
},
element: {
type: [Element, Text],
default: undefined
},
xmlData: String
},
data() {
return {
depth: 1
}
},
methods: {
collapse() {
this.children.forEach(x => x.classList?.add("d-none"))
},
log(message) {
console.log(message)
}
},
computed: {
elem() {
this.depth = parseInt(this.d) + 1
if (this.element) {
return this.element
} else {
const parser = new DOMParser()
const xmlDoc = parser.parseFromString(this.xmlData, "text/xml")
return xmlDoc.documentElement
}
},
children() {
let children = []
let node = this.elem.firstChild
while (node) {
children.push(node)
node = node.nextSibling
}
return children
},
isText() {
if (this.children.length === 1) {
if (this.children[0].nodeType === 3) {
return true
}
}
return false
}
}
}
</script>

View file

@ -0,0 +1,21 @@
button {
padding: 5px;
margin: 10px;
}
th, td {
padding-right: 10px
}
.selected {
background-color: cyan;
}
.simulink {
cursor: pointer;
text-underline: blue;
}
.simulink:hover {
background-color: cyan;
}

24
web/docroot/src/fetch.js Normal file
View file

@ -0,0 +1,24 @@
import { ref, isRef, unref, watchEffect } from 'vue'
const baseUrl = 'http://bubba:8000'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
async function doFetch() {
data.value = null
error.value = null
const urlValue = unref(baseUrl + url)
try {
const res = await fetch(urlValue)
data.value = await res.json()
} catch (e) {
error.value = e
}
}
if (isRef(url)) {
watchEffect(doFetch)
} else {
doFetch()
}
return { data, error, retry: doFetch }
}

31
web/docroot/src/main.js Normal file
View file

@ -0,0 +1,31 @@
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap'
import 'bootstrap-icons/font/bootstrap-icons.css'
import './css/index.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import { useErrorStore } from '@/stores/error.js'
// Create an instance of our Vue app
const app = createApp(App)
// Add the store
app.use(createPinia())
// Global properties
// The back end URL
app.config.globalProperties.baseUrl = window.location.protocol + '//' + window.location.hostname + ':8000'
// The steam workshop URL
app.config.globalProperties.steamUrl = 'https://steamcommunity.com/sharedfiles/filedetails/?id='
// A global error handler
app.config.errorHandler = (err, instance, info) => {
const errorStore = useErrorStore()
errorStore.errorText = err.message
console.error('GLOBAL ERROR HANDLER! ', err, instance, info)
}
// Mount it
app.mount('#app')

View file

@ -0,0 +1,9 @@
import { ref, watch } from 'vue'
import { defineStore } from 'pinia'
export const useErrorStore = defineStore('errors', () => {
const errorText = ref(null)
watch(errorText, (t) => {
console.log("errorText: " + t)
})
return { errorText }
})

View file

@ -0,0 +1,17 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
port: 8001
}
})

View file

@ -13,4 +13,7 @@ fi
cd /web
npm i
export DEBUG='express:*'
npx nodemon web.js
npx nodemon web.js &
cd docroot
npm run dev