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:
77
frontend/src/utils/graniteLoaderConfig.js
Normal file
77
frontend/src/utils/graniteLoaderConfig.js
Normal 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);
|
||||
}
|
||||
82
frontend/src/utils/graniteLoaderExport.js
Normal file
82
frontend/src/utils/graniteLoaderExport.js
Normal 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';
|
||||
}
|
||||
Reference in New Issue
Block a user