dashboarddefaults: pick a kiosk from a dropdown (auto-fill FQDN) + Location wording
Adds GET /api/computers/display-kiosks - the displays that reported in (Kiosk type), each with its derived FQDN (F<serial>.<domain>). The Dashboard Defaults form gets a kiosk dropdown that fills the FQDN so admins pick a display instead of typing an IP; IP stays an optional manual field. Table shows FQDN or IP. 'Business Unit' label -> 'Location' on this page + the settings nav.
This commit is contained in:
@@ -120,6 +120,9 @@ export const computersApi = {
|
|||||||
list(params = {}) {
|
list(params = {}) {
|
||||||
return api.get('/computers', { params })
|
return api.get('/computers', { params })
|
||||||
},
|
},
|
||||||
|
displayKiosks() {
|
||||||
|
return api.get('/computers/display-kiosks')
|
||||||
|
},
|
||||||
get(id) {
|
get(id) {
|
||||||
return api.get(`/computers/${id}`)
|
return api.get(`/computers/${id}`)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,16 +19,16 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>IP Address</th>
|
<th>Kiosk (FQDN / IP)</th>
|
||||||
<th>Display</th>
|
<th>Display</th>
|
||||||
<th>Business Unit</th>
|
<th>Location</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="d in items" :key="d.dashboarddefaultid">
|
<tr v-for="d in items" :key="d.dashboarddefaultid">
|
||||||
<td class="mono">{{ d.ipaddress }}</td>
|
<td class="mono">{{ d.fqdn || d.ipaddress }}</td>
|
||||||
<td>{{ roleLabel(d.displayrole) }}</td>
|
<td>{{ roleLabel(d.displayrole) }}</td>
|
||||||
<td>{{ d.businessunit || '-' }}</td>
|
<td>{{ d.businessunit || '-' }}</td>
|
||||||
<td class="cell-truncate" :title="d.description">{{ d.description || '-' }}</td>
|
<td class="cell-truncate" :title="d.description">{{ d.description || '-' }}</td>
|
||||||
@@ -57,9 +57,19 @@
|
|||||||
<form @submit.prevent="save">
|
<form @submit.prevent="save">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="ipaddress">IP Address *</label>
|
<label for="kiosk">Kiosk</label>
|
||||||
|
<select id="kiosk" v-model="form.fqdn" class="form-control">
|
||||||
|
<option value="">Select a kiosk (or enter an IP below)...</option>
|
||||||
|
<option v-for="k in kiosks" :key="k.computerid" :value="k.fqdn" :disabled="!k.fqdn">
|
||||||
|
{{ k.hostname }}{{ k.fqdn ? ' - ' + k.fqdn : ' (no serial reported yet)' }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-hint">Pick a reporting display to fill its FQDN, or enter an IP manually below.</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ipaddress">IP Address (optional)</label>
|
||||||
<input id="ipaddress" v-model="form.ipaddress" type="text" class="form-control"
|
<input id="ipaddress" v-model="form.ipaddress" type="text" class="form-control"
|
||||||
placeholder="e.g., 10.20.30.40" required />
|
placeholder="e.g., 10.20.30.40" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="displayrole">Display *</label>
|
<label for="displayrole">Display *</label>
|
||||||
@@ -68,10 +78,10 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" v-if="form.displayrole === 'dashboard'">
|
<div class="form-group" v-if="form.displayrole === 'dashboard'">
|
||||||
<label for="businessunitid">Business Unit *</label>
|
<label for="businessunitid">Location *</label>
|
||||||
<select id="businessunitid" v-model="form.businessunitid" class="form-control"
|
<select id="businessunitid" v-model="form.businessunitid" class="form-control"
|
||||||
:required="form.displayrole === 'dashboard'">
|
:required="form.displayrole === 'dashboard'">
|
||||||
<option value="">Select business unit...</option>
|
<option value="">Select location...</option>
|
||||||
<option v-for="bu in businessUnits" :key="bu.businessunitid" :value="bu.businessunitid">
|
<option v-for="bu in businessUnits" :key="bu.businessunitid" :value="bu.businessunitid">
|
||||||
{{ bu.businessunit }}
|
{{ bu.businessunit }}
|
||||||
</option>
|
</option>
|
||||||
@@ -112,7 +122,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { dashboardDefaultsApi, businessUnitsApi } from '../../api'
|
import { dashboardDefaultsApi, businessUnitsApi, computersApi } from '../../api'
|
||||||
import { useToast } from '../../composables/toast'
|
import { useToast } from '../../composables/toast'
|
||||||
import { apiError } from '../../utils/apiError'
|
import { apiError } from '../../utils/apiError'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -128,6 +138,7 @@ function roleLabel(value) {
|
|||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
const businessUnits = ref([])
|
const businessUnits = ref([])
|
||||||
|
const kiosks = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
@@ -138,7 +149,7 @@ const error = ref('')
|
|||||||
const showDeleteModal = ref(false)
|
const showDeleteModal = ref(false)
|
||||||
const toDelete = ref(null)
|
const toDelete = ref(null)
|
||||||
|
|
||||||
const form = ref({ ipaddress: '', displayrole: 'dashboard', businessunitid: '', description: '' })
|
const form = ref({ fqdn: '', ipaddress: '', displayrole: 'dashboard', businessunitid: '', description: '' })
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -147,6 +158,12 @@ onMounted(async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading business units:', err)
|
console.error('Error loading business units:', err)
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
const kioskResp = await computersApi.displayKiosks()
|
||||||
|
kiosks.value = kioskResp.data.data || []
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading display kiosks:', err)
|
||||||
|
}
|
||||||
await loadData()
|
await loadData()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -165,11 +182,12 @@ async function loadData() {
|
|||||||
function openModal(item = null) {
|
function openModal(item = null) {
|
||||||
editing.value = item
|
editing.value = item
|
||||||
form.value = item ? {
|
form.value = item ? {
|
||||||
|
fqdn: item.fqdn || '',
|
||||||
ipaddress: item.ipaddress || '',
|
ipaddress: item.ipaddress || '',
|
||||||
displayrole: item.displayrole || 'dashboard',
|
displayrole: item.displayrole || 'dashboard',
|
||||||
businessunitid: item.businessunitid || '',
|
businessunitid: item.businessunitid || '',
|
||||||
description: item.description || ''
|
description: item.description || ''
|
||||||
} : { ipaddress: '', displayrole: 'dashboard', businessunitid: '', description: '' }
|
} : { fqdn: '', ipaddress: '', displayrole: 'dashboard', businessunitid: '', description: '' }
|
||||||
error.value = ''
|
error.value = ''
|
||||||
showModal.value = true
|
showModal.value = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export const settingsGroups = [
|
|||||||
{
|
{
|
||||||
title: 'Displays & Kiosks',
|
title: 'Displays & Kiosks',
|
||||||
cards: [
|
cards: [
|
||||||
{ to: '/settings/dashboarddefaults', icon: MonitorSmartphone, title: 'Dashboard Defaults', description: 'Map kiosk IPs to a default business unit' },
|
{ to: '/settings/dashboarddefaults', icon: MonitorSmartphone, title: 'Dashboard Defaults', description: 'Map kiosk IPs to a default location' },
|
||||||
{ to: '/settings/notificationtypes', icon: Bell, title: 'Notification Types', description: 'Manage notification types, display styles, colors, and auto-expiry' },
|
{ to: '/settings/notificationtypes', icon: Bell, title: 'Notification Types', description: 'Manage notification types, display styles, colors, and auto-expiry' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -298,6 +298,42 @@ def _sync_access_methods(comp, data):
|
|||||||
# Computers CRUD
|
# Computers CRUD
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
@computers_bp.route('/display-kiosks', methods=['GET'])
|
||||||
|
@jwt_required(optional=True)
|
||||||
|
def list_display_kiosks():
|
||||||
|
"""Reporting display kiosks, for the Dashboard Defaults picker.
|
||||||
|
|
||||||
|
Returns the computers whose type is the one gea-shopfloor-display maps to
|
||||||
|
(default 'Kiosk'), each with its DERIVED FQDN (F<serial>.<domain>, domain
|
||||||
|
from the display_fqdn_domain setting) so an admin picks a kiosk from a
|
||||||
|
dropdown instead of typing an IP/FQDN. Keeps the same F<serial>.<domain>
|
||||||
|
format as core derive_display_fqdn (duplicated to avoid a contract bump).
|
||||||
|
"""
|
||||||
|
from ..pctypemap import pctype_mapping
|
||||||
|
from shopdb.api import Setting
|
||||||
|
|
||||||
|
display_type_name = pctype_mapping().get('gea-shopfloor-display', 'Kiosk')
|
||||||
|
ctype = ComputerType.query.filter_by(computertype=display_type_name).first()
|
||||||
|
if not ctype:
|
||||||
|
return success_response([])
|
||||||
|
domain = (Setting.get('display_fqdn_domain', 'device.geaerospace.net')
|
||||||
|
or 'device.geaerospace.net').strip().strip('.')
|
||||||
|
rows = (db.session.query(Computer).join(Asset)
|
||||||
|
.filter(Computer.computertypeid == ctype.computertypeid,
|
||||||
|
Asset.isactive == True)
|
||||||
|
.order_by(Computer.hostname).all())
|
||||||
|
out = []
|
||||||
|
for comp in rows:
|
||||||
|
serial = (comp.asset.serialnumber or '').strip() if comp.asset else ''
|
||||||
|
out.append({
|
||||||
|
'computerid': comp.computerid,
|
||||||
|
'hostname': comp.hostname,
|
||||||
|
'serialnumber': serial or None,
|
||||||
|
'fqdn': f'F{serial}.{domain}'.lower() if serial else None,
|
||||||
|
})
|
||||||
|
return success_response(out)
|
||||||
|
|
||||||
|
|
||||||
@computers_bp.route('', methods=['GET'])
|
@computers_bp.route('', methods=['GET'])
|
||||||
@jwt_required(optional=True)
|
@jwt_required(optional=True)
|
||||||
def list_computers():
|
def list_computers():
|
||||||
|
|||||||
45
tests/test_core/test_display_kiosks.py
Normal file
45
tests/test_core/test_display_kiosks.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"""The /computers/display-kiosks picker: reporting displays with a derived FQDN.
|
||||||
|
|
||||||
|
Feeds the Dashboard Defaults kiosk dropdown so an admin picks a display instead
|
||||||
|
of typing an IP. The FQDN is F<serial>.<domain> (device naming), derived from the
|
||||||
|
BIOS serial the collector already reports.
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from shopdb.core.models import AssetType
|
||||||
|
from plugins.computers.models import ComputerType
|
||||||
|
|
||||||
|
KEY = 'testcollectorkey'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def collector_key(app):
|
||||||
|
old = app.config.get('COLLECTOR_API_KEY')
|
||||||
|
app.config['COLLECTOR_API_KEY'] = KEY
|
||||||
|
yield KEY
|
||||||
|
app.config['COLLECTOR_API_KEY'] = old
|
||||||
|
|
||||||
|
|
||||||
|
def test_display_kiosks_derives_fqdn(client, db, collector_key):
|
||||||
|
db.session.add(AssetType(assettype='computer', pluginname='computers',
|
||||||
|
tablename='computers'))
|
||||||
|
db.session.add(ComputerType(computertype='Kiosk'))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# a display reports in via the collector (pctype -> Kiosk type, with serial)
|
||||||
|
resp = client.post('/api/collector/computers', json={
|
||||||
|
'hostname': 'WJDISP01', 'serialnumber': 'ABC1234',
|
||||||
|
'pctype': 'gea-shopfloor-display',
|
||||||
|
}, headers={'X-API-Key': KEY})
|
||||||
|
assert resp.status_code == 200, resp.data
|
||||||
|
|
||||||
|
rows = client.get('/api/computers/display-kiosks').get_json()['data']
|
||||||
|
match = [k for k in rows if k['hostname'] == 'WJDISP01']
|
||||||
|
assert match, rows
|
||||||
|
assert match[0]['fqdn'] == 'fabc1234.device.geaerospace.net'
|
||||||
|
assert match[0]['serialnumber'] == 'ABC1234'
|
||||||
|
|
||||||
|
|
||||||
|
def test_display_kiosks_empty_when_no_kiosk_type(client, db):
|
||||||
|
# No 'Kiosk' ComputerType -> empty list, not an error.
|
||||||
|
assert client.get('/api/computers/display-kiosks').get_json()['data'] == []
|
||||||
Reference in New Issue
Block a user