From c3fb9750585a9e9d8947986490b98252660ed7e4 Mon Sep 17 00:00:00 2001 From: John Manhart Date: Fri, 18 Apr 2025 10:27:29 -0700 Subject: [PATCH 1/7] Adding in a components directory and moving tags over. --- script_runner/frontend/src/Nav.tsx | 2 +- script_runner/frontend/src/Script.tsx | 2 +- script_runner/frontend/src/{ => components/Tag}/Tag.css | 0 script_runner/frontend/src/{ => components/Tag}/Tag.tsx | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename script_runner/frontend/src/{ => components/Tag}/Tag.css (100%) rename script_runner/frontend/src/{ => components/Tag}/Tag.tsx (100%) diff --git a/script_runner/frontend/src/Nav.tsx b/script_runner/frontend/src/Nav.tsx index 604132d..adc77a1 100644 --- a/script_runner/frontend/src/Nav.tsx +++ b/script_runner/frontend/src/Nav.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import "./App.css"; import { ConfigGroup, Route } from "./types.tsx"; import { FolderPlusIcon, FolderMinusIcon } from "@heroicons/react/24/outline"; -import Tag from "./Tag"; +import Tag from "./components/Tag/Tag"; interface Props { route: Route; diff --git a/script_runner/frontend/src/Script.tsx b/script_runner/frontend/src/Script.tsx index 7e981dd..ccecdd1 100644 --- a/script_runner/frontend/src/Script.tsx +++ b/script_runner/frontend/src/Script.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import ScriptResult from "./ScriptResult.tsx"; import { RunResult, ConfigFunction } from "./types.tsx"; -import Tag from "./Tag"; +import Tag from "./components/Tag/Tag"; interface Props { regions: string[]; diff --git a/script_runner/frontend/src/Tag.css b/script_runner/frontend/src/components/Tag/Tag.css similarity index 100% rename from script_runner/frontend/src/Tag.css rename to script_runner/frontend/src/components/Tag/Tag.css diff --git a/script_runner/frontend/src/Tag.tsx b/script_runner/frontend/src/components/Tag/Tag.tsx similarity index 100% rename from script_runner/frontend/src/Tag.tsx rename to script_runner/frontend/src/components/Tag/Tag.tsx From 4c50f21cf2bb6849c783336c8ac317f445efad3c Mon Sep 17 00:00:00 2001 From: John Manhart Date: Fri, 18 Apr 2025 10:32:08 -0700 Subject: [PATCH 2/7] Adding in a global button and replacing the hard coded buttons --- script_runner/frontend/src/Script.tsx | 18 +- script_runner/frontend/src/ScriptResult.tsx | 272 ++++++++++-------- .../frontend/src/components/Button/Button.css | 57 ++++ .../frontend/src/components/Button/Button.tsx | 32 +++ 4 files changed, 261 insertions(+), 118 deletions(-) create mode 100644 script_runner/frontend/src/components/Button/Button.css create mode 100644 script_runner/frontend/src/components/Button/Button.tsx diff --git a/script_runner/frontend/src/Script.tsx b/script_runner/frontend/src/Script.tsx index ccecdd1..94eb73e 100644 --- a/script_runner/frontend/src/Script.tsx +++ b/script_runner/frontend/src/Script.tsx @@ -3,6 +3,7 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import ScriptResult from "./ScriptResult.tsx"; import { RunResult, ConfigFunction } from "./types.tsx"; import Tag from "./components/Tag/Tag"; +import Button from "./components/Button/Button"; interface Props { regions: string[]; @@ -136,7 +137,9 @@ function Script(props: Props) { Select a region to run this function )} - + {error && ( @@ -157,9 +160,13 @@ function Script(props: Props) { {} {codeCollapsed ? (
- +
) : (
@@ -174,12 +181,13 @@ function Script(props: Props) { {source}
- +
)} diff --git a/script_runner/frontend/src/ScriptResult.tsx b/script_runner/frontend/src/ScriptResult.tsx index 9a13274..d2f6b74 100644 --- a/script_runner/frontend/src/ScriptResult.tsx +++ b/script_runner/frontend/src/ScriptResult.tsx @@ -1,38 +1,39 @@ -import { useEffect, useState } from 'react'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { coy } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import { AgGridReact } from 'ag-grid-react'; -import jq from 'jq-web'; -import { AgCharts } from 'ag-charts-react'; -import { RunResult } from './types'; - -import { AllCommunityModule, ModuleRegistry } from 'ag-grid-community'; +import { useEffect, useState } from "react"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { coy } from "react-syntax-highlighter/dist/esm/styles/prism"; +import { AgGridReact } from "ag-grid-react"; +import jq from "jq-web"; +import { AgCharts } from "ag-charts-react"; +import { RunResult } from "./types"; +import Button from "./components/Button/Button"; + +import { AllCommunityModule, ModuleRegistry } from "ag-grid-community"; ModuleRegistry.registerModules([AllCommunityModule]); type RowData = { [key: string]: unknown; -} +}; type ChartData = { - data: unknown[], - series: { xKey: string, yKey: string, yName: string }[] -} + data: unknown[]; + series: { xKey: string; yKey: string; yName: string }[]; +}; type Props = { - group: string, - function: string, + group: string; + function: string; data: RunResult | null; - regions: string[] -} - + regions: string[]; +}; // Either return merged data or null function mergeRegionKeys(data: unknown, regions: string[]) { try { - if (typeof data === 'object' && data !== null) { - const shouldMerge = Object.keys(data).every((r: string) => regions.includes(r)); + if (typeof data === "object" && data !== null) { + const shouldMerge = Object.keys(data).every((r: string) => + regions.includes(r) + ); if (shouldMerge) { - const firstRegionData = Object.values(data)[0]; if (!Array.isArray(firstRegionData)) { return null; @@ -41,38 +42,48 @@ function mergeRegionKeys(data: unknown, regions: string[]) { return null; } - if (!firstRegionData.every(el => typeof el === 'object' && el !== null)) { + if ( + !firstRegionData.every((el) => typeof el === "object" && el !== null) + ) { return null; } - const firstRowKeys = Object.keys(firstRegionData[0]) + const firstRowKeys = Object.keys(firstRegionData[0]); // All keys match for (let i = 1; i < firstRegionData.length; i++) { - const rowKeys = Object.keys(firstRegionData[i]) - if (rowKeys.length !== firstRowKeys.length || !rowKeys.every(k => firstRowKeys.includes(k))) { - return null - } - } - - const processed = Object.entries(data).map(([region, regionData]) => { - if (!Array.isArray(regionData)) { + const rowKeys = Object.keys(firstRegionData[i]); + if ( + rowKeys.length !== firstRowKeys.length || + !rowKeys.every((k) => firstRowKeys.includes(k)) + ) { return null; } + } - return regionData.map((row) => { - const rowData = Object.keys(row).reduce((acc: RowData, key) => { - const value = row[key]; - acc[key] = typeof value === 'object' && value !== null ? JSON.stringify(value) : value; - return acc; - }, {}); - - return { - region, - ...rowData, + const processed = Object.entries(data) + .map(([region, regionData]) => { + if (!Array.isArray(regionData)) { + return null; } - }); - }).flat(1); + + return regionData.map((row) => { + const rowData = Object.keys(row).reduce((acc: RowData, key) => { + const value = row[key]; + acc[key] = + typeof value === "object" && value !== null + ? JSON.stringify(value) + : value; + return acc; + }, {}); + + return { + region, + ...rowData, + }; + }); + }) + .flat(1); return processed; } @@ -82,16 +93,20 @@ function mergeRegionKeys(data: unknown, regions: string[]) { } return null; - } - // returns table formatted data if data is table like // otherwise return null -function getGridData(data: { [region: string]: unknown[] } | unknown, regions: string[]) { +function getGridData( + data: { [region: string]: unknown[] } | unknown, + regions: string[] +) { const mergedData = mergeRegionKeys(data, regions) || data; - if (Array.isArray(mergedData) && mergedData.every(row => typeof row === 'object' && row !== null)) { + if ( + Array.isArray(mergedData) && + mergedData.every((row) => typeof row === "object" && row !== null) + ) { if (mergedData.length === 0) { return null; } @@ -115,24 +130,34 @@ function getChartData(data: unknown, regions: string[]) { if (merged && Array.isArray(merged)) { if (DATE_KEY in merged[0]) { - const numericFields = Object.entries(merged[0]).filter(([, value]) => typeof value === 'number').map(([key,]) => key); - - const series = regions.map(region => { - return numericFields.map(f => [region, f]) - }).flat(1); - - const mergedByDate: object = merged.reduce((acc: { [date: string]: { [key: string]: unknown } }, curr: { date: string, [key: string]: unknown }) => { - const date = curr[DATE_KEY]; - if (!acc[date]) { - acc[date] = {}; - } + const numericFields = Object.entries(merged[0]) + .filter(([, value]) => typeof value === "number") + .map(([key]) => key); + + const series = regions + .map((region) => { + return numericFields.map((f) => [region, f]); + }) + .flat(1); + + const mergedByDate: object = merged.reduce( + ( + acc: { [date: string]: { [key: string]: unknown } }, + curr: { date: string; [key: string]: unknown } + ) => { + const date = curr[DATE_KEY]; + if (!acc[date]) { + acc[date] = {}; + } - numericFields.forEach(field => { - acc[date][`${curr["region"]}-${field}`] = curr[field]; - }); + numericFields.forEach((field) => { + acc[date][`${curr["region"]}-${field}`] = curr[field]; + }); - return acc; - }, {}); + return acc; + }, + {} + ); const arr = Object.entries(mergedByDate).map(([date, obj]) => { return { date, ...obj }; @@ -145,8 +170,8 @@ function getChartData(data: unknown, regions: string[]) { xKey: "date", yKey: `${region}-${field}`, yName: `${field} (${region})`, - })) - } + })), + }; } } } @@ -155,27 +180,26 @@ function getChartData(data: unknown, regions: string[]) { } return null; - } - function ScriptResult(props: Props) { - const [displayType, setDisplayType] = useState('json'); + const [displayType, setDisplayType] = useState("json"); const [filteredData, setFilteredData] = useState(props.data); - const [displayOptions, setDisplayOptions] = useState<{ [k: string]: boolean }>( - { 'json': true, 'grid': false, 'chart': false, 'download': true } - ); + const [displayOptions, setDisplayOptions] = useState<{ + [k: string]: boolean; + }>({ json: true, grid: false, chart: false, download: true }); const [rowData, setRowData] = useState(null); const [colDefs, setColumnDefs] = useState<{ field: string }[] | null>(null); const [chartOptions, setChartOptions] = useState(null); - function download() { - const blob = new Blob([JSON.stringify(filteredData)], { type: 'application/json' }); - const timestamp = new Date().toISOString().replace(/[:.]/g, ''); - const fileName = `${props.group}_${props.function}_${timestamp}.json` - const link = document.createElement('a'); + const blob = new Blob([JSON.stringify(filteredData)], { + type: "application/json", + }); + const timestamp = new Date().toISOString().replace(/[:.]/g, ""); + const fileName = `${props.group}_${props.function}_${timestamp}.json`; + const link = document.createElement("a"); link.download = fileName; link.href = URL.createObjectURL(blob); link.click(); @@ -189,24 +213,25 @@ function ScriptResult(props: Props) { function applyJqFilter(raw: RunResult | null, filter: string) { // eslint-disable-next-line @typescript-eslint/no-explicit-any - jq.then((jq: any) => jq.json(raw, filter)).catch(() => { - // If any error occurs, display the raw data - return raw - } - ).then(setFilteredData).catch(() => { }) + jq.then((jq: any) => jq.json(raw, filter)) + .catch(() => { + // If any error occurs, display the raw data + return raw; + }) + .then(setFilteredData) + .catch(() => {}); } useEffect(() => { const gridData = getGridData(filteredData, props.regions); const chartData = getChartData(filteredData, props.regions); - if (gridData) { - setColumnDefs(gridData.columns.map(f => ({ "field": f }))) + setColumnDefs(gridData.columns.map((f) => ({ field: f }))); setRowData(gridData.data); } else { - setColumnDefs(null) - setRowData(null) + setColumnDefs(null); + setRowData(null); } if (chartData) { @@ -215,64 +240,85 @@ function ScriptResult(props: Props) { setChartOptions(null); } - setDisplayOptions(prev => { + setDisplayOptions((prev) => { prev["grid"] = gridData !== null; prev["chart"] = chartData !== null; return prev; }); if (displayOptions[displayType] === false) { - setDisplayType('json'); + setDisplayType("json"); } - - }, [filteredData, props.regions, displayOptions, displayType]); return (
- {Object.entries(displayOptions).filter(([, active]) => active).map(([opt,]) => ( - - ))} + {Object.entries(displayOptions) + .filter(([, active]) => active) + .map(([opt]) => ( + + ))}
- applyJqFilter(props.data, e.target.value)} /> + applyJqFilter(props.data, e.target.value)} + />
- { - displayType === 'json' &&
- + {displayType === "json" && ( +
+ {JSON.stringify(filteredData)}
- } - { - displayType === "grid" &&
+ )} + {displayType === "grid" && ( +
- } - { - displayType === "chart" && chartOptions &&
+ )} + {displayType === "chart" && chartOptions && ( +
- } + )} - { - displayType === "download" &&
+ {displayType === "download" && ( +
-
-
+
+ +
+
+ +
- } + )}
- ) - + ); } -export default ScriptResult +export default ScriptResult; diff --git a/script_runner/frontend/src/components/Button/Button.css b/script_runner/frontend/src/components/Button/Button.css new file mode 100644 index 0000000..15df10c --- /dev/null +++ b/script_runner/frontend/src/components/Button/Button.css @@ -0,0 +1,57 @@ +/* Base styles for ALL buttons */ +.button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 16px; + border: 1px solid transparent; /* Make border transparent by default */ + border-radius: 4px; + font-size: 14px; + font-weight: 500; + text-align: center; + cursor: pointer; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, + color 0.15s ease-in-out; + white-space: nowrap; +} + +/* Primary Button Styles (current default) */ +.button-primary { + background-color: #0d6efd; /* Example primary blue */ + color: white; + border-color: #0d6efd; +} + +.button-primary:hover:not(:disabled) { + background-color: #0b5ed7; + border-color: #0a58ca; +} + +.button-primary:disabled { + background-color: #0d6efd; + border-color: #0d6efd; +} + +/* Secondary Button Styles */ +.button-secondary { + background-color: #f8f9fa; /* Light background */ + color: #212529; /* Dark text */ + border-color: #ccc; /* Simple border */ +} + +.button-secondary:hover:not(:disabled) { + background-color: #e2e6ea; + border-color: #adb5bd; +} + +.button-secondary:disabled { + background-color: #e9ecef; + color: #6c757d; + border-color: #ced4da; +} + +/* General Disabled State Styles (applied to all variants) */ +.button:disabled { + cursor: not-allowed; + opacity: 0.65; +} diff --git a/script_runner/frontend/src/components/Button/Button.tsx b/script_runner/frontend/src/components/Button/Button.tsx new file mode 100644 index 0000000..22a6e1a --- /dev/null +++ b/script_runner/frontend/src/components/Button/Button.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import "./Button.css"; + +// Define button variants +type ButtonVariant = "primary" | "secondary"; + +// Define the props for the Button component, extending standard button attributes +interface ButtonProps extends React.ButtonHTMLAttributes { + children: React.ReactNode; + variant?: ButtonVariant; // Add optional variant prop, default to primary +} + +// The Button component +function Button({ + children, + className, + variant = "primary", + ...props +}: ButtonProps) { + // Combine base class, variant class, and any additional classes + const combinedClassName = `button button-${variant} ${ + className || "" + }`.trim(); + + return ( + + ); +} + +export default Button; From bdd9b892f8e1ea3a86520e1c7b9db70c7c6c9362 Mon Sep 17 00:00:00 2001 From: John Manhart Date: Fri, 18 Apr 2025 10:40:01 -0700 Subject: [PATCH 3/7] Adding in a applying the search bar --- script_runner/frontend/src/App.css | 57 +------- script_runner/frontend/src/Home.tsx | 101 ++------------ .../src/components/SearchBar/SearchBar.css | 77 +++++++++++ .../src/components/SearchBar/SearchBar.tsx | 130 ++++++++++++++++++ 4 files changed, 227 insertions(+), 138 deletions(-) create mode 100644 script_runner/frontend/src/components/SearchBar/SearchBar.css create mode 100644 script_runner/frontend/src/components/SearchBar/SearchBar.tsx diff --git a/script_runner/frontend/src/App.css b/script_runner/frontend/src/App.css index 95ca77d..171dfde 100644 --- a/script_runner/frontend/src/App.css +++ b/script_runner/frontend/src/App.css @@ -138,65 +138,18 @@ a.group-text { height: 100%; padding: 10px; display: flex; - justify-content: center; -} - -.home-search { - width: 60%; - min-width: 500px; - max-width: 1200px; + flex-direction: column; + align-items: center; + padding-top: 40px; } -.home-search-text { +.home-search-prompt { width: 100%; font-family: var(--font-heavy); font-size: 16px; text-align: center; font-weight: 900; - margin: 20px 0; -} - -.home-search-input input { - width: 100%; - border: 1px solid var(--border); - border-radius: 3px; - padding: 10px 20px; - font-size: 14px; -} - -.home-search-input input:not(:focus) { - background-color: var(--bg-light); -} - -.home-search-results ul { - margin: 0; - padding: 0; - list-style-type: none; -} - -.home-search-results li { - border: 1px solid var(--border); - border-top: 0; -} - -.home-no-result { - font-size: 14px; - line-height: 20px; - padding: 10px; -} - -.home-search-results li a { - display: block; - width: 100%; - padding: 10px; -} - -li.home-search-result:hover { - background-color: var(--border); -} - -.function-group { - color: var(--text-hover); + margin-bottom: 15px; } .functions { diff --git a/script_runner/frontend/src/Home.tsx b/script_runner/frontend/src/Home.tsx index a0e4f13..bc72d90 100644 --- a/script_runner/frontend/src/Home.tsx +++ b/script_runner/frontend/src/Home.tsx @@ -1,95 +1,24 @@ -import React, {useState, useRef} from 'react'; -import {ConfigFunction, ConfigGroup, Route} from './types.tsx' +import React from "react"; +import { ConfigGroup, Route } from "./types"; +import SearchBar from "./components/SearchBar/SearchBar"; type Props = { - route: Route, - navigate: (to: Route) => void, - groups: ConfigGroup[], -} + route: Route; + navigate: (to: Route) => void; + groups: ConfigGroup[]; +}; function Home(props: Props) { - const [searchResults, setSearchResults] = useState<{function: ConfigFunction, group: string}[] | null>(null); - const [showResults, setShowResults] = useState(false); - const dropdownRef = useRef(null); - - - function search(query: string) { - if (query === "") { - setSearchResults(null); - setShowResults(false); - return; - } - - const results = []; - - const substrings = query.split(" "); - - for (const group of props.groups) { - for (const f of group.functions) { - let found = true; - for (const substr of substrings) { - if (!f.name.includes(substr) && !group.group.includes(substr)) { - found = false; - break; - } - } - // All substrings found - if (found) { - results.push({function: f, group: group.group}); - } - - } - } - - setSearchResults(results.slice(0, 10)); - setShowResults(true); - } - - function handleBlur(e: React.FocusEvent) { - if (dropdownRef.current && !dropdownRef.current.contains(e.relatedTarget)) { - setShowResults(false); - } - } - - function handleFocus() { - setShowResults(true); - } - return (
-
-
What do you want to do today?
-
- search(e.target.value)} - onFocus={handleFocus} - onBlur={handleBlur} - /> -
-
- {searchResults !== null && showResults && ( - - )} -
- - -
+
What do you want to do today?
+
- ) + ); } -export default Home +export default Home; diff --git a/script_runner/frontend/src/components/SearchBar/SearchBar.css b/script_runner/frontend/src/components/SearchBar/SearchBar.css new file mode 100644 index 0000000..7a6c7fa --- /dev/null +++ b/script_runner/frontend/src/components/SearchBar/SearchBar.css @@ -0,0 +1,77 @@ +/* Container for the search bar */ +.search-bar-container { + width: 100%; /* Take full width of its parent */ + max-width: 600px; /* Max width for better readability */ + position: relative; /* Needed for absolute positioning of results */ + margin: 0 auto; /* Center it if parent allows */ +} + +/* Input field styles */ +.search-bar-input input { + width: 100%; + border: 1px solid var(--border, #ccc); + border-radius: 4px; + padding: 10px 15px; + font-size: 14px; + box-sizing: border-box; /* Include padding and border in width */ +} + +.search-bar-input input:focus { + outline: none; + border-color: var(--highlight-border, #0d6efd); + box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25); /* Subtle focus ring */ +} + +/* Results dropdown styles */ +.search-bar-results { + position: absolute; + top: 100%; /* Position below the input */ + left: 0; + right: 0; + background-color: var(--bg-primary, white); + border: 1px solid var(--border, #ccc); + border-top: none; /* Avoid double border with input */ + border-radius: 0 0 4px 4px; + z-index: 10; /* Ensure it appears above other content */ + max-height: 300px; /* Limit height and allow scrolling */ + overflow-y: auto; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.search-bar-results ul { + list-style: none; + margin: 0; + padding: 0; +} + +.search-bar-results li { + padding: 0; + margin: 0; +} + +.search-bar-results li a { + display: block; + padding: 8px 15px; + cursor: pointer; + text-decoration: none; + color: var(--text-primary, #212529); +} + +.search-bar-results li a:hover { + background-color: var(--bg-hover, #f8f9fa); +} + +.result-group { + color: var(--text-secondary, #6c757d); + font-size: 0.9em; +} + +.result-function { + /* Add specific styles if needed */ +} + +.search-bar-no-result { + padding: 8px 15px; + color: var(--text-secondary, #6c757d); + font-style: italic; +} diff --git a/script_runner/frontend/src/components/SearchBar/SearchBar.tsx b/script_runner/frontend/src/components/SearchBar/SearchBar.tsx new file mode 100644 index 0000000..266f737 --- /dev/null +++ b/script_runner/frontend/src/components/SearchBar/SearchBar.tsx @@ -0,0 +1,130 @@ +import React, { useState, useRef, useEffect } from "react"; +import { ConfigGroup, ConfigFunction, Route } from "../../types"; // Adjust path +import "./SearchBar.css"; + +interface SearchResult { + function: ConfigFunction; + group: string; +} + +interface SearchBarProps { + groups: ConfigGroup[]; + route: Route; // Needed for navigation context + navigate: (to: Route) => void; +} + +function SearchBar({ groups, route, navigate }: SearchBarProps) { + const [searchResults, setSearchResults] = useState( + null + ); + const [showResults, setShowResults] = useState(false); + const dropdownRef = useRef(null); + const inputRef = useRef(null); + + useEffect(() => { + // Add event listener to close dropdown when clicking outside + function handleClickOutside(event: MouseEvent) { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) && + inputRef.current && + !inputRef.current.contains(event.target as Node) + ) { + setShowResults(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [dropdownRef, inputRef]); + + function performSearch(currentQuery: string) { + if (currentQuery === "") { + setSearchResults(null); + setShowResults(false); + return; + } + + const results: SearchResult[] = []; + const substrings = currentQuery + .toLowerCase() + .split(" ") + .filter((s) => s); + + for (const group of groups) { + for (const f of group.functions) { + const searchableText = `${group.group.toLowerCase()} ${f.name.toLowerCase()}`; + let found = true; + for (const substr of substrings) { + if (!searchableText.includes(substr)) { + found = false; + break; + } + } + if (found) { + results.push({ function: f, group: group.group }); + } + } + } + + setSearchResults(results.slice(0, 10)); // Limit results + setShowResults(true); + } + + function handleResultClick(result: SearchResult) { + navigate({ ...route, group: result.group, function: result.function.name }); + setShowResults(false); // Close dropdown after selection + if (inputRef.current) { + inputRef.current.value = ""; // Clear visual input value + } + } + + function handleFocus() { + if (searchResults && searchResults.length > 0) { + setShowResults(true); + } + } + + return ( +
+
+ performSearch(e.target.value)} + onFocus={handleFocus} + // onBlur handling is done via click outside listener + /> +
+ {showResults && searchResults !== null && ( +
+ +
+ )} +
+ ); +} + +export default SearchBar; From 6dbc585f9dcce12a0181a42de01e4a144ddfaead Mon Sep 17 00:00:00 2001 From: John Manhart Date: Fri, 18 Apr 2025 10:46:47 -0700 Subject: [PATCH 4/7] Adding in the input component and applying it around the app --- script_runner/frontend/src/Script.tsx | 56 +++++------------ .../frontend/src/components/Input/Input.css | 45 +++++++++++++ .../frontend/src/components/Input/Input.tsx | 63 +++++++++++++++++++ 3 files changed, 124 insertions(+), 40 deletions(-) create mode 100644 script_runner/frontend/src/components/Input/Input.css create mode 100644 script_runner/frontend/src/components/Input/Input.tsx diff --git a/script_runner/frontend/src/Script.tsx b/script_runner/frontend/src/Script.tsx index 94eb73e..3e21237 100644 --- a/script_runner/frontend/src/Script.tsx +++ b/script_runner/frontend/src/Script.tsx @@ -4,6 +4,7 @@ import ScriptResult from "./ScriptResult.tsx"; import { RunResult, ConfigFunction } from "./types.tsx"; import Tag from "./components/Tag/Tag"; import Button from "./components/Button/Button"; +import Input from "./components/Input/Input"; interface Props { regions: string[]; @@ -83,51 +84,26 @@ function Script(props: Props) { {functionName}
-
+ { + e.preventDefault(); + executeFunction(); + }} + > {parameters.length > 0 && (
To execute this function, provide the following parameters:
- {parameters.map((arg, idx) => { - return ( -
-
- -
-
- {arg.enumValues && ( - - )} - {!arg.enumValues && ( - - handleInputChange(idx, e.target.value) - } - required - disabled={inputDisabled} - /> - )} -
-
- ); - })} + {parameters.map((arg, idx) => ( + handleInputChange(idx, value)} + /> + ))}
)}
diff --git a/script_runner/frontend/src/components/Input/Input.css b/script_runner/frontend/src/components/Input/Input.css new file mode 100644 index 0000000..836ffc5 --- /dev/null +++ b/script_runner/frontend/src/components/Input/Input.css @@ -0,0 +1,45 @@ +/* Styles for the input group */ +.input-group { + margin-bottom: 15px; + display: flex; + flex-direction: column; +} + +.input-label { + margin-bottom: 5px; + font-weight: 500; +} + +/* Common styles for input and select controls */ +.input-control input[type="text"], +.input-control select { + width: 100%; + padding: 8px 12px; + border: 1px solid var(--border, #ccc); + border-radius: 4px; + font-size: 14px; + box-sizing: border-box; +} + +.input-control input[type="text"]:focus, +.input-control select:focus { + outline: none; + border-color: var(--highlight-border, #0d6efd); + box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25); +} + +.input-control input[type="text"]:disabled, +.input-control select:disabled { + background-color: #e9ecef; + cursor: not-allowed; + opacity: 0.7; +} + +/* Specific classes if needed, otherwise handled by selectors above */ +.input-select { + /* Add specific select styles here if different from text input */ +} + +.input-text { + /* Add specific text input styles here if different */ +} diff --git a/script_runner/frontend/src/components/Input/Input.tsx b/script_runner/frontend/src/components/Input/Input.tsx new file mode 100644 index 0000000..b950712 --- /dev/null +++ b/script_runner/frontend/src/components/Input/Input.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { ConfigParam } from "../../types"; // Adjust path if needed +import "./Input.css"; + +interface InputProps { + parameter: ConfigParam; + value: string | null; + isDisabled: boolean; + onChange: (value: string) => void; +} + +function Input({ parameter, value, isDisabled, onChange }: InputProps) { + const { name, enumValues } = parameter; + const inputId = `param-${name}`; // Create unique ID for label association + + return ( +
+ {" "} + {/* Renamed class */} +
+ {" "} + {/* Renamed class */} + +
+
+ {" "} + {/* Renamed class */} + {enumValues ? ( + + ) : ( + onChange(e.target.value)} + className="input-text" // Renamed class + /> + )} +
+
+ ); +} + +export default Input; From 542722a0cc16c5b12c594eb23675360be91713ff Mon Sep 17 00:00:00 2001 From: John Manhart Date: Fri, 18 Apr 2025 10:53:11 -0700 Subject: [PATCH 5/7] Adding in the codeviewer component --- script_runner/frontend/src/App.css | 21 +------ script_runner/frontend/src/Script.tsx | 40 +----------- .../src/components/CodeViewer/CodeViewer.css | 47 ++++++++++++++ .../src/components/CodeViewer/CodeViewer.tsx | 61 +++++++++++++++++++ 4 files changed, 111 insertions(+), 58 deletions(-) create mode 100644 script_runner/frontend/src/components/CodeViewer/CodeViewer.css create mode 100644 script_runner/frontend/src/components/CodeViewer/CodeViewer.tsx diff --git a/script_runner/frontend/src/App.css b/script_runner/frontend/src/App.css index 171dfde..c343c00 100644 --- a/script_runner/frontend/src/App.css +++ b/script_runner/frontend/src/App.css @@ -216,31 +216,12 @@ a.group-text { height: 100%; } -.function-left, -.function-right { +.function-left { flex: 1; height: calc(100vh - 70px); overflow-y: scroll; } -.function-left { - display: flex; - flex-direction: column; -} - -.function-right { - background-color: var(--bg-light); -} - -.function-right-button { - padding: 10px; -} - -.function-right-description { - padding: 10px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; -} - .function-result { margin: 10px; flex-grow: 1; diff --git a/script_runner/frontend/src/Script.tsx b/script_runner/frontend/src/Script.tsx index 3e21237..d60072d 100644 --- a/script_runner/frontend/src/Script.tsx +++ b/script_runner/frontend/src/Script.tsx @@ -1,10 +1,10 @@ import { useState, useEffect } from "react"; -import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import ScriptResult from "./ScriptResult.tsx"; import { RunResult, ConfigFunction } from "./types.tsx"; import Tag from "./components/Tag/Tag"; import Button from "./components/Button/Button"; import Input from "./components/Input/Input"; +import CodeViewer from "./components/CodeViewer/CodeViewer"; interface Props { regions: string[]; @@ -24,18 +24,15 @@ function Script(props: Props) { parameters.map((a) => a.default) ); const [result, setResult] = useState(null); - // we keep another piece of state because the result value might itself be null const [hasResult, setHasResult] = useState(false); const [error, setError] = useState(null); const [isRunning, setIsRunning] = useState(false); - const [codeCollapsed, setCodeCollapsed] = useState(false); // If the selected function changes, reset all state useEffect(() => { setParams(parameters.map((a) => a.default)); setResult(null); setHasResult(false); - setCodeCollapsed(false); setError(null); }, [parameters, props.group, props.function, props.execute]); @@ -133,40 +130,7 @@ function Script(props: Props) { /> )}
- {} - {codeCollapsed ? ( -
- -
- ) : ( -
-
- ✨ This is the {functionName} function definition - ✨ -
- - {source} - -
- -
-
- )} +
); } diff --git a/script_runner/frontend/src/components/CodeViewer/CodeViewer.css b/script_runner/frontend/src/components/CodeViewer/CodeViewer.css new file mode 100644 index 0000000..923fd36 --- /dev/null +++ b/script_runner/frontend/src/components/CodeViewer/CodeViewer.css @@ -0,0 +1,47 @@ +/* Styles for the expanded code viewer container */ +.code-viewer-expanded { + display: flex; + flex-direction: column; + height: 100%; /* Occupy available height */ + border-left: 1px solid var(--border, #ccc); /* Add a separator line */ + background-color: var( + --bg-light, + #f8f9fa + ); /* Slightly different background */ +} + +/* Header section within the expanded view */ +.code-viewer-header { + display: flex; + justify-content: space-between; /* Push title and button apart */ + align-items: center; + padding: 8px 12px; + border-bottom: 1px solid var(--border, #ccc); + font-size: 0.9em; + font-weight: 500; + flex-shrink: 0; /* Prevent header from shrinking */ +} + +/* Content area for the syntax highlighter */ +.code-viewer-content { + flex-grow: 1; /* Allow content to fill remaining space */ + overflow: hidden; /* Prevent parent overflow, rely on SyntaxHighlighter scroll */ +} + +/* Styles for the collapsed state placeholder */ +.code-viewer-collapsed { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + border-left: 1px solid var(--border, #ccc); + background-color: var(--bg-light, #f8f9fa); + padding: 10px; +} + +/* Make the hide/show buttons slightly smaller */ +.code-viewer-header .button, +.code-viewer-collapsed .button { + padding: 4px 8px; + font-size: 12px; +} diff --git a/script_runner/frontend/src/components/CodeViewer/CodeViewer.tsx b/script_runner/frontend/src/components/CodeViewer/CodeViewer.tsx new file mode 100644 index 0000000..b8c7fc5 --- /dev/null +++ b/script_runner/frontend/src/components/CodeViewer/CodeViewer.tsx @@ -0,0 +1,61 @@ +import React, { useState } from "react"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import Button from "../Button/Button"; // Adjust path as needed +import "./CodeViewer.css"; + +interface CodeViewerProps { + functionName: string; + source: string; +} + +function CodeViewer({ functionName, source }: CodeViewerProps) { + const [isCollapsed, setIsCollapsed] = useState(false); + + if (isCollapsed) { + return ( +
+ +
+ ); + } + + return ( +
+
+ ✨ {functionName} source ✨ + +
+
+ + {source} + +
+
+ ); +} + +export default CodeViewer; From f49788d52168f7959e66e0e64bb1234f5c92ed24 Mon Sep 17 00:00:00 2001 From: John Manhart Date: Fri, 18 Apr 2025 11:02:29 -0700 Subject: [PATCH 6/7] Cleaning up and adding in some style updates --- script_runner/frontend/src/ScriptResult.tsx | 128 +++++++++--------- .../frontend/src/components/Button/Button.css | 41 +++--- .../frontend/src/components/Button/Button.tsx | 8 +- .../src/components/CodeViewer/CodeViewer.css | 12 +- .../src/components/CodeViewer/CodeViewer.tsx | 2 + 5 files changed, 97 insertions(+), 94 deletions(-) diff --git a/script_runner/frontend/src/ScriptResult.tsx b/script_runner/frontend/src/ScriptResult.tsx index d2f6b74..26599b4 100644 --- a/script_runner/frontend/src/ScriptResult.tsx +++ b/script_runner/frontend/src/ScriptResult.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coy } from "react-syntax-highlighter/dist/esm/styles/prism"; import { AgGridReact } from "ag-grid-react"; @@ -184,7 +184,6 @@ function getChartData(data: unknown, regions: string[]) { function ScriptResult(props: Props) { const [displayType, setDisplayType] = useState("json"); - const [filteredData, setFilteredData] = useState(props.data); const [displayOptions, setDisplayOptions] = useState<{ [k: string]: boolean; @@ -223,15 +222,25 @@ function ScriptResult(props: Props) { } useEffect(() => { - const gridData = getGridData(filteredData, props.regions); - const chartData = getChartData(filteredData, props.regions); + const gridData = getGridData(props.data, props.regions); + const chartData = getChartData(props.data, props.regions); + + const options = { + json: true, + grid: !!gridData, + chart: !!chartData, + download: true, + }; + + setDisplayOptions(options); + setFilteredData(props.data); if (gridData) { - setColumnDefs(gridData.columns.map((f) => ({ field: f }))); + setDisplayType("grid"); setRowData(gridData.data); + setColumnDefs(gridData.columns.map((field) => ({ field }))); } else { - setColumnDefs(null); - setRowData(null); + setDisplayType("json"); } if (chartData) { @@ -239,32 +248,32 @@ function ScriptResult(props: Props) { } else { setChartOptions(null); } - - setDisplayOptions((prev) => { - prev["grid"] = gridData !== null; - prev["chart"] = chartData !== null; - return prev; - }); - - if (displayOptions[displayType] === false) { - setDisplayType("json"); - } - }, [filteredData, props.regions, displayOptions, displayType]); + }, [props.data, props.regions]); return ( -
+
{Object.entries(displayOptions) - .filter(([, active]) => active) - .map(([opt]) => ( + .filter(([key, value]) => value && key !== "download") + .map(([key]) => (
setDisplayType(key)} > - setDisplayType(opt)}>{opt} + {key}
))} +
+ + +
applyJqFilter(props.data, e.target.value)} />
- {displayType === "json" && ( -
- - {JSON.stringify(filteredData)} - -
- )} - {displayType === "grid" && ( -
- -
- )} - {displayType === "chart" && chartOptions && ( -
- -
- )} - - {displayType === "download" && ( -
-
-
- -
-
- -
+
+ {displayType === "json" && ( +
+ + {JSON.stringify(filteredData)} +
-
- )} + )} + {displayType === "grid" && rowData && colDefs && ( +
+ +
+ )} + {displayType === "chart" && chartOptions && ( +
+ +
+ )} +
); } diff --git a/script_runner/frontend/src/components/Button/Button.css b/script_runner/frontend/src/components/Button/Button.css index 15df10c..37c9b21 100644 --- a/script_runner/frontend/src/components/Button/Button.css +++ b/script_runner/frontend/src/components/Button/Button.css @@ -1,12 +1,11 @@ -/* Base styles for ALL buttons */ +/* Base styles for ALL buttons (layout, alignment, transitions, etc.) */ .button { display: inline-flex; align-items: center; justify-content: center; - padding: 8px 16px; - border: 1px solid transparent; /* Make border transparent by default */ + border: 1px solid transparent; border-radius: 4px; - font-size: 14px; + /* font-size and padding are moved to size classes */ font-weight: 500; text-align: center; cursor: pointer; @@ -15,42 +14,52 @@ white-space: nowrap; } -/* Primary Button Styles (current default) */ +/* --- Size Modifiers --- */ +.button-md { + /* Default size */ + padding: 8px 16px; + font-size: 14px; +} + +.button-sm { + padding: 4px 8px; + font-size: 12px; +} + +/* --- Variant Modifiers (Colors, Backgrounds, Borders) --- */ + +/* Primary Button Styles */ .button-primary { - background-color: #0d6efd; /* Example primary blue */ + background-color: #0d6efd; color: white; border-color: #0d6efd; } - .button-primary:hover:not(:disabled) { background-color: #0b5ed7; border-color: #0a58ca; } - .button-primary:disabled { - background-color: #0d6efd; - border-color: #0d6efd; + /* Use general disabled style opacity */ } /* Secondary Button Styles */ .button-secondary { - background-color: #f8f9fa; /* Light background */ - color: #212529; /* Dark text */ - border-color: #ccc; /* Simple border */ + background-color: #f8f9fa; + color: #212529; + border-color: #ccc; } - .button-secondary:hover:not(:disabled) { background-color: #e2e6ea; border-color: #adb5bd; } - .button-secondary:disabled { background-color: #e9ecef; color: #6c757d; border-color: #ced4da; + /* Use general disabled style opacity */ } -/* General Disabled State Styles (applied to all variants) */ +/* General Disabled State Styles (applied to all variants/sizes) */ .button:disabled { cursor: not-allowed; opacity: 0.65; diff --git a/script_runner/frontend/src/components/Button/Button.tsx b/script_runner/frontend/src/components/Button/Button.tsx index 22a6e1a..97b60d3 100644 --- a/script_runner/frontend/src/components/Button/Button.tsx +++ b/script_runner/frontend/src/components/Button/Button.tsx @@ -3,11 +3,14 @@ import "./Button.css"; // Define button variants type ButtonVariant = "primary" | "secondary"; +// Define button sizes +type ButtonSize = "sm" | "md"; // Add more sizes if needed (e.g., "lg") // Define the props for the Button component, extending standard button attributes interface ButtonProps extends React.ButtonHTMLAttributes { children: React.ReactNode; variant?: ButtonVariant; // Add optional variant prop, default to primary + size?: ButtonSize; // Add optional size prop } // The Button component @@ -15,10 +18,11 @@ function Button({ children, className, variant = "primary", + size = "md", // Default size to medium ...props }: ButtonProps) { - // Combine base class, variant class, and any additional classes - const combinedClassName = `button button-${variant} ${ + // Combine base class, variant class, size class, and any additional classes + const combinedClassName = `button button-${variant} button-${size} ${ className || "" }`.trim(); diff --git a/script_runner/frontend/src/components/CodeViewer/CodeViewer.css b/script_runner/frontend/src/components/CodeViewer/CodeViewer.css index 923fd36..19fcc8d 100644 --- a/script_runner/frontend/src/components/CodeViewer/CodeViewer.css +++ b/script_runner/frontend/src/components/CodeViewer/CodeViewer.css @@ -1,6 +1,7 @@ /* Styles for the expanded code viewer container */ .code-viewer-expanded { display: flex; + flex: 1; /* Make it take available horizontal space */ flex-direction: column; height: 100%; /* Occupy available height */ border-left: 1px solid var(--border, #ccc); /* Add a separator line */ @@ -31,17 +32,10 @@ /* Styles for the collapsed state placeholder */ .code-viewer-collapsed { display: flex; - align-items: center; - justify-content: center; + align-items: flex-start; /* Align button to the top */ + justify-content: flex-start; /* Align button to the left */ height: 100%; border-left: 1px solid var(--border, #ccc); background-color: var(--bg-light, #f8f9fa); padding: 10px; } - -/* Make the hide/show buttons slightly smaller */ -.code-viewer-header .button, -.code-viewer-collapsed .button { - padding: 4px 8px; - font-size: 12px; -} diff --git a/script_runner/frontend/src/components/CodeViewer/CodeViewer.tsx b/script_runner/frontend/src/components/CodeViewer/CodeViewer.tsx index b8c7fc5..b2706e5 100644 --- a/script_runner/frontend/src/components/CodeViewer/CodeViewer.tsx +++ b/script_runner/frontend/src/components/CodeViewer/CodeViewer.tsx @@ -16,6 +16,7 @@ function CodeViewer({ functionName, source }: CodeViewerProps) {
- +
); } diff --git a/script_runner/frontend/src/components/CodeViewer/CodeViewer.css b/script_runner/frontend/src/components/CodeViewer/CodeViewer.css index 19fcc8d..54912e6 100644 --- a/script_runner/frontend/src/components/CodeViewer/CodeViewer.css +++ b/script_runner/frontend/src/components/CodeViewer/CodeViewer.css @@ -14,7 +14,7 @@ /* Header section within the expanded view */ .code-viewer-header { display: flex; - justify-content: space-between; /* Push title and button apart */ + justify-content: space-between; /* Push title group and button apart */ align-items: center; padding: 8px 12px; border-bottom: 1px solid var(--border, #ccc); @@ -23,6 +23,13 @@ flex-shrink: 0; /* Prevent header from shrinking */ } +/* Style for the group containing tag and title */ +.code-viewer-title-group { + display: flex; + align-items: center; + gap: 6px; /* Add space between tag and title */ +} + /* Content area for the syntax highlighter */ .code-viewer-content { flex-grow: 1; /* Allow content to fill remaining space */ diff --git a/script_runner/frontend/src/components/CodeViewer/CodeViewer.tsx b/script_runner/frontend/src/components/CodeViewer/CodeViewer.tsx index b2706e5..8a76c06 100644 --- a/script_runner/frontend/src/components/CodeViewer/CodeViewer.tsx +++ b/script_runner/frontend/src/components/CodeViewer/CodeViewer.tsx @@ -1,14 +1,16 @@ import React, { useState } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import Button from "../Button/Button"; // Adjust path as needed +import Tag from "../Tag/Tag"; // Import Tag component import "./CodeViewer.css"; interface CodeViewerProps { functionName: string; source: string; + type: "read" | "write"; // Add type prop } -function CodeViewer({ functionName, source }: CodeViewerProps) { +function CodeViewer({ functionName, source, type }: CodeViewerProps) { const [isCollapsed, setIsCollapsed] = useState(false); if (isCollapsed) { @@ -29,7 +31,10 @@ function CodeViewer({ functionName, source }: CodeViewerProps) { return (
- ✨ {functionName} source ✨ +
+ + ✨ {functionName} source ✨ +