Add Granite Loader Sheet generator with CARD enrichment

Implement the Granite Team_Device Loader xlsx export feature:
- Add graniteLoaderConfig.js with all 41 columns, groupings, and
  operation-type requirements (Change/Add/Delete/Move)
- Add graniteLoaderExport.js for client-side xlsx generation using
  the xlsx library
- Add LoaderModal component with operation type selection, column
  checkboxes, bulk defaults with per-row overrides, editable preview
  table, CARD enrichment integration, and standalone paste-IPs mode
- Add POST /api/card/enrich-batch endpoint for batch IP lookup in
  CARD returning EQUIP_INST_ID, hostname, site, ASN, team
- Integrate 'Generate Loader Sheet' button in Ivanti Queue floating
  action bar (visible when CARD/GRANITE/DECOM items selected)
- Add card-connectivity-test.js script for verifying CARD API access
This commit is contained in:
Jordan Ramos
2026-05-27 17:18:36 -06:00
parent 1903e41088
commit fe82362afa
7 changed files with 1071 additions and 8 deletions

View File

@@ -0,0 +1,77 @@
/**
* Granite Team_Device Loader column configuration.
* Defines all 41 columns in canonical order, their groupings,
* and which are required for each operation type.
*/
export const LOADER_COLUMNS = [
{ id: 'DELETE', label: 'DELETE', group: 'Identification', requiredFor: ['Delete'] },
{ id: 'SET_CONFIRMED', label: 'SET_CONFIRMED', group: 'Identification', requiredFor: [] },
{ id: 'EQUIPMENT_CLASS', label: 'EQUIPMENT CLASS', group: 'Identification', requiredFor: ['Add'] },
{ id: 'EQUIP_INST_ID', label: 'EQUIP_INST_ID', group: 'Identification', requiredFor: ['Change', 'Move', 'Delete'] },
{ id: 'SITE_NAME', label: 'SITE_NAME', group: 'Identification', requiredFor: ['Add', 'Move'] },
{ id: 'EQUIP_NAME', label: 'EQUIP_NAME', group: 'Identification', requiredFor: ['Add'] },
{ id: 'EQUIP_TEMPLATE', label: 'EQUIP_TEMPLATE', group: 'Identification', requiredFor: ['Add'] },
{ id: 'EQUIP_STATUS', label: 'EQUIP_STATUS', group: 'Identification', requiredFor: ['Add'] },
{ id: 'RESPONSIBLE_TEAM', label: 'UDA#RESPONSIBLE ORGANIZATION#RESPONSIBLE TEAM', group: 'Responsible Org', requiredFor: ['Add'] },
{ id: 'IPV4_ADDRESS', label: 'UDA#IP_ADDRESSING#IPV4_ADDRESS', group: 'IP Addressing', requiredFor: ['Add'] },
{ id: 'MAC_ADDRESS', label: 'UDA#IP_ADDRESSING#MAC ADDRESS', group: 'IP Addressing', requiredFor: [] },
{ id: 'MGMT_IP_ASN', label: 'UDA#IP_ADDRESSING#MGMT_IP_ASN', group: 'IP Addressing', requiredFor: ['Add'] },
{ id: 'SERIALNUMBER', label: 'SERIALNUMBER', group: 'Equipment Info', requiredFor: [] },
{ id: 'EXCLUDED_DISCOVERY', label: 'UDA#AUTO DISCOVERY#EXCLUDED FROM DISCOVERY', group: 'Discovery', requiredFor: [] },
{ id: 'EXCLUDED_REASON', label: 'UDA#AUTO DISCOVERY#EXCLUDED FROM DISCOVERY REASON', group: 'Discovery', requiredFor: [] },
{ id: 'IPV6_ADDRESS', label: 'UDA#IP_ADDRESSING#IPV6_ADDRESS', group: 'IP Addressing', requiredFor: [] },
{ id: 'ILOM_ADDRESS', label: 'UDA#IP_ADDRESSING#IPV4_ILOM_ADDRESS', group: 'IP Addressing', requiredFor: [] },
{ id: 'APP_ID_ASSET_TAG', label: 'UDA#CHERWELL_CMDB#APP ID ASSET TAG', group: 'Cyber Metrics', requiredFor: [] },
{ id: 'DEVICE_FUNCTION', label: 'UDA#CHERWELL_CMDB#DEVICE_FUNCTION', group: 'Cyber Metrics', requiredFor: [] },
{ id: 'ENVIRONMENT', label: 'UDA#CHERWELL_CMDB#ENVIRONMENT', group: 'Cyber Metrics', requiredFor: [] },
{ id: 'SECONDARY_MGMT_IP', label: 'UDA#IP_ADDRESSING#SECONDARY_MGMT_IP_ADDRESS', group: 'IP Addressing', requiredFor: [] },
{ id: 'VIP', label: 'UDA#IP_ADDRESSING#VIP', group: 'IP Addressing', requiredFor: [] },
{ id: 'FLOATING_IP', label: 'UDA#IP_ADDRESSING#FLOATING IP ADDRESS', group: 'IP Addressing', requiredFor: [] },
{ id: 'SCAN_IP_1', label: 'UDA#IP_ADDRESSING#SCAN IP ADDRESS 1', group: 'IP Addressing', requiredFor: [] },
{ id: 'SCAN_IP_2', label: 'UDA#IP_ADDRESSING#SCAN IP ADDRESS 2', group: 'IP Addressing', requiredFor: [] },
{ id: 'SCAN_IP_3', label: 'UDA#IP_ADDRESSING#SCAN IP ADDRESS 3', group: 'IP Addressing', requiredFor: [] },
{ id: 'EQUIP_MODEL', label: 'EQUIP_MODEL', group: 'Equipment Info', requiredFor: [] },
{ id: 'EQUIP_COMMENTS', label: 'EQUIP_COMMENTS', group: 'Equipment Info', requiredFor: [] },
{ id: 'EQUIP_PARTNUMBER', label: 'EQUIP_PARTNUMBER', group: 'Equipment Info', requiredFor: [] },
{ id: 'OS', label: 'UDA#CONTROLLER CONFIG#OS', group: 'Equipment Info', requiredFor: [] },
{ id: 'OS_VERSION', label: 'UDA#CONTROLLER CONFIG#OS VERSION', group: 'Equipment Info', requiredFor: [] },
{ id: 'CPU_CORES', label: 'UDA#CONTROLLER CONFIG#TOTAL CPU CORES', group: 'Equipment Info', requiredFor: [] },
{ id: 'RAM_GB', label: 'UDA#CONTROLLER CONFIG#RAM IN GB', group: 'Equipment Info', requiredFor: [] },
{ id: 'STORAGE_GB', label: 'UDA#CONTROLLER CONFIG#STORAGE IN GB', group: 'Equipment Info', requiredFor: [] },
{ id: 'ARCHER_ID', label: 'UDA#WIFI EQUIP INFO#ARCHER ID', group: 'Other', requiredFor: [] },
{ id: 'INSTALL_LOCATION', label: 'UDA#WIFI EQUIP INFO#INSTALL LOCATION', group: 'Other', requiredFor: [] },
{ id: 'SYSNAME', label: 'UDA#EQUIPMENT_INFO#SYSNAME', group: 'Other', requiredFor: [] },
{ id: 'LATITUDE', label: 'UDA#WIFI EQUIP INFO#LATITUDE', group: 'Other', requiredFor: [] },
{ id: 'LONGITUDE', label: 'UDA#WIFI EQUIP INFO#LONGITUDE', group: 'Other', requiredFor: [] },
{ id: 'OSTYPE', label: 'UDA#EQUIP MIGRATION#OSTYPE', group: 'Other', requiredFor: [] },
{ id: 'OSVERSION', label: 'UDA#EQUIP MIGRATION#OSVERSION', group: 'Other', requiredFor: [] },
];
export const COLUMN_GROUPS = [
'Identification',
'Responsible Org',
'IP Addressing',
'Discovery',
'Cyber Metrics',
'Equipment Info',
'Other',
];
export const OPERATION_TYPES = ['Change', 'Add', 'Delete', 'Move'];
/**
* Returns column IDs required for the given operation type.
*/
export function getRequiredColumns(operationType) {
return LOADER_COLUMNS
.filter(col => col.requiredFor.includes(operationType))
.map(col => col.id);
}
/**
* Returns columns belonging to a specific group.
*/
export function getColumnsByGroup(group) {
return LOADER_COLUMNS.filter(col => col.group === group);
}

View File

@@ -0,0 +1,82 @@
/**
* Granite Team_Device Loader xlsx generation.
* Produces a properly formatted xlsx file for upload to SNIP XperLoad.
*/
import * as XLSX from 'xlsx';
import { LOADER_COLUMNS } from './graniteLoaderConfig';
/**
* Generate a Granite Loader Sheet xlsx file.
*
* @param {Object} config
* @param {string} config.operationType - 'Change' | 'Add' | 'Delete' | 'Move'
* @param {Array<string>} config.columnIds - selected column IDs (in any order; output uses canonical order)
* @param {Array<Object>} config.rows - device rows, each keyed by column ID with string values
* @returns {Blob} xlsx file as a Blob for browser download
*/
export function generateLoaderXlsx(config) {
const { operationType, columnIds, rows } = config;
// Filter LOADER_COLUMNS to only selected columns, preserving canonical order
const selectedColumns = LOADER_COLUMNS.filter(col => columnIds.includes(col.id));
// Build header row from canonical labels
const headers = selectedColumns.map(col => col.label);
// Build data rows
const dataRows = rows.map(row => {
return selectedColumns.map(col => {
// DELETE column auto-fill for Delete operations
if (col.id === 'DELETE' && operationType === 'Delete') {
return 'X';
}
// Get the value from the row
let value = row[col.id];
// EQUIPMENT CLASS defaults to "S" if not explicitly set
if (col.id === 'EQUIPMENT_CLASS' && (value === undefined || value === null || value === '')) {
value = 'S';
}
// Convert null/undefined to empty string (not "null" or "undefined")
if (value === null || value === undefined) {
return '';
}
return String(value);
});
});
// Combine headers + data into array-of-arrays
const aoa = [headers, ...dataRows];
// Create workbook and worksheet
const ws = XLSX.utils.aoa_to_sheet(aoa);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Load_Sheet');
// Write to array buffer
const wbOut = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
// Return as Blob
return new Blob([wbOut], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
}
/**
* Generate a descriptive filename for the loader sheet.
*
* @param {string} operationType - 'Change' | 'Add' | 'Delete' | 'Move'
* @param {string} [teamName] - optional team name to include in filename
* @returns {string} filename like "Loader_Change_NTS-AEO-STEAM_2026-05-27.xlsx"
*/
export function generateFilename(operationType, teamName) {
const date = new Date().toISOString().slice(0, 10);
const parts = ['Loader', operationType];
if (teamName) {
parts.push(teamName.replace(/[^a-zA-Z0-9_-]/g, '-'));
}
parts.push(date);
return parts.join('_') + '.xlsx';
}