+
@@ -5904,7 +6231,1178 @@
Available Log Files
setTimeout(refreshLogs, 100);
});
}
+
+ // ================================
+ // BULK IMPORT FUNCTIONALITY
+ // ================================
+
+ let bulkImportData = null;
+
+ // Open bulk import modal - make it globally available
+ window.openBulkImportModal = function () {
+ console.log("openBulkImportModal called");
+ const modal = document.getElementById("bulk-import-modal");
+ console.log("Modal element:", modal);
+ if (modal) {
+ modal.classList.remove("hidden");
+ resetBulkImportModal();
+ } else {
+ console.error("Modal element not found!");
+ }
+ };
+
+ // Close bulk import modal
+ window.closeBulkImportModal = function () {
+ document.getElementById("bulk-import-modal").classList.add("hidden");
+ resetBulkImportModal();
+ };
+
+ // Reset modal to initial state
+ function resetBulkImportModal() {
+ // Reset radio buttons
+ document.querySelector(
+ 'input[name="import-method"][value="file"]',
+ ).checked = true;
+ toggleImportMethod("file");
+
+ // Clear file input
+ document.getElementById("bulk-import-file").value = "";
+ document.getElementById("file-info").classList.add("hidden");
+
+ // Clear JSON textarea
+ document.getElementById("bulk-import-json").value = "";
+
+ // Hide sections
+ document.getElementById("import-preview").classList.add("hidden");
+ document.getElementById("import-status").classList.add("hidden");
+ document.getElementById("import-loading").classList.add("hidden");
+
+ // Reset validation status
+ document.getElementById("json-validation-status").textContent = "";
+ document.getElementById("json-validation-status").className =
+ "text-sm";
+
+ // Disable submit button
+ document.getElementById("import-submit-btn").disabled = true;
+
+ // Reset global data
+ bulkImportData = null;
+ }
+
+ // Toggle between file upload and JSON paste
+ window.toggleImportMethod = function (method) {
+ const fileSection = document.getElementById("file-upload-section");
+ const pasteSection = document.getElementById("json-paste-section");
+
+ if (method === "file") {
+ fileSection.classList.remove("hidden");
+ pasteSection.classList.add("hidden");
+ } else {
+ fileSection.classList.add("hidden");
+ pasteSection.classList.remove("hidden");
+ }
+
+ // Clear preview when switching methods
+ document.getElementById("import-preview").classList.add("hidden");
+ document.getElementById("import-submit-btn").disabled = true;
+ bulkImportData = null;
+ };
+
+ // Handle file selection
+ window.handleFileSelect = function (input) {
+ const file = input.files[0];
+ if (!file) {
+ document.getElementById("file-info").classList.add("hidden");
+ return;
+ }
+
+ // Validate file type
+ if (!file.name.toLowerCase().endsWith(".json")) {
+ showImportError("Please select a JSON file.");
+ input.value = "";
+ return;
+ }
+
+ // Show file info
+ document.getElementById("file-name").textContent = file.name;
+ document.getElementById("file-size").textContent =
+ `(${formatFileSize(file.size)})`;
+ document.getElementById("file-info").classList.remove("hidden");
+
+ // Read file content
+ const reader = new FileReader();
+ reader.onload = function (e) {
+ try {
+ const jsonData = JSON.parse(e.target.result);
+ processImportData(jsonData);
+ } catch (error) {
+ showImportError(`Invalid JSON file: ${error.message}`);
+ input.value = "";
+ document.getElementById("file-info").classList.add("hidden");
+ }
+ };
+ reader.onerror = function () {
+ showImportError("Error reading file.");
+ input.value = "";
+ document.getElementById("file-info").classList.add("hidden");
+ };
+ reader.readAsText(file);
+ };
+
+ // Validate JSON input from textarea
+ window.validateJsonInput = function () {
+ const jsonText = document
+ .getElementById("bulk-import-json")
+ .value.trim();
+ const statusElement = document.getElementById(
+ "json-validation-status",
+ );
+
+ if (!jsonText) {
+ statusElement.textContent = "Please enter JSON data";
+ statusElement.className =
+ "text-sm text-yellow-600 dark:text-yellow-400";
+ return;
+ }
+
+ try {
+ const jsonData = JSON.parse(jsonText);
+
+ // Validate structure with helpful messages
+ if (!Array.isArray(jsonData)) {
+ statusElement.textContent =
+ "✗ JSON must be an array of tool objects [{}], not a single object {}";
+ statusElement.className =
+ "text-sm text-red-600 dark:text-red-400";
+ document.getElementById("import-preview").classList.add("hidden");
+ document.getElementById("import-submit-btn").disabled = true;
+ bulkImportData = null;
+ return;
+ }
+
+ if (jsonData.length === 0) {
+ statusElement.textContent =
+ "✗ Array cannot be empty - add at least one tool";
+ statusElement.className =
+ "text-sm text-red-600 dark:text-red-400";
+ document.getElementById("import-preview").classList.add("hidden");
+ document.getElementById("import-submit-btn").disabled = true;
+ bulkImportData = null;
+ return;
+ }
+
+ // Success with helpful info
+ const toolCount = jsonData.length;
+ const toolsWithNames = jsonData.filter((tool) => tool && tool.name);
+
+ if (toolsWithNames.length === toolCount) {
+ statusElement.textContent = `✓ Valid JSON array with ${toolCount} tool(s) - names will be auto-fixed for compatibility`;
+ statusElement.className =
+ "text-sm text-green-600 dark:text-green-400";
+ } else {
+ statusElement.textContent = `⚠ Valid JSON but ${toolCount - toolsWithNames.length} tool(s) missing required name field`;
+ statusElement.className =
+ "text-sm text-yellow-600 dark:text-yellow-400";
+ }
+
+ processImportData(jsonData);
+ } catch (error) {
+ let helpText = "";
+ if (error.message.includes("Unexpected")) {
+ helpText = " (Check for missing commas, quotes, or brackets)";
+ }
+ statusElement.textContent = `✗ Invalid JSON: ${error.message}${helpText}`;
+ statusElement.className = "text-sm text-red-600 dark:text-red-400";
+ document.getElementById("import-preview").classList.add("hidden");
+ document.getElementById("import-submit-btn").disabled = true;
+ bulkImportData = null;
+ }
+ };
+
+ // Process and validate import data
+ function processImportData(data) {
+ try {
+ // Validate data structure
+ if (!Array.isArray(data)) {
+ throw new Error("Data must be an array of tool definitions");
+ }
+
+ if (data.length === 0) {
+ throw new Error("Array cannot be empty");
+ }
+
+ if (data.length > 200) {
+ throw new Error(
+ `Too many tools (${data.length}). Maximum 200 allowed.`,
+ );
+ }
+
+ // Auto-fix and validate each tool
+ const errors = [];
+ const fixedData = data.map((tool, index) => {
+ if (!tool || typeof tool !== "object") {
+ errors.push(`Item ${index + 1}: Must be an object`);
+ return tool;
+ }
+
+ // Create a copy to avoid modifying original
+ const fixedTool = { ...tool };
+
+ // Fix tool name to meet validation requirements
+ if (fixedTool.name) {
+ const originalName = fixedTool.name;
+ // Convert to valid format: start with letter, only letters/numbers/dots/underscores/hyphens
+ fixedTool.name = String(originalName)
+ .trim()
+ .replace(/\s+/g, "_") // Replace spaces with underscores
+ .replace(/[^a-zA-Z0-9._-]/g, "") // Remove invalid characters
+ .replace(/^[^a-zA-Z]/, "tool_") // Ensure starts with letter
+ .toLowerCase();
+
+ if (fixedTool.name !== originalName) {
+ console.log(
+ `Auto-fixed tool name: "${originalName}" -> "${fixedTool.name}"`,
+ );
+ }
+ }
+
+ // Fix tags if present (tags must be an array, not a string)
+ if (fixedTool.tags) {
+ const originalTags = fixedTool.tags;
+ if (typeof fixedTool.tags === "string") {
+ // Convert comma-separated string to array of strings
+ fixedTool.tags = fixedTool.tags
+ .split(",") // Split by comma
+ .map((tag) => tag.trim()) // Trim whitespace
+ .filter((tag) => tag.length >= 2) // Remove empty/too short tags
+ .map((tag) => tag.toLowerCase()) // Convert to lowercase
+ .map((tag) => tag.replace(/[^a-z0-9\-\:\.]/g, "")) // Remove invalid characters
+ .filter((tag) => tag.length >= 2); // Filter again after cleaning
+
+ if (
+ JSON.stringify(fixedTool.tags) !==
+ JSON.stringify(originalTags)
+ ) {
+ console.log(
+ `Auto-fixed tags: "${originalTags}" -> ${JSON.stringify(fixedTool.tags)}`,
+ );
+ }
+ } else if (Array.isArray(fixedTool.tags)) {
+ // Clean up array of tags
+ fixedTool.tags = fixedTool.tags
+ .map((tag) => String(tag).trim().toLowerCase())
+ .map((tag) => tag.replace(/[^a-z0-9\-\:\.]/g, ""))
+ .filter((tag) => tag.length >= 2);
+
+ if (
+ JSON.stringify(fixedTool.tags) !==
+ JSON.stringify(originalTags)
+ ) {
+ console.log(
+ `Auto-fixed tags array: ${JSON.stringify(originalTags)} -> ${JSON.stringify(fixedTool.tags)}`,
+ );
+ }
+ }
+ }
+
+ // Only name is strictly required
+ const required = ["name"];
+ const missing = required.filter(
+ (field) =>
+ !fixedTool[field] ||
+ (typeof fixedTool[field] === "string" &&
+ fixedTool[field].trim() === ""),
+ );
+ if (missing.length > 0) {
+ errors.push(
+ `Item ${index + 1}: Missing required fields: ${missing.join(", ")}`,
+ );
+ }
+
+ // Warn about missing recommended fields but don't block import
+ const recommended = ["url"];
+ const missingRecommended = recommended.filter(
+ (field) =>
+ !fixedTool[field] ||
+ (typeof fixedTool[field] === "string" &&
+ fixedTool[field].trim() === ""),
+ );
+ if (missingRecommended.length > 0) {
+ console.warn(
+ `Item ${index + 1} (${fixedTool.name || "unnamed"}): Missing recommended fields: ${missingRecommended.join(", ")}`,
+ );
+ }
+
+ return fixedTool;
+ });
+
+ if (errors.length > 0) {
+ throw new Error(
+ `Validation errors:\\n${errors.slice(0, 5).join("\\n")}${errors.length > 5 ? `\\n... and ${errors.length - 5} more errors` : ""}`,
+ );
+ }
+
+ // Store the fixed data instead of original
+ bulkImportData = fixedData;
+
+ // Show preview
+ showImportPreview(fixedData);
+
+ // Enable submit button
+ document.getElementById("import-submit-btn").disabled = false;
+
+ // Clear any previous error messages
+ document.getElementById("import-status").classList.add("hidden");
+ } catch (error) {
+ showImportError(error.message);
+ bulkImportData = null;
+ document.getElementById("import-submit-btn").disabled = true;
+ }
+ } // Show import preview
+ function showImportPreview(data) {
+ document.getElementById("preview-count").textContent = data.length;
+
+ // Populate preview list
+ const previewList = document.getElementById("preview-list");
+ previewList.innerHTML = "";
+
+ data.slice(0, 10).forEach((tool, index) => {
+ const li = document.createElement("li");
+ li.className = "flex justify-between";
+ li.innerHTML = `
+
${escapeHtml(tool.name || "Unnamed")}
+
${escapeHtml(tool.integrationType || "REST")}
+ `;
+ previewList.appendChild(li);
+ });
+
+ if (data.length > 10) {
+ const li = document.createElement("li");
+ li.className = "text-gray-500 italic";
+ li.textContent = `... and ${data.length - 10} more tools`;
+ previewList.appendChild(li);
+ }
+
+ document.getElementById("import-preview").classList.remove("hidden");
+ }
+
+ // Toggle validation guide visibility
+ window.toggleValidationGuide = function () {
+ const guide = document.getElementById("validation-guide");
+ const toggle = document.getElementById("validation-guide-toggle");
+
+ if (guide.classList.contains("hidden")) {
+ guide.classList.remove("hidden");
+ toggle.textContent = "Hide Validation Rules";
+ } else {
+ guide.classList.add("hidden");
+ toggle.textContent = "Show Validation Rules";
+ }
+ };
+
+ // Toggle preview details
+ window.togglePreviewDetails = function () {
+ const details = document.getElementById("preview-details");
+ const toggleText = document.getElementById("preview-toggle-text");
+
+ if (details.classList.contains("hidden")) {
+ details.classList.remove("hidden");
+ toggleText.textContent = "Hide Details";
+ } else {
+ details.classList.add("hidden");
+ toggleText.textContent = "Show Details";
+ }
+ };
+
+ // Submit bulk import
+ window.submitBulkImport = async function () {
+ if (!bulkImportData) {
+ showImportError("No valid data to import");
+ return;
+ }
+
+ // Show loading
+ document.getElementById("import-loading").classList.remove("hidden");
+ document.getElementById("import-submit-btn").disabled = true;
+
+ try {
+ const response = await fetch(
+ `${window.location.origin}/admin/tools/import`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(bulkImportData),
+ },
+ );
+
+ const result = await response.json();
+
+ // Debug logging
+ console.log("Response status:", response.status);
+ console.log("Response ok:", response.ok);
+ console.log("Result:", result);
+ console.log("Result.success:", result.success);
+ console.log("Result.created_count:", result.created_count);
+ console.log("Result.failed_count:", result.failed_count);
+ console.log("Result.errors:", result.errors);
+
+ // Log individual errors for debugging
+ if (result.errors && result.errors.length > 0) {
+ console.log("Detailed errors:");
+ result.errors.forEach((error, index) => {
+ console.log(`Error ${index + 1}:`, error);
+ if (error.error) {
+ console.log(` Error message:`, error.error.message);
+ console.log(
+ ` Full error object:`,
+ JSON.stringify(error.error, null, 2),
+ );
+ }
+ });
+ }
+
+ // Hide loading
+ document.getElementById("import-loading").classList.add("hidden");
+
+ if (response.ok) {
+ if (result.success) {
+ // Complete success
+ showImportSuccess(result);
+
+ // Auto-close modal after success and refresh page
+ setTimeout(() => {
+ closeBulkImportModal();
+ location.reload(); // Refresh to show new tools
+ }, 3000);
+ } else if (result.created_count > 0) {
+ // Partial success - some tools imported successfully
+ showImportSuccess(result);
+
+ // Auto-close modal after partial success and refresh page
+ setTimeout(() => {
+ closeBulkImportModal();
+ location.reload(); // Refresh to show new tools
+ }, 5000); // Longer delay to see the message
+ } else {
+ // Complete failure
+ showImportError(result.message || "All tools failed to import");
+ document.getElementById("import-submit-btn").disabled = false;
+ }
+ } else {
+ showImportError(result.message || "Import failed");
+ document.getElementById("import-submit-btn").disabled = false;
+ }
+ } catch (error) {
+ document.getElementById("import-loading").classList.add("hidden");
+ showImportError(`Network error: ${error.message}`);
+ document.getElementById("import-submit-btn").disabled = false;
+ }
+ };
+
+ // Show import success message
+ function showImportSuccess(result) {
+ const statusDiv = document.getElementById("import-status");
+ const isPartialSuccess = result.failed_count > 0;
+ const bgColor = isPartialSuccess ? "yellow" : "green";
+ const textColor = isPartialSuccess ? "yellow" : "green";
+
+ statusDiv.innerHTML = `
+
+
+
+
+
+ ${isPartialSuccess ? "Import Partially Completed" : "Import Completed Successfully"}
+
+
+
${result.created_count} tools imported successfully
+ ${result.failed_count > 0 ? `
${result.failed_count} tools failed to import
` : ""}
+ ${
+ result.errors && result.errors.length > 0
+ ? `
+
+ View errors
+
+ ${result.errors
+ .slice(0, 5)
+ .map(
+ (error) =>
+ `- ${error.name || "Tool"}: ${error.error?.message || "Unknown error"}
`,
+ )
+ .join("")}
+ ${result.errors.length > 5 ? `- ... and ${result.errors.length - 5} more errors
` : ""}
+
+
+ `
+ : ""
+ }
+
Page will refresh automatically...
+
+
+
+
+ `;
+ statusDiv.classList.remove("hidden");
+ }
+
+ // Show import error message
+ function showImportError(message) {
+ const statusDiv = document.getElementById("import-status");
+ statusDiv.innerHTML = `
+
+
+
+
+
+ Import Error
+
+
+
${escapeHtml(message)}
+
+
+
+
+ `;
+ statusDiv.classList.remove("hidden");
+ }
+
+ // Format file size
+ function formatFileSize(bytes) {
+ if (bytes === 0) return "0 Bytes";
+ const k = 1024;
+ const sizes = ["Bytes", "KB", "MB", "GB"];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return (
+ parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
+ );
+ }
+
+ // Auto-trigger JSON validation when typing in textarea
+ document.addEventListener("DOMContentLoaded", function () {
+ const jsonTextarea = document.getElementById("bulk-import-json");
+ if (jsonTextarea) {
+ let validationTimeout;
+ jsonTextarea.addEventListener("input", function () {
+ clearTimeout(validationTimeout);
+ validationTimeout = setTimeout(function () {
+ if (jsonTextarea.value.trim()) {
+ validateJsonInput();
+ }
+ }, 1000); // Validate 1 second after user stops typing
+ });
+ }
+ });
+
+ // Dropdown Functions
+ window.toggleBulkImportDropdown = function () {
+ const dropdown = document.getElementById("bulk-import-dropdown");
+ dropdown.classList.toggle("hidden");
+ };
+
+ window.toggleDropdownImportMethod = function (method) {
+ const fileSection = document.getElementById("dropdown-file-section");
+ const pasteSection = document.getElementById(
+ "dropdown-paste-section",
+ );
+
+ if (method === "file") {
+ fileSection.classList.remove("hidden");
+ pasteSection.classList.add("hidden");
+ } else {
+ fileSection.classList.add("hidden");
+ pasteSection.classList.remove("hidden");
+ }
+ resetDropdownImport();
+ };
+
+ window.handleDropdownFileSelect = function (input) {
+ const fileInfo = document.getElementById("dropdown-file-info");
+ const preview = document.getElementById("dropdown-preview");
+ const status = document.getElementById("dropdown-status");
+
+ if (!input.files[0]) {
+ resetDropdownImport();
+ return;
+ }
+
+ const file = input.files[0];
+ fileInfo.textContent = `Selected: ${file.name} (${(file.size / 1024).toFixed(1)} KB)`;
+ fileInfo.classList.remove("hidden");
+
+ const reader = new FileReader();
+ reader.onload = function (e) {
+ try {
+ const data = JSON.parse(e.target.result);
+ validateDropdownData(data);
+ } catch (error) {
+ showDropdownStatus("error", `Invalid JSON: ${error.message}`);
+ }
+ };
+ reader.readAsText(file);
+ };
+
+ window.validateDropdownJson = function () {
+ const textarea = document.getElementById("dropdown-json-textarea");
+ const status = document.getElementById("dropdown-json-status");
+
+ if (!textarea.value.trim()) {
+ status.textContent = "";
+ document.getElementById("dropdown-import-btn").disabled = true;
+ document.getElementById("dropdown-preview").classList.add("hidden");
+ return;
+ }
+
+ try {
+ const data = JSON.parse(textarea.value);
+ status.innerHTML =
+ '
✓ Valid JSON';
+ validateDropdownData(data);
+ } catch (error) {
+ status.innerHTML = `
✗ Invalid JSON: ${error.message}`;
+ document.getElementById("dropdown-import-btn").disabled = true;
+ document.getElementById("dropdown-preview").classList.add("hidden");
+ }
+ };
+
+ window.validateDropdownData = function (data) {
+ const preview = document.getElementById("dropdown-preview");
+ const count = document.getElementById("dropdown-preview-count");
+ const importBtn = document.getElementById("dropdown-import-btn");
+
+ if (!Array.isArray(data)) {
+ showDropdownStatus("error", "Data must be an array of tools");
+ return;
+ }
+
+ if (data.length === 0) {
+ showDropdownStatus("error", "Array cannot be empty");
+ return;
+ }
+
+ if (data.length > 200) {
+ showDropdownStatus(
+ "error",
+ "Cannot import more than 200 tools at once",
+ );
+ return;
+ }
+
+ count.textContent = data.length;
+ preview.classList.remove("hidden");
+ importBtn.disabled = false;
+ showDropdownStatus(
+ "success",
+ `${data.length} tools ready for import`,
+ );
+ };
+
+ window.showDropdownStatus = function (type, message) {
+ const status = document.getElementById("dropdown-status");
+ status.innerHTML = `
${message}
`;
+ status.classList.remove("hidden");
+ };
+
+ window.resetDropdownImport = function () {
+ document.getElementById("dropdown-file-input").value = "";
+ document.getElementById("dropdown-json-textarea").value = "";
+ document.getElementById("dropdown-file-info").classList.add("hidden");
+ document.getElementById("dropdown-json-status").textContent = "";
+ document.getElementById("dropdown-preview").classList.add("hidden");
+ document.getElementById("dropdown-status").classList.add("hidden");
+ document.getElementById("dropdown-import-btn").disabled = true;
+ };
+
+ window.submitDropdownImport = async function () {
+ const importBtn = document.getElementById("dropdown-import-btn");
+ const status = document.getElementById("dropdown-status");
+
+ let data;
+ const fileInput = document.getElementById("dropdown-file-input");
+ const textarea = document.getElementById("dropdown-json-textarea");
+
+ if (fileInput.files[0]) {
+ const reader = new FileReader();
+ reader.onload = async function (e) {
+ try {
+ data = JSON.parse(e.target.result);
+ await performDropdownImport(data);
+ } catch (error) {
+ showDropdownStatus(
+ "error",
+ `Failed to parse file: ${error.message}`,
+ );
+ }
+ };
+ reader.readAsText(fileInput.files[0]);
+ } else if (textarea.value.trim()) {
+ try {
+ data = JSON.parse(textarea.value);
+ await performDropdownImport(data);
+ } catch (error) {
+ showDropdownStatus("error", `Invalid JSON: ${error.message}`);
+ }
+ }
+ };
+
+ window.performDropdownImport = async function (data) {
+ const importBtn = document.getElementById("dropdown-import-btn");
+ const status = document.getElementById("dropdown-status");
+
+ importBtn.disabled = true;
+ importBtn.textContent = "Importing...";
+
+ try {
+ const response = await fetch("/admin/tools/import", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(data),
+ });
+
+ const result = await response.json();
+
+ if (response.ok) {
+ showDropdownStatus(
+ "success",
+ `✅ Successfully imported ${result.imported} tools`,
+ );
+ resetDropdownImport();
+ setTimeout(() => {
+ window.location.reload();
+ }, 2000);
+ } else {
+ showDropdownStatus(
+ "error",
+ `❌ Import failed: ${result.detail || "Unknown error"}`,
+ );
+ }
+ } catch (error) {
+ showDropdownStatus("error", `❌ Network error: ${error.message}`);
+ } finally {
+ importBtn.disabled = false;
+ importBtn.textContent = "Import Tools";
+ }
+ };
+
+ // Close dropdown when clicking outside
+ document.addEventListener("click", function (event) {
+ const dropdown = document.getElementById("bulk-import-dropdown");
+ const button = document.getElementById("bulk-import-dropdown-btn");
+
+ if (
+ dropdown &&
+ !dropdown.contains(event.target) &&
+ !button.contains(event.target)
+ ) {
+ dropdown.classList.add("hidden");
+ }
+ });
});
+
+
+
+
+
+
+
+
+ Bulk Import Tools
+
+
+
+
+
+
+
+
+
+
+
+
+ Import Instructions
+
+
+
+
+
+
+
+
+ 📋 JSON Validation Rules
+
+
+
+ ✅ Required:
+ name
+ - must start with letter, only
+ letters/numbers/dots/underscores/hyphens
+
+
+ 🔹 Optional:
+ url
+ (http/https/ws/wss only),
+ description,
+ tags
+ (array only)
+
+
+ ⚠️ Case Sensitive:
+ requestType
+ ("GET", "POST"),
+ integrationType
+ ("REST", "MCP")
+
+
+ ❌ Not Allowed:
+ Names starting with numbers, FTP URLs, tag strings
+ "tag1,tag2" (use ["tag1","tag2"])
+
+
+ 🔧 Auto-Fixed:
+ Spaces in names, tag strings → arrays, case
+ normalization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
or drag and drop
+
+
+ JSON files only
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Import Preview
+
+
+
+
+ 0 tools ready for import
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Processing import...
+
+
+
+
+
+
+
+
+
+
+