Skip to content

Instantly share code, notes, and snippets.

@npretto
Last active June 11, 2025 13:26
Show Gist options
  • Select an option

  • Save npretto/da31954c42f83d9bbb4e194f1727f712 to your computer and use it in GitHub Desktop.

Select an option

Save npretto/da31954c42f83d9bbb4e194f1727f712 to your computer and use it in GitHub Desktop.
handy userscript for working on metabase
/* 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