diff --git a/frontend/src/utils/siteSettings.js b/frontend/src/utils/siteSettings.js index a333a950..7edf5914 100644 --- a/frontend/src/utils/siteSettings.js +++ b/frontend/src/utils/siteSettings.js @@ -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. diff --git a/plugins/computers/frontend/views/PCDetail.vue b/plugins/computers/frontend/views/PCDetail.vue index 044691dc..820aa606 100644 --- a/plugins/computers/frontend/views/PCDetail.vue +++ b/plugins/computers/frontend/views/PCDetail.vue @@ -196,13 +196,35 @@ >

Entra Device Info

+
Device ID - {{ computer.computer.azuredeviceid }} + + {{ computer.computer.azuredeviceid }} + Intune +
Object ID - {{ computer.computer.azureobjectid }} + + {{ computer.computer.azureobjectid }} + Entra +
Device Category @@ -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() {