Expose detailed Windows OS info in machine inventory
This commit is contained in:
parent
3d89c5fd32
commit
64e4e02a9a
1 changed files with 216 additions and 35 deletions
|
|
@ -116,17 +116,6 @@ type WindowsDiskEntry = {
|
||||||
MediaType?: string
|
MediaType?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type WindowsOsInfo = {
|
|
||||||
ProductName?: string
|
|
||||||
CurrentBuild?: string | number
|
|
||||||
CurrentBuildNumber?: string | number
|
|
||||||
DisplayVersion?: string
|
|
||||||
ReleaseId?: string
|
|
||||||
EditionID?: string
|
|
||||||
LicenseStatus?: number
|
|
||||||
IsActivated?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type WindowsExtended = {
|
type WindowsExtended = {
|
||||||
software?: MachineSoftware[]
|
software?: MachineSoftware[]
|
||||||
services?: Array<{ name?: string; status?: string; displayName?: string }>
|
services?: Array<{ name?: string; status?: string; displayName?: string }>
|
||||||
|
|
@ -213,6 +202,105 @@ function readNumber(record: Record<string, unknown>, ...keys: string[]): number
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseWindowsInstallDate(value: unknown): Date | null {
|
||||||
|
if (!value) return null
|
||||||
|
if (value instanceof Date) return value
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
|
return new Date(value)
|
||||||
|
}
|
||||||
|
if (typeof value !== "string") return null
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!trimmed) return null
|
||||||
|
const wmiMatch = trimmed.match(/Date\((\d+)\)/)
|
||||||
|
if (wmiMatch) {
|
||||||
|
const timestamp = Number(wmiMatch[1])
|
||||||
|
if (Number.isFinite(timestamp)) {
|
||||||
|
return new Date(timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const digitsOnly = trimmed.replace(/[^0-9]/g, "")
|
||||||
|
if (digitsOnly.length >= 8 && digitsOnly.length <= 14) {
|
||||||
|
const year = Number(digitsOnly.slice(0, 4))
|
||||||
|
const month = Number(digitsOnly.slice(4, 6)) - 1
|
||||||
|
const day = Number(digitsOnly.slice(6, 8))
|
||||||
|
const hours = Number(digitsOnly.slice(8, 10) || "0")
|
||||||
|
const minutes = Number(digitsOnly.slice(10, 12) || "0")
|
||||||
|
const seconds = Number(digitsOnly.slice(12, 14) || "0")
|
||||||
|
const parsed = new Date(Date.UTC(year, month, day, hours, minutes, seconds))
|
||||||
|
if (!Number.isNaN(parsed.getTime())) {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parsed = new Date(trimmed)
|
||||||
|
return Number.isNaN(parsed.getTime()) ? null : parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowsOsInfo = {
|
||||||
|
productName?: string
|
||||||
|
editionId?: string
|
||||||
|
displayVersion?: string
|
||||||
|
releaseId?: string
|
||||||
|
currentBuild?: string
|
||||||
|
currentBuildNumber?: string
|
||||||
|
licenseStatus?: number
|
||||||
|
isActivated?: boolean
|
||||||
|
installDate?: Date | null
|
||||||
|
experience?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseWindowsOsInfo(raw: unknown): WindowsOsInfo | null {
|
||||||
|
if (!raw) return null
|
||||||
|
const parseRecord = (value: Record<string, unknown>) => {
|
||||||
|
const productName = readString(value, "ProductName", "productName")
|
||||||
|
const editionId = readString(value, "EditionID", "editionId")
|
||||||
|
const displayVersion = readString(value, "DisplayVersion", "displayVersion")
|
||||||
|
const releaseId = readString(value, "ReleaseId", "releaseId")
|
||||||
|
const currentBuild = readString(value, "CurrentBuild", "currentBuild")
|
||||||
|
const currentBuildNumber = readString(value, "CurrentBuildNumber", "currentBuildNumber")
|
||||||
|
const licenseStatus = readNumber(value, "LicenseStatus", "licenseStatus")
|
||||||
|
const isActivatedRaw = value["IsActivated"]
|
||||||
|
const isActivated =
|
||||||
|
typeof isActivatedRaw === "boolean"
|
||||||
|
? isActivatedRaw
|
||||||
|
: typeof isActivatedRaw === "string"
|
||||||
|
? isActivatedRaw.toLowerCase() === "true"
|
||||||
|
: undefined
|
||||||
|
const installDate =
|
||||||
|
parseWindowsInstallDate(value["InstallDate"]) ??
|
||||||
|
parseWindowsInstallDate(value["InstallationDate"]) ??
|
||||||
|
parseWindowsInstallDate(value["InstallDateTime"])
|
||||||
|
const experience = readString(value, "Experience", "experience", "UBR")
|
||||||
|
return {
|
||||||
|
productName,
|
||||||
|
editionId,
|
||||||
|
displayVersion,
|
||||||
|
releaseId,
|
||||||
|
currentBuild,
|
||||||
|
currentBuildNumber,
|
||||||
|
licenseStatus,
|
||||||
|
isActivated,
|
||||||
|
installDate,
|
||||||
|
experience,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
for (const entry of raw) {
|
||||||
|
const record = toRecord(entry)
|
||||||
|
if (record) {
|
||||||
|
return parseRecord(record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (typeof raw === "string") {
|
||||||
|
return { productName: raw }
|
||||||
|
}
|
||||||
|
const record = toRecord(raw)
|
||||||
|
if (!record) return null
|
||||||
|
return parseRecord(record)
|
||||||
|
}
|
||||||
|
|
||||||
function parseBytesLike(value: unknown): number | undefined {
|
function parseBytesLike(value: unknown): number | undefined {
|
||||||
if (typeof value === "number" && Number.isFinite(value)) return value
|
if (typeof value === "number" && Number.isFinite(value)) return value
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
|
|
@ -383,6 +471,14 @@ function formatDate(date?: Date | null) {
|
||||||
return format(date, "dd/MM/yyyy HH:mm")
|
return format(date, "dd/MM/yyyy HH:mm")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatAbsoluteDateTime(date?: Date | null) {
|
||||||
|
if (!date) return "—"
|
||||||
|
return new Intl.DateTimeFormat("pt-BR", {
|
||||||
|
dateStyle: "long",
|
||||||
|
timeStyle: "short",
|
||||||
|
}).format(date)
|
||||||
|
}
|
||||||
|
|
||||||
function formatBytes(bytes?: number | null) {
|
function formatBytes(bytes?: number | null) {
|
||||||
if (!bytes || Number.isNaN(bytes)) return "—"
|
if (!bytes || Number.isNaN(bytes)) return "—"
|
||||||
const units = ["B", "KB", "MB", "GB", "TB"]
|
const units = ["B", "KB", "MB", "GB", "TB"]
|
||||||
|
|
@ -631,24 +727,50 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
const linuxExt = extended?.linux ?? null
|
const linuxExt = extended?.linux ?? null
|
||||||
const windowsExt = extended?.windows ?? null
|
const windowsExt = extended?.windows ?? null
|
||||||
const macosExt = extended?.macos ?? null
|
const macosExt = extended?.macos ?? null
|
||||||
|
const windowsOsInfo = parseWindowsOsInfo(windowsExt?.osInfo)
|
||||||
|
const windowsActivationStatus = windowsOsInfo?.isActivated ?? (typeof windowsOsInfo?.licenseStatus === "number" ? windowsOsInfo.licenseStatus === 1 : null)
|
||||||
const windowsMemoryModulesRaw = windowsExt?.memoryModules
|
const windowsMemoryModulesRaw = windowsExt?.memoryModules
|
||||||
const windowsVideoControllersRaw = windowsExt?.videoControllers
|
const windowsVideoControllersRaw = windowsExt?.videoControllers
|
||||||
const windowsDiskEntriesRaw = windowsExt?.disks
|
const windowsDiskEntriesRaw = windowsExt?.disks
|
||||||
const windowsMemoryModules = Array.isArray(windowsMemoryModulesRaw)
|
const windowsServicesRaw = windowsExt?.services
|
||||||
? windowsMemoryModulesRaw
|
const windowsSoftwareRaw = windowsExt?.software
|
||||||
: windowsMemoryModulesRaw && typeof windowsMemoryModulesRaw === "object"
|
const windowsBaseboardRaw = windowsExt?.baseboard
|
||||||
? [windowsMemoryModulesRaw]
|
const windowsBaseboard = Array.isArray(windowsBaseboardRaw)
|
||||||
: []
|
? windowsBaseboardRaw[0]
|
||||||
const windowsVideoControllers = Array.isArray(windowsVideoControllersRaw)
|
: windowsBaseboardRaw && typeof windowsBaseboardRaw === "object"
|
||||||
? windowsVideoControllersRaw
|
? windowsBaseboardRaw
|
||||||
: windowsVideoControllersRaw && typeof windowsVideoControllersRaw === "object"
|
: null
|
||||||
? [windowsVideoControllersRaw]
|
const windowsSerialNumber = windowsBaseboard ? readString(toRecord(windowsBaseboard) ?? {}, "SerialNumber", "serialNumber") : undefined
|
||||||
: []
|
const windowsMemoryModules = useMemo(() => {
|
||||||
const windowsDiskEntries = Array.isArray(windowsDiskEntriesRaw)
|
if (Array.isArray(windowsMemoryModulesRaw)) return windowsMemoryModulesRaw
|
||||||
? windowsDiskEntriesRaw
|
if (windowsMemoryModulesRaw && typeof windowsMemoryModulesRaw === "object") return [windowsMemoryModulesRaw]
|
||||||
: windowsDiskEntriesRaw && typeof windowsDiskEntriesRaw === "object"
|
return []
|
||||||
? [windowsDiskEntriesRaw]
|
}, [windowsMemoryModulesRaw])
|
||||||
: []
|
const windowsVideoControllers = useMemo(() => {
|
||||||
|
if (Array.isArray(windowsVideoControllersRaw)) return windowsVideoControllersRaw
|
||||||
|
if (windowsVideoControllersRaw && typeof windowsVideoControllersRaw === "object") return [windowsVideoControllersRaw]
|
||||||
|
return []
|
||||||
|
}, [windowsVideoControllersRaw])
|
||||||
|
const windowsDiskEntries = useMemo(() => {
|
||||||
|
if (Array.isArray(windowsDiskEntriesRaw)) return windowsDiskEntriesRaw
|
||||||
|
if (windowsDiskEntriesRaw && typeof windowsDiskEntriesRaw === "object") return [windowsDiskEntriesRaw]
|
||||||
|
return []
|
||||||
|
}, [windowsDiskEntriesRaw])
|
||||||
|
const windowsServices = useMemo(() => {
|
||||||
|
if (Array.isArray(windowsServicesRaw)) return windowsServicesRaw
|
||||||
|
if (windowsServicesRaw && typeof windowsServicesRaw === "object") return [windowsServicesRaw]
|
||||||
|
return []
|
||||||
|
}, [windowsServicesRaw])
|
||||||
|
const windowsSoftware = useMemo(() => {
|
||||||
|
if (Array.isArray(windowsSoftwareRaw)) return windowsSoftwareRaw
|
||||||
|
if (windowsSoftwareRaw && typeof windowsSoftwareRaw === "object") return [windowsSoftwareRaw]
|
||||||
|
return []
|
||||||
|
}, [windowsSoftwareRaw])
|
||||||
|
const windowsEditionLabel = windowsOsInfo?.productName ?? windowsOsInfo?.editionId ?? null
|
||||||
|
const windowsVersionLabel = windowsOsInfo?.displayVersion ?? windowsOsInfo?.releaseId ?? null
|
||||||
|
const windowsBuildLabel = windowsOsInfo?.currentBuildNumber ?? windowsOsInfo?.currentBuild ?? null
|
||||||
|
const windowsInstallDateLabel = windowsOsInfo?.installDate ? formatAbsoluteDateTime(windowsOsInfo.installDate) : null
|
||||||
|
const windowsExperienceLabel = windowsOsInfo?.experience ?? null
|
||||||
const linuxLsblk = linuxExt?.lsblk ?? []
|
const linuxLsblk = linuxExt?.lsblk ?? []
|
||||||
const linuxSmartEntries = linuxExt?.smart ?? []
|
const linuxSmartEntries = linuxExt?.smart ?? []
|
||||||
const normalizedHardwareGpus = Array.isArray(hardware?.gpus)
|
const normalizedHardwareGpus = Array.isArray(hardware?.gpus)
|
||||||
|
|
@ -684,8 +806,6 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
? windowsCpuRaw
|
? windowsCpuRaw
|
||||||
: [windowsCpuRaw]
|
: [windowsCpuRaw]
|
||||||
: []
|
: []
|
||||||
const windowsServices = windowsExt?.services ?? []
|
|
||||||
const windowsSoftware = windowsExt?.software ?? []
|
|
||||||
const winDiskStats = windowsDiskEntries.length > 0
|
const winDiskStats = windowsDiskEntries.length > 0
|
||||||
? {
|
? {
|
||||||
count: windowsDiskEntries.length,
|
count: windowsDiskEntries.length,
|
||||||
|
|
@ -753,6 +873,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
(machine?.persona === "manager" || collaborator?.role === "manager") ? "manager" : "collaborator"
|
(machine?.persona === "manager" || collaborator?.role === "manager") ? "manager" : "collaborator"
|
||||||
)
|
)
|
||||||
const [savingAccess, setSavingAccess] = useState(false)
|
const [savingAccess, setSavingAccess] = useState(false)
|
||||||
|
const [showAllWindowsSoftware, setShowAllWindowsSoftware] = useState(false)
|
||||||
const jsonText = useMemo(() => {
|
const jsonText = useMemo(() => {
|
||||||
const payload = {
|
const payload = {
|
||||||
id: machine?.id,
|
id: machine?.id,
|
||||||
|
|
@ -781,6 +902,15 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
setAccessRole((machine?.persona === "manager" || collaborator?.role === "manager") ? "manager" : "collaborator")
|
setAccessRole((machine?.persona === "manager" || collaborator?.role === "manager") ? "manager" : "collaborator")
|
||||||
}, [machine?.id, machine?.persona, collaborator?.email, collaborator?.name, collaborator?.role])
|
}, [machine?.id, machine?.persona, collaborator?.email, collaborator?.name, collaborator?.role])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShowAllWindowsSoftware(false)
|
||||||
|
}, [machine?.id])
|
||||||
|
|
||||||
|
const displayedWindowsSoftware = useMemo(
|
||||||
|
() => (showAllWindowsSoftware ? windowsSoftware : windowsSoftware.slice(0, 8)),
|
||||||
|
[showAllWindowsSoftware, windowsSoftware]
|
||||||
|
)
|
||||||
|
|
||||||
const handleSaveAccess = async () => {
|
const handleSaveAccess = async () => {
|
||||||
if (!machine) return
|
if (!machine) return
|
||||||
if (!accessEmail.trim()) {
|
if (!accessEmail.trim()) {
|
||||||
|
|
@ -853,14 +983,20 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
||||||
{machine.architecture?.toUpperCase() ?? "Arquitetura indefinida"}
|
{machine.architecture?.toUpperCase() ?? "Arquitetura indefinida"}
|
||||||
</Badge>
|
</Badge>
|
||||||
{windowsExt?.osInfo ? (
|
{windowsOsInfo ? (
|
||||||
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
||||||
Build: {String(windowsExt.osInfo?.CurrentBuildNumber ?? windowsExt.osInfo?.CurrentBuild ?? "—")}
|
Build: {windowsBuildLabel ?? "—"}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
{windowsExt?.osInfo ? (
|
{windowsOsInfo ? (
|
||||||
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
||||||
Ativado: {windowsExt.osInfo?.IsActivated === true ? "Sim" : "Não"}
|
Ativado: {
|
||||||
|
windowsActivationStatus == null
|
||||||
|
? "—"
|
||||||
|
: windowsActivationStatus
|
||||||
|
? "Sim"
|
||||||
|
: "Não"
|
||||||
|
}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
{primaryGpu?.name ? (
|
{primaryGpu?.name ? (
|
||||||
|
|
@ -1251,7 +1387,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
{windowsExt ? (
|
{windowsExt ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* Cards resumidos: CPU / RAM / GPU / Discos */}
|
{/* Cards resumidos: CPU / RAM / GPU / Discos */}
|
||||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-5">
|
||||||
<Card className="border-slate-200">
|
<Card className="border-slate-200">
|
||||||
<CardContent className="flex items-center gap-3 py-3">
|
<CardContent className="flex items-center gap-3 py-3">
|
||||||
<Cpu className="size-5 text-slate-500" />
|
<Cpu className="size-5 text-slate-500" />
|
||||||
|
|
@ -1270,6 +1406,20 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
<Card className="border-slate-200">
|
||||||
|
<CardContent className="flex items-center gap-3 py-3">
|
||||||
|
<Terminal className="size-5 text-slate-500" />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="truncate text-xs text-muted-foreground">Windows</p>
|
||||||
|
<p className="truncate text-sm font-semibold text-foreground">
|
||||||
|
{windowsEditionLabel ?? machine?.osName ?? "—"}
|
||||||
|
</p>
|
||||||
|
<p className="truncate text-xs text-muted-foreground">
|
||||||
|
{windowsVersionLabel ?? windowsBuildLabel ?? "Versão desconhecida"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
<Card className="border-slate-200">
|
<Card className="border-slate-200">
|
||||||
<CardContent className="flex items-center gap-3 py-3">
|
<CardContent className="flex items-center gap-3 py-3">
|
||||||
<Monitor className="size-5 text-slate-500" />
|
<Monitor className="size-5 text-slate-500" />
|
||||||
|
|
@ -1289,6 +1439,24 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
{windowsOsInfo ? (
|
||||||
|
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
|
||||||
|
<p className="text-xs font-semibold uppercase text-slate-500">Informações do Windows</p>
|
||||||
|
<div className="mt-2 grid gap-1 text-sm text-muted-foreground">
|
||||||
|
<DetailLine label="Edição" value={windowsEditionLabel ?? "—"} classNameValue="break-words" />
|
||||||
|
<DetailLine label="Versão" value={windowsVersionLabel ?? "—"} />
|
||||||
|
<DetailLine label="Compilação do SO" value={windowsBuildLabel ?? "—"} />
|
||||||
|
<DetailLine label="Instalado em" value={windowsInstallDateLabel ?? "—"} />
|
||||||
|
<DetailLine label="Experiência" value={windowsExperienceLabel ?? "—"} />
|
||||||
|
<DetailLine
|
||||||
|
label="Ativação"
|
||||||
|
value={windowsActivationStatus == null ? "—" : windowsActivationStatus ? "Ativado" : "Não ativado"}
|
||||||
|
/>
|
||||||
|
<DetailLine label="Número de série" value={windowsSerialNumber ?? "—"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{windowsCpuDetails.length > 0 ? (
|
{windowsCpuDetails.length > 0 ? (
|
||||||
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
|
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
|
||||||
<p className="text-xs font-semibold uppercase text-slate-500">CPU</p>
|
<p className="text-xs font-semibold uppercase text-slate-500">CPU</p>
|
||||||
|
|
@ -1359,9 +1527,22 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
|
|
||||||
{windowsSoftware.length > 0 ? (
|
{windowsSoftware.length > 0 ? (
|
||||||
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
|
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
|
||||||
<p className="text-xs font-semibold uppercase text-slate-500">Softwares (amostra)</p>
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<p className="text-xs font-semibold uppercase text-slate-500">Aplicativos instalados</p>
|
||||||
|
{windowsSoftware.length > 8 ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-2 text-xs text-slate-600 hover:bg-slate-200/60"
|
||||||
|
onClick={() => setShowAllWindowsSoftware((prev) => !prev)}
|
||||||
|
>
|
||||||
|
{showAllWindowsSoftware ? "Mostrar menos" : `Ver todos (${windowsSoftware.length})`}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<ul className="mt-2 grid gap-1 text-xs text-muted-foreground">
|
<ul className="mt-2 grid gap-1 text-xs text-muted-foreground">
|
||||||
{windowsSoftware.slice(0, 8).map((softwareItem, index) => {
|
{displayedWindowsSoftware.map((softwareItem, index) => {
|
||||||
const record = toRecord(softwareItem) ?? {}
|
const record = toRecord(softwareItem) ?? {}
|
||||||
const name = readString(record, "DisplayName", "name") ?? "—"
|
const name = readString(record, "DisplayName", "name") ?? "—"
|
||||||
const version = readString(record, "DisplayVersion", "version")
|
const version = readString(record, "DisplayVersion", "version")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue