Last active
June 11, 2025 13:26
-
-
Save npretto/da31954c42f83d9bbb4e194f1727f712 to your computer and use it in GitHub Desktop.
handy userscript for working on metabase
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* eslint-disable no-color-literals */ | |
| // ==UserScript== | |
| // @name Metabase dev stuff | |
| // @namespace Violentmonkey Scripts | |
| // @version 1.3 | |
| // @description Fills forms and do things | |
| // @author Your Name | |
| // @match *://*/* | |
| // @grant GM_registerMenuCommand | |
| // @grant GM_setValue | |
| // @grant GM_getValue | |
| // @require https://cdn.jsdelivr.net/npm/@violentmonkey/shortcut@1 | |
| // ==/UserScript== | |
| (function () { | |
| console.log("FormFiller script loaded!"); | |
| /** | |
| * Fills form fields based on the provided values object. | |
| * Keys in the values object should correspond to the 'name' attribute of form elements. | |
| * @param {object} values - An object where keys are field names and values are the values to set. | |
| */ | |
| function fillForm(values) { | |
| // console.log("Attempting to fill form with values:", values); | |
| for (const name in values) { | |
| // eslint-disable-next-line no-prototype-builtins | |
| if (Object.prototype.hasOwnProperty.call(values, name)) { | |
| const elements = document.querySelectorAll(`[name="${name}"]`); | |
| if (elements.length === 0) { | |
| console.warn(`No element found with name: ${name}`); | |
| continue; | |
| } | |
| elements.forEach((element) => { | |
| const valueToSet = values[name]; | |
| // console.log( | |
| // `Processing element [name="${name}"] (type: ${element.type}) with value:`, | |
| // valueToSet, | |
| // ); | |
| // Get the native value setter for the element type | |
| let nativeInputValueSetter; | |
| if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") { | |
| nativeInputValueSetter = Object.getOwnPropertyDescriptor( | |
| window.HTMLInputElement.prototype, // Or HTMLTextAreaElement.prototype for textareas | |
| "value", | |
| )?.set; | |
| if (element.tagName === "TEXTAREA") { | |
| nativeInputValueSetter = Object.getOwnPropertyDescriptor( | |
| window.HTMLTextAreaElement.prototype, | |
| "value", | |
| )?.set; | |
| } | |
| } | |
| if (element.type === "checkbox") { | |
| // For checkboxes, React often expects a click to toggle state | |
| if (element.checked !== Boolean(valueToSet)) { | |
| element.click(); // This should trigger Formik's handleChange | |
| } | |
| // As a fallback or for non-React contexts, also set checked property | |
| element.checked = Boolean(valueToSet); | |
| } else if (element.type === "radio") { | |
| if (element.value === String(valueToSet)) { | |
| if (!element.checked) { | |
| element.click(); // Trigger Formik's handleChange | |
| } | |
| element.checked = true; | |
| } else { | |
| element.checked = false; | |
| } | |
| } else if (element.tagName === "SELECT") { | |
| element.value = valueToSet; | |
| if (element.value !== valueToSet) { | |
| for (let i = 0; i < element.options.length; i++) { | |
| if ( | |
| element.options[i].text === valueToSet || | |
| element.options[i].value === valueToSet | |
| ) { | |
| element.options[i].selected = true; | |
| break; | |
| } | |
| } | |
| } | |
| } else { | |
| // Use native setter for text inputs and textareas | |
| if (nativeInputValueSetter) { | |
| nativeInputValueSetter.call(element, valueToSet); | |
| } else { | |
| element.value = valueToSet; // Fallback for other element types | |
| } | |
| } | |
| // Dispatch events to ensure frameworks like React/Vue/Angular recognize the change | |
| const eventInput = new Event("input", { | |
| bubbles: true, | |
| cancelable: true, | |
| }); | |
| const eventChange = new Event("change", { | |
| bubbles: true, | |
| cancelable: true, | |
| }); | |
| element.dispatchEvent(eventInput); | |
| element.dispatchEvent(eventChange); | |
| // For some React applications, a blur event might also be necessary | |
| const eventBlur = new FocusEvent("blur", { | |
| bubbles: true, | |
| cancelable: true, | |
| }); | |
| element.dispatchEvent(eventBlur); | |
| // console.log( | |
| // `Set value for [name="${name}"]:`, | |
| // element.type === "checkbox" ? element.checked : element.value, | |
| // ); | |
| }); | |
| } | |
| } | |
| console.log("Form fill process completed."); | |
| } | |
| // Simple command palette with search and key support | |
| function showCommandPalette(commands) { | |
| // Remove existing palette if any | |
| const existing = document.getElementById("formfiller-palette"); | |
| if (existing) existing.remove(); | |
| // Create overlay | |
| const overlay = document.createElement("div"); | |
| overlay.id = "formfiller-palette"; | |
| overlay.style.cssText = ` | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100vw; | |
| height: 100vh; | |
| background: rgba(0, 0, 0, 0.5); | |
| z-index: 999999; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-family: monospace; | |
| backdrop-filter: blur(2px); | |
| `; | |
| const palette = document.createElement("div"); | |
| palette.style.cssText = ` | |
| background: white; | |
| border-radius: 8px; | |
| padding: 20px; | |
| min-width: 400px; | |
| max-height: 80vh; | |
| overflow-y: auto; | |
| box-sizing: border-box; | |
| box-shadow: var(--formfiller-shadow); | |
| `; | |
| const title = document.createElement("h3"); | |
| title.textContent = "Select Form to Fill"; | |
| title.style.margin = "0 0 15px 0"; | |
| palette.appendChild(title); | |
| // Search input | |
| const search = document.createElement("input"); | |
| search.type = "text"; | |
| search.placeholder = "Type to filter..."; | |
| search.style.cssText = ` | |
| width: 100%; | |
| padding: 8px; | |
| margin-bottom: 10px; | |
| font-size: 16px; | |
| border: 1px solid var(--formfiller-border); | |
| border-radius: 4px; | |
| box-sizing: border-box; | |
| `; | |
| palette.appendChild(search); | |
| // Command list container | |
| const list = document.createElement("div"); | |
| palette.appendChild(list); | |
| // Track selected index for arrow navigation | |
| let filtered = commands; | |
| let selectedIdx = 0; | |
| // --- Palette cleanup --- | |
| function closePalette() { | |
| if (overlay.parentNode) overlay.remove(); | |
| document.removeEventListener("keydown", escHandler); | |
| document.removeEventListener("keydown", keyHandler); | |
| } | |
| // --- Render and event logic --- | |
| function renderList() { | |
| list.innerHTML = ""; | |
| filtered.forEach((cmd, i) => { | |
| const btn = document.createElement("button"); | |
| let keyLabel = cmd.shortcut ? ` [${cmd.shortcut}]` : ""; | |
| btn.textContent = `${i + 1}. ${cmd.name}${keyLabel}`; | |
| btn.style.cssText = ` | |
| display: block; | |
| width: 100%; | |
| padding: 10px; | |
| margin: 5px 0; | |
| background: var(--formfiller-btn-bg); | |
| border: 1px solid var(--formfiller-border); | |
| border-radius: 4px; | |
| cursor: pointer; | |
| text-align: left; | |
| outline: none; | |
| ${i === selectedIdx ? "background: var(--formfiller-btn-selected-bg, #d0eaff); border-color: var(--formfiller-btn-selected-border, #3399ff);" : ""} | |
| `; | |
| if (i === selectedIdx) { | |
| btn.setAttribute("tabindex", "0"); | |
| btn.focus(); | |
| } | |
| btn.onclick = () => { | |
| closePalette(); | |
| cmd.handler(); | |
| }; | |
| list.appendChild(btn); | |
| }); | |
| } | |
| renderList(); | |
| // Filter on input | |
| search.addEventListener("input", () => { | |
| const val = search.value.trim().toLowerCase(); | |
| filtered = commands.filter((cmd) => cmd.name.toLowerCase().includes(val)); | |
| selectedIdx = 0; | |
| renderList(); | |
| }); | |
| overlay.appendChild(palette); | |
| document.body.appendChild(overlay); | |
| search.focus(); | |
| // Close on Escape or click outside | |
| overlay.onclick = (e) => { | |
| if (e.target === overlay) closePalette(); | |
| }; | |
| function escHandler(e) { | |
| if (e.key === "Escape") { | |
| closePalette(); | |
| } | |
| } | |
| document.addEventListener("keydown", escHandler); | |
| // Key handler for number, custom, and arrow keys | |
| function keyHandler(e) { | |
| // Arrow navigation | |
| if (e.key === "ArrowDown") { | |
| if (filtered.length > 0) { | |
| selectedIdx = (selectedIdx + 1) % filtered.length; | |
| renderList(); | |
| } | |
| e.preventDefault(); | |
| return; | |
| } | |
| if (e.key === "ArrowUp") { | |
| if (filtered.length > 0) { | |
| selectedIdx = (selectedIdx - 1 + filtered.length) % filtered.length; | |
| renderList(); | |
| } | |
| e.preventDefault(); | |
| return; | |
| } | |
| if (e.key === "Enter") { | |
| if (filtered[selectedIdx]) { | |
| closePalette(); | |
| filtered[selectedIdx].handler(); | |
| } | |
| return; | |
| } | |
| // Only trigger shortcuts if Cmd (metaKey) or Ctrl (ctrlKey) is held | |
| if (e.metaKey || e.ctrlKey) { | |
| // Number keys (1-9) | |
| if (/^\d$/.test(e.key) && e.key !== "0") { | |
| const idx = parseInt(e.key, 10) - 1; | |
| if (filtered[idx]) { | |
| closePalette(); | |
| filtered[idx].handler(); | |
| } | |
| } | |
| // Custom shortcut keys | |
| for (let i = 0; i < filtered.length; i++) { | |
| if ( | |
| filtered[i].shortcut && | |
| e.key.toLowerCase() === filtered[i].shortcut.toLowerCase() | |
| ) { | |
| closePalette(); | |
| filtered[i].handler(); | |
| } | |
| } | |
| } | |
| } | |
| document.addEventListener("keydown", keyHandler); | |
| } | |
| /** | |
| * Registers a form-filling configuration with a menu command and a keyboard shortcut. | |
| * @param {object} options - The configuration options. | |
| * @param {string} options.name - The display name for the form command. | |
| * @param {object} options.data - The data object to fill the form with. | |
| * @param {string} [options.formSelector='form'] - A CSS selector to find the form on the page. | |
| * @param {string} [options.shortcut] - Optional single-character shortcut key for this command. | |
| */ | |
| function addForm({ name, formSelector = "form", data, shortcut }) { | |
| const handler = () => { | |
| // console.log("Filling form", name); | |
| if (document.querySelector(formSelector)) { | |
| fillForm(data); | |
| // console.log(`Form \"${name}\" filled.`); | |
| } else { | |
| console.log( | |
| `Form with selector \"${formSelector}\" not found for \"${name}\".`, | |
| ); | |
| } | |
| }; | |
| // Add to command list | |
| formCommands.push({ | |
| name, | |
| handler, | |
| shortcut, | |
| }); | |
| // Also register a menu command for accessibility | |
| // eslint-disable-next-line no-undef | |
| GM_registerMenuCommand(name, handler); | |
| } | |
| // Add this function next to addForm | |
| function addCommand({ name, handler, shortcut }) { | |
| formCommands.push({ name, handler, shortcut }); | |
| // eslint-disable-next-line no-undef | |
| GM_registerMenuCommand(name, handler); | |
| } | |
| // --- Configuration for Forms --- | |
| // ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ | |
| // ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░▌ ▐░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ | |
| // ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀█░▌▐░▌░▌ ▐░▌▐░█▀▀▀▀▀▀▀▀▀ ▀▀▀▀█░█▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ | |
| // ▐░▌ ▐░▌ ▐░▌▐░▌▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ | |
| // ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ ▄▄▄▄▄▄▄▄ | |
| // ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌▐░░░░░░░░░░░▌ ▐░▌ ▐░▌▐░░░░░░░░▌ | |
| // ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌▐░█▀▀▀▀▀▀▀▀▀ ▐░▌ ▐░▌ ▀▀▀▀▀▀█░▌ | |
| // ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ | |
| // ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄█░▌▐░▌ ▐░▐░▌▐░▌ ▄▄▄▄█░█▄▄▄▄ ▐░█▄▄▄▄▄▄▄█░▌ | |
| // ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░▌ ▐░░▌▐░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ | |
| // ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ | |
| // HOW TO USE | |
| // ctrl+v opens the command palette | |
| // then you can either search, or press the numbers to select the commands | |
| // or press ctrl/cmd + the shortcut inside [ ] to select the command | |
| // so for example: press ctrl, press v, keep ctrl pressed and press e and it goes to the embed setup page | |
| const formCommands = []; | |
| /** | |
| * snapshots require the following environment variables: | |
| * MB_DB_TYPE=h2 | |
| * MB_ENABLE_TEST_ENDPOINTS=true | |
| * MB_DANGEROUS_UNSAFE_ENABLE_TESTING_H2_CONNECTIONS_DO_NOT_ENABLE=true | |
| */ | |
| addCommand({ | |
| name: "Snapshot - Create", | |
| shortcut: "s", | |
| handler: async () => { | |
| const name = prompt("Snapshot name?"); | |
| if (!name) return; | |
| await fetch(`/api/testing/snapshot/${encodeURIComponent(name)}`, { | |
| method: "POST", | |
| }); | |
| alert(`Snapshot '${name}' created!`); | |
| }, | |
| }); | |
| addCommand({ | |
| name: "Restore Blank Snapshot", | |
| shortcut: "b", | |
| handler: async () => { | |
| await fetch(`/api/testing/restore/blank`, { | |
| method: "POST", | |
| }); | |
| alert(`Blank snapshot restored!`); | |
| window.location.reload(); | |
| }, | |
| }); | |
| addCommand({ | |
| name: "Restore Snapshot", | |
| shortcut: "r", | |
| handler: async () => { | |
| const name = prompt("Snapshot name to restore?"); | |
| if (!name) return; | |
| await fetch(`/api/testing/restore/${encodeURIComponent(name)}`, { | |
| method: "POST", | |
| }); | |
| alert(`Snapshot '${name}' restored!`); | |
| window.location.reload(); | |
| }, | |
| }); | |
| addCommand({ | |
| name: "go embed setup", | |
| shortcut: "e", | |
| handler: async () => { | |
| window.location.href = "/setup/embedding"; | |
| }, | |
| }); | |
| // 1. Postgres Sample Database Form | |
| addForm({ | |
| name: "Fill DB Form (Postgres Sample)", | |
| formSelector: 'form[data-testid="database-form"]', | |
| data: { | |
| name: "metabase-qa pg", | |
| "details.host": "localhost", | |
| "details.port": "5432", | |
| "details.dbname": "sample", | |
| "details.user": "metabase", | |
| "details.password": "metasample123", | |
| }, | |
| shortcut: "p", | |
| }); | |
| // 2. Metabase User Setup Form | |
| addForm({ | |
| name: "mb user info", | |
| data: { | |
| first_name: "Nicolo", | |
| last_name: "Pretto", | |
| email: "info@npretto.com", | |
| site_name: "Test Company", | |
| // eslint-disable-next-line no-literal-metabase-strings -- User-provided data for a local script | |
| password: "Metabase1!", | |
| // eslint-disable-next-line no-literal-metabase-strings -- User-provided data for a local script | |
| password_confirm: "Metabase1!", | |
| }, | |
| shortcut: "u", | |
| }); | |
| // Register the main shortcut to open the command palette | |
| // eslint-disable-next-line no-undef | |
| VM.shortcut.register("c-v", () => { | |
| showCommandPalette(formCommands); | |
| }); | |
| console.log("FormFiller script ready! Press Ctrl+V to open command palette."); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment