Link Entra ids to the Azure portal

This commit is contained in:
2026-08-31 16:24:00 -04:00
parent 7081ea5d41
commit 46e14f457d
3 changed files with 104 additions and 2 deletions

View File

@@ -115,6 +115,21 @@ export async function getServicenowUrls() {
return { enabled, incidentUrl, changeUrl, searchUrl }
}
// Deep links into the Azure portal for a device ShopDB knows the Entra ids of.
//
// Both are TEMPLATES held in settings rather than built here, because the host
// is a tenant fact: this site is on the US Government cloud (portal.azure.us)
// and the commercial cloud is portal.azure.com. Microsoft also moves these
// blade paths from time to time, and a site can fix that itself by editing a
// setting instead of waiting for a release.
//
// Empty is the default and means "no link" - the id still renders as text.
export async function getEntraDeviceUrls() {
const entraUrl = await getSetting('entra_device_url', '')
const intuneUrl = await getSetting('intune_device_url', '')
return { entraUrl, intuneUrl }
}
// Printer hostname template. {ip} is replaced with the dash-separated IP.
export async function getPrinterHostnameTemplate() {
return getSetting('printer_hostname_template', 'Printer-{ip}.printer.geaerospace.net') // ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.

View File

@@ -196,13 +196,35 @@
>
<h3 class="section-title">Entra Device Info</h3>
<div class="info-list">
<!-- The link sits beside the id it is built from, not in a row
of its own at the top: the pairing is counterintuitive - the
DEVICE id opens Intune and the OBJECT id opens Entra - and a
separate action row would have to re-label which is which. -->
<div v-if="computer.computer?.azuredeviceid" class="info-row">
<span class="info-label">Device ID</span>
<span class="info-value">{{ computer.computer.azuredeviceid }}</span>
<span class="info-value">
{{ computer.computer.azuredeviceid }}
<a
v-if="intuneDeviceLink"
:href="intuneDeviceLink"
target="_blank"
rel="noopener noreferrer"
class="portal-link"
>Intune</a>
</span>
</div>
<div v-if="computer.computer?.azureobjectid" class="info-row">
<span class="info-label">Object ID</span>
<span class="info-value">{{ computer.computer.azureobjectid }}</span>
<span class="info-value">
{{ computer.computer.azureobjectid }}
<a
v-if="entraDeviceLink"
:href="entraDeviceLink"
target="_blank"
rel="noopener noreferrer"
class="portal-link"
>Entra</a>
</span>
</div>
<div v-if="computer.computer?.azuredevicecategory" class="info-row">
<span class="info-label">Device Category</span>
@@ -342,6 +364,7 @@ import PluginAssetPanels from '@/components/PluginAssetPanels.vue'
import AssetDetailTabs from '@/components/AssetDetailTabs.vue'
import AssetRelationships from '@/components/AssetRelationships.vue'
import { useIdentifierFlags } from '@/composables/identifierSettings'
import { getEntraDeviceUrls } from '@/utils/siteSettings'
import { setRecordTitle } from '@/composables/pageTitle'
import { useToast } from '@/composables/toast'
import { apiError } from '@/utils/apiError'
@@ -374,6 +397,22 @@ const assetPanels = ref(null)
// which is the answer somebody arriving from a search or a link came for.
const activegroup = ref('details')
// Azure portal deep links, built from templates a site configures. Empty
// templates mean no link at all, which is the default: the host differs per
// tenant (this site is on the US Government cloud) and Microsoft moves these
// blade paths, so guessing one would send people to a dead page.
const entraUrlTemplate = ref('')
const intuneUrlTemplate = ref('')
function fillTemplate(template, token, value) {
if (!template || !value) return null
return template.split(token).join(encodeURIComponent(value))
}
const entraDeviceLink = computed(() => fillTemplate(
entraUrlTemplate.value, '{objectid}', computer.value?.computer?.azureobjectid))
const intuneDeviceLink = computed(() => fillTemplate(
intuneUrlTemplate.value, '{deviceid}', computer.value?.computer?.azuredeviceid))
// Only offer a tab that has something in it. An empty tab is worse than an
// absent one - it reads as broken rather than as "this site does not collect
// that". Details always shows because core fills it regardless of plugins.
@@ -451,6 +490,13 @@ function relativeAge(value) {
}
onMounted(async () => {
// Non-critical: a site that has not configured these just gets no link, so a
// failure here must not stop the page loading.
getEntraDeviceUrls().then(({ entraUrl, intuneUrl }) => {
entraUrlTemplate.value = entraUrl
intuneUrlTemplate.value = intuneUrl
}).catch(() => {})
try {
const response = await computersApi.get(route.params.id)
computer.value = response.data.data
@@ -606,6 +652,18 @@ async function confirmRetire() {
</script>
<style scoped>
.portal-link {
font-size: 0.78rem;
margin-left: 0.5rem;
padding: 0.05rem 0.4rem;
border: 1px solid var(--border);
border-radius: 3px;
color: var(--link, var(--primary));
text-decoration: none;
white-space: nowrap;
}
.portal-link:hover { border-color: var(--primary); }
/* PC-specific styles - shared styles are in global style.css */

View File

@@ -1085,6 +1085,35 @@ def build_default_settings():
'category': 'integrations',
'description': 'ServiceNow global-search URL template. {ticket} is the ticket number.'
},
{
# ADR-015-OK: the DEFAULT is empty, so no cloud is assumed. Which
# Azure portal a site uses is a tenant fact, not a product one -
# this site is on the US Government cloud (portal.azure.us), and a
# commercial-cloud default would be silently wrong for it. The link
# simply does not render until a site fills these in.
'key': 'entra_device_url',
'value': '',
'valuetype': 'string',
'category': 'integrations',
'description': (
'Entra device properties URL template. {objectid} is the '
'device object id. Example (US Gov): https://portal.azure.us/'
'?feature.msaljs=false#view/Microsoft_AAD_Devices/'
'DeviceDetailsMenuBlade/~/Properties/objectId/{objectid}/deviceId/'
)
},
{
'key': 'intune_device_url',
'value': '',
'valuetype': 'string',
'category': 'integrations',
'description': (
'Intune device URL template. {deviceid} is the Entra device '
'id. Example (US Gov): https://portal.azure.us/'
'?feature.msaljs=false#view/Microsoft_Intune_Devices/'
'DeviceSettingsMenuBlade/~/overview/aadDeviceId/{deviceid}'
)
},
{
'key': 'servicenow_ticket_prefixes',
'value': 'GEINC,GECHG,GERIT,GESCT',