UI patches (#905)

- Show 0 clients when NT server props are undefined
- Add Prettier 

---------

Co-authored-by: Matthew Morley <matthew.morley.ca@gmail.com>
This commit is contained in:
Sriman Achanta
2023-08-31 16:56:58 -04:00
committed by GitHub
parent de394418f6
commit 08892b9e68
55 changed files with 3323 additions and 2808 deletions

View File

@@ -5,54 +5,56 @@ import CvSelect from "@/components/common/cv-select.vue";
import axios from "axios";
const restartProgram = () => {
axios.post("/utils/restartProgram")
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully sent program restart request",
color: "success"
});
})
.catch(error => {
// This endpoint always return 204 regardless of outcome
if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
axios
.post("/utils/restartProgram")
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully sent program restart request",
color: "success"
});
})
.catch((error) => {
// This endpoint always return 204 regardless of outcome
if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
});
};
const restartDevice = () => {
axios.post("/utils/restartDevice")
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully dispatched the restart command. It isn't confirmed if a device restart will occur.",
color: "success"
});
})
.catch(error => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfil the request to restart the device.",
color: "error"
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
axios
.post("/utils/restartDevice")
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully dispatched the restart command. It isn't confirmed if a device restart will occur.",
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfil the request to restart the device.",
color: "error"
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
});
};
const address = inject<string>("backendHost");
@@ -61,55 +63,60 @@ const offlineUpdate = ref();
const openOfflineUpdatePrompt = () => {
offlineUpdate.value.click();
};
const handleOfflineUpdate = ({ files } : { files: FileList}) => {
useStateStore().showSnackbarMessage({ message: "New Software Upload in Progress...", color: "secondary", timeout: -1 });
const handleOfflineUpdate = ({ files }: { files: FileList }) => {
useStateStore().showSnackbarMessage({
message: "New Software Upload in Progress...",
color: "secondary",
timeout: -1
});
const formData = new FormData();
formData.append("jarData", files[0]);
axios.post("/utils/offlineUpdate", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }) => {
const uploadPercentage = ((progress || 0) * 100.0);
if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({
message: "New Software Upload in Process, " + uploadPercentage.toFixed(2) + "% complete",
color: "secondary",
timeout: -1
});
} else {
useStateStore().showSnackbarMessage({
message: "Installing uploaded software...",
color: "secondary",
timeout: -1
});
}
}
})
.then(response => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
})
.catch(error => {
if (error.response) {
axios
.post("/utils/offlineUpdate", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: ({ progress }) => {
const uploadPercentage = (progress || 0) * 100.0;
if (uploadPercentage < 99.5) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
message: "New Software Upload in Process, " + uploadPercentage.toFixed(2) + "% complete",
color: "secondary",
timeout: -1
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
message: "Installing uploaded software...",
color: "secondary",
timeout: -1
});
}
}
})
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
});
};
const exportLogFile = ref();
@@ -154,33 +161,34 @@ const handleSettingsImport = () => {
break;
}
axios.post(`/settings${settingsEndpoint}`, formData, {
headers: { "Content-Type": "multipart/form-data" }
})
.then(response => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
})
.catch(error => {
if (error.response) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
axios
.post(`/settings${settingsEndpoint}`, formData, {
headers: { "Content-Type": "multipart/form-data" }
})
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
});
showImportDialog.value = false;
importType.value = -1;
@@ -189,99 +197,52 @@ const handleSettingsImport = () => {
</script>
<template>
<v-card
dark
class="mb-3 pr-6 pb-3"
style="background-color: #006492;"
>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title>Device Control</v-card-title>
<div class="ml-5">
<v-row>
<v-col
cols="12"
lg="4"
md="6"
>
<v-btn
color="red"
@click="restartProgram"
>
<v-icon left>
mdi-restart
</v-icon>
<v-col cols="12" lg="4" md="6">
<v-btn color="red" @click="restartProgram">
<v-icon left> mdi-restart </v-icon>
Restart PhotonVision
</v-btn>
</v-col>
<v-col
cols="12"
lg="4"
md="6"
>
<v-btn
color="red"
@click="restartDevice"
>
<v-icon left>
mdi-restart-alert
</v-icon>
<v-col cols="12" lg="4" md="6">
<v-btn color="red" @click="restartDevice">
<v-icon left> mdi-restart-alert </v-icon>
Restart Device
</v-btn>
</v-col>
<v-col
cols="12"
lg="4"
>
<v-btn
color="secondary"
@click="openOfflineUpdatePrompt"
>
<v-icon left>
mdi-upload
</v-icon>
<v-col cols="12" lg="4">
<v-btn color="secondary" @click="openOfflineUpdatePrompt">
<v-icon left> mdi-upload </v-icon>
Offline Update
</v-btn>
<input
ref="offlineUpdate"
type="file"
accept=".jar"
style="display: none;"
@change="handleOfflineUpdate"
>
<input ref="offlineUpdate" type="file" accept=".jar" style="display: none" @change="handleOfflineUpdate" />
</v-col>
</v-row>
<v-divider style="margin: 12px 0;" />
<v-divider style="margin: 12px 0" />
<v-row>
<v-col
cols="12"
sm="6"
>
<v-btn
color="secondary"
@click="() => showImportDialog = true"
>
<v-icon left>
mdi-import
</v-icon>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="() => (showImportDialog = true)">
<v-icon left> mdi-import </v-icon>
Import Settings
</v-btn>
<v-dialog
v-model="showImportDialog"
width="600"
@input="() => {
importType = -1;
importFile = null;
}"
@input="
() => {
importType = -1;
importFile = null;
}
"
>
<v-card
color="primary"
dark
>
<v-card color="primary" dark>
<v-card-title>Import Settings</v-card-title>
<v-card-text>
Upload and apply previously saved or exported PhotonVision settings to this device
<v-row
class="mt-6 ml-4"
>
<v-row class="mt-6 ml-4">
<cv-select
v-model="importType"
label="Type"
@@ -290,14 +251,12 @@ const handleSettingsImport = () => {
:select-cols="10"
/>
</v-row>
<v-row
class="mt-6 ml-4 mr-8"
>
<v-row class="mt-6 ml-4 mr-8">
<v-file-input
:disabled="importType === -1"
:error-messages="importType === -1 ? 'Settings type not selected' : ''"
:accept="importType === ImportType.AllSettings ? '.zip' : '.json'"
@change="(file) => importFile = file"
@change="(file) => (importFile = file)"
/>
</v-row>
<v-row
@@ -305,14 +264,8 @@ const handleSettingsImport = () => {
style="display: flex; align-items: center; justify-content: center"
align="center"
>
<v-btn
color="secondary"
:disabled="importFile === null"
@click="handleSettingsImport"
>
<v-icon left>
mdi-import
</v-icon>
<v-btn color="secondary" :disabled="importFile === null" @click="handleSettingsImport">
<v-icon left> mdi-import </v-icon>
Import Settings
</v-btn>
</v-row>
@@ -320,17 +273,9 @@ const handleSettingsImport = () => {
</v-card>
</v-dialog>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-btn
color="secondary"
@click="openExportSettingsPrompt"
>
<v-icon left>
mdi-export
</v-icon>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="openExportSettingsPrompt">
<v-icon left> mdi-export </v-icon>
Export Settings
</v-btn>
<a
@@ -341,17 +286,9 @@ const handleSettingsImport = () => {
target="_blank"
/>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-btn
color="secondary"
@click="openExportLogsPrompt"
>
<v-icon left>
mdi-download
</v-icon>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="openExportLogsPrompt">
<v-icon left> mdi-download </v-icon>
Download Current Log
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
@@ -364,17 +301,9 @@ const handleSettingsImport = () => {
/>
</v-btn>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-btn
color="secondary"
@click="useStateStore().showLogModal = true"
>
<v-icon left>
mdi-eye
</v-icon>
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="useStateStore().showLogModal = true">
<v-icon left> mdi-eye </v-icon>
Show log viewer
</v-btn>
</v-col>

View File

@@ -4,11 +4,7 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
</script>
<template>
<v-card
dark
class="mb-3 pr-6 pb-3"
style="background-color: #006492;"
>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title>LED Control</v-card-title>
<div class="ml-5">
<cv-slider
@@ -18,7 +14,7 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
:slider-cols="12"
:min="0"
:max="100"
@input="args => useSettingsStore().changeLEDBrightness(args)"
@input="(args) => useSettingsStore().changeLEDBrightness(args)"
/>
</div>
</v-card>

View File

@@ -5,27 +5,28 @@ import { useStateStore } from "@/stores/StateStore";
import CvIcon from "@/components/common/cv-icon.vue";
interface MetricItem {
header: string,
value?: string
header: string;
value?: string;
}
const generalMetrics = computed<MetricItem[]>(() => [
{
header: "Version",
value: useSettingsStore().general.version || "Unknown"
},
{
header: "Hardware Model",
value: useSettingsStore().general.hardwareModel || "Unknown"
},
{
header: "Platform",
value: useSettingsStore().general.hardwarePlatform || "Unknown"
},
{
header: "GPU Acceleration",
value: useSettingsStore().general.gpuAcceleration || "Unknown"
}]);
{
header: "Version",
value: useSettingsStore().general.version || "Unknown"
},
{
header: "Hardware Model",
value: useSettingsStore().general.hardwareModel || "Unknown"
},
{
header: "Platform",
value: useSettingsStore().general.hardwarePlatform || "Unknown"
},
{
header: "GPU Acceleration",
value: useSettingsStore().general.gpuAcceleration || "Unknown"
}
]);
const platformMetrics = computed<MetricItem[]>(() => [
{
header: "CPU Temp",
@@ -33,15 +34,21 @@ const platformMetrics = computed<MetricItem[]>(() => [
},
{
header: "CPU Usage",
value: useSettingsStore().metrics.cpuUtil === undefined ? "Unknown" :`${useSettingsStore().metrics.cpuUtil}%`
value: useSettingsStore().metrics.cpuUtil === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuUtil}%`
},
{
header: "CPU Memory Usage",
value: useSettingsStore().metrics.ramUtil === undefined || useSettingsStore().metrics.cpuMem === undefined ? "Unknown" : `${useSettingsStore().metrics.ramUtil || "Unknown"}MB of ${useSettingsStore().metrics.cpuMem}MB`
value:
useSettingsStore().metrics.ramUtil === undefined || useSettingsStore().metrics.cpuMem === undefined
? "Unknown"
: `${useSettingsStore().metrics.ramUtil || "Unknown"}MB of ${useSettingsStore().metrics.cpuMem}MB`
},
{
header: "GPU Memory Usage",
value: useSettingsStore().metrics.gpuMemUtil === undefined || useSettingsStore().metrics.gpuMem === undefined ? "Unknown" : `${useSettingsStore().metrics.gpuMemUtil}MB of ${useSettingsStore().metrics.gpuMem}MB`
value:
useSettingsStore().metrics.gpuMemUtil === undefined || useSettingsStore().metrics.gpuMem === undefined
? "Unknown"
: `${useSettingsStore().metrics.gpuMemUtil}MB of ${useSettingsStore().metrics.gpuMem}MB`
},
{
header: "CPU Throttling",
@@ -60,28 +67,28 @@ const platformMetrics = computed<MetricItem[]>(() => [
const metricsLastFetched = ref("Never");
const fetchMetrics = () => {
useSettingsStore()
.requestMetricsUpdate()
.catch(error => {
if(error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Unable to fetch Metrics! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to fetch Metrics."
});
}
})
.finally(() => {
const pad = (num: number): string => {
return String(num).padStart(2, "0");
};
.requestMetricsUpdate()
.catch((error) => {
if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Unable to fetch Metrics! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to fetch Metrics."
});
}
})
.finally(() => {
const pad = (num: number): string => {
return String(num).padStart(2, "0");
};
const date = new Date();
metricsLastFetched.value = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
});
const date = new Date();
metricsLastFetched.value = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
});
};
onBeforeMount(() => {
@@ -90,47 +97,24 @@ onBeforeMount(() => {
</script>
<template>
<v-card
dark
class="mb-3 pr-6 pb-3"
style="background-color: #006492;"
>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title style="display: flex; justify-content: space-between">
<span>Stats</span>
<cv-icon
icon-name="mdi-reload"
color="white"
tooltip="Reload Metrics"
hover
@click="fetchMetrics"
/>
<cv-icon icon-name="mdi-reload" color="white" tooltip="Reload Metrics" hover @click="fetchMetrics" />
</v-card-title>
<v-row class="pt-2 pa-4 ma-0 ml-5 pb-1">
<v-card-subtitle
class="ma-0 pa-0 pb-2"
style="font-size: 16px"
>
General Metrics
</v-card-subtitle>
<v-card-subtitle class="ma-0 pa-0 pb-2" style="font-size: 16px"> General Metrics </v-card-subtitle>
<v-simple-table class="metrics-table">
<thead>
<tr>
<th
v-for="(item, itemIndex) in generalMetrics"
:key="itemIndex"
class="metric-item metric-item-title"
>
<th v-for="(item, itemIndex) in generalMetrics" :key="itemIndex" class="metric-item metric-item-title">
{{ item.header }}
</th>
</tr>
</thead>
<tbody>
<tr>
<td
v-for="(item, itemIndex) in generalMetrics"
:key="itemIndex"
class="metric-item"
>
<td v-for="(item, itemIndex) in generalMetrics" :key="itemIndex" class="metric-item">
{{ item.value }}
</td>
</tr>
@@ -138,31 +122,18 @@ onBeforeMount(() => {
</v-simple-table>
</v-row>
<v-row class="pa-4 ma-0 ml-5">
<v-card-subtitle
class="ma-0 pa-0 pb-2"
style="font-size: 16px"
>
Hardware Metrics
</v-card-subtitle>
<v-card-subtitle class="ma-0 pa-0 pb-2" style="font-size: 16px"> Hardware Metrics </v-card-subtitle>
<v-simple-table class="metrics-table">
<thead>
<tr>
<th
v-for="(item, itemIndex) in platformMetrics"
:key="itemIndex"
class="metric-item metric-item-title"
>
<th v-for="(item, itemIndex) in platformMetrics" :key="itemIndex" class="metric-item metric-item-title">
{{ item.header }}
</th>
</tr>
</thead>
<tbody>
<tr>
<td
v-for="(item, itemIndex) in platformMetrics"
:key="itemIndex"
class="metric-item"
>
<td v-for="(item, itemIndex) in platformMetrics" :key="itemIndex" class="metric-item">
<span v-if="useSettingsStore().metrics.cpuUtil !== undefined">{{ item.value }}</span>
<span v-else>---</span>
</td>
@@ -177,7 +148,7 @@ onBeforeMount(() => {
</template>
<style scoped lang="scss">
.metrics-table{
.metrics-table {
border-collapse: separate;
border-spacing: 0;
border-radius: 5px;
@@ -203,7 +174,8 @@ onBeforeMount(() => {
}
.v-data-table {
thead, tbody {
thead,
tbody {
background-color: #006492;
}

View File

@@ -14,7 +14,7 @@ const isValidNetworkTablesIP = (v: string | undefined): boolean => {
// Check if it is a team number longer than 5 digits
const badTeamNumberRegex = /^[0-9]{5,}$/;
if(v === undefined) return false;
if (v === undefined) return false;
if (teamNumberRegex.test(v)) return true;
if (isValidIPv4(v)) return true;
// need to check these before the hostname. "0" and "99999" are valid hostnames, but we don't want to allow then
@@ -26,77 +26,80 @@ const isValidIPv4 = (v: string | undefined) => {
// https://stackoverflow.com/a/17871737
const ipv4Regex = /^((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])$/;
if(v === undefined) return false;
if (v === undefined) return false;
return ipv4Regex.test(v);
};
const isValidHostname = (v: string | undefined) => {
// https://stackoverflow.com/a/18494710
const hostnameRegex = /^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)+(\.([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*))*$/;
if(v === undefined) return false;
if (v === undefined) return false;
return hostnameRegex.test(v);
};
const saveGeneralSettings = () => {
const changingStaticIp = useSettingsStore().network.connectionType === NetworkConnectionType.Static;
useSettingsStore().saveGeneralSettings()
.then(response => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
})
.catch(error => {
if(error.response) {
if (error.status === 504 || changingStaticIp) {
useStateStore().showSnackbarMessage({
color: "error",
message: `Connection lost! Try the new static IP at ${useSettingsStore().network.staticIp}:5800 or ${useSettingsStore().network.hostname}:5800?`
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: error.response.data.text || error.response.data
});
}
} else if(error.request) {
useSettingsStore()
.saveGeneralSettings()
.then((response) => {
useStateStore().showSnackbarMessage({
message: response.data.text || response.data,
color: "success"
});
})
.catch((error) => {
if (error.response) {
if (error.status === 504 || changingStaticIp) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
message: `Connection lost! Try the new static IP at ${useSettingsStore().network.staticIp}:5800 or ${
useSettingsStore().network.hostname
}:5800?`
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
message: error.response.data.text || error.response.data
});
}
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
color: "error",
message: "Error while trying to process the request! The backend didn't respond."
});
} else {
useStateStore().showSnackbarMessage({
color: "error",
message: "An error occurred while trying to process the request."
});
}
});
};
</script>
<template>
<v-card
dark
class="mb-3 pr-6 pb-3"
style="background-color: #006492;"
>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title>Networking</v-card-title>
<div class="ml-5">
<v-form
ref="form"
v-model="settingsValid"
>
<v-form ref="form" v-model="settingsValid">
<cv-input
v-model="useSettingsStore().network.ntServerAddress"
label="Team Number/NetworkTables Server Address"
tooltip="Enter the Team Number or the IP address of the NetworkTables Server"
:label-cols="3"
:disabled="useSettingsStore().network.runNTServer"
:rules="[v => isValidNetworkTablesIP(v) || 'The NetworkTables Server Address must be a valid Team Number, IP address, or Hostname']"
:rules="[
(v) =>
isValidNetworkTablesIP(v) ||
'The NetworkTables Server Address must be a valid Team Number, IP address, or Hostname'
]"
/>
<v-banner
v-show="!isValidNetworkTablesIP(useSettingsStore().network.ntServerAddress) && !useSettingsStore().network.runNTServer"
v-show="
!isValidNetworkTablesIP(useSettingsStore().network.ntServerAddress) &&
!useSettingsStore().network.runNTServer
"
rounded
color="red"
text-color="white"
@@ -110,22 +113,22 @@ const saveGeneralSettings = () => {
v-model="useSettingsStore().network.connectionType"
label="IP Assignment Mode"
tooltip="DHCP will make the radio (router) automatically assign an IP address; this may result in an IP address that changes across reboots. Static IP assignment means that you pick the IP address and it won't change."
:input-cols="12-3"
:list="['DHCP','Static']"
:input-cols="12 - 3"
:list="['DHCP', 'Static']"
/>
<cv-input
v-if="useSettingsStore().network.connectionType === NetworkConnectionType.Static"
v-model="useSettingsStore().network.staticIp"
:input-cols="12-3"
:input-cols="12 - 3"
label="Static IP"
:rules="[v => isValidIPv4(v) || 'Invalid IPv4 address']"
:rules="[(v) => isValidIPv4(v) || 'Invalid IPv4 address']"
/>
<cv-input
v-show="useSettingsStore().network.shouldManage"
v-model="useSettingsStore().network.hostname"
label="Hostname"
:input-cols="12-3"
:rules="[v => isValidHostname(v) || 'Invalid hostname']"
:input-cols="12 - 3"
:rules="[(v) => isValidHostname(v) || 'Invalid hostname']"
/>
<cv-switch
v-model="useSettingsStore().network.runNTServer"
@@ -147,7 +150,7 @@ const saveGeneralSettings = () => {
<v-btn
color="accent"
:class="useSettingsStore().network.runNTServer ? 'mt-3' : ''"
style="color: black; width: 100%;"
style="color: black; width: 100%"
:disabled="!settingsValid && !useSettingsStore().network.runNTServer"
@click="saveGeneralSettings"
>