Skip to content

Instantly share code, notes, and snippets.

@davidlu1001
Last active May 14, 2025 22:56
Show Gist options
  • Save davidlu1001/47fcb30b7f92a649ba897c180ffbc6d9 to your computer and use it in GitHub Desktop.
Save davidlu1001/47fcb30b7f92a649ba897c180ffbc6d9 to your computer and use it in GitHub Desktop.
immuta-create-groups.js
// This script automates the creation of Immuta Groups from CSV or Excel files
// To use:
// 1. Navigate to the Immuta Groups page (People > Groups)
// 2. Open browser's developer console (F12 or right-click > Inspect > Console)
// 3. Paste this entire script and press Enter
// 4. Follow the on-screen prompts to select your CSV or Excel file
// Global variables
let isOfflineMode = false;
// First, let's try to load required libraries for handling Excel files
async function loadLibraries() {
// Check if the library is already loaded
if (window.XLSX) return true;
try {
// Load SheetJS (xlsx) library for Excel support
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js';
script.async = true;
document.head.appendChild(script);
// Wait for the script to load
return new Promise((resolve, reject) => {
script.onload = () => resolve(true);
script.onerror = () => {
console.warn('Failed to load XLSX library. Working in offline mode.');
isOfflineMode = true;
resolve(false);
};
// Timeout after 5 seconds
setTimeout(() => {
console.warn('Timeout loading XLSX library. Working in offline mode.');
isOfflineMode = true;
resolve(false);
}, 5000);
});
} catch (error) {
console.warn('Error loading XLSX library:', error.message);
isOfflineMode = true;
return false;
}
}
// Function to read CSV file
function parseCSV(text) {
// Split by newline and filter out empty lines
return text.split(/\r?\n/)
.map(line => line.trim())
.filter(line => line.length > 0)
// Handle CSV with headers by taking the first column if comma-separated
.map(line => {
if (line.includes(',')) {
return line.split(',')[0].trim().replace(/^["'](.*)["']$/, '$1');
}
return line;
})
.filter((value, index, self) => self.indexOf(value) === index); // Remove duplicates
}
// Function to read Excel file
function parseExcel(data) {
try {
const workbook = XLSX.read(data, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
// Convert to array of objects
const rows = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
// Take first column values, skip header row if it exists
const hasHeader = typeof rows[0][0] === 'string' &&
(rows[0][0].toLowerCase().includes('name') ||
rows[0][0].toLowerCase().includes('group'));
const startRow = hasHeader ? 1 : 0;
// Extract group names from first column
return rows.slice(startRow)
.map(row => row[0])
.filter(name => name && String(name).trim().length > 0)
.map(name => String(name).trim())
.filter((value, index, self) => self.indexOf(value) === index); // Remove duplicates
} catch (error) {
console.error('Error parsing Excel file:', error);
return null;
}
}
// Simple function to try and extract text from binary Excel file in offline mode
function attemptSimpleExcelParse(arrayBuffer) {
try {
// Convert array buffer to a regular string
const textDecoder = new TextDecoder('utf-8');
const text = textDecoder.decode(arrayBuffer);
// Look for patterns that might indicate cell values
const potentialNames = [];
// Basic extraction using regex to find potential group names
// This is a simple approach and won't work for all Excel files
const stringMatches = text.match(/[a-zA-Z0-9_\-\s]+[^\x00-\x7F]{1,4}/g) || [];
for (const match of stringMatches) {
// Clean up the match
const cleaned = match.replace(/[^\x20-\x7E]/g, '').trim();
if (cleaned && cleaned.length > 1 && !/^\d+$/.test(cleaned)) {
potentialNames.push(cleaned);
}
}
// Remove duplicates
const uniqueNames = [...new Set(potentialNames)];
// If we found some potential names, return them
if (uniqueNames.length > 0) {
return uniqueNames;
}
return null;
} catch (error) {
console.error('Error in simple Excel parsing:', error);
return null;
}
}
// Function to read file content (CSV or Excel)
async function readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async (event) => {
try {
const result = event.target.result;
if (file.name.toLowerCase().endsWith('.csv')) {
// For CSV files
const text = event.target.result;
const groupNames = parseCSV(text);
resolve(groupNames);
} else if (file.name.toLowerCase().endsWith('.xlsx') || file.name.toLowerCase().endsWith('.xls')) {
// For Excel files
if (!isOfflineMode) {
// Try to use the XLSX library if available
await loadLibraries();
if (window.XLSX) {
const data = new Uint8Array(result);
const groupNames = parseExcel(data);
if (groupNames) {
resolve(groupNames);
return;
}
}
}
// If we're in offline mode or the XLSX parsing failed
const simpleResults = attemptSimpleExcelParse(result);
if (simpleResults && simpleResults.length > 0) {
// We managed to extract something
console.log('Using simplified Excel parsing in offline mode');
resolve(simpleResults);
} else {
// Show the file conversion instructions
showFileConversionInstructions();
reject(new Error('Unable to parse Excel file in offline mode. Please convert to CSV first.'));
}
} else {
reject(new Error('Unsupported file format. Please use CSV or Excel file.'));
}
} catch (error) {
reject(new Error(`Error parsing file: ${error.message}`));
}
};
reader.onerror = () => reject(new Error('Error reading file'));
if (file.name.toLowerCase().endsWith('.csv')) {
reader.readAsText(file);
} else {
reader.readAsArrayBuffer(file);
}
});
}
// Function to show instructions for converting Excel to CSV
function showFileConversionInstructions() {
// Create styled container for instructions
const container = document.createElement('div');
container.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10000;
background: white;
padding: 25px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
max-width: 600px;
width: 90%;
font-family: Arial, sans-serif;
line-height: 1.5;
`;
const content = `
<h2 style="margin-top: 0; color: #d9534f;">Excel File Cannot Be Processed</h2>
<p>You appear to be in an offline or restricted environment where the Excel processing library cannot be loaded.</p>
<p><strong>Please convert your Excel file to CSV format and try again:</strong></p>
<ol>
<li>Open your Excel file</li>
<li>Go to File > Save As</li>
<li>Select "CSV (Comma delimited) (*.csv)" from the file type dropdown</li>
<li>Save the file</li>
<li>Return to Immuta and upload the CSV file instead</li>
</ol>
<p><strong>Important:</strong> Make sure your group names are in the first column of your spreadsheet before converting.</p>
<div style="text-align: right; margin-top: 20px;">
<button id="close-instructions" style="padding: 8px 16px; background: #5bc0de; color: white; border: none; border-radius: 4px; cursor: pointer;">Close</button>
</div>
`;
container.innerHTML = content;
document.body.appendChild(container);
// Add event listener to close button
document.getElementById('close-instructions').addEventListener('click', () => {
container.remove();
});
}
// Function to find People menu in the left navigation panel
function findPeopleMenu() {
// Try different strategies to find the People menu
// 1. Try by specific selectors
const peopleSelectors = [
'button[aria-label="People"]',
'button[data-track-id="people-admin-nav"]',
'a[aria-label="People"]',
'a[href*="/admin/people"]',
'a[href*="/people"]',
'li[data-section="people"]',
'.people-section',
];
for (const selector of peopleSelectors) {
const element = document.querySelector(selector);
if (element && isElementVisible(element)) {
return element;
}
}
// 2. Try by text content in any element
const allElements = document.querySelectorAll('a, button, div, span, li');
for (const el of allElements) {
if (el.textContent.trim() === 'People' && isElementVisible(el)) {
return el;
}
}
// 3. Try elements containing icon + text
const potentialPeopleElements = Array.from(allElements).filter(el => {
return (el.textContent.includes('People') ||
el.innerHTML.includes('People')) &&
isElementVisible(el);
});
if (potentialPeopleElements.length > 0) {
return potentialPeopleElements[0];
}
// 4. Look for any element in the left sidebar that might be the People section
const sidebarElements = document.querySelectorAll('.sidebar a, .sidebar button, .sidebar li, [class*="sidebar"] a, [class*="sidebar"] button, [class*="sidebar"] li, [class*="nav"] a, [class*="nav"] button, [class*="nav"] li');
for (const el of sidebarElements) {
if (el.textContent.includes('People') && isElementVisible(el)) {
return el;
}
}
return null;
}
// Function to find Groups tab
function findGroupsTab() {
// Try different strategies to find the Groups tab
// 1. Try by specific selectors
const groupsSelectors = [
'a[href*="/admin/groups"]',
'a[href*="/groups"]',
'button[data-track-id="groups-tab"]',
'li[data-tab="groups"]',
'.groups-tab',
];
for (const selector of groupsSelectors) {
const element = document.querySelector(selector);
if (element && isElementVisible(element)) {
return element;
}
}
// 2. Try by text content in any element
const allElements = document.querySelectorAll('a, button, div, span, li');
for (const el of allElements) {
if (el.textContent.trim() === 'Groups' && isElementVisible(el)) {
return el;
}
}
// 3. Try elements containing the text
const potentialGroupsElements = Array.from(allElements).filter(el => {
return (el.textContent.includes('Groups') ||
el.innerHTML.includes('Groups')) &&
isElementVisible(el);
});
if (potentialGroupsElements.length > 0) {
return potentialGroupsElements[0];
}
return null;
}
// Function to find the New Group button
function findNewGroupButton() {
// Try different selectors to find the button
const selectors = [
'button#add-group',
'button[data-track-id="add-group"]',
'button[id*="add-group"]',
'button[class*="add-group"]',
'button.dp-button--solid.dp-button--sm.dp-button--icon',
'button[class*="dp-button--solid"][class*="dp-button--sm"]',
];
// Try each selector
for (const selector of selectors) {
const button = document.querySelector(selector);
if (button && isElementVisible(button)) return button;
}
// Try finding by text content
const allButtons = Array.from(document.querySelectorAll('button'));
// First, try to find a button with exact text "New Group"
let button = allButtons.find(b => b.textContent.trim() === 'New Group' && isElementVisible(b));
if (button) return button;
// Try with "Add Group"
button = allButtons.find(b => b.textContent.trim() === 'Add Group' && isElementVisible(b));
if (button) return button;
// Then look for buttons containing these texts
button = allButtons.find(b =>
(b.textContent.includes('New Group') ||
b.textContent.includes('Add Group') ||
b.innerHTML.includes('New Group') ||
b.innerHTML.includes('Add Group')) &&
isElementVisible(b)
);
if (button) return button;
// Look for buttons with icons that might be the add button
button = allButtons.find(b => {
return isElementVisible(b) && (
b.classList.contains('dp-button--icon') ||
b.classList.contains('dp-button--add') ||
b.querySelector('i[class*="add"]') ||
b.querySelector('span[class*="add"]')
);
});
if (button) return button;
// Last resort - look for a span with a plus icon inside a button
const plusButtons = Array.from(document.querySelectorAll('button')).filter(b => {
return isElementVisible(b) && (b.textContent.includes('+') || b.innerHTML.includes('add'));
});
if (plusButtons.length > 0) {
return plusButtons[0];
}
return null;
}
// Function to find the input field in the dialog
function findGroupNameInput() {
// Try different selectors to find the input field
const selectors = [
'input[name="newGroup"]',
'input[id*="group-name"]',
'input[placeholder*="Group Name"]',
'input[class*="dp-textfield--input"]',
'input[required]',
];
// Try each selector
for (const selector of selectors) {
const inputs = document.querySelectorAll(selector);
if (inputs.length === 1) return inputs[0];
if (inputs.length > 1) {
// If multiple inputs, try to find the one that's visible
for (const input of inputs) {
if (isElementVisible(input)) return input;
}
}
}
// Look for any input in a dialog/modal
const dialog = document.querySelector('.dp-dialog, .dp-modal, [class*="dialog"], [class*="modal"]');
if (dialog) {
const inputs = dialog.querySelectorAll('input[type="text"]');
for (const input of inputs) {
if (isElementVisible(input)) return input;
}
}
// Last resort - try any visible text input
const allInputs = document.querySelectorAll('input[type="text"]');
for (const input of allInputs) {
if (isElementVisible(input)) return input;
}
return null;
}
// Function to find the Save button in the dialog
function findSaveButton() {
// Try different selectors to find the save button
const selectors = [
'button#save-button',
'button[data-track-id="save"]',
'button[id*="save"]',
'button[class*="save"]',
'button[type="submit"]',
];
// Try each selector
for (const selector of selectors) {
const button = document.querySelector(selector);
if (button && isElementVisible(button)) return button;
}
// Try finding by text content
const allButtons = Array.from(document.querySelectorAll('button'));
// First, try to find a button with exact text "Save"
let button = allButtons.find(b => b.textContent.trim() === 'Save' && isElementVisible(b));
if (button) return button;
// Look for buttons containing "Save"
button = allButtons.find(b =>
isElementVisible(b) && (
b.textContent.includes('Save') ||
b.innerHTML.includes('Save')
)
);
if (button) return button;
// Look for dialog buttons
const dialog = document.querySelector('.dp-dialog, .dp-modal, [class*="dialog"], [class*="modal"]');
if (dialog) {
const buttons = Array.from(dialog.querySelectorAll('button')).filter(b => isElementVisible(b));
// The save button is often the right-most or last button in a dialog
if (buttons.length >= 2) return buttons[buttons.length - 1];
if (buttons.length === 1) return buttons[0];
}
return null;
}
// Function to find the Cancel button in the dialog
function findCancelButton() {
// Try different selectors to find the cancel button
const selectors = [
'button#cancel-button',
'button[data-track-id="cancel"]',
'button[id*="cancel"]',
'button[class*="cancel"]',
];
// Try each selector
for (const selector of selectors) {
const button = document.querySelector(selector);
if (button && isElementVisible(button)) return button;
}
// Try finding by text content
const allButtons = Array.from(document.querySelectorAll('button'));
// First, try to find a button with exact text "Cancel"
let button = allButtons.find(b => b.textContent.trim() === 'Cancel' && isElementVisible(b));
if (button) return button;
// Look for buttons containing "Cancel"
button = allButtons.find(b =>
isElementVisible(b) && (
b.textContent.includes('Cancel') ||
b.innerHTML.includes('Cancel')
)
);
if (button) return button;
// Look for dialog buttons
const dialog = document.querySelector('.dp-dialog, .dp-modal, [class*="dialog"], [class*="modal"]');
if (dialog) {
const buttons = Array.from(dialog.querySelectorAll('button')).filter(b => isElementVisible(b));
// The cancel button is often the left-most or first button in a dialog
if (buttons.length >= 2) return buttons[0];
}
return null;
}
// Helper function to check if an element is visible
function isElementVisible(element) {
if (!element) return false;
// Check computed style
const style = window.getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
return false;
}
// Check dimensions
const rect = element.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) {
return false;
}
// Check if element is within viewport
if (rect.bottom < 0 || rect.top > window.innerHeight ||
rect.right < 0 || rect.left > window.innerWidth) {
return false;
}
return true;
}
// Helper function to check if a dialog is open
function isDialogOpen() {
const possibleDialogs = document.querySelectorAll('.dp-dialog, .dp-modal, [class*="dialog"], [class*="modal"]');
for (const dialog of possibleDialogs) {
if (isElementVisible(dialog)) {
return true;
}
}
return false;
}
// Function to wait for an element to be visible
async function waitForElement(findElementFn, timeoutMs = 5000, intervalMs = 100) {
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
const element = findElementFn();
if (element && isElementVisible(element)) {
return element;
}
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
return null;
}
// Function to navigate to the Groups page
async function navigateToGroupsPage() {
console.log('Navigating to the Groups page...');
// First, try to find and click the People menu in the left sidebar
const peopleMenu = await waitForElement(findPeopleMenu, 5000);
if (!peopleMenu) {
throw new Error('People menu not found in the left navigation panel');
}
console.log('Clicking People menu...');
peopleMenu.click();
// Wait for the People section to load
await new Promise(resolve => setTimeout(resolve, 2000));
// Then, find and click the Groups tab
const groupsTab = await waitForElement(findGroupsTab, 5000);
if (!groupsTab) {
throw new Error('Groups tab not found in the People section');
}
console.log('Clicking Groups tab...');
groupsTab.click();
// Wait for the Groups page to load
await new Promise(resolve => setTimeout(resolve, 2000));
// Verify we're on the groups page by checking for the New Group button
const newGroupButton = await waitForElement(findNewGroupButton, 5000);
if (!newGroupButton) {
throw new Error('New Group button not found. Navigation to Groups page may have failed.');
}
console.log('Successfully navigated to the Groups page');
return true;
}
// Function to create a group with given name
async function createGroup(groupName) {
console.log(`Attempting to create group: "${groupName}"`);
try {
// Always navigate to the Groups page first for each group creation
// This is critical since Immuta may navigate away after each group creation
await navigateToGroupsPage();
// Now find and click the New Group button
const addGroupBtn = await waitForElement(findNewGroupButton, 5000);
if (!addGroupBtn) {
throw new Error('New Group button not found after navigation.');
}
console.log('Clicking New Group button...');
addGroupBtn.click();
// Wait for dialog to appear
await new Promise(resolve => setTimeout(resolve, 1500));
// Check if a dialog is open
if (!isDialogOpen()) {
throw new Error('Dialog did not open after clicking New Group button');
}
// Find and fill the group name input
const nameInput = await waitForElement(findGroupNameInput, 3000);
if (!nameInput) {
throw new Error('Group name input field not found in the dialog.');
}
console.log('Entering group name...');
// Set value and trigger input event
nameInput.value = groupName;
nameInput.dispatchEvent(new Event('input', { bubbles: true }));
nameInput.dispatchEvent(new Event('change', { bubbles: true }));
// Wait for input to register
await new Promise(resolve => setTimeout(resolve, 500));
// Click Save button
const saveBtn = await waitForElement(findSaveButton, 3000);
if (!saveBtn) {
throw new Error('Save button not found in the dialog.');
}
console.log('Clicking Save button...');
saveBtn.click();
// Wait for save operation to complete and dialog to close
let dialogClosed = false;
const startTime = Date.now();
while (!dialogClosed && Date.now() - startTime < 5000) {
await new Promise(resolve => setTimeout(resolve, 500));
dialogClosed = !isDialogOpen();
}
if (!dialogClosed) {
// If dialog is still open, look for errors
const errorElements = document.querySelectorAll('.pxl-alert-warn, .pxl-alert-error, [class*="error"], [class*="alert"]');
let errorMsg = 'Unknown error - dialog remained open';
for (const errorEl of errorElements) {
if (isElementVisible(errorEl)) {
errorMsg = errorEl.textContent.trim();
break;
}
}
throw new Error(`Error from Immuta: ${errorMsg}`);
}
// Wait a bit longer for any page updates
await new Promise(resolve => setTimeout(resolve, 1000));
return true;
} catch (error) {
console.error(`Failed to create group "${groupName}": ${error.message}`);
// Try to close the dialog if it's still open
if (isDialogOpen()) {
try {
const cancelBtn = findCancelButton();
if (cancelBtn) {
console.log('Closing dialog...');
cancelBtn.click();
await new Promise(resolve => setTimeout(resolve, 1000));
}
} catch (e) {
console.warn('Could not close dialog:', e);
}
}
return false;
}
}
// Function to process multiple group names
async function processGroups(groupNames) {
if (!groupNames || groupNames.length === 0) {
console.error('No valid group names provided.');
return;
}
// Create a results tracker
const results = {
total: groupNames.length,
success: 0,
failed: 0,
failedGroups: []
};
console.log(`Starting creation of ${groupNames.length} groups...`);
console.log('----------------------------------------');
for (let i = 0; i < groupNames.length; i++) {
const groupName = groupNames[i];
console.log(`Processing ${i+1}/${groupNames.length}: "${groupName}"`);
const success = await createGroup(groupName);
if (success) {
results.success++;
console.log(`✅ Group "${groupName}" created successfully`);
} else {
results.failed++;
results.failedGroups.push(groupName);
console.error(`❌ Failed to create group "${groupName}"`);
}
console.log('----------------------------------------');
// Brief pause between operations to avoid overwhelming the server
await new Promise(resolve => setTimeout(resolve, 2000));
}
// Display final summary
console.log('\n==== FINAL RESULTS ====');
console.log(`Total groups processed: ${results.total}`);
console.log(`Successfully created: ${results.success}`);
console.log(`Failed to create: ${results.failed}`);
if (results.failed > 0) {
console.log('\nFailed groups:');
results.failedGroups.forEach((name, i) => {
console.log(`${i+1}. "${name}"`);
});
}
return results;
}
// Function to create file input and handle selection
function promptForFile() {
return new Promise((resolve) => {
// Create styled container for file input
const container = document.createElement('div');
container.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 10000;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
max-width: 500px;
width: 90%;
font-family: Arial, sans-serif;
`;
// Add title and instruction
const title = document.createElement('h3');
title.textContent = 'Import Groups to Immuta';
title.style.margin = '0 0 10px 0';
const instructions = document.createElement('p');
instructions.innerHTML = 'Select a CSV or Excel file containing group names. The first column will be used for group names.<br><small style="color: #666;">Note: In restricted environments, CSV files are recommended.</small>';
instructions.style.margin = '0 0 15px 0';
instructions.style.fontSize = '14px';
// Create file input
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.csv,.xlsx,.xls';
fileInput.style.width = '100%';
fileInput.style.marginBottom = '15px';
// Create buttons container
const buttonsContainer = document.createElement('div');
buttonsContainer.style.display = 'flex';
buttonsContainer.style.justifyContent = 'flex-end';
buttonsContainer.style.gap = '10px';
// Cancel button
const cancelButton = document.createElement('button');
cancelButton.textContent = 'Cancel';
cancelButton.style.cssText = `
padding: 8px 16px;
border: 1px solid #ccc;
background: #f5f5f5;
border-radius: 4px;
cursor: pointer;
`;
// Handle file selection
fileInput.onchange = () => {
const file = fileInput.files[0];
if (file) {
container.remove();
resolve(file);
}
};
// Handle cancel
cancelButton.onclick = () => {
container.remove();
resolve(null);
};
// Add elements to container
buttonsContainer.appendChild(cancelButton);
container.appendChild(title);
container.appendChild(instructions);
container.appendChild(fileInput);
container.appendChild(buttonsContainer);
// Add to body
document.body.appendChild(container);
});
}
// Main function
async function main() {
console.clear();
console.log('Immuta Group Creation Tool');
console.log('=========================');
console.log('This tool will help you create multiple Immuta groups from a CSV or Excel file.');
try {
// Check if we can load required libraries
const librariesLoaded = await loadLibraries();
if (librariesLoaded) {
console.log('✅ Required libraries loaded successfully');
} else {
console.log('⚠️ Running in offline mode - Excel support may be limited');
console.log(' CSV files are recommended in this environment');
}
// Prompt for file
console.log('Please select a CSV or Excel file...');
const file = await promptForFile();
if (!file) {
console.log('❌ Operation cancelled: No file selected');
return;
}
console.log(`📁 File selected: ${file.name}`);
try {
// Read file and extract group names
const groupNames = await readFile(file);
if (!groupNames || groupNames.length === 0) {
console.error('❌ No valid group names found in the file.');
console.log(' Please check that your file has group names in the first column.');
return;
}
console.log(`📋 Found ${groupNames.length} group names:`);
groupNames.forEach((name, i) => {
if (i < 10 || i >= groupNames.length - 5) {
console.log(` ${i+1}. ${name}`);
} else if (i === 10) {
console.log(` ... (${groupNames.length - 15} more) ...`);
}
});
// Confirm with user
const confirmMessage = `Do you want to create ${groupNames.length} groups in Immuta?`;
if (!confirm(confirmMessage)) {
console.log('❌ Operation cancelled by user');
return;
}
// Process groups
const results = await processGroups(groupNames);
if (results && results.failed > 0 && results.failed < results.total) {
// Ask if user wants to retry failed groups
if (confirm(`Do you want to retry the ${results.failed} failed groups?`)) {
console.log('\nRetrying failed groups...');
await processGroups(results.failedGroups);
}
}
console.log('\n✅ Group creation process completed!');
} catch (error) {
if (error.message.includes('Unable to parse Excel file in offline mode')) {
console.error('❌ Excel parsing failed in offline mode.');
console.log(' Please convert your Excel file to CSV format and try again.');
} else {
console.error('❌ Error processing file:', error.message);
}
}
} catch (error) {
console.error('❌ Error:', error.message);
console.log('If the error persists, try refreshing the page and running the script again.');
}
}
// Execute the main function
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment