mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-18 13:06:30 -06:00
migrate napalm device status to typescript
This commit is contained in:
parent
4827cd24d8
commit
08b955f8b6
BIN
netbox/project-static/dist/config.js
vendored
BIN
netbox/project-static/dist/config.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/config.js.map
vendored
BIN
netbox/project-static/dist/config.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/jobs.js
vendored
BIN
netbox/project-static/dist/jobs.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/jobs.js.map
vendored
BIN
netbox/project-static/dist/jobs.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js
vendored
BIN
netbox/project-static/dist/lldp.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js.map
vendored
BIN
netbox/project-static/dist/lldp.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/status.js
vendored
Normal file
BIN
netbox/project-static/dist/status.js
vendored
Normal file
Binary file not shown.
BIN
netbox/project-static/dist/status.js.map
vendored
Normal file
BIN
netbox/project-static/dist/status.js.map
vendored
Normal file
Binary file not shown.
@ -5,7 +5,7 @@
|
||||
"license": "Apache2",
|
||||
"scripts": {
|
||||
"bundle:css": "parcel build --public-url /static -o netbox.css main.scss && parcel build --public-url /static -o rack_elevation.css rack_elevation.scss",
|
||||
"bundle:js": "parcel build --public-url /static -o netbox.js src/index.ts && parcel build --public-url /static -o jobs.js src/jobs.ts && parcel build --public-url /static -o lldp.js src/device/lldp.ts && parcel build --public-url /static -o config.js src/device/config.ts",
|
||||
"bundle:js": "parcel build --public-url /static -o netbox.js src/index.ts && parcel build --public-url /static -o jobs.js src/jobs.ts && parcel build --public-url /static -o lldp.js src/device/lldp.ts && parcel build --public-url /static -o config.js src/device/config.ts && parcel build --public-url /static -o status.js src/device/status.ts",
|
||||
"bundle": "yarn bundle:css && yarn bundle:js"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -16,6 +16,7 @@
|
||||
"clipboard": "2.0.6",
|
||||
"color2k": "^1.2.4",
|
||||
"cookie": "^0.4.1",
|
||||
"dayjs": "^1.10.4",
|
||||
"flatpickr": "4.6.3",
|
||||
"jquery": "3.5.1",
|
||||
"jquery-ui": "1.12.1",
|
||||
|
@ -13,6 +13,7 @@ function initConfig() {
|
||||
.then(data => {
|
||||
if (hasError(data)) {
|
||||
createToast('danger', 'Error Fetching Device Config', data.error).show();
|
||||
return;
|
||||
} else {
|
||||
const configTypes = [
|
||||
'running',
|
||||
|
379
netbox/project-static/src/device/status.ts
Normal file
379
netbox/project-static/src/device/status.ts
Normal file
@ -0,0 +1,379 @@
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import advancedFormat from 'dayjs/plugin/advancedFormat';
|
||||
|
||||
import { createToast } from '../bs';
|
||||
import { apiGetBase, getNetboxData, hasError, toggleLoader, createElement, cToF } from '../util';
|
||||
|
||||
type Uptime = {
|
||||
utc: string;
|
||||
zoned: string | null;
|
||||
duration: string;
|
||||
};
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(advancedFormat);
|
||||
dayjs.extend(duration);
|
||||
|
||||
const factKeys = [
|
||||
'hostname',
|
||||
'fqdn',
|
||||
'vendor',
|
||||
'model',
|
||||
'serial_number',
|
||||
'os_version',
|
||||
] as (keyof DeviceFacts)[];
|
||||
|
||||
type DurationKeys = 'years' | 'months' | 'days' | 'hours' | 'minutes' | 'seconds';
|
||||
const formatKeys = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'] as DurationKeys[];
|
||||
|
||||
/**
|
||||
* From a number of seconds that have elapsed since reboot, extract human-readable dates in the
|
||||
* following formats:
|
||||
* - Relative time since reboot (e.g. 1 month, 28 days, 1 hour, 30 seconds).
|
||||
* - Time stamp in browser-relative timezone.
|
||||
* - Time stamp in UTC.
|
||||
* @param seconds Seconds since reboot.
|
||||
*/
|
||||
function getUptime(seconds: number): Uptime {
|
||||
const relDate = new Date();
|
||||
|
||||
// Get the user's UTC offset, to determine if the user is in UTC or not.
|
||||
const offset = relDate.getTimezoneOffset();
|
||||
const relNow = dayjs(relDate);
|
||||
|
||||
// Get a dayjs object for the device reboot time (now - number of seconds).
|
||||
const relThen = relNow.subtract(seconds, 'seconds');
|
||||
|
||||
// Get a human-readable version of the time in UTC.
|
||||
const utc = relThen.tz('Etc/UTC').format('YYYY-MM-DD HH:MM:ss z');
|
||||
|
||||
// We only want to show the UTC time if the user is not already in UTC time.
|
||||
let zoned = null;
|
||||
if (offset !== 0) {
|
||||
// If the user is not in UTC time, return a human-readable version in the user's timezone.
|
||||
zoned = relThen.format('YYYY-MM-DD HH:MM:ss z');
|
||||
}
|
||||
// Get a dayjs duration object to create a human-readable relative time string.
|
||||
const between = dayjs.duration(seconds, 'seconds');
|
||||
|
||||
// Array of all non-zero-value duration properties. For example, if duration.year() is 0, we
|
||||
// don't care about it and shouldn't show it to the user.
|
||||
let parts = [] as string[];
|
||||
for (const key of formatKeys) {
|
||||
// Get the property value. For example, duration.year(), duration.month(), etc.
|
||||
const value = between[key]();
|
||||
if (value === 1) {
|
||||
// If the duration for this key is 1, drop the trailing 's'. For example, '1 seconds' would
|
||||
// become '1 second'.
|
||||
const label = key.replace(/s$/, '');
|
||||
parts = [...parts, `${value} ${label}`];
|
||||
} else if (value > 1) {
|
||||
// If the duration for this key is more than one, add it to the array as-is.
|
||||
parts = [...parts, `${value} ${key}`];
|
||||
}
|
||||
}
|
||||
// Set the duration to something safe, so we don't show 'undefined' or an empty string to the user.
|
||||
let duration = 'None';
|
||||
if (parts.length > 0) {
|
||||
// If the array actually has elements, reassign the duration to a human-readable version.
|
||||
duration = parts.join(', ');
|
||||
}
|
||||
|
||||
return { utc, zoned, duration };
|
||||
}
|
||||
|
||||
/**
|
||||
* After the `get_facts` result is received, parse its content and update HTML elements
|
||||
* accordingly.
|
||||
*
|
||||
* @param facts NAPALM Device Facts
|
||||
*/
|
||||
function processFacts(facts: DeviceFacts) {
|
||||
for (const key of factKeys) {
|
||||
if (key in facts) {
|
||||
// Find the target element which should have its innerHTML/innerText set to a NAPALM value.
|
||||
const element = document.getElementById(key);
|
||||
if (element !== null) {
|
||||
element.innerHTML = String(facts[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
const { uptime } = facts;
|
||||
const { utc, zoned, duration } = getUptime(uptime);
|
||||
|
||||
// Find the duration (relative time) element and set its value.
|
||||
const uptimeDurationElement = document.getElementById('uptime-duration');
|
||||
if (uptimeDurationElement !== null) {
|
||||
uptimeDurationElement.innerHTML = duration;
|
||||
}
|
||||
// Find the time stamp element and set its value.
|
||||
const uptimeElement = document.getElementById('uptime');
|
||||
if (uptimeElement !== null) {
|
||||
if (zoned === null) {
|
||||
// If the user is in UTC time, only add the UTC time stamp.
|
||||
uptimeElement.innerHTML = utc;
|
||||
} else {
|
||||
// Otherwise, add both time stamps.
|
||||
uptimeElement.innerHTML = [zoned, `<span class="fst-italic d-block">${utc}</span>`].join('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a title row before the next table row. The title row describes each environment key/value
|
||||
* pair from the NAPALM response.
|
||||
*
|
||||
* @param next Next adjacent element. For example, if this is the CPU data, `next` would be the
|
||||
* memory row.
|
||||
* @param title1 Column 1 Title
|
||||
* @param title2 Column 2 Title
|
||||
*/
|
||||
function insertTitleRow<E extends HTMLElement>(next: E, title1: string, title2: string): void {
|
||||
// Create cell element that contains the key title.
|
||||
const col1Title = createElement('th', { innerText: title1 }, ['border-end', 'text-end']);
|
||||
// Create cell element that contains the value title.
|
||||
const col2Title = createElement('th', { innerText: title2 }, ['border-start', 'text-start']);
|
||||
// Create title row element with the two header cells as children.
|
||||
const titleRow = createElement('tr', {}, [], [col1Title, col2Title]);
|
||||
// Insert the entire row just before the beginning of the next row (i.e., at the end of this row).
|
||||
next.insertAdjacentElement('beforebegin', titleRow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a "No Data" row, for when the NAPALM response doesn't contain this type of data.
|
||||
*
|
||||
* @param next Next adjacent element.For example, if this is the CPU data, `next` would be the
|
||||
* memory row.
|
||||
*/
|
||||
function insertNoneRow<E extends Nullable<HTMLElement>>(next: E) {
|
||||
const none = createElement('td', { colSpan: '2', innerText: 'No Data' }, [
|
||||
'text-muted',
|
||||
'text-center',
|
||||
]);
|
||||
const titleRow = createElement('tr', {}, [], [none]);
|
||||
if (next !== null) {
|
||||
next.insertAdjacentElement('beforebegin', titleRow);
|
||||
}
|
||||
}
|
||||
|
||||
function getNext<E extends HTMLElement>(id: string): Nullable<E> {
|
||||
const element = document.getElementById(id);
|
||||
if (element !== null) {
|
||||
return element.nextElementSibling as Nullable<E>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create & insert table rows for each CPU in the NAPALM response.
|
||||
*
|
||||
* @param cpu NAPALM CPU data.
|
||||
*/
|
||||
function processCpu(cpu: DeviceEnvironment['cpu']) {
|
||||
// Find the next adjacent element, so we can insert elements before it.
|
||||
const next = getNext<HTMLTableRowElement>('status-cpu');
|
||||
if (typeof cpu !== 'undefined') {
|
||||
if (next !== null) {
|
||||
insertTitleRow(next, 'Name', 'Usage');
|
||||
for (const [core, data] of Object.entries(cpu)) {
|
||||
const usage = data['%usage'];
|
||||
const kCell = createElement('td', { innerText: core }, ['border-end', 'text-end']);
|
||||
const vCell = createElement('td', { innerText: `${usage} %` }, [
|
||||
'border-start',
|
||||
'text-start',
|
||||
]);
|
||||
const row = createElement('tr', {}, [], [kCell, vCell]);
|
||||
next.insertAdjacentElement('beforebegin', row);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
insertNoneRow(next);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create & insert table rows for the memory in the NAPALM response.
|
||||
*
|
||||
* @param mem NAPALM memory data.
|
||||
*/
|
||||
function processMemory(mem: DeviceEnvironment['memory']) {
|
||||
// Find the next adjacent element, so we can insert elements before it.
|
||||
const next = getNext<HTMLTableRowElement>('status-memory');
|
||||
if (typeof mem !== 'undefined') {
|
||||
if (next !== null) {
|
||||
insertTitleRow(next, 'Available', 'Used');
|
||||
const { available_ram: avail, used_ram: used } = mem;
|
||||
const aCell = createElement('td', { innerText: avail }, ['border-end', 'text-end']);
|
||||
const uCell = createElement('td', { innerText: used }, ['border-start', 'text-start']);
|
||||
const row = createElement('tr', {}, [], [aCell, uCell]);
|
||||
next.insertAdjacentElement('beforebegin', row);
|
||||
}
|
||||
} else {
|
||||
insertNoneRow(next);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create & insert table rows for each temperature sensor in the NAPALM response.
|
||||
*
|
||||
* @param temp NAPALM temperature data.
|
||||
*/
|
||||
function processTemp(temp: DeviceEnvironment['temperature']) {
|
||||
// Find the next adjacent element, so we can insert elements before it.
|
||||
const next = getNext<HTMLTableRowElement>('status-temperature');
|
||||
if (typeof temp !== 'undefined') {
|
||||
if (next !== null) {
|
||||
insertTitleRow(next, 'Sensor', 'Value');
|
||||
for (const [sensor, data] of Object.entries(temp)) {
|
||||
const tempC = data.temperature;
|
||||
const tempF = cToF(tempC);
|
||||
const innerHTML = `${tempC} °C <span class="ms-1 text-muted small">${tempF} °F</span>`;
|
||||
const status = data.is_alert ? 'warning' : data.is_critical ? 'danger' : 'success';
|
||||
const kCell = createElement('td', { innerText: sensor }, ['border-end', 'text-end']);
|
||||
const vCell = createElement('td', { innerHTML }, ['border-start', 'text-start']);
|
||||
const row = createElement('tr', {}, [`table-${status}`], [kCell, vCell]);
|
||||
next.insertAdjacentElement('beforebegin', row);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
insertNoneRow(next);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create & insert table rows for each fan in the NAPALM response.
|
||||
*
|
||||
* @param fans NAPALM fan data.
|
||||
*/
|
||||
function processFans(fans: DeviceEnvironment['fans']) {
|
||||
// Find the next adjacent element, so we can insert elements before it.
|
||||
const next = getNext<HTMLTableRowElement>('status-fans');
|
||||
if (typeof fans !== 'undefined') {
|
||||
if (next !== null) {
|
||||
insertTitleRow(next, 'Fan', 'Status');
|
||||
for (const [fan, data] of Object.entries(fans)) {
|
||||
const { status } = data;
|
||||
const goodIcon = createElement('i', {}, ['mdi', 'mdi-check-bold', 'text-success']);
|
||||
const badIcon = createElement('i', {}, ['mdi', 'mdi-close', 'text-warning']);
|
||||
const kCell = createElement('td', { innerText: fan }, ['border-end', 'text-end']);
|
||||
const vCell = createElement(
|
||||
'td',
|
||||
{},
|
||||
['border-start', 'text-start'],
|
||||
[status ? goodIcon : badIcon],
|
||||
);
|
||||
const row = createElement(
|
||||
'tr',
|
||||
{},
|
||||
[`table-${status ? 'success' : 'warning'}`],
|
||||
[kCell, vCell],
|
||||
);
|
||||
next.insertAdjacentElement('beforebegin', row);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
insertNoneRow(next);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create & insert table rows for each PSU in the NAPALM response.
|
||||
*
|
||||
* @param power NAPALM power data.
|
||||
*/
|
||||
function processPower(power: DeviceEnvironment['power']) {
|
||||
// Find the next adjacent element, so we can insert elements before it.
|
||||
const next = getNext<HTMLTableRowElement>('status-power');
|
||||
if (typeof power !== 'undefined') {
|
||||
if (next !== null) {
|
||||
insertTitleRow(next, 'PSU', 'Status');
|
||||
for (const [psu, data] of Object.entries(power)) {
|
||||
const { status } = data;
|
||||
const goodIcon = createElement('i', {}, ['mdi', 'mdi-check-bold', 'text-success']);
|
||||
const badIcon = createElement('i', {}, ['mdi', 'mdi-close', 'text-warning']);
|
||||
const kCell = createElement('td', { innerText: psu }, ['border-end', 'text-end']);
|
||||
const vCell = createElement(
|
||||
'td',
|
||||
{},
|
||||
['border-start', 'text-start'],
|
||||
[status ? goodIcon : badIcon],
|
||||
);
|
||||
const row = createElement(
|
||||
'tr',
|
||||
{},
|
||||
[`table-${status ? 'success' : 'warning'}`],
|
||||
[kCell, vCell],
|
||||
);
|
||||
next.insertAdjacentElement('beforebegin', row);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
insertNoneRow(next);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After the `get_environment` result is received, parse its content and update HTML elements
|
||||
* accordingly.
|
||||
*
|
||||
* @param env NAPALM Device Environment
|
||||
*/
|
||||
function processEnvironment(env: DeviceEnvironment) {
|
||||
const { cpu, memory, temperature, fans, power } = env;
|
||||
processCpu(cpu);
|
||||
processMemory(memory);
|
||||
processTemp(temperature);
|
||||
processFans(fans);
|
||||
processPower(power);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize NAPALM device status handlers.
|
||||
*/
|
||||
function initStatus() {
|
||||
// Show loading state for both Facts & Environment cards.
|
||||
toggleLoader('show');
|
||||
|
||||
const url = getNetboxData('data-object-url');
|
||||
|
||||
if (url !== null) {
|
||||
apiGetBase<DeviceStatus>(url)
|
||||
.then(data => {
|
||||
if (hasError(data)) {
|
||||
// If the API returns an error, show it to the user.
|
||||
createToast('danger', 'Error Fetching Device Status', data.error).show();
|
||||
} else {
|
||||
if (!hasError(data.get_facts)) {
|
||||
processFacts(data.get_facts);
|
||||
} else {
|
||||
// If the device facts data contains an error, show it to the user.
|
||||
createToast('danger', 'Error Fetching Device Facts', data.get_facts.error).show();
|
||||
}
|
||||
if (!hasError(data.get_environment)) {
|
||||
processEnvironment(data.get_environment);
|
||||
} else {
|
||||
// If the device environment data contains an error, show it to the user.
|
||||
createToast(
|
||||
'danger',
|
||||
'Error Fetching Device Environment Data',
|
||||
data.get_environment.error,
|
||||
).show();
|
||||
}
|
||||
}
|
||||
return;
|
||||
})
|
||||
.finally(() => toggleLoader('hide'));
|
||||
} else {
|
||||
toggleLoader('hide');
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState !== 'loading') {
|
||||
initStatus();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', initStatus);
|
||||
}
|
41
netbox/project-static/src/global.d.ts
vendored
41
netbox/project-static/src/global.d.ts
vendored
@ -121,6 +121,47 @@ type DeviceConfig = {
|
||||
};
|
||||
};
|
||||
|
||||
type DeviceEnvironment = {
|
||||
cpu?: {
|
||||
[core: string]: { '%usage': number };
|
||||
};
|
||||
memory?: {
|
||||
available_ram: number;
|
||||
used_ram: number;
|
||||
};
|
||||
power?: {
|
||||
[psu: string]: { capacity: number; output: number; status: boolean };
|
||||
};
|
||||
temperature?: {
|
||||
[sensor: string]: {
|
||||
is_alert: boolean;
|
||||
is_critical: boolean;
|
||||
temperature: number;
|
||||
};
|
||||
};
|
||||
fans?: {
|
||||
[fan: string]: {
|
||||
status: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type DeviceFacts = {
|
||||
fqdn: string;
|
||||
hostname: string;
|
||||
interface_list: string[];
|
||||
model: string;
|
||||
os_version: string;
|
||||
serial_number: string;
|
||||
uptime: number;
|
||||
vendor: string;
|
||||
};
|
||||
|
||||
type DeviceStatus = {
|
||||
get_environment: DeviceEnvironment | ErrorBase;
|
||||
get_facts: DeviceFacts | ErrorBase;
|
||||
};
|
||||
|
||||
interface ObjectWithGroup extends APIObjectBase {
|
||||
group: Nullable<APIReference>;
|
||||
}
|
||||
|
@ -5,15 +5,15 @@ type Method = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
||||
type ReqData = URLSearchParams | Dict | undefined | unknown;
|
||||
type SelectedOption = { name: string; options: string[] };
|
||||
|
||||
// interface TableValue {
|
||||
// row: {
|
||||
// element: HTMLTableRowElement;
|
||||
// };
|
||||
// cell: {
|
||||
// element: HTMLTableCellElement;
|
||||
// value: string;
|
||||
// };
|
||||
// }
|
||||
type HTMLElementProperties<E extends HTMLElement> =
|
||||
| {
|
||||
[k in keyof E]: E[k];
|
||||
}
|
||||
| {};
|
||||
|
||||
type InferredProps<T extends keyof HTMLElementTagNameMap> = HTMLElementProperties<
|
||||
HTMLElementTagNameMap[T]
|
||||
>;
|
||||
|
||||
export function isApiError(data: Record<string, unknown>): data is APIError {
|
||||
return 'error' in data && 'exception' in data;
|
||||
@ -170,6 +170,12 @@ export function scrollTo(element: Element, offset: number = 0): void {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through a select element's options and return an array of options that are selected.
|
||||
*
|
||||
* @param base Select element.
|
||||
* @returns Array of selected options.
|
||||
*/
|
||||
export function getSelectedOptions<E extends HTMLElement>(base: E): SelectedOption[] {
|
||||
let selected = [] as SelectedOption[];
|
||||
for (const element of base.querySelectorAll<HTMLSelectElement>('select')) {
|
||||
@ -186,6 +192,16 @@ export function getSelectedOptions<E extends HTMLElement>(base: E): SelectedOpti
|
||||
return selected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data that can only be accessed via Django context, and is thus already rendered in the HTML
|
||||
* template.
|
||||
*
|
||||
* @see Templates requiring Django context data have a `{% block data %}` block.
|
||||
*
|
||||
* @param key Property name, which must exist on the HTML element. If not already prefixed with
|
||||
* `data-`, `data-` will be prepended to the property.
|
||||
* @returns Value if it exists, `null` if not.
|
||||
*/
|
||||
export function getNetboxData(key: string): string | null {
|
||||
if (!key.startsWith('data-')) {
|
||||
key = `data-${key}`;
|
||||
@ -203,12 +219,11 @@ export function getNetboxData(key: string): string | null {
|
||||
* Toggle visibility of card loader.
|
||||
*/
|
||||
export function toggleLoader(action: 'show' | 'hide') {
|
||||
const spinnerContainer = document.querySelector('div.card-overlay');
|
||||
if (spinnerContainer !== null) {
|
||||
for (const element of getElements<HTMLDivElement>('div.card-overlay')) {
|
||||
if (action === 'show') {
|
||||
spinnerContainer.classList.remove('d-none');
|
||||
element.classList.remove('d-none');
|
||||
} else {
|
||||
spinnerContainer.classList.add('d-none');
|
||||
element.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -250,3 +265,51 @@ export function findFirstAdjacent<R extends HTMLElement, B extends Element = Ele
|
||||
}
|
||||
return match(base);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for creating HTML elements.
|
||||
*
|
||||
* @param tag HTML element type.
|
||||
* @param properties Properties/attributes to apply to the element.
|
||||
* @param classes CSS classes to apply to the element.
|
||||
* @param children Child elements.
|
||||
*/
|
||||
export function createElement<
|
||||
T extends keyof HTMLElementTagNameMap,
|
||||
C extends HTMLElement = HTMLElement
|
||||
>(
|
||||
tag: T,
|
||||
properties: InferredProps<T>,
|
||||
classes: string[],
|
||||
children: C[] = [],
|
||||
): HTMLElementTagNameMap[T] {
|
||||
// Create the base element.
|
||||
const element = document.createElement<T>(tag);
|
||||
|
||||
for (const k of Object.keys(properties)) {
|
||||
// Add each property to the element.
|
||||
const key = k as keyof HTMLElementProperties<HTMLElementTagNameMap[T]>;
|
||||
const value = properties[key];
|
||||
if (key in element) {
|
||||
element[key] = value;
|
||||
}
|
||||
}
|
||||
// Add each CSS class to the element's class list.
|
||||
element.classList.add(...classes);
|
||||
|
||||
for (const child of children) {
|
||||
// Add each child element to the base element.
|
||||
element.appendChild(child);
|
||||
}
|
||||
return element as HTMLElementTagNameMap[T];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Celsius to Fahrenheit, for NAPALM temperature sensors.
|
||||
*
|
||||
* @param celsius Degrees in Celsius.
|
||||
* @returns Degrees in Fahrenheit.
|
||||
*/
|
||||
export function cToF(celsius: number): number {
|
||||
return celsius * (9 / 5) + 32;
|
||||
}
|
||||
|
@ -2688,6 +2688,11 @@ data-urls@^1.1.0:
|
||||
whatwg-mimetype "^2.2.0"
|
||||
whatwg-url "^7.0.0"
|
||||
|
||||
dayjs@^1.10.4:
|
||||
version "1.10.4"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2"
|
||||
integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw==
|
||||
|
||||
de-indent@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
|
||||
|
@ -1,13 +1,21 @@
|
||||
{% extends 'dcim/device/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ device }} - Status{% endblock %}
|
||||
{% block title %}{{ object }} - Status{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<script type="text/javascript" src="{% static 'status.js' %}" onerror="window.location='{% url 'media_failure' %}?filename=status.js'"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/ajax_loader.html' %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-overlay d-none">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="card-header">Device Facts</h5>
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
@ -29,15 +37,20 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Serial Number</th>
|
||||
<td id="serial_number"></td>
|
||||
<td>
|
||||
<code id="serial_number"></code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">OS Version</th>
|
||||
<td id="os_version"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr class="align-middle">
|
||||
<th scope="row">Uptime</th>
|
||||
<td id="uptime"></td>
|
||||
<td>
|
||||
<div id="uptime-duration"></div>
|
||||
<div id="uptime" class="small text-muted"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@ -45,24 +58,31 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-overlay d-none">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="card-header">Environment</h5>
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<tr id="cpu">
|
||||
<tr id="status-cpu" class="bg-light">
|
||||
<th colspan="2"><i class="mdi mdi-gauge"></i> CPU</th>
|
||||
</tr>
|
||||
<tr id="memory">
|
||||
<tr id="status-memory" class="bg-light">
|
||||
<th colspan="2"><i class="mdi mdi-chip"></i> Memory</th>
|
||||
</tr>
|
||||
<tr id="temperature">
|
||||
<tr id="status-temperature" class="bg-light">
|
||||
<th colspan="2"><i class="mdi mdi-thermometer"></i> Temperature</th>
|
||||
</tr>
|
||||
<tr id="fans">
|
||||
<tr id="status-fans" class="bg-light">
|
||||
<th colspan="2"><i class="mdi mdi-fan"></i> Fans</th>
|
||||
</tr>
|
||||
<tr id="power">
|
||||
<tr id="status-power" class="bg-light">
|
||||
<th colspan="2"><i class="mdi mdi-power"></i> Power</th>
|
||||
</tr>
|
||||
<tr class="napalm-table-placeholder d-none invisible">
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -70,73 +90,6 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$.ajax({
|
||||
url: "{% url 'dcim-api:device-napalm' pk=object.pk %}?method=get_facts&method=get_environment",
|
||||
dataType: 'json',
|
||||
success: function(json) {
|
||||
if (!json['get_facts']['error']) {
|
||||
$('#hostname').html(json['get_facts']['hostname']);
|
||||
$('#fqdn').html(json['get_facts']['fqdn']);
|
||||
$('#vendor').html(json['get_facts']['vendor']);
|
||||
$('#model').html(json['get_facts']['model']);
|
||||
$('#serial_number').html(json['get_facts']['serial_number']);
|
||||
$('#os_version').html(json['get_facts']['os_version']);
|
||||
// Calculate uptime
|
||||
var uptime = json['get_facts']['uptime'];
|
||||
console.log(uptime);
|
||||
var uptime_days = Math.floor(uptime / 86400);
|
||||
var uptime_hours = Math.floor(uptime % 86400 / 3600);
|
||||
var uptime_minutes = Math.floor(uptime % 3600 / 60);
|
||||
$('#uptime').html(uptime_days + "d " + uptime_hours + "h " + uptime_minutes + "m");
|
||||
}
|
||||
|
||||
if (!json['get_environment']['error']) {
|
||||
$.each(json['get_environment']['cpu'], function(name, obj) {
|
||||
var row="<tr><td>" + name + "</td><td>" + obj['%usage'] + "%</td></tr>";
|
||||
$("#cpu").after(row)
|
||||
});
|
||||
if (json['get_environment']['memory']) {
|
||||
var memory = $('#memory');
|
||||
memory.after("<tr><td>Used</td><td>" + json['get_environment']['memory']['used_ram'] + "</td></tr>");
|
||||
memory.after("<tr><td>Available</td><td>" + json['get_environment']['memory']['available_ram'] + "</td></tr>");
|
||||
}
|
||||
$.each(json['get_environment']['temperature'], function(name, obj) {
|
||||
var style = "success";
|
||||
if (obj['is_alert']) {
|
||||
style = "warning";
|
||||
} else if (obj['is_critical']) {
|
||||
style = "danger";
|
||||
}
|
||||
var row="<tr class=\"" + style +"\"><td>" + name + "</td><td>" + obj['temperature'] + "°C</td></tr>";
|
||||
$("#temperature").after(row)
|
||||
});
|
||||
$.each(json['get_environment']['fans'], function(name, obj) {
|
||||
var row;
|
||||
if (obj['status']) {
|
||||
row="<tr class=\"success\"><td>" + name + "</td><td><i class=\"mdi mdi-check-bold text-success\"></i></td></tr>";
|
||||
} else {
|
||||
row="<tr class=\"error\"><td>" + name + "</td><td><i class=\"mdi mdi-close text-error\"></i></td></tr>";
|
||||
}
|
||||
$("#fans").after(row)
|
||||
});
|
||||
$.each(json['get_environment']['power'], function(name, obj) {
|
||||
var row;
|
||||
if (obj['status']) {
|
||||
row="<tr class=\"success\"><td>" + name + "</td><td><i class=\"mdi mdi-check-bold text-success\"></i></td></tr>";
|
||||
} else {
|
||||
row="<tr class=\"danger\"><td>" + name + "</td><td><i class=\"mdi mdi-close text-danger\"></i></td></tr>";
|
||||
}
|
||||
$("#power").after(row)
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
alert(xhr.responseText);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% block data %}
|
||||
<span data-object-url="{% url 'dcim-api:device-napalm' pk=object.pk %}?method=get_facts&method=get_environment"></span>
|
||||
{% endblock %}
|
||||
|
Loading…
Reference in New Issue
Block a user