Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ import {
loadWBHistory,
processWBCommand,
sendWBCommandAction,
sendWbQueryAction,
workbenchResultsSelector,
} from 'uiSrc/slices/workbench/wb-results'
import { useDatabaseEnvironment } from 'uiSrc/components/hooks/useDatabaseEnvironment'
import { Environment } from 'apiClient'
import { DBInstanceFactory } from 'uiSrc/mocks/factories/database/DBInstance.factory'
import { ConnectionType } from 'uiSrc/slices/interfaces'

import WBViewWrapper from './WBViewWrapper'

Expand All @@ -36,15 +41,20 @@ jest.mock('uiSrc/pages/workbench/components/query', () => ({
default: jest.fn(),
}))

const QueryWrapperMock = (props: QueryProps) => (
<div
onKeyDown={(e: any) => props.onKeyDown?.(e, 'get')}
data-testid="query"
aria-label="query"
role="textbox"
tabIndex={0}
/>
)
let capturedOnSubmit: QueryProps['onSubmit'] | null = null

const QueryWrapperMock = (props: QueryProps) => {
capturedOnSubmit = props.onSubmit
return (
<div
onKeyDown={(e: any) => props.onKeyDown?.(e, 'get')}
data-testid="query"
aria-label="query"
role="textbox"
tabIndex={0}
/>
)
}

jest.mock('uiSrc/services', () => ({
...jest.requireActual('uiSrc/services'),
Expand All @@ -71,6 +81,10 @@ jest.mock('uiSrc/slices/app/plugins', () => ({

jest.mock('uiSrc/slices/workbench/wb-results', () => ({
...jest.requireActual('uiSrc/slices/workbench/wb-results'),
sendWbQueryAction: jest.fn(() => ({ type: '__test_sendWbQueryAction__' })),
sendWBCommandAction: jest.fn(() => ({
type: '__test_sendWBCommandAction__',
})),
sendWBCommandClusterAction: jest.fn(),
processUnsupportedCommand: jest.fn(),
updateCliCommandHistory: jest.fn,
Expand All @@ -80,6 +94,13 @@ jest.mock('uiSrc/slices/workbench/wb-results', () => ({
}),
}))

jest.mock('uiSrc/components/hooks/useDatabaseEnvironment', () => ({
useDatabaseEnvironment: jest.fn().mockReturnValue({
environment: 'unspecified',
isDangerousCommand: () => false,
}),
}))

jest.mock('uiSrc/slices/workbench/wb-tutorials', () => {
const defaultState = jest.requireActual(
'uiSrc/slices/workbench/wb-tutorials',
Expand All @@ -92,6 +113,10 @@ jest.mock('uiSrc/slices/workbench/wb-tutorials', () => {
}
})

const sendWbQueryActionMock = sendWbQueryAction as jest.Mock
const useDatabaseEnvironmentMock = useDatabaseEnvironment as jest.Mock
const connectedInstanceSelectorMock = connectedInstanceSelector as jest.Mock

describe('WBViewWrapper', () => {
beforeAll(() => {
QueryWrapper.mockImplementation(QueryWrapperMock)
Expand Down Expand Up @@ -153,6 +178,151 @@ describe('WBViewWrapper', () => {
expect(screen.queryByTestId('clear-history-btn')).not.toBeInTheDocument()
})

describe('dangerous-command gating', () => {
const instance = DBInstanceFactory.build({
name: 'prod-db',
connectionType: ConnectionType.Standalone,
})

beforeEach(() => {
capturedOnSubmit = null
sendWbQueryActionMock.mockClear()
connectedInstanceSelectorMock.mockImplementation(() => instance)
})

afterEach(() => {
useDatabaseEnvironmentMock.mockReturnValue({
environment: 'unspecified',
isDangerousCommand: () => false,
})
})

it('does not show the modal and dispatches when no command is dangerous', () => {
useDatabaseEnvironmentMock.mockReturnValue({
environment: Environment.Production,
isDangerousCommand: () => false,
})

render(<WBViewWrapper />)
act(() => {
capturedOnSubmit?.('PING')
})

expect(
screen.queryByTestId('type-to-confirm-modal-title'),
).not.toBeInTheDocument()
expect(sendWbQueryAction).toHaveBeenCalledTimes(1)
expect(sendWbQueryActionMock.mock.calls[0][0]).toBe('PING')
})

it('shows the modal and defers dispatch when a command is dangerous', () => {
useDatabaseEnvironmentMock.mockReturnValue({
environment: Environment.Production,
isDangerousCommand: (cmd: string) =>
['FLUSHALL', 'FLUSHDB'].includes(cmd.toUpperCase()),
})

render(<WBViewWrapper />)
act(() => {
capturedOnSubmit?.('PING\nFLUSHALL\nFLUSHDB')
})

expect(
screen.getByTestId('type-to-confirm-modal-title'),
).toBeInTheDocument()
const description = screen.getByTestId(
'type-to-confirm-modal-description',
)
expect(description).toHaveTextContent(instance.name!)
expect(description).toHaveTextContent('FLUSHALL, FLUSHDB')
expect(sendWbQueryAction).not.toHaveBeenCalled()
})

it('gates a multi-line dangerous command (Monaco continuation)', () => {
useDatabaseEnvironmentMock.mockReturnValue({
environment: Environment.Production,
isDangerousCommand: (cmd: string) => cmd.toUpperCase() === 'FLUSHALL',
})

render(<WBViewWrapper />)
// Monaco joins continuation lines (the leading whitespace on the next
// line is preserved), so the verb arrives with an embedded newline.
act(() => {
capturedOnSubmit?.('FLUSHALL\n ASYNC')
})

expect(
screen.getByTestId('type-to-confirm-modal-title'),
).toBeInTheDocument()
expect(sendWbQueryAction).not.toHaveBeenCalled()
})

it('falls back to host:port when the connected instance has no name', () => {
const namelessInstance = DBInstanceFactory.build({
name: undefined,
host: 'h',
port: 6379,
connectionType: ConnectionType.Standalone,
})
connectedInstanceSelectorMock.mockImplementation(() => namelessInstance)
useDatabaseEnvironmentMock.mockReturnValue({
environment: Environment.Production,
isDangerousCommand: (cmd: string) => cmd.toUpperCase() === 'FLUSHALL',
})

render(<WBViewWrapper />)
act(() => {
capturedOnSubmit?.('FLUSHALL')
})

expect(
screen.getByTestId('type-to-confirm-modal-description'),
).toHaveTextContent('h:6379')
})

it('dispatches the original batch after typing the database name and confirming', () => {
useDatabaseEnvironmentMock.mockReturnValue({
environment: Environment.Production,
isDangerousCommand: (cmd: string) => cmd.toUpperCase() === 'FLUSHALL',
})

render(<WBViewWrapper />)
act(() => {
capturedOnSubmit?.('PING\nFLUSHALL')
})

fireEvent.change(screen.getByTestId('type-to-confirm-modal-input'), {
target: { value: instance.name },
})
fireEvent.click(screen.getByTestId('type-to-confirm-modal-confirm-btn'))

expect(
screen.queryByTestId('type-to-confirm-modal-title'),
).not.toBeInTheDocument()
expect(sendWbQueryAction).toHaveBeenCalledTimes(1)
expect(sendWbQueryActionMock.mock.calls[0][0]).toBe('PING\nFLUSHALL')
})

it('does not dispatch when the modal is cancelled', () => {
useDatabaseEnvironmentMock.mockReturnValue({
environment: Environment.Production,
isDangerousCommand: (cmd: string) => cmd.toUpperCase() === 'FLUSHALL',
})

render(<WBViewWrapper />)
act(() => {
capturedOnSubmit?.('FLUSHALL')
})

fireEvent.click(screen.getByTestId('type-to-confirm-modal-cancel-btn'))

expect(
screen.queryByTestId('type-to-confirm-modal-title'),
).not.toBeInTheDocument()
expect(sendWbQueryAction).not.toHaveBeenCalled()
})
})

it.skip('"onSubmit" for Cluster connection should call "sendWBCommandClusterAction"', async () => {
;(connectedInstanceSelector as jest.Mock).mockImplementation(() => ({
id: '123',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,18 @@ import {
changeSidePanel,
} from 'uiSrc/slices/panels/sidePanels'
import { InsightsPanelTabs, SidePanels } from 'uiSrc/slices/interfaces/insights'
import { useDatabaseEnvironment } from 'uiSrc/components/hooks/useDatabaseEnvironment'
import TypeToConfirmModal from 'uiSrc/components/type-to-confirm-modal'
import { getCommandsForExecution } from 'uiSrc/utils/monaco/monacoUtils'
import WBView from './WBView'

interface PendingSubmit {
value: string
commandId?: Nullable<string>
executeParams: CodeButtonParams
dangerousCommands: string[]
}

interface IState {
loading: boolean
instance: Instance
Expand Down Expand Up @@ -91,8 +101,11 @@ const WBViewWrapper = () => {
const [script, setScript] = useState(scriptContext)
const [scriptEl, setScriptEl] =
useState<Nullable<monacoEditor.editor.IStandaloneCodeEditor>>(null)
const [pendingSubmit, setPendingSubmit] =
useState<Nullable<PendingSubmit>>(null)

const instance = useSelector(connectedInstanceSelector)
const { isDangerousCommand } = useDatabaseEnvironment()
const { visualizations = [] } = useSelector(appPluginsSelector)
state = {
scriptEl,
Expand Down Expand Up @@ -235,13 +248,11 @@ const WBViewWrapper = () => {
setScript('')
}

const sourceValueSubmit = (
value: string = script,
commandId?: Nullable<string>,
executeParams: CodeButtonParams = { clearEditor: true },
const runSubmission = (
value: string,
commandId: Maybe<Nullable<string>>,
executeParams: CodeButtonParams,
) => {
if (state.loading || (!value && !script)) return

const lines = getMonacoLines(value)
const parsedParams: Maybe<CodeButtonParams> = getParsedParamsInQuery(value)

Expand All @@ -253,25 +264,83 @@ const WBViewWrapper = () => {
}
}

const sourceValueSubmit = (
value: string = script,
commandId?: Nullable<string>,
executeParams: CodeButtonParams = { clearEditor: true },
) => {
if (state.loading || (!value && !script)) return

const effectiveValue = value || script
const commands = getCommandsForExecution(effectiveValue)
const dangerousCommands = commands.filter((cmd) =>
isDangerousCommand(cmd.split(/\s+/)[0]),
)
Comment thread
cursor[bot] marked this conversation as resolved.
if (dangerousCommands.length > 0) {
setPendingSubmit({
value: effectiveValue,
commandId,
executeParams,
dangerousCommands,
})
return
}

runSubmission(effectiveValue, commandId, executeParams)
}

const handleConfirmPendingSubmit = () => {
if (!pendingSubmit) return
const submission = pendingSubmit
setPendingSubmit(null)
runSubmission(
submission.value,
submission.commandId,
submission.executeParams,
)
}

const confirmationText =
instance?.name || `${instance?.host}:${instance?.port}`

return (
<WBView
items={items}
clearing={clearing}
processing={processing}
isResultsLoaded={isLoaded}
script={script}
setScript={setScript}
setScriptEl={setScriptEl}
scrollDivRef={scrollDivRef}
activeMode={activeRunQueryMode}
onSubmit={sourceValueSubmit}
onQueryOpen={handleQueryOpen}
onQueryDelete={handleQueryDelete}
onAllQueriesDelete={handleAllQueriesDelete}
onQueryChangeMode={handleChangeQueryRunMode}
resultsMode={resultsMode}
onChangeGroupMode={handleChangeGroupMode}
/>
<>
<WBView
items={items}
clearing={clearing}
processing={processing}
isResultsLoaded={isLoaded}
script={script}
setScript={setScript}
setScriptEl={setScriptEl}
scrollDivRef={scrollDivRef}
activeMode={activeRunQueryMode}
onSubmit={sourceValueSubmit}
onQueryOpen={handleQueryOpen}
onQueryDelete={handleQueryDelete}
onAllQueriesDelete={handleAllQueriesDelete}
onQueryChangeMode={handleChangeQueryRunMode}
resultsMode={resultsMode}
onChangeGroupMode={handleChangeGroupMode}
/>
{pendingSubmit && (
<TypeToConfirmModal
title="Run dangerous commands?"
confirmationText={confirmationText}
actionDescription={
<>
You&apos;re about to run{' '}
<strong>{pendingSubmit.dangerousCommands.join(', ')}</strong>{' '}
against the production database{' '}
<strong>{confirmationText}</strong>.
</>
}
confirmButtonText="Run command"
onConfirm={handleConfirmPendingSubmit}
onCancel={() => setPendingSubmit(null)}
/>
)}
</>
)
}

Expand Down
Loading