Add 'tags' to event model. Add a dataimport system - currently for MaxMind zip files

This commit is contained in:
Dan Milne
2025-11-11 10:31:36 +11:00
parent 772fae7e8b
commit 26216da9ca
34 changed files with 3580 additions and 14 deletions

View File

@@ -1,3 +1,4 @@
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"
import "tom-select"

View File

@@ -0,0 +1,145 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "select" ]
static values = {
options: Array,
placeholder: String
}
connect() {
// Check if the element is visible, if not, wait for it to become visible
if (this.isHidden()) {
// Element is hidden, set up a MutationObserver to watch for visibility changes
this.observer = new MutationObserver(() => {
if (!this.isHidden()) {
this.initializeTomSelect()
this.observer.disconnect()
}
})
this.observer.observe(this.element, {
attributes: true,
attributeFilter: ['class']
})
// Also check periodically as a fallback
this.checkInterval = setInterval(() => {
if (!this.isHidden()) {
this.initializeTomSelect()
this.cleanup()
}
}, 500)
} else {
// Element is already visible, initialize immediately
this.initializeTomSelect()
}
}
isHidden() {
return this.element.offsetParent === null || this.element.classList.contains('hidden')
}
cleanup() {
if (this.observer) {
this.observer.disconnect()
this.observer = null
}
if (this.checkInterval) {
clearInterval(this.checkInterval)
this.checkInterval = null
}
}
initializeTomSelect() {
if (!this.hasSelectTarget) {
console.log('No select target found')
return
}
// Check if Tom Select is available
if (typeof TomSelect === 'undefined') {
console.log('Tom Select is not loaded')
return
}
// If TomSelect is already initialized, destroy it first
if (this.tomSelect) {
this.tomSelect.destroy()
}
console.log('Initializing Tom Select with options:', this.optionsValue.length, 'countries')
console.log('First few country options:', this.optionsValue.slice(0, 3))
// Prepare options for Tom Select
const options = this.optionsValue.map(([display, value]) => ({
value: value,
text: display,
// Add searchable fields for better search
search: display + ' ' + value
}))
// Get currently selected values from the hidden select
const selectedValues = Array.from(this.selectTarget.selectedOptions).map(option => option.value)
try {
// Initialize Tom Select
this.tomSelect = new TomSelect(this.selectTarget, {
options: options,
items: selectedValues,
plugins: ['remove_button'],
maxItems: null,
maxOptions: 1000,
create: false,
placeholder: this.placeholderValue || "Search and select countries...",
searchField: ['text', 'search'],
searchConjunction: 'or',
onItemAdd: function() {
// Clear the search input after selecting an item
this.setTextboxValue('');
this.refreshOptions();
},
render: {
option: function(data, escape) {
return `<div class="flex items-center p-2">
<span>${escape(data.text)}</span>
</div>`
},
item: function(data, escape) {
return `<div class="flex items-center text-sm">
<span>${escape(data.text)}</span>
</div>`
}
},
dropdownParent: 'body',
copyClassesToDropdown: false
})
console.log('Tom Select successfully initialized for country selector')
// Make sure the wrapper is visible
setTimeout(() => {
if (this.tomSelect && this.tomSelect.wrapper) {
this.tomSelect.wrapper.style.visibility = 'visible'
this.tomSelect.wrapper.style.display = 'block'
console.log('Tom Select wrapper made visible')
}
}, 100)
} catch (error) {
console.error('Error initializing Tom Select:', error)
}
}
// Public method to reinitialize if needed
reinitialize() {
this.initializeTomSelect()
}
disconnect() {
this.cleanup()
if (this.tomSelect) {
this.tomSelect.destroy()
}
}
}

View File

@@ -0,0 +1,76 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["progressBar", "totalRecords", "processedRecords", "failedRecords", "recordsPerSecond"]
static values = {
importId: Number,
refreshInterval: { type: Number, default: 2000 }
}
connect() {
if (this.hasImportIdValue) {
this.startUpdating()
}
}
disconnect() {
this.stopUpdating()
}
startUpdating() {
this.updateProgress()
this.interval = setInterval(() => {
this.updateProgress()
}, this.refreshIntervalValue)
}
stopUpdating() {
if (this.interval) {
clearInterval(this.interval)
}
}
async updateProgress() {
try {
const response = await fetch(`/data_imports/${this.importIdValue}/progress`)
const data = await response.json()
this.updateProgressBar(data.progress_percentage)
this.updateStats(data)
// If completed or failed, reload the page
if (data.status === 'completed' || data.status === 'failed') {
setTimeout(() => {
window.location.reload()
}, 2000)
this.stopUpdating()
}
} catch (error) {
console.error('Error updating progress:', error)
}
}
updateProgressBar(percentage) {
if (this.hasProgressBarTarget) {
this.progressBarTarget.style.width = `${percentage}%`
}
}
updateStats(data) {
if (this.hasTotalRecordsTarget) {
this.totalRecordsTarget.textContent = data.total_records.toLocaleString()
}
if (this.hasProcessedRecordsTarget) {
this.processedRecordsTarget.textContent = data.processed_records.toLocaleString()
}
if (this.hasFailedRecordsTarget) {
this.failedRecordsTarget.textContent = data.failed_records.toLocaleString()
}
if (this.hasRecordsPerSecondTarget) {
this.recordsPerSecondTarget.textContent = data.records_per_second.toLocaleString()
}
}
}