diff --git a/README.md b/README.md new file mode 100644 index 0000000..565277b --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# FHIR Query Builder + +The FHIR Query Builder is a web application that provides an interface to interact with FHIR (Fast Healthcare Interoperability Resources) servers, facilitating the construction and execution of FHIR standard queries. + +## Table of Contents +- [FHIR Query Builder](#fhir-query-builder) + - [Table of Contents](#table-of-contents) + - [Features](#features) + - [User Interface Elements](#user-interface-elements) + - [Detailed Functionality](#detailed-functionality) + - [Server URL Input](#server-url-input) + - [Resource Selection](#resource-selection) + - [Parameter Entry](#parameter-entry) + - [Query Operations Toggle](#query-operations-toggle) + - [Query Submission and Result Display](#query-submission-and-result-display) + - [Technical Notes](#technical-notes) + +## Features +This application provides several interactive features: + +1. Dynamic resource selection +2. Query parameter manipulation +3. Support for 'Read' and 'Patient Everything' operations +4. Query execution +5. Result display with pagination + +## User Interface Elements + +The application is composed of several UI elements, each serving a specific purpose: + +- **Server URL Input**: This is where you input the URL of your FHIR server. +- **Resource Select**: A dropdown menu where you can choose the resource type you want to query. +- **Parameter Input Fields**: These fields allow you to enter the parameter name and its corresponding value. +- **Add Parameter Button**: Click this to add the parameter to the query. +- **Read Operation Toggle**: A checkbox to enable or disable 'Read' operation. +- **Patient Everything Toggle**: A checkbox to enable or disable 'Patient Everything' operation. +- **Submit Button**: Click this to submit the query to the server. +- **Query Result Output**: The area where the query results will be displayed. +- **Next/Previous Page Button**: Buttons for navigating through pages of query results. +- **JSON Display**: This is where the JSON representation of a specific resource will be displayed. + +## Detailed Functionality + +### Server URL Input + +- The server URL input field requires the user to enter the URL of the FHIR server they wish to query. +- Upon entering a URL, the application fetches and validates the server's capability statement to confirm its availability and capabilities. +- In case the server's capability statement cannot be fetched, the input field will be highlighted as invalid and an error message will be displayed. + +### Resource Selection + +- The dropdown menu is dynamically populated based on the types of resources available in the server's capability statement. +- Upon selecting a resource type, the application fetches the search parameters related to the chosen resource. + +### Parameter Entry + +- The parameter input fields allow the user to define additional parameters for the query. +- When the user fills in a parameter name and its value, then clicks the 'Add Parameter' button, a new parameter is added to the list of parameters used in constructing the query. + +### Query Operations Toggle + +- The 'Read Operation' checkbox modifies the query such that it reads the resource with a specific ID provided in the parameters. +- The 'Patient Everything Operation' checkbox modifies the query such that it fetches all information related to a specific patient when the resource type is 'Patient'. + +### Query Submission and Result Display + +- The 'Submit Query' button triggers the constructed query to be sent to the server. +- The results are then fetched and displayed in the 'Query Result Output' area. +- Each result entry can be clicked to view the full JSON representation of the resource. +- If the query results contain links to 'next' and 'previous' pages, the 'Next Page' and 'Previous Page' buttons are enabled to allow navigation through result pages. + +## Technical Notes + +This application uses modern JavaScript and web technologies to provide its functionality. Here are some important technical details: + +- **Fetch API**: The application uses the Fetch API to send HTTP requests to the FHIR server. +- **Event Listeners**: Event listeners are used to react to user interactions like button clicks, dropdown selection diff --git a/app.js b/app.js new file mode 100644 index 0000000..90a736c --- /dev/null +++ b/app.js @@ -0,0 +1,703 @@ +class App { + constructor() { + this.getElement = { + nextPageButton: document.getElementById("next-page-button"), + previousPageButton: document.getElementById("previous-page-button"), + serverUrl: document.getElementById("server-url"), + addParameterButton: document.getElementById("add-parameter-button"), + resourceSelect: document.getElementById("resource-select"), + parametersContainer: document.getElementById("parameters-container"), + queryButton: document.getElementById("query-button"), + queryStringOutput: document.getElementById("query-string"), + queryResultOutput: document.getElementById("query-result"), + jsonModal: document.getElementById("json-modal"), + closeModal: document.getElementById("close-modal"), + modal: document.getElementById("json-modal"), + jsonDisplay: document.getElementById("json-display"), + parameterNameInput: document.getElementById("parameter-name"), + parameterValueInput: document.getElementById("parameter-value"), + form: document.getElementById("query-form"), + errorMessage: document.getElementById("server-url-error"), + readOperationToggle: document.getElementById("read-operation-toggle"), + patientEverythingToggle: document.getElementById( + "patient-everything-toggle" + ), + patientEverythingToggleContainer: document.getElementById( + "patient-everything-toggle-container" + ), + }; + } + + // Asynchronous function to initialize the application + async initApp() { + // Fetch and validate the capability statement from the server. + // The capability statement is a kind of metadata document that describes what operations the server supports. + // It's used to populate options in the user interface. + let capabilityStatement = await this.fetchAndValidateCapabilityStatement(); + + if (capabilityStatement) { + // Populate the resource options dropdown in the user interface based on the capability statement + this.populateAndSetupResourceOptions(capabilityStatement); + + // Setup event listeners for the UI elements based on the capability statement + this.setupEventListeners(capabilityStatement); + } + } + + // Asynchronous function to fetch and validate the capability statement + async fetchAndValidateCapabilityStatement() { + // Fetch the capability statement from the server using the server URL specified in the user interface + const capabilityStatement = await fetchCapabilityStatement( + this.getElement.serverUrl.value + ); + + if (capabilityStatement) { + // Remove the 'invalid-server-url' class from the server URL input field, indicating a successful fetch + this.getElement.serverUrl.classList.remove("invalid-server-url"); + + // Hide the error message related to the server URL + this.getElement.errorMessage.style.display = "none"; + } else { + // If the capability statement could not be fetched, add the 'invalid-server-url' class to the server URL input field + this.getElement.serverUrl.classList.add("invalid-server-url"); + + // Display an error message indicating that the server's CapabilityStatement could not be retrieved + this.getElement.errorMessage.textContent = + "Unable to retrieve the server's CapabilityStatement. Check the console for additional details."; + this.getElement.errorMessage.style.display = "block"; + } + // Return the capability statement, or undefined if it could not be fetched + return capabilityStatement; + } + + // Function to populate the resource options dropdown and setup parameters based on the capability statement + populateAndSetupResourceOptions(capabilityStatement) { + // Populate the resource options dropdown in the user interface based on the capability statement + // The capability statement contains a list of resources that the server supports, which are used to fill the dropdown + this.populateResourceOptions(capabilityStatement); + + // Get the currently selected value in the resource select dropdown + const selectedResource = this.getElement.resourceSelect.value; + + // Populate the parameters for the selected resource, based on what parameters the capability statement says the resource supports + this.populateParameters(selectedResource, capabilityStatement); + } + + // Function to set up event listeners for UI elements based on the capability statement + setupEventListeners(capabilityStatement) { + // Add an event listener to the resource select dropdown that handles changing the displayed parameters when the selected resource changes + this.getElement.resourceSelect.addEventListener("change", () => + this.handleResourceSelectChange(capabilityStatement) + ); + + // Add an event listener to the "Add Parameter" button that adds a new parameter to the parameters list when clicked + this.getElement.addParameterButton.addEventListener("click", () => + this.addParameter() + ); + + // Add an event listener to the form that handles sending the query when the form is submitted + this.getElement.form.addEventListener("submit", (event) => + this.handleSubmit(event) + ); + + // Add an event listener to the server URL input field that handles re-fetching and re-validating the capability statement when the server URL changes + this.getElement.serverUrl.addEventListener("change", () => + this.handleServerUrlChange(capabilityStatement) + ); + + // Add an event listener to the resource select dropdown that shows or hides the "Patient Everything" toggle based on the selected resource + this.getElement.resourceSelect.addEventListener("change", (event) => + this.handlePatientToggleChange(event) + ); + + // Set up the event listeners for the pagination buttons + this.setupPaginationButtons(); + } + + // Function to set up event listeners for pagination buttons + setupPaginationButtons() { + // Add an event listener to the "Next Page" button + // When the button is clicked, it retrieves the next page of results + this.getElement.nextPageButton.addEventListener( + "click", + () => this.getNextPage() + ); + + // Add an event listener to the "Previous Page" button + // When the button is clicked, it retrieves the previous page of results + this.getElement.previousPageButton.addEventListener( + "click", + () => this.getPreviousPage() + ); + } + + // Function to handle the event when the server URL input field changes + async handleServerUrlChange(capabilityStatement) { + // Get the value of the server URL input field + const newServerUrl = this.getElement.serverUrl.value; + + // Fetch and validate the capability statement for the new server URL + // The returned capabilityStatement object will be null if the server URL is invalid + capabilityStatement = await this.fetchAndValidateCapabilityStatement( + newServerUrl + ); + + // If the new server URL is valid, populate the resource options dropdown and setup parameters based on the new capability statement + // If the new server URL is invalid, this will not execute + if (capabilityStatement) { + this.populateAndSetupResourceOptions(capabilityStatement); + } + } + + // Function to handle the event when the resource select dropdown changes + handleResourceSelectChange(capabilityStatement) { + // Get the value of the resource select dropdown + const selectedResource = this.getElement.resourceSelect.value; + + // Populate the parameters for the selected resource based on the capability statement + this.populateParameters(selectedResource, capabilityStatement); + } + + // Function to handle the event when the form is submitted + handleSubmit(event) { + // Prevent the default form submission action + // This is done because we want to handle form submission using JavaScript, not the default HTML form submission + event.preventDefault(); + + // Send the query based on the form's current state + // This involves building the query string from the form inputs and sending a request to the server + this.sendQuery(); + } + + // Function to handle the event when the resource select dropdown changes + handlePatientToggleChange(event) { + // If the selected resource is "Patient" + if (event.target.value === "Patient") { + // Display the "Patient Everything" toggle because this operation is only applicable to the Patient resource + this.getElement.patientEverythingToggleContainer.style.display = "block"; + } else { + // Hide the "Patient Everything" toggle because this operation is not applicable to other resources + this.getElement.patientEverythingToggleContainer.style.display = "none"; + + // Uncheck the "Patient Everything" toggle because it is not applicable when the selected resource is not "Patient" + this.getElement.patientEverythingToggle.checked = false; + } + } + + // Function to handle the event when the 'next page' button is clicked + async getNextPage() { + // Get the URL of the next page from the 'next page' button's data attribute + const nextUrl = this.getElement.nextPageButton.dataset.nextUrl; + + // If a next page URL is available + if (nextUrl) { + // Send a query to the server using the next page URL + await this.sendQuery(nextUrl); + } + } + + // Function to handle the event when the 'previous page' button is clicked + async getPreviousPage() { + // Get the URL of the previous page from the 'previous page' button's data attribute + const previousUrl = this.getElement.previousPageButton.dataset.previousUrl; + + // If a previous page URL is available + if (previousUrl) { + // Send a query to the server using the previous page URL + await this.sendQuery(previousUrl); + } + } + + // Function to populate the resource dropdown based on the server's CapabilityStatement + populateResourceOptions(capabilityStatement) { + // Clear the existing options from the resource dropdown + while (this.getElement.resourceSelect.firstChild) { + this.getElement.resourceSelect.removeChild( + this.getElement.resourceSelect.firstChild + ); + } + + // Extract the list of resource types from the CapabilityStatement + const resourceTypes = capabilityStatement.rest[0].resource.map( + (resource) => resource.type + ); + + // For each resource type + resourceTypes.forEach((resourceType) => { + // Create a new option element + const option = document.createElement("option"); + + // Set the option's value and display text to the resource type + option.value = resourceType; + option.text = resourceType; + + // Add the option to the resource dropdown + this.getElement.resourceSelect.appendChild(option); + }); + } + + // Method to handle addition of a new parameter by the user + addParameter() { + // Retrieve user input for parameter name and value + const parameterName = this.getElement.parameterNameInput.value; + const parameterValue = this.getElement.parameterValueInput.value; + + // If both parameter name and value are not empty + if (parameterName && parameterValue) { + // Create a container element to display the user added parameter + const parameterContainer = this.createParameterContainer( + parameterName, + parameterValue + ); + + // Append this new parameter container to the parameters container element + this.getElement.parametersContainer.appendChild(parameterContainer); + + // Rebuild the query string to include this new parameter + this.buildQueryString(); + + // Clear the parameter input fields for next entry + this.getElement.parameterNameInput.value = ""; + this.getElement.parameterValueInput.value = ""; + } + } + + // Function to populate the parameter fields based on the selected resource + populateParameters(resource, capabilityStatement) { + // Find the specific resource from the CapabilityStatement + const resourceDefinition = capabilityStatement.rest[0].resource.find( + (res) => res.type === resource + ); + + // Get the array of search parameters for the found resource, or an empty array if no parameters are found + const searchParams = resourceDefinition.searchParam || []; + + // Sort the search parameters by their names + searchParams.sort((a, b) => { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; + }); + + // Clear existing parameters from the parameters container + this.getElement.parametersContainer.innerHTML = ""; + + // For each search parameter + searchParams.forEach((parameter) => { + // Create an input container for the parameter + const inputContainer = createInputContainer(parameter); + + // Append the input container to the parameters container + this.getElement.parametersContainer.appendChild(inputContainer); + }); + + // Add an event listener to each input and select element in the parameters container + // When the input value changes, rebuild the query string + document + .querySelectorAll( + "#parameters-container input, #parameters-container select" + ) + .forEach((input) => { + input.addEventListener("input", () => { + this.buildQueryString(); + }); + }); + } + + // Function to build the query string based on the current state of the form + buildQueryString() { + // Get the server URL from the form + const serverUrl = this.getElement.serverUrl.value; + + // Get the selected resource from the form + const resourceType = this.getElement.resourceSelect.value; + + // Collect all search parameters from the form + const searchParams = collectSearchParams(); + + // Create an instance of URLSearchParams with the collected search parameters + const queryParams = new URLSearchParams(searchParams); + + // Check if the read operation toggle is checked + const isReadOperation = this.getElement.readOperationToggle.checked; + + // Check if the patient everything operation is checked and the selected resource is 'Patient' + const isPatientEverything = + this.getElement.patientEverythingToggle.checked && + resourceType === "Patient"; + + // Start building the URL with the server URL and the selected resource + let queryString = `${serverUrl}/${resourceType}`; + + // If it's a read operation and there's an _id parameter, append it to the URL + if (isReadOperation && searchParams._id) { + // Read operation + queryString += `/${searchParams._id}`; + } else if (isPatientEverything) { + // If it's a patient everything operation, append '$everything' to the URL + // If there's an _id parameter, append it to the URL before '$everything' + if (searchParams._id) { + // Patient $everything operation with _id + queryString += `/${searchParams._id}/$everything`; + queryParams.delete("_id"); + } else { + // Patient $everything operation without _id + queryString += `/$everything`; + } + + //TODO - I think this IF/ELSE statement is doing the same thing + // If there are any parameters left after deleting _id, append them to the URL + if (queryParams.toString().length > 0) { + queryString += `?${queryParams}`; + } + } else { + // If there are any parameters, append them to the URL + const hasSearchParams = Array.from(queryParams).length > 0; + queryString += `${hasSearchParams ? "?" : ""}${queryParams}`; + } + + // Update the query string output text in the form + if (this.getElement.queryStringOutput) { + this.getElement.queryStringOutput.innerText = queryString; + } else { + console.error("Query string output element not found."); + } + + // Return the built URL + return queryString; + } + + // Function to send the query to the server + async sendQuery(url) { + // If no url is provided, generate it using buildQueryString method + const queryString = url || this.buildQueryString(this.getElement); + try { + const response = await fetch(queryString); + const data = await response.json(); + this.displayQueryResult(this.getElement.queryResultOutput, data); + } catch (error) { + console.error(error); + } + } + + displayQueryResult(queryResultOutput, data) { + if (queryResultOutput) { + queryResultOutput.innerHTML = ""; + if (data.entry) { + // Create a new container div for the card grid and the bundle card + const resultContainer = document.createElement("div"); + resultContainer.classList.add("result-container"); + + const cardGrid = this.createCardGrid(data.entry); + resultContainer.appendChild(cardGrid); + + // Add a new div after the card-grid for the bundle card + const bundleCardDiv = document.createElement("div"); + bundleCardDiv.classList.add("bundle-card-container"); + + // Create the bundle card + const bundleCard = this.createResourceCard(data); + bundleCardDiv.appendChild(bundleCard); + resultContainer.appendChild(bundleCardDiv); + + // Append the result container to the queryResultOutput + queryResultOutput.appendChild(resultContainer); + } else { + const card = this.createResourceCard(data); + queryResultOutput.appendChild(card); + } + } else { + console.error("Query result output element not found."); + } + if (data.link) { + const nextLink = data.link.find((link) => link.relation === "next"); + const previousLink = data.link.find( + (link) => link.relation === "previous" + ); + + if (nextLink) { + this.getElement.nextPageButton.dataset.nextUrl = nextLink.url; + this.getElement.nextPageButton.style.display = "inline-block"; + } else { + this.getElement.nextPageButton.style.display = "none"; + } + + if (previousLink) { + this.getElement.previousPageButton.dataset.previousUrl = + previousLink.url; + this.getElement.previousPageButton.style.display = "inline-block"; + } else { + this.getElement.previousPageButton.style.display = "none"; + } + } else { + this.getElement.nextPageButton.style.display = "none"; + this.getElement.previousPageButton.style.display = "none"; + } + } + + createCardGrid(entries) { + const cardGrid = document.createElement("div"); + cardGrid.classList.add("card-grid"); + + entries.forEach((entry) => { + const resource = entry.resource; + const card = this.createResourceCard(resource, this.getElement); + cardGrid.appendChild(card); + }); + + return cardGrid; + } + + // Create resource card + createResourceCard(resource) { + const card = document.createElement("div"); + card.classList.add("card"); + + const cardBody = createCardBody(resource); + + card.appendChild(cardBody); + card.addEventListener("click", () => { + const jsonString = JSON.stringify(resource, null, 2); + + this.getElement.jsonDisplay.textContent = jsonString; + hljs.addPlugin(new CopyButtonPlugin()); + hljs.highlightElement(this.getElement.jsonDisplay); // Syntax highlighting + + // Show the modal + this.getElement.modal.style.display = "block"; + + // Close the modal when the close button is clicked + this.getElement.closeModal.onclick = () => { + this.getElement.modal.style.display = "none"; + }; + + // Close the modal when clicked outside the modal content + window.onclick = (event) => { + if (event.target === this.getElement.modal) { + this.getElement.modal.style.display = "none"; + } + }; + }); + + return card; + } + createParameterContainer(parameterName, parameterValue) { + const parameterContainer = document.createElement("div"); + parameterContainer.classList.add("form-group"); + + const parameterNameElement = document.createElement("span"); + parameterNameElement.classList.add("userParameterName"); + parameterNameElement.label = "Parameter"; + parameterNameElement.innerText = parameterName; + + const parameterValueElement = document.createElement("span"); + parameterValueElement.classList.add("userParameterValue"); + parameterValueElement.innerText = parameterValue; + + const removeButton = document.createElement("button"); + removeButton.classList.add("remove-button"); + removeButton.classList.add("btn"); + removeButton.innerText = "x"; + removeButton.addEventListener("click", () => { + parameterContainer.remove(); + this.buildQueryString(); + }); + + parameterContainer.appendChild(parameterNameElement); + parameterContainer.appendChild(parameterValueElement); + parameterContainer.appendChild(removeButton); + + return parameterContainer; + } +} + +async function fetchCapabilityStatement(serverUrlValue) { + const timeOutInMilliseconds = 8000; // Adjust the timeout value as needed + try { + const response = await fetchWithTimeout( + `${serverUrlValue}/metadata`, + {}, + timeOutInMilliseconds + ); + if (!response.ok) { + console.error("Error fetching CapabilityStatement:", response.status); + return false; + } + const capabilityStatement = await response.json(); + return capabilityStatement; + } catch (error) { + console.error("Error fetching CapabilityStatement:", error); + return false; + } +} + +async function fetchWithTimeout(url, options, timeout) { + return Promise.race([ + fetch(url, options), + new Promise((_, reject) => + setTimeout(() => reject(new Error("Request timed out")), timeout) + ), + ]); +} + +function createInputContainer(parameter) { + const inputContainer = document.createElement("div"); + inputContainer.classList.add("form-group"); + + // Create a label element + const label = document.createElement("label"); + label.htmlFor = parameter.name; + label.innerText = `${parameter.name}`; + + // Create a custom tooltip element + const tooltip = document.createElement("span"); + tooltip.classList.add("tooltip-text"); + tooltip.innerText = parameter.documentation || "No description available"; + // Append the tooltip to the label + label.appendChild(tooltip); + + // Create an input element + let input; + if (parameter.type === "token") { + input = createTokenInput(parameter); + } else if (parameter.type === "date") { + input = createDateInput(parameter); + } else { + input = document.createElement("input"); + input.type = "text"; + } + input.classList.add("form-control"); + input.name = parameter.name; + input.id = parameter.name; + input.dataset.parameter = parameter.name; + input.placeholder = parameter.type; + + // Append the label and input this.getElement to the input container + inputContainer.appendChild(label); + inputContainer.appendChild(input); + + return inputContainer; +} + +function createDateInput(parameter) { + const input = document.createElement("input"); + input.type = "text"; + return input; +} + +function createTokenInput(parameter) { + const input = document.createElement("input"); + input.type = "text"; + return input; +} + +function collectSearchParams() { + const searchParams = {}; + + document + .querySelectorAll( + "#parameters-container select, #parameters-container input" + ) + .forEach((input) => { + if (input.value !== "") { + searchParams[input.name] = input.value; + } + }); + + const userParameters = document.querySelectorAll(".form-group"); + userParameters.forEach((parameterContainer) => { + const parameterNameElement = + parameterContainer.querySelector(".userParameterName"); + const parameterValueElement = parameterContainer.querySelector( + ".userParameterValue" + ); + + if (parameterNameElement && parameterValueElement) { + const parameterName = parameterNameElement.innerText; + const parameterValue = parameterValueElement.innerText; + + if (parameterName && parameterValue) { + searchParams[parameterName] = parameterValue; + } + } + }); + + return searchParams; +} + +function findReferenceValue(resource, key) { + let results = []; // variable to store the results + + // loop through the object + for (const prop in resource) { + if (typeof resource[prop] === "object") { + // recursively search nested objects and update the results array + results = results.concat(findReferenceValue(resource[prop], key)); + } else if (prop === key) { + // add the key-value pair to the results array + results.push(`${key}: ${resource[prop]}`); + } + } + + return results; // return the results array once the loop is finished +} + +function createCardBody(resource) { + const cardBody = document.createElement("div"); + cardBody.classList.add("card-body"); + + if (resource.resourceType === "Patient") { + const nameElement = document.createElement("h3"); + + if (resource.name && resource.name[0]) { + const givenNames = resource.name[0].given; + const familyName = resource.name[0].family || ""; + + let fullName = ""; + + if (givenNames && givenNames.length > 0) { + fullName = givenNames[0]; // First name + + if (givenNames.length > 1) { + // Middle initial + fullName += ` ${givenNames[1].charAt(0)}.`; + } + } + + fullName += ` ${familyName}`; // Family name + nameElement.innerText = fullName; + } else { + nameElement.innerText = "Unnamed Patient"; + } + + cardBody.appendChild(nameElement); + } + + Object.keys(resource).forEach((key) => { + const value = resource[key]; + if (typeof value !== "object") { + const element = document.createElement("p"); + element.innerText = `${key}: ${value}`; + cardBody.appendChild(element); + } + }); + + if (resource.resourceType != "Bundle") { + const referenceValues = findReferenceValue(resource, "reference"); + + if (referenceValues.length > 0) { + referenceValues.forEach((value) => { + const element = document.createElement("p"); + element.innerText = value; + cardBody.appendChild(element); + }); + } + } + return cardBody; +} + +window.addEventListener("DOMContentLoaded", () => new App().initApp()); diff --git a/index.html b/index.html new file mode 100644 index 0000000..719c81a --- /dev/null +++ b/index.html @@ -0,0 +1,85 @@ + + + + + FHIR Query Builder + + + + + + + +
+
+ (        )  (    (                                                                         
+ )\ )  ( /(  )\ ) )\ )     (                              (             (   (               
+(()/(  )\())(()/((()/(   ( )\     (     (   (    (      ( )\    (   (   )\  )\ )   (   (    
+ /(_))((_)\  /(_))/(_))  )((_)   ))\   ))\  )(   )\ )   )((_)  ))\  )\ ((_)(()/(  ))\  )(   
+(_))_| _((_)(_)) (_))   ((_)_   /((_) /((_)(()\ (()/(  ((_)_  /((_)((_) _   ((_))/((_)(()\  
+| |_  | || ||_ _|| _ \   / _ \ (_))( (_))   ((_) )(_))  | _ )(_))(  (_)| |  _| |(_))   ((_) 
+| __| | __ | | | |   /  | (_) || || |/ -_) | '_|| || |  | _ \| || | | || |/ _` |/ -_) | '_| 
+|_|   |_||_||___||_|_\   \__\_\ \_,_|\___| |_|   \_, |  |___/ \_,_| |_||_|\__,_|\___| |_|   
+                                                  |__/                                       
+        
+
+
+
+ + + +
+
+ + +
+
+ + +
+ +
+
+
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+

Query Result

+

+            
+            
+            
+
+
+ + + + + + + + \ No newline at end of file diff --git a/resources/highlightjs-copy.min.css b/resources/highlightjs-copy.min.css new file mode 100644 index 0000000..465042d --- /dev/null +++ b/resources/highlightjs-copy.min.css @@ -0,0 +1,51 @@ +.hljs-copy-wrapper { + position: relative; + overflow: hidden; +} +.hljs-copy-wrapper:hover .hljs-copy-button, +.hljs-copy-button:focus { + transform: translateX(0); +} +.hljs-copy-button { + position: absolute; + transform: translateX(calc(100% + 1.125em)); + top: 1em; + right: 1em; + width: 2rem; + height: 2rem; + text-indent: -9999px; + color: #fff; + border-radius: 0.25rem; + border: 1px solid #ffffff22; + background-color: #2d2b57; + background-color: var(--hljs-theme-background); + background-image: url('data:image/svg+xml;utf-8,'); + background-repeat: no-repeat; + background-position: center; + transition: background-color 200ms ease, transform 200ms ease-out; +} +.hljs-copy-button:hover { + border-color: #ffffff44; +} +.hljs-copy-button:active { + border-color: #ffffff66; +} +.hljs-copy-button[data-copied="true"] { + text-indent: 0; + width: auto; + background-image: none; +} +@media (prefers-reduced-motion) { + .hljs-copy-button { + transition: none; + } +} +.hljs-copy-alert { + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} diff --git a/resources/highlightjs-copy.min.js b/resources/highlightjs-copy.min.js new file mode 100644 index 0000000..35ada50 --- /dev/null +++ b/resources/highlightjs-copy.min.js @@ -0,0 +1,47 @@ +class CopyButtonPlugin { + constructor(options = {}) { + self.hook = options.hook; + self.callback = options.callback; + } + "after:highlightElement"({ el, text }) { + let button = Object.assign(document.createElement("button"), { + innerHTML: "Copy", + className: "hljs-copy-button", + }); + button.dataset.copied = false; + el.parentElement.classList.add("hljs-copy-wrapper"); + el.parentElement.appendChild(button); + el.parentElement.style.setProperty( + "--hljs-theme-background", + window.getComputedStyle(el).backgroundColor + ); + button.onclick = function () { + if (!navigator.clipboard) return; + let newText = text; + if (hook && typeof hook === "function") { + newText = hook(text, el) || text; + } + navigator.clipboard + .writeText(newText) + .then(function () { + button.innerHTML = "Copied!"; + button.dataset.copied = true; + let alert = Object.assign(document.createElement("div"), { + role: "status", + className: "hljs-copy-alert", + innerHTML: "Copied to clipboard", + }); + el.parentElement.appendChild(alert); + setTimeout(() => { + button.innerHTML = "Copy"; + button.dataset.copied = false; + el.parentElement.removeChild(alert); + alert = null; + }, 2e3); + }) + .then(function () { + if (typeof callback === "function") return callback(newText, el); + }); + }; + } +} diff --git a/resources/old-styles.css b/resources/old-styles.css new file mode 100644 index 0000000..9ffb036 --- /dev/null +++ b/resources/old-styles.css @@ -0,0 +1,307 @@ +/* Add some basic styles for the page */ +body { + font-family: "Roboto", sans-serif; + margin: 0; + padding: 0; + /* background-color: #f5f5f5; */ + background-color: #7c3e3e; +} + +.ascii-art { + /* Typography */ + font-family: 'Courier New', Courier, monospace; /* Monospace font for uniform character width */ + font-size: 16px; + font-weight: bolder; + color: white; /* Dark gray color for the text */ + + /* Optional: Control width and overflow */ + max-width: 100%; /* Limit width to 90% of the parent container */ + overflow-x: auto; /* Add horizontal scrollbar if content overflows */ + text-align: center; +} + + +.container { + flex-direction: column; + align-items: center; + padding: 2rem; +} + +h1 { + margin: 0; + text-align: center; + color: #4a4a4a; +} + +form { + display: flex; + flex-direction: column; + margin-top: 2rem; +} + +.form-group { + display: flex; + flex-direction: column; + margin-bottom: 1rem; +} + +.form-operation-toggle { + display: block; +} + +.form-group label { + margin-bottom: 0.5rem; + color: #4a4a4a; +} + +.form-group input[type="text"], +.form-group input[type="date"], +.form-group select { + padding: 0.5rem; + font-size: 1rem; + border-bottom: 1px solid #ccc; + border-top: none; + border-left: none; + border-right: none; + background-color: #f9f9f9; +} + +.form-group input.invalid-server-url { + background-color: #bc6367; +} + +textarea:focus, +input:focus { + outline: none; + background-color: #f1f1f1; + opacity: 1; +} + +.form-group select { + width: 100%; +} + +.form-row { + display: flex; + justify-content: space-between; + align-items: center; + background-color: #fbfafa; + border: 1px solid #ccc; + border-radius: 0.5rem; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2); + padding: 1rem; + margin-top: 2rem; +} + +.form-col { + flex: 1; + margin-right: 20px; +} + +.grid-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + grid-gap: 20px; + margin-top: 20px; +} + +#parameters-container, +.server-container { + background-color: #fbfafa; + border: 1px solid #ccc; + border-radius: 0.5rem; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2); + padding: 1rem; + margin-top: 2rem; +} + +#query-button { + margin-top: 2rem; + background-color: #00a65a; +} + +.btn { + padding: 0.5rem 1rem; + background-color: #aaaaaa; + border: none; + border-radius: 0.5rem; + color: #fff; + font-size: 1rem; + cursor: pointer; +} + +#add-parameter-button { + bottom: 0; +} + +.btn:hover { + background-color: #367fa9; +} + +.result-container { + display: grid; + grid-template-columns: 1fr; + grid-gap: 1rem; +} + +.bundle-card-container { + display: grid; + grid-template-columns: 1fr; + margin-top: 1rem; +} + +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-gap: 1rem; +} + +.result-card { + background-color: #fbfafa; + color: #4a4a4a; + border: 1px solid #ccc; + border-radius: 0.5rem; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2); + padding: 1rem; + margin-top: 2rem; +} + +.result-card h2 { + margin-top: 0; + color: #4a4a4a; +} + +.card { + background-color: #fbfafa; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-gap: 1rem; + border: 1px solid #888; + border-radius: 0.5rem; + padding: 1rem; + margin-bottom: 1rem; +} + +.modal { + display: none; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.4); +} + +.modal-content { + background-color: #fefefe; + margin: 15% auto; + padding: 20px; + border: 1px solid #888; + border-radius: 0.5rem; + width: 80%; +} + +.json-display { + border-radius: 0.5rem; +} + +.close { + color: #aaaaaa; + float: right; + font-size: 28px; + font-weight: bold; +} + +.close:hover, +.close:focus { + color: #000; + text-decoration: none; + cursor: pointer; +} + +/* Tooltip container */ +label { + position: relative; + display: inline-block; + cursor: help; +} + +/* Tooltip text */ +.tooltip-text { + visibility: hidden; + width: 200px; + background-color: #555; + color: #fff; + text-align: center; + padding: 5px; + border-radius: 3px; + position: absolute; + z-index: 1; + bottom: 125%; /* Position the tooltip above the label */ + left: 50%; + margin-left: -100px; /* Center the tooltip */ + opacity: 0; + transition: opacity 0.3s; +} + +/* Show the tooltip when hovering over the label */ +label:hover .tooltip-text { + visibility: visible; + opacity: 1; +} + + +/* highlighjs copy feature */ +.hljs-copy-wrapper { + position: relative; + overflow: hidden; +} +.hljs-copy-wrapper:hover .hljs-copy-button, +.hljs-copy-button:focus { + transform: translateX(0); +} +.hljs-copy-button { + position: absolute; + transform: translateX(calc(100% + 1.125em)); + top: 1em; + right: 1em; + width: 2rem; + height: 2rem; + text-indent: -9999px; + color: #fff; + border-radius: 0.25rem; + border: 1px solid #ffffff22; + background-color: #2d2b57; + background-color: #4a4a4a; + background-image: url('data:image/svg+xml;utf-8,'); + background-repeat: no-repeat; + background-position: center; + transition: background-color 200ms ease, transform 200ms ease-out; +} +.hljs-copy-button:hover { + border-color: #ffffff44; +} +.hljs-copy-button:active { + border-color: #ffffff66; +} +.hljs-copy-button[data-copied="true"] { + text-indent: 0; + width: auto; + background-image: none; +} +@media (prefers-reduced-motion) { + .hljs-copy-button { + transition: none; + } +} +.hljs-copy-alert { + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} \ No newline at end of file diff --git a/resources/styles.css b/resources/styles.css new file mode 100644 index 0000000..f4014b7 --- /dev/null +++ b/resources/styles.css @@ -0,0 +1,363 @@ +body { + font-family: "Courier New", Courier, monospace; /* Retro font */ + margin: 0; + padding: 0; + background-color: #7c3e3e; + color: #f5e6d8; /* A light, retro cream color */ +} + +.ascii-art { + font-size: 16px; + font-weight: bolder; + color: #f9e4bc; /* Light cream color for ASCII */ + text-align: center; + max-width: 100%; + overflow-x: auto; +} + +.container { + flex-direction: column; + align-items: center; + padding: 2rem; +} + +h1 { + margin: 0; + text-align: center; + color: #f5e6d8; +} + +form { + display: flex; + flex-direction: column; + margin-top: 2rem; +} + +.form-group { + display: flex; + flex-direction: column; + margin-bottom: 1rem; +} + +.form-operation-toggle { + display: block; +} + +.form-group label { + margin-bottom: 0.5rem; + color: #f5e6d8; +} + +.form-group input[type="text"], +.form-group input[type="date"], +.form-group select { + padding: 0.5rem; + font-size: 1rem; + background-color: #a86969; /* Darker, earthy tone */ + border: 1px solid #f5e6d8; + color: #f5e6d8; +} + +.form-group input.invalid-server-url { + background-color: #b17806; + color: #f5e6d8; +} + +textarea:focus, +input:focus { + outline: none; + background-color: #b97878; /* Slightly lighter tone for focus */ + opacity: 1; +} + +.form-group select { + width: 100%; +} + +.form-row, +#parameters-container, +.server-container, +.result-card, +.card { + background-color: #a86969; + border: 2px solid #f5e6d8; /* Thicker border */ + padding: 1rem; + margin-top: 2rem; +} + +.grid-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + grid-gap: 20px; + margin-top: 20px; +} + +.form-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +/* Hide the default checkbox */ +.retro-checkbox { + display: none; + } + + /* Style the label to look like a retro checkbox */ + .retro-checkbox + label { + position: relative; + padding-left: 30px; /* Room for the checkbox and some space */ + cursor: pointer; + display: inline-block; + line-height: 24px; /* Adjust to vertically center the text with the checkbox. */ + } + + /* Style the background of the checkbox using ::before */ + .retro-checkbox + label::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 20px; + height: 20px; + background-color: #f5e6d8; + border: 2px solid #7c3e3e; + cursor: pointer; + transition: background-color 0.3s; /* Smooth transition for hover effect */ + } + + /* Hover effect on the custom checkbox */ + .retro-checkbox + label:hover::before { + background-color: #e5d6c8; /* Adjust color as per your preference */ + } + + /* Style the checkmark using the ::after pseudo-element */ + .retro-checkbox:checked + label::after { + content: "\25A0"; + position: absolute; + top: 13px; /* Adjust this value so the checkmark appears over the custom checkbox area */ + left: 7.5px; /* Adjust this value so the checkmark appears over the custom checkbox area */ + transform: translateY(-50%); /* Vertically center the checkmark */ + font-size: 16px; + font-weight: bold; + color: #7c3e3e; + } + +.result-card h2 { + margin-top: 0; + color: #f5e6d8; +} + +.card { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-gap: 1rem; +} + +/* Standard syntax */ +input::placeholder { + color: #f5e6d8; + font-family: "Courier New", Courier, monospace; + opacity: 0.7; /* Use opacity for lighter color; not all browsers support the 'color' property for placeholders */ +} + +.btn { + background-color: #f5e6d8; + color: #7c3e3e; + border: 2px solid #f5e6d8; + cursor: pointer; +} + +#add-parameter-button { + bottom: 0; +} + +.btn:hover { + background-color: #e5d6c8; +} + +.result-container { + display: grid; + grid-template-columns: 1fr; + grid-gap: 1rem; +} + +.bundle-card-container { + display: grid; + grid-template-columns: 1fr; + margin-top: 1rem; +} + +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-gap: 1rem; +} + +/* check css styling */ +.modal { + display: none; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.4); +} + +.modal-content { + background-color: #a86969; + border: 2px solid #f5e6d8; /* Thicker border */ + margin: 15% auto; + padding: 20px; + width: 80%; +} + +.close { + color: #f5e6d8; + float: right; + font-size: 28px; + font-weight: bold; +} + +.close:hover, +.close:focus { + color: #e5d6c8; + text-decoration: none; + cursor: pointer; +} + +/* Tooltip container */ +label { + position: relative; + display: inline-block; + cursor: help; +} + +/* Tooltip text */ +.tooltip-text { + visibility: hidden; + width: 230px; + background-color: #f5e6d8; + color: #7c3e3e; + border: 2px solid #7c3e3e; + text-align: center; + padding: 5px; + position: absolute; + z-index: 1; + bottom: 125%; /* Position the tooltip above the label */ + left: 50%; + margin-left: -100px; /* Center the tooltip */ + transition: opacity 0.3s; +} + +/* Show the tooltip when hovering over the label */ +label:hover .tooltip-text { + visibility: visible; +} + +/* highlighjs copy feature */ +.hljs-copy-wrapper { + position: relative; + overflow: hidden; +} +.hljs-copy-wrapper:hover .hljs-copy-button, +.hljs-copy-button:focus { + transform: translateX(0); +} +.hljs-copy-button { + position: absolute; + transform: translateX(calc(100% + 1.125em)); + top: 1em; + right: 1em; + width: 2rem; + height: 2rem; + text-indent: -9999px; + color: #f5e6d8; + border-radius: 0.25rem; + border: 1px solid #f5e6d8; + background-color: #7c3e3e; + background-image: url('data:image/svg+xml;utf-8,'); + background-repeat: no-repeat; + background-position: center; + transition: background-color 200ms ease, transform 200ms ease-out; +} +.hljs-copy-button:hover { + border-color: #ffffff44; +} +.hljs-copy-button:active { + border-color: #ffffff66; +} +.hljs-copy-button[data-copied="true"] { + text-indent: 0; + width: auto; + background-image: none; +} +@media (prefers-reduced-motion) { + .hljs-copy-button { + transition: none; + } +} +.hljs-copy-alert { + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} + +/* Base style */ +.hljs { + display: block; + overflow-x: auto; + padding: 10px; + font-family: 'Courier New', Courier, monospace; /* Monospace font for retro feel */ + color: #a86969; /* Darker text on the light background */ + background: #f5e6d8; + } + + /* Punctuation, like braces and commas */ + .hljs-punctuation, .hljs-symbol { + color: #a86969; + } + + /* JSON keys or properties */ + .hljs-attr { + color: #7c3e3e; /* A hue that remains consistent with the retro theme */ + } + + /* String values */ + .hljs-string { + color: #5a3f3f; /* A darker hue for better contrast on a light background */ + } + + /* Number values */ + .hljs-number { + color: #8b6e6e; /* A neutral hue for numbers */ + } + + /* Boolean values */ + .hljs-literal { + color: #8b6e6e; /* A neutral hue for booleans and null values */ + } + + /* Meta and other categories */ + .hljs-meta, .hljs-meta-keyword, .hljs-meta-string { + color: #a86969; + } + + /* Selectors, tags, and attributes for other languages */ + .hljs-tag, .hljs-selector-id, .hljs-selector-class, .hljs-name { + color: #a86969; + } + + /* Comments */ + .hljs-comment { + color: #5a3f3f; + font-style: italic; + } + \ No newline at end of file