From f18580af4b69ed7e3f0ff78f2e51ec1ba4453fec Mon Sep 17 00:00:00 2001 From: ShahanaFarooqui Date: Thu, 12 Mar 2026 14:49:55 -0700 Subject: [PATCH 1/5] Misc Fix --- .../AccountEvents/AccountEventsTable/AccountEventsTable.scss | 3 +-- apps/frontend/src/components/ui/Loading/Loading.tsx | 2 +- apps/frontend/src/styles/constants.scss | 1 + apps/frontend/src/styles/shared.scss | 1 + apps/frontend/src/utilities/constants.ts | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/frontend/src/components/bookkeeper/AccountEvents/AccountEventsTable/AccountEventsTable.scss b/apps/frontend/src/components/bookkeeper/AccountEvents/AccountEventsTable/AccountEventsTable.scss index 331d1d12..076fb977 100644 --- a/apps/frontend/src/components/bookkeeper/AccountEvents/AccountEventsTable/AccountEventsTable.scss +++ b/apps/frontend/src/components/bookkeeper/AccountEvents/AccountEventsTable/AccountEventsTable.scss @@ -2,11 +2,10 @@ .account-events-table { padding: 0 !important; - border-radius: 1rem; + border-radius: $border-radius-sm; overflow: hidden; border: 1px solid $border-color; height: 100%; - border-radius: 1rem; & .expandable-table { table-layout: fixed; overflow: hidden; diff --git a/apps/frontend/src/components/ui/Loading/Loading.tsx b/apps/frontend/src/components/ui/Loading/Loading.tsx index 85958916..b5cee2f3 100644 --- a/apps/frontend/src/components/ui/Loading/Loading.tsx +++ b/apps/frontend/src/components/ui/Loading/Loading.tsx @@ -7,7 +7,7 @@ export const Loading = () => { -
Loading...
+
Loading...
); diff --git a/apps/frontend/src/styles/constants.scss b/apps/frontend/src/styles/constants.scss index b2fcc3b6..f50f0d58 100644 --- a/apps/frontend/src/styles/constants.scss +++ b/apps/frontend/src/styles/constants.scss @@ -39,6 +39,7 @@ $theme-colors: ( light: #9f9f9f, ); +$border-radius-sm: 0.375rem; $border-radius: 1.25rem; $btn-link-color: $primary; $btn-padding-x: 0.625rem; diff --git a/apps/frontend/src/styles/shared.scss b/apps/frontend/src/styles/shared.scss index 4aa78538..3c23d326 100755 --- a/apps/frontend/src/styles/shared.scss +++ b/apps/frontend/src/styles/shared.scss @@ -477,6 +477,7 @@ svg { opacity: 1; z-index: 99; width: 0.5rem; + background-color: transparent; & .ps__thumb-y { width: 0.25rem; background-color: #DFB316; diff --git a/apps/frontend/src/utilities/constants.ts b/apps/frontend/src/utilities/constants.ts index 5b8e055c..95beb43d 100755 --- a/apps/frontend/src/utilities/constants.ts +++ b/apps/frontend/src/utilities/constants.ts @@ -68,7 +68,7 @@ export const channelStateMap: Record = { DUALOPEND_OPEN_COMMIT_READY: "Dual Open Commit Ready", }; -export const CURRENCY_UNITS = ['SATS', 'BTC']; +export const CURRENCY_UNITS: Units[] = [Units.SATS, Units.BTC]; export const CURRENCY_UNIT_FORMATS = { Sats: '1.0-0', BTC: '1.6-6', OTHER: '1.2-2' }; From d88c1eddabfaf7af401d71e4fc1800b9f52f4c85 Mon Sep 17 00:00:00 2001 From: ShahanaFarooqui Date: Thu, 12 Mar 2026 14:50:24 -0700 Subject: [PATCH 2/5] Fixed Toggle Switch for Generic Use --- .../shared/ToggleSwitch/ToggleSwitch.scss | 7 +++ .../shared/ToggleSwitch/ToggleSwitch.test.tsx | 6 +- .../shared/ToggleSwitch/ToggleSwitch.tsx | 62 ++++++++++--------- .../src/components/ui/Settings/Settings.tsx | 24 +++++-- 4 files changed, 62 insertions(+), 37 deletions(-) diff --git a/apps/frontend/src/components/shared/ToggleSwitch/ToggleSwitch.scss b/apps/frontend/src/components/shared/ToggleSwitch/ToggleSwitch.scss index f6411e08..dbfaafa9 100644 --- a/apps/frontend/src/components/shared/ToggleSwitch/ToggleSwitch.scss +++ b/apps/frontend/src/components/shared/ToggleSwitch/ToggleSwitch.scss @@ -16,6 +16,13 @@ display: flex; justify-content: flex-start; + &.toggle-disabled { + opacity: 0.45; + &:hover { + cursor: not-allowed; + } + } + & .toggle-bg-text { height: 100%; } diff --git a/apps/frontend/src/components/shared/ToggleSwitch/ToggleSwitch.test.tsx b/apps/frontend/src/components/shared/ToggleSwitch/ToggleSwitch.test.tsx index 314bf6b4..65079e48 100644 --- a/apps/frontend/src/components/shared/ToggleSwitch/ToggleSwitch.test.tsx +++ b/apps/frontend/src/components/shared/ToggleSwitch/ToggleSwitch.test.tsx @@ -4,7 +4,6 @@ import { APP_ANIMATION_DURATION } from '../../../utilities/constants'; import { createMockStore } from '../../../utilities/test-utilities/mockStore'; import { mockAppStore } from '../../../utilities/test-utilities/mockData'; import ToggleSwitch from './ToggleSwitch'; -import { Units } from '../../../utilities/constants'; describe('ToggleSwitch component ', () => { it('should be in the document', async () => { @@ -13,9 +12,8 @@ describe('ToggleSwitch component ', () => { {}} /> ); diff --git a/apps/frontend/src/components/shared/ToggleSwitch/ToggleSwitch.tsx b/apps/frontend/src/components/shared/ToggleSwitch/ToggleSwitch.tsx index 6098f619..37405c27 100755 --- a/apps/frontend/src/components/shared/ToggleSwitch/ToggleSwitch.tsx +++ b/apps/frontend/src/components/shared/ToggleSwitch/ToggleSwitch.tsx @@ -1,52 +1,56 @@ import './ToggleSwitch.scss'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { motion } from 'framer-motion'; -import { SPRING_VARIANTS } from '../../../utilities/constants'; -import { RootService } from '../../../services/http.service'; -import { setConfig } from '../../../store/rootSlice'; -import { useDispatch, useSelector } from 'react-redux'; -import { selectAppConfig } from '../../../store/rootSelectors'; +import { BOUNCY_SPRING_VARIANTS_1 } from '../../../utilities/constants'; -const ToggleSwitch = props => { - const dispatch = useDispatch(); - const [isSwitchOn, setIsSwitchOn] = useState(props.selValue === props.values[1]); - const appConfig = useSelector(selectAppConfig); - - const changeValueHandler = async() => { - setIsSwitchOn((prevValue) => !prevValue); - const currValue = isSwitchOn ? 0 : 1; - const updatedConfig = { - ...appConfig, - uiConfig: { - ...appConfig.uiConfig, - unit: props.values[currValue] - } - }; - await RootService.updateConfig(updatedConfig); - dispatch(setConfig(updatedConfig)); +const ToggleSwitch = ({ + values, + selIndex, + onChange, + className = '', + disabled = false, +}: { + values: (string | React.ReactNode)[]; + selIndex: number; + onChange: (index: number) => void; + className?: string; + disabled?: boolean; +}) => { + const [isSwitchOn, setIsSwitchOn] = useState(selIndex === 1); + + useEffect(() => { + setIsSwitchOn(selIndex === 1); + }, [selIndex]); + + const changeValueHandler = () => { + if (disabled) return; + const nextIndex = isSwitchOn ? 0 : 1; + setIsSwitchOn(!isSwitchOn); + onChange(nextIndex); }; return (
- {props.values[0]} - {props.values[1]} + {values[0]} + {values[1]}
- {props.selValue} + {isSwitchOn ? values[1] : values[0]}
); diff --git a/apps/frontend/src/components/ui/Settings/Settings.tsx b/apps/frontend/src/components/ui/Settings/Settings.tsx index 9256eff8..4e3acae1 100755 --- a/apps/frontend/src/components/ui/Settings/Settings.tsx +++ b/apps/frontend/src/components/ui/Settings/Settings.tsx @@ -1,18 +1,22 @@ import './Settings.scss'; import { Dropdown } from 'react-bootstrap'; - +import { useDispatch, useSelector } from 'react-redux'; import logger from '../../../services/logger.service'; import useBreakpoint from '../../../hooks/use-breakpoint'; -import { CURRENCY_UNITS } from '../../../utilities/constants'; +import { CURRENCY_UNITS, Units } from '../../../utilities/constants'; import { SettingsSVG } from '../../../svgs/Settings'; import FiatSelection from '../../shared/FiatSelection/FiatSelection'; import ToggleSwitch from '../../shared/ToggleSwitch/ToggleSwitch'; import { setShowModals } from '../../../store/rootSlice'; -import { useDispatch, useSelector } from 'react-redux'; +import { RootService } from '../../../services/http.service'; +import { setConfig } from '../../../store/rootSlice'; +import { selectAppConfig } from '../../../store/rootSelectors'; import { selectIsAuthenticated, selectNodeInfo, selectServerConfig, selectShowModals, selectUIConfigUnit, selectWalletConnect } from '../../../store/rootSelectors'; +import { ApplicationConfiguration } from '../../../types/root.type'; const Settings = (props) => { const dispatch = useDispatch(); + const appConfig = useSelector(selectAppConfig); const isAuthenticated = useSelector(selectIsAuthenticated); const uiConfigUnit = useSelector(selectUIConfigUnit); const nodeInfo = useSelector(selectNodeInfo); @@ -22,6 +26,18 @@ const Settings = (props) => { const currentScreenSize = useBreakpoint(); logger.info('Screen Size Changed: ' + currentScreenSize); + const changeCurrencyUnitHandler = async(changedIndex: number) => { + const updatedConfig: ApplicationConfiguration = { + ...appConfig, + uiConfig: { + ...appConfig.uiConfig, + unit: CURRENCY_UNITS[changedIndex] + } + }; + await RootService.updateConfig(updatedConfig); + dispatch(setConfig(updatedConfig)); + }; + return ( @@ -40,7 +56,7 @@ const Settings = (props) => { } Fiat Currency - Currency + Currency ); From 324191fad1cf0e4a232763deefbcb0adf0c91551 Mon Sep 17 00:00:00 2001 From: ShahanaFarooqui Date: Thu, 12 Mar 2026 14:50:41 -0700 Subject: [PATCH 3/5] Add listsqlschema call --- apps/frontend/src/services/http.service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/frontend/src/services/http.service.ts b/apps/frontend/src/services/http.service.ts index 2a20a51b..e140e5ba 100644 --- a/apps/frontend/src/services/http.service.ts +++ b/apps/frontend/src/services/http.service.ts @@ -227,6 +227,10 @@ export class RootService { return HttpService.clnCall('sql', { query }); } + static async listSqlSchemas(table?: string) { + return HttpService.clnCall('listsqlschemas', { table }); + } + static async fetchAuthData() { const [ config, authStatus ] = await Promise.all([ this.getAppConfigurations(), From 8f52025cf7af8360a998b96252fb9a528fb43d75 Mon Sep 17 00:00:00 2001 From: ShahanaFarooqui Date: Thu, 12 Mar 2026 14:51:26 -0700 Subject: [PATCH 4/5] Display query results in Raw or Table view --- .../modals/SQLTerminal/SQLTerminal.scss | 117 ++++++- .../modals/SQLTerminal/SQLTerminal.tsx | 300 ++++++++++++++---- 2 files changed, 344 insertions(+), 73 deletions(-) diff --git a/apps/frontend/src/components/modals/SQLTerminal/SQLTerminal.scss b/apps/frontend/src/components/modals/SQLTerminal/SQLTerminal.scss index e213e983..40fe7011 100644 --- a/apps/frontend/src/components/modals/SQLTerminal/SQLTerminal.scss +++ b/apps/frontend/src/components/modals/SQLTerminal/SQLTerminal.scss @@ -14,29 +14,75 @@ } .terminal-output { - height: 47vh; + height: 40vh; overflow: hidden; width: 100%; resize: none; font-size: $font-size-base; white-space: pre-wrap; - padding: 1rem 0 1rem 2rem; + padding: 1rem; margin-bottom: 1.5rem; background-color: $body-bg-light; - border-radius: $border-radius; + border-radius: $border-radius-sm; border: $input-border-width solid $input-border-color; box-shadow: 0px 1px 2px rgba($dark-blue, 0.05); + + & .terminal-json-header { + padding: 1rem 1rem 0.75rem 1rem; + margin-bottom: 0.5rem; + display: flex; + align-items: flex-start; + justify-content: space-between; + } + + &.terminal-table-wrapper { + padding: 0; + display: flex; + flex-direction: column; + + .terminal-table-header { + padding: 0.75rem 1rem 0.25rem; + margin-bottom: 0.5rem; + display: flex; + align-items: flex-start; + justify-content: space-between; + } + + .ps { + flex: 1; + min-height: 0; + overflow: hidden !important; + &.ps--active-x > .ps__rail-x { + display: block !important; + background-color: transparent; + opacity: 1; + z-index: 99; + height: 0.5rem; + & .ps__thumb-x { + height: 0.25rem; + background-color: #DFB316; + } + } + } + } + + & .terminal-output-scroll-container { + height: 33vh; + overflow: hidden; + width: 100%; + resize: none; + white-space: pre-wrap; + background-color: transparent; + } } .btn-copy-output { - padding: 1rem; + padding: 0; border: none; cursor: pointer; - position: absolute; - right: 0; background: transparent; z-index: 1; - border-radius: $border-radius; + border-radius: $border-radius-sm; &:hover, &:focus-visible { outline: none; @@ -50,9 +96,64 @@ } } +.sql-table { + height: 100%; + min-width: 100%; + border-collapse: collapse; + + & .sql-table-head { + & .sql-table-header-row > th { + color: $primary; + border: 1px solid $border-color; + background-color: $body-bg; + padding: 0.5rem 0.75rem; + position: sticky; + top: 0; + z-index: 1; + } + } + + & .sql-table-body { + & .sql-table-row { + background-color: $card-bg; + + &:hover { + background-color: $body-bg-light; + } + + & > td { + border: 1px solid $border-color; + padding: 0.5rem 0.75rem; + } + } + } +} + @include color-mode(dark) { .terminal-output { background-color: $body-bg-dark; - border: $input-border-width solid $input-border-color; + } + + .sql-table { + & .sql-table-head { + & .sql-table-header-row > th { + background-color: $tooltip-bg-dark; + border: 1px solid $border-color-dark; + } + } + + & .sql-table-body { + & .sql-table-row { + background-color: $card-bg-dark; + + &:hover { + background-color: $tooltip-bg-dark; + } + + & > td { + border: 1px solid $border-color-dark; + } + } + } } } diff --git a/apps/frontend/src/components/modals/SQLTerminal/SQLTerminal.tsx b/apps/frontend/src/components/modals/SQLTerminal/SQLTerminal.tsx index 59abcfdf..e0d997f1 100644 --- a/apps/frontend/src/components/modals/SQLTerminal/SQLTerminal.tsx +++ b/apps/frontend/src/components/modals/SQLTerminal/SQLTerminal.tsx @@ -1,25 +1,78 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { ButtonGroup, Form, InputGroup, Modal } from 'react-bootstrap'; import './SQLTerminal.scss'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { ButtonGroup, Form, InputGroup, Modal, OverlayTrigger, Tooltip } from 'react-bootstrap'; import PerfectScrollbar from 'react-perfect-scrollbar'; +import { AnimatePresence, motion } from 'framer-motion'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCode, faTable } from '@fortawesome/free-solid-svg-icons'; import { CloseSVG } from '../../../svgs/Close'; import logger from '../../../services/logger.service'; import { SQLSVG } from '../../../svgs/SQL'; import { CopySVG } from '../../../svgs/Copy'; -import { copyTextToClipboard } from '../../../utilities/data-formatters'; +import { copyTextToClipboard, titleCase } from '../../../utilities/data-formatters'; import { RootService } from '../../../services/http.service'; import { setShowModals, setShowToast } from '../../../store/rootSlice'; import { useDispatch, useSelector } from 'react-redux'; import { selectShowModals } from '../../../store/rootSelectors'; +import ToggleSwitch from '../../shared/ToggleSwitch/ToggleSwitch'; +import { TRANSITION_DURATION } from '../../../utilities/constants'; + +const parseTableName = (sql: string): string | null => { + const match = sql.replace(/\s+/g, ' ').trim().match(/\bFROM\s+([`"\[]?[\w]+[`"\]]?)/i); + if (!match) return null; + return match[1].replace(/[`"[\]]/g, ''); +}; + +const parseColumnNames = (sql: string): string[] | null => { + const trimmed = sql.replace(/\s+/g, ' ').trim(); + const match = trimmed.match(/^SELECT\s+([\s\S]+?)\s+FROM\s+/i); + if (!match) return null; + + const colsPart = match[1].trim(); + if (colsPart === '*') return null; + + const cols: string[] = []; + let depth = 0; + let current = ''; + for (const ch of colsPart) { + if (ch === '(') depth++; + else if (ch === ')') depth--; + else if (ch === ',' && depth === 0) { + cols.push(current.trim()); + current = ''; + continue; + } + current += ch; + } + if (current.trim()) cols.push(current.trim()); + + return cols.map(col => { + const asMatch = col.match(/\bAS\s+([`"\[]?[\w]+[`"\]]?)\s*$/i); + if (asMatch) return asMatch[1].replace(/[`"[\]]/g, ''); + const tokens = col.split(/\s+/); + const last = tokens[tokens.length - 1].replace(/[`"[\]]/g, ''); + if (/^[\w]+$/.test(last)) return last; + return col; + }); +}; + +type ViewMode = 'Table' | 'JSON'; + +type OutputState = + | { type: 'empty' } + | { type: 'error'; message: string } + | { type: 'result'; columns: string[] | null; rows: any[][] }; const SQLTerminal = () => { const containerRef = useRef(null); const showModals = useSelector(selectShowModals); const dispatch = useDispatch(); - const outputRef = useRef(null); + const outputRef = useRef(null); const [executed, setExecuted] = useState(false); const [query, setQuery] = useState(''); - const [output, setOutput] = useState(''); + const [viewMode, setViewMode] = useState('Table'); + const [direction, setDirection] = useState(-1); + const [outputState, setOutputState] = useState({ type: 'empty' }); const scrollToBottom = () => { if (outputRef.current) { @@ -32,38 +85,53 @@ const SQLTerminal = () => { setQuery(e.target.value); }; - const handleCopy = e => { - let textToCopy = ''; - if (e.target.id === 'SQL Query') { - textToCopy = query; - } else if (outputRef.current) { - textToCopy = outputRef.current.innerText; - } + const handleCopy = (e) => { + const id = e.target.id; + + const textToCopy = + id === 'SQL Query' ? query + : id === 'Table Output' && outputState.type === 'error' ? outputState.message + : id === 'Table Output' && outputState.type === 'result' ? JSON.stringify([outputState.columns, ...outputState.rows], null, 2) + : outputRef.current?.innerText || ''; + copyTextToClipboard(textToCopy) - .then(() => { - dispatch(setShowToast({ - show: true, - message: e.target.id + ' Copied Successfully!', - bg: 'success', - })); - }) - .catch(err => { - logger.error(err); - }); + .then(() => dispatch(setShowToast({ show: true, message: `${id} Copied Successfully!`, bg: 'success' }))) + .catch(err => logger.error(err)); }; const handleExecute = useCallback(async () => { const formattedQuery = query.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim(); try { const result: any = await RootService.executeSql(formattedQuery); - setOutput(JSON.stringify(result.rows, null, 2) + '\n\n'); - setOutput(formattedQuery + '\n' + JSON.stringify(result.rows, null, 2) + '\n\n'); - } catch (error: any) { - if (error && error.message) { - setOutput(formattedQuery + '\nError: ' + error.message); - } else { - setOutput(formattedQuery + '\nError: ' + error); + const rows: any[][] = result.rows; + + if (!Array.isArray(rows) || rows.length === 0) { + setOutputState({ type: 'result', columns: null, rows: [] }); + return; + } + + let columns = parseColumnNames(formattedQuery); + + if (!columns) { + const tableName = parseTableName(formattedQuery); + if (tableName) { + try { + const schema: any = await RootService.listSqlSchemas(tableName); + const tableSchema = schema?.schemas?.[0]; + if (tableSchema?.columns && Array.isArray(tableSchema.columns)) { + columns = tableSchema.columns.map((col: any) => col.name ?? String(col)); + } + } catch (schemaError: any) { + logger.error('Failed to fetch SQL schema for table: ' + tableName, schemaError); + } + } } + setOutputState({ type: 'result', columns, rows }); + } catch (error: any) { + setOutputState({ + type: 'error', + message: error?.message ? error.message : String(error), + }); } }, [query]); @@ -73,11 +141,17 @@ const SQLTerminal = () => { const handleClear = () => { setQuery(''); - setOutput(''); + setOutputState({ type: 'empty' }); + setViewMode('Table'); + }; + + const handleViewModeChange = (changedIndex: number) => { + const next: ViewMode = changedIndex === 0 ? 'Table' : 'JSON'; + setDirection(next === 'Table' ? -1 : 1); + setViewMode(next); }; useEffect(() => { - // Check if the last character is a newline and the character before it is a semicolon if (!executed && query.endsWith('\n') && query.trimEnd().endsWith(';')) { setExecuted(true); handleExecute(); @@ -86,11 +160,118 @@ const SQLTerminal = () => { useEffect(() => { scrollToBottom(); - }, [output]); + }, [outputState]); const closeHandler = () => { + handleClear(); dispatch(setShowModals({ ...showModals, sqlTerminalModal: false })); - } + }; + + const getHeaders = (columns: string[] | null, firstRow: any[]): string[] => { + if (columns && columns.length === firstRow.length) return columns; + return firstRow.map((_, i) => `Col ${i}`); + }; + + const renderOutput = () => { + switch (outputState.type) { + case 'empty': + return ( +
+ Results will appear here… +
+ ); + + case 'error': + return ( +
+            Error: {outputState.message}
+          
+ ); + + case 'result': { + const { columns, rows } = outputState; + + if (rows.length === 0) { + return ( +
+ Query returned no rows. +
+ ); + } + let headers; + if (viewMode === 'Table') { + headers = getHeaders(columns, rows[0]); + } + + return ( + + + {viewMode === 'Table' ? ( +
+
+
+ {rows.length} row{rows.length !== 1 ? 's' : ''} +
+ +
+ +
+ + + + {headers.map((h, i) => ( + + ))} + + + + {rows.map((row, ri) => ( + + {(Array.isArray(row) ? row : [row]).map((cell, ci) => ( + + ))} + + ))} + +
{titleCase(h.replaceAll('_', ' '))}
+ {cell === null || cell === undefined + ? null + : String(cell)} +
+
+
+
+ ) : ( +
+
+
+ {rows.length} record{rows.length !== 1 ? 's' : ''} +
+ +
+
+                    
+                      {JSON.stringify(rows, null, 2)}
+                    
+                  
+
+ )} +
+
+ ); + } + } + }; return ( @@ -128,44 +309,33 @@ const SQLTerminal = () => { -
- -
-              
-                {output}
-              
-            
+
+ Table View}> + + , + JSON View}> + + , + ]} + selIndex={viewMode === 'Table' ? 0 : 1} + /> +
+
+ {renderOutput()}
- - - From 536fb1ef364508dbcb178fdccdf0316a1321021e Mon Sep 17 00:00:00 2001 From: ShahanaFarooqui Date: Fri, 13 Mar 2026 14:20:03 -0700 Subject: [PATCH 5/5] Fixed SQL Terminal Tests --- .../modals/SQLTerminal/SQLTerminal.test.tsx | 74 +++++++++++++------ 1 file changed, 51 insertions(+), 23 deletions(-) diff --git a/apps/frontend/src/components/modals/SQLTerminal/SQLTerminal.test.tsx b/apps/frontend/src/components/modals/SQLTerminal/SQLTerminal.test.tsx index 2d1c49c3..08e1ea73 100644 --- a/apps/frontend/src/components/modals/SQLTerminal/SQLTerminal.test.tsx +++ b/apps/frontend/src/components/modals/SQLTerminal/SQLTerminal.test.tsx @@ -16,6 +16,7 @@ describe('SQLTerminal', () => { cln: mockCLNStoreData, bkpr: mockBKPRStoreData }; + it('should be in the document', async () => { await renderWithProviders(, { preloadedState: customMockStore }); expect(screen.getByTestId('terminal-container')).not.toBeEmptyDOMElement(); @@ -23,14 +24,12 @@ describe('SQLTerminal', () => { it('should render the terminal container', async () => { await renderWithProviders(, { preloadedState: customMockStore }); - const terminalContainer = screen.getByTestId('terminal-container'); - expect(terminalContainer).toBeInTheDocument(); + expect(screen.getByTestId('terminal-container')).toBeInTheDocument(); }); it('should display initial placeholder in the input field', async () => { await renderWithProviders(, { preloadedState: customMockStore }); - const inputField = screen.getByTestId('query-input'); - expect(inputField).toBeInTheDocument(); + expect(screen.getByTestId('query-input')).toBeInTheDocument(); }); it('should update query state on input change', async () => { @@ -40,16 +39,29 @@ describe('SQLTerminal', () => { expect(inputField).toHaveValue('select * from bkpr_accountevents'); }); - it('should call executeSql and display result in the output area', async () => { + it('should call executeSql and display result as table by default', async () => { spyOnExecuteSql(); await renderWithProviders(, { preloadedState: customMockStore }); const inputField = screen.getByTestId('query-input'); - const executeButton = screen.getByText('Execute'); fireEvent.change(inputField, { target: { value: 'select * from bkpr_accountevents' } }); - fireEvent.click(executeButton); + fireEvent.click(screen.getByText('Execute')); + await waitFor(() => { + expect(screen.getByTestId('terminal-container').querySelector('.sql-table')).toBeInTheDocument(); + }); + }); + + it('should display result as JSON when switched to JSON view', async () => { + spyOnExecuteSql(); + await renderWithProviders(, { preloadedState: customMockStore }); + const inputField = screen.getByTestId('query-input'); + fireEvent.change(inputField, { target: { value: 'select * from bkpr_accountevents' } }); + fireEvent.click(screen.getByText('Execute')); + await waitFor(() => { + expect(screen.getByTestId('terminal-container').querySelector('.sql-table')).toBeInTheDocument(); + }); + fireEvent.click(screen.getByTestId('toggle-switch')); await waitFor(() => { const output = screen.getByTestId('terminal-container').textContent; - expect(output).toContain('select * from bkpr_accountevents'); expect(output).toContain(JSON.stringify(mockSQLResponse.rows, null, 2)); }); }); @@ -58,9 +70,8 @@ describe('SQLTerminal', () => { spyOnExecuteSql(); await renderWithProviders(, { preloadedState: customMockStore }); const inputField = screen.getByTestId('query-input'); - const executeButton = screen.getByText('Execute'); fireEvent.change(inputField, { target: { value: 'select * from non_existing_table' } }); - fireEvent.click(executeButton); + fireEvent.click(screen.getByText('Execute')); await waitFor(() => { const output = screen.getByTestId('terminal-container').textContent; expect(output).not.toContain(JSON.stringify(mockSQLResponse.rows, null, 2)); @@ -68,29 +79,46 @@ describe('SQLTerminal', () => { }); it('should open the help link when Help button is clicked', async () => { - await renderWithProviders(, { preloadedState: customMockStore }); - const helpButton = screen.getByText('Help'); + await renderWithProviders(, { preloadedState: customMockStore }); const windowOpenSpy = jest.spyOn(window, 'open').mockImplementation(() => null); - fireEvent.click(helpButton); + fireEvent.click(screen.getByText('Help')); expect(windowOpenSpy).toHaveBeenCalledWith('https://docs.corelightning.org/reference/sql', '_blank'); }); - it('should clear the output when Clear button is clicked', async () => { + it('should clear the query and output when Clear button is clicked', async () => { spyOnExecuteSql(); await renderWithProviders(, { preloadedState: customMockStore }); const inputField = screen.getByTestId('query-input'); - const executeButton = screen.getByText('Execute'); - const clearButton = screen.getByText('Clear'); fireEvent.change(inputField, { target: { value: 'select * from bkpr_accountevents' } }); - fireEvent.click(executeButton); + fireEvent.click(screen.getByText('Execute')); await waitFor(() => { - const output = screen.getByTestId('terminal-container').textContent; - expect(output).toContain('select * from bkpr_accountevents'); - expect(output).toContain(JSON.stringify(mockSQLResponse.rows, null, 2)); + expect(screen.getByTestId('terminal-container').querySelector('.sql-table')).toBeInTheDocument(); }); - fireEvent.click(clearButton); + fireEvent.click(screen.getByText('Clear')); + await waitFor(() => { + expect(screen.getByTestId('query-input')).toHaveValue(''); + expect(screen.getByTestId('terminal-container').querySelector('.sql-table')).not.toBeInTheDocument(); + }); + }); - const outputAfterClear = screen.getByTestId('terminal-container').textContent; - expect(outputAfterClear).not.toContain('select * from bkpr_accountevents'); + it('should reset to Table view when Clear button is clicked', async () => { + spyOnExecuteSql(); + await renderWithProviders(, { preloadedState: customMockStore }); + const inputField = screen.getByTestId('query-input'); + fireEvent.change(inputField, { target: { value: 'select * from bkpr_accountevents' } }); + fireEvent.click(screen.getByText('Execute')); + await waitFor(() => { + expect(screen.getByTestId('terminal-container').querySelector('.sql-table')).toBeInTheDocument(); + }); + fireEvent.click(screen.getByTestId('toggle-switch')); + await waitFor(() => { + expect(screen.getByTestId('terminal-container').querySelector('.sql-table')).not.toBeInTheDocument(); + }); + fireEvent.click(screen.getByText('Clear')); + fireEvent.change(inputField, { target: { value: 'select * from bkpr_accountevents' } }); + fireEvent.click(screen.getByText('Execute')); + await waitFor(() => { + expect(screen.getByTestId('terminal-container').querySelector('.sql-table')).toBeInTheDocument(); + }); }); });