From 439c91fd7a56ad5ad3e0f51818ab5c58da29ef8d Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 1 Aug 2024 13:52:54 +0300 Subject: [PATCH] feat: release query settings dialog --- .github/workflows/ci.yml | 93 +++++++++++- playwright.config.ts | 9 ++ .../QueryExecutionStatus.tsx | 4 +- .../Query/ExecuteResult/ExecuteResult.tsx | 6 +- .../Query/ExplainResult/ExplainResult.tsx | 7 +- .../Query/QueriesHistory/QueriesHistory.tsx | 25 +-- .../Tenant/Query/QueryEditor/QueryEditor.tsx | 104 ++++++------- .../QueryEditorControls.tsx | 100 +----------- .../QuerySettingsDialog.tsx | 52 ++++--- src/containers/UserSettings/i18n/en.json | 4 +- src/containers/UserSettings/settings.tsx | 12 +- src/services/settings.ts | 6 +- src/store/reducers/executeQuery.ts | 37 ++--- .../reducers/explainQuery/explainQuery.ts | 12 +- src/utils/constants.ts | 6 +- src/utils/hooks/useQueryExecutionSettings.ts | 19 ++- .../suites/tenant/queryEditor/QueryEditor.ts | 143 ++++++++++++++---- .../tenant/queryEditor/queryEditor.test.ts | 96 +++++++++++- 18 files changed, 453 insertions(+), 282 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7152493b..ceb4bede8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,10 @@ jobs: e2e_tests: name: Playwright Tests runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + pages: write services: backend: @@ -58,8 +62,95 @@ jobs: - name: Install dependencies run: npm ci - - name: Install Plawright deps + - name: Install Playwright deps run: npm run test:e2e:install - name: Run Playwright tests run: npm run test:e2e + env: + CI: true + PLAYWRIGHT_VIDEO: 'on' + + - name: Upload Playwright artifacts + if: always() + uses: actions/upload-artifact@v3 + with: + name: playwright-artifacts + path: playwright-artifacts + retention-days: 30 + + - name: Setup Pages + if: always() + uses: actions/configure-pages@v3 + + - name: Deploy report to GitHub Pages + if: always() + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./playwright-artifacts/playwright-report + destination_dir: ${{ github.event.pull_request.number }} + + - name: Get test results + if: always() + id: test-results + run: | + TOTAL=$(grep -oP 'total="\K[^"]+' playwright-artifacts/playwright-report/index.html || echo "0") + PASSED=$(grep -oP 'passed="\K[^"]+' playwright-artifacts/playwright-report/index.html || echo "0") + FAILED=$(grep -oP 'failed="\K[^"]+' playwright-artifacts/playwright-report/index.html || echo "0") + SKIPPED=$(grep -oP 'skipped="\K[^"]+' playwright-artifacts/playwright-report/index.html || echo "0") + echo "total=$TOTAL" >> $GITHUB_OUTPUT + echo "passed=$PASSED" >> $GITHUB_OUTPUT + echo "failed=$FAILED" >> $GITHUB_OUTPUT + echo "skipped=$SKIPPED" >> $GITHUB_OUTPUT + + - name: Comment PR + if: always() + uses: actions/github-script@v6 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const reportUrl = `https://${context.repo.owner}.github.io/${context.repo.repo}/${context.issue.number}/`; + const testResults = { + total: ${{ steps.test-results.outputs.total }}, + passed: ${{ steps.test-results.outputs.passed }}, + failed: ${{ steps.test-results.outputs.failed }}, + skipped: ${{ steps.test-results.outputs.skipped }} + }; + const status = testResults.failed > 0 ? '❌ FAILED' : '✅ PASSED'; + const statusColor = testResults.failed > 0 ? 'red' : 'green'; + + const comment = `## Playwright Test Results + + **Status**: ${status} + + | Total | Passed | Failed | Skipped | + |-------|--------|--------|---------| + | ${testResults.total} | ${testResults.passed} | ${testResults.failed} | ${testResults.skipped} | + + ### 📊 Test Report + + For detailed results, please check the [Playwright Report](${reportUrl}) + + ### đŸŽĨ Test Recordings + + Video recordings of failed tests (if any) are available in the report. + + --- + +
+ ℹī¸ How to use this report + + 1. Click on the "Playwright Report" link above to view the full test results. + 2. In the report, you can see a breakdown of all tests, including any failures. + 3. For failed tests, you can view screenshots and video recordings to help debug the issues. + 4. Use the filters in the report to focus on specific test statuses or suites. + +
`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }) diff --git a/playwright.config.ts b/playwright.config.ts index f47feb143..0e6d9fb86 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,6 +6,12 @@ const baseUrl = process.env.PLAYWRIGHT_BASE_URL; const config: PlaywrightTestConfig = { testDir: 'tests/suites', timeout: 2 * 60 * 1000, + outputDir: './playwright-artifacts/test-results', + reporter: [ + ['html', {outputFolder: './playwright-artifacts/playwright-report'}], + ['json', {outputFile: './playwright-artifacts/test-results.json'}], + ], + // If there is no url provided, playwright starts webServer with the app in dev mode webServer: baseUrl ? undefined @@ -16,6 +22,9 @@ const config: PlaywrightTestConfig = { use: { baseURL: baseUrl || 'http://localhost:3000/', testIdAttribute: 'data-qa', + trace: 'on-first-retry', + video: process.env.PLAYWRIGHT_VIDEO === 'on' ? 'on' : 'off', + screenshot: 'only-on-failure', }, projects: [ { diff --git a/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx b/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx index f6a202ccb..7471c5f34 100644 --- a/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx +++ b/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx @@ -5,7 +5,6 @@ import {Icon, Tooltip} from '@gravity-ui/uikit'; import {isAxiosError} from 'axios'; import i18n from '../../containers/Tenant/Query/i18n'; -import {QUERY_SETTINGS, useSetting} from '../../lib'; import {cn} from '../../utils/cn'; import {useChangedQuerySettings} from '../../utils/hooks/useChangedQuerySettings'; import QuerySettingsDescription from '../QuerySettingsDescription/QuerySettingsDescription'; @@ -20,10 +19,9 @@ interface QueryExecutionStatusProps { } const QuerySettingsIndicator = () => { - const [useQuerySettings] = useSetting(QUERY_SETTINGS); const {isIndicatorShown, changedLastExecutionSettingsDescriptions} = useChangedQuerySettings(); - if (!isIndicatorShown || !useQuerySettings) { + if (!isIndicatorShown) { return null; } diff --git a/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx b/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx index f977cac0c..0125f830b 100644 --- a/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx +++ b/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx @@ -11,14 +11,13 @@ import Fullscreen from '../../../../components/Fullscreen/Fullscreen'; import {YDBGraph} from '../../../../components/Graph/Graph'; import {QueryExecutionStatus} from '../../../../components/QueryExecutionStatus'; import {QueryResultTable} from '../../../../components/QueryResultTable/QueryResultTable'; -import {QUERY_SETTINGS} from '../../../../lib'; import {disableFullscreen} from '../../../../store/reducers/fullscreen'; import type {ColumnType, KeyValueRow} from '../../../../types/api/query'; import type {ValueOf} from '../../../../types/common'; import type {IQueryResult} from '../../../../types/store/query'; import {getArray} from '../../../../utils'; import {cn} from '../../../../utils/cn'; -import {useSetting, useTypedDispatch} from '../../../../utils/hooks'; +import {useTypedDispatch} from '../../../../utils/hooks'; import {parseQueryError} from '../../../../utils/query'; import {PaneVisibilityToggleButtons} from '../../utils/paneVisibilityToggleHelpers'; import {SimplifiedPlan} from '../ExplainResult/components/SimplifiedPlan/SimplifiedPlan'; @@ -63,7 +62,6 @@ export function ExecuteResult({ const [selectedResultSet, setSelectedResultSet] = React.useState(0); const [activeSection, setActiveSection] = React.useState(resultOptionsIds.result); const dispatch = useTypedDispatch(); - const [useQuerySettings] = useSetting(QUERY_SETTINGS); const stats = data?.stats; const resultsSetsCount = data?.resultSets?.length; @@ -237,7 +235,7 @@ export function ExecuteResult({ /> - {useQuerySettings && } + {renderResultSection()} ); diff --git a/src/containers/Tenant/Query/ExplainResult/ExplainResult.tsx b/src/containers/Tenant/Query/ExplainResult/ExplainResult.tsx index 015279884..7a8dff67d 100644 --- a/src/containers/Tenant/Query/ExplainResult/ExplainResult.tsx +++ b/src/containers/Tenant/Query/ExplainResult/ExplainResult.tsx @@ -7,12 +7,11 @@ import EnableFullscreenButton from '../../../../components/EnableFullscreenButto import Fullscreen from '../../../../components/Fullscreen/Fullscreen'; import {LoaderWrapper} from '../../../../components/LoaderWrapper/LoaderWrapper'; import {QueryExecutionStatus} from '../../../../components/QueryExecutionStatus'; -import {QUERY_SETTINGS} from '../../../../lib'; import type {PreparedExplainResponse} from '../../../../store/reducers/explainQuery/types'; import {disableFullscreen} from '../../../../store/reducers/fullscreen'; import type {ValueOf} from '../../../../types/common'; import {cn} from '../../../../utils/cn'; -import {useSetting, useTypedDispatch} from '../../../../utils/hooks'; +import {useTypedDispatch} from '../../../../utils/hooks'; import {parseQueryErrorToString} from '../../../../utils/query'; import {PaneVisibilityToggleButtons} from '../../utils/paneVisibilityToggleHelpers'; import {QuerySettingsBanner} from '../QuerySettingsBanner/QuerySettingsBanner'; @@ -82,8 +81,6 @@ export function ExplainResult({ ); const [isPending, startTransition] = React.useTransition(); - const [useQuerySettings] = useSetting(QUERY_SETTINGS); - React.useEffect(() => { return () => { dispatch(disableFullscreen()); @@ -167,7 +164,7 @@ export function ExplainResult({ )} - {useQuerySettings && } + {renderContent()} diff --git a/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx b/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx index 295dbf010..1f5e600ea 100644 --- a/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx +++ b/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx @@ -7,12 +7,7 @@ import {TENANT_QUERY_TABS_ID} from '../../../../store/reducers/tenant/constants' import {setQueryTab} from '../../../../store/reducers/tenant/tenant'; import type {QueryInHistory} from '../../../../types/store/executeQuery'; import {cn} from '../../../../utils/cn'; -import { - useQueryExecutionSettings, - useTypedDispatch, - useTypedSelector, -} from '../../../../utils/hooks'; -import {QUERY_MODES, QUERY_SYNTAX} from '../../../../utils/query'; +import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; import {MAX_QUERY_HEIGHT, QUERY_TABLE_SETTINGS} from '../../utils/constants'; import i18n from '../i18n'; @@ -29,19 +24,10 @@ interface QueriesHistoryProps { function QueriesHistory({changeUserInput}: QueriesHistoryProps) { const dispatch = useTypedDispatch(); - const [settings, setQuerySettings] = useQueryExecutionSettings(); - const queriesHistory = useTypedSelector(selectQueriesHistory); const reversedHistory = [...queriesHistory].reverse(); const onQueryClick = (query: QueryInHistory) => { - if (query.syntax === QUERY_SYNTAX.pg && settings.queryMode !== QUERY_MODES.pg) { - setQuerySettings({...settings, queryMode: QUERY_MODES.pg}); - } else if (query.syntax !== QUERY_SYNTAX.pg && settings.queryMode === QUERY_MODES.pg) { - // Set query mode for queries with yql syntax - setQuerySettings({...settings, queryMode: QUERY_MODES.script}); - } - changeUserInput({input: query.queryText}); dispatch(setQueryTab(TENANT_QUERY_TABS_ID.newQuery)); }; @@ -60,15 +46,6 @@ function QueriesHistory({changeUserInput}: QueriesHistoryProps) { sortable: false, width: 600, }, - { - name: 'syntax', - header: 'Syntax', - render: ({row}) => { - return row.syntax === QUERY_SYNTAX.pg ? 'PostgreSQL' : 'YQL'; - }, - sortable: false, - width: 200, - }, ]; return ( diff --git a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx index 84d619261..ecbad65de 100644 --- a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx +++ b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx @@ -22,20 +22,20 @@ import {setShowPreview} from '../../../../store/reducers/schema/schema'; import type {EPathType} from '../../../../types/api/schema'; import type {ValueOf} from '../../../../types/common'; import type {ExecuteQueryState} from '../../../../types/store/executeQuery'; -import type {IQueryResult, QueryAction, QuerySettings} from '../../../../types/store/query'; +import type {IQueryResult, QueryAction} from '../../../../types/store/query'; import {cn} from '../../../../utils/cn'; import { DEFAULT_IS_QUERY_RESULT_COLLAPSED, DEFAULT_SIZE_RESULT_PANE_KEY, LAST_USED_QUERY_ACTION_KEY, - QUERY_SETTINGS, QUERY_USE_MULTI_SCHEMA_KEY, + TRACING_LEVEL_VERBOSITY_KEY, } from '../../../../utils/constants'; import {useQueryExecutionSettings, useSetting} from '../../../../utils/hooks'; import {useChangedQuerySettings} from '../../../../utils/hooks/useChangedQuerySettings'; import {useLastQueryExecutionSettings} from '../../../../utils/hooks/useLastQueryExecutionSettings'; import {YQL_LANGUAGE_ID} from '../../../../utils/monaco/constats'; -import {QUERY_ACTIONS, STATISTICS_MODES} from '../../../../utils/query'; +import {QUERY_ACTIONS} from '../../../../utils/query'; import type {InitialPaneState} from '../../utils/paneVisibilityToggleHelpers'; import { PaneVisibilityActionTypes, @@ -106,9 +106,9 @@ function QueryEditor(props: QueryEditorProps) { const {tenantPath: savedPath} = executeQuery; const [resultType, setResultType] = React.useState(RESULT_TYPES.EXECUTE); - const [querySettingsFlag] = useSetting(QUERY_SETTINGS); const [isResultLoaded, setIsResultLoaded] = React.useState(false); - const [querySettings, setQuerySettings] = useQueryExecutionSettings(); + const [querySettings] = useQueryExecutionSettings(); + const [tracingLevelVerbosity] = useSetting(TRACING_LEVEL_VERBOSITY_KEY); const [lastQueryExecutionSettings, setLastQueryExecutionSettings] = useLastQueryExecutionSettings(); const {resetBanner} = useChangedQuerySettings(); @@ -193,32 +193,26 @@ function QueryEditor(props: QueryEditorProps) { }, [executeQuery]); const handleSendExecuteClick = React.useCallback( - (settings: QuerySettings, text?: string) => { + (text?: string) => { const {input, history} = executeQuery; const schema = useMultiSchema ? 'multi' : 'modern'; - const query = text ?? input; + const query = text && typeof text === 'string' ? text : input; setLastUsedQueryAction(QUERY_ACTIONS.execute); - if (!isEqual(lastQueryExecutionSettings, settings)) { + if (!isEqual(lastQueryExecutionSettings, querySettings)) { resetBanner(); - setLastQueryExecutionSettings(settings); + setLastQueryExecutionSettings(querySettings); } - const executeQuerySettings = querySettingsFlag - ? querySettings - : { - queryMode: querySettings.queryMode, - statisticsMode: STATISTICS_MODES.full, - }; - setResultType(RESULT_TYPES.EXECUTE); sendExecuteQuery({ query, database: tenantName, - querySettings: executeQuerySettings, + querySettings, schema, + tracingLevelVerbosity, }); setIsResultLoaded(true); setShowPreview(false); @@ -227,17 +221,17 @@ function QueryEditor(props: QueryEditorProps) { if (!text) { const {queries, currentIndex} = history; if (query !== queries[currentIndex]?.queryText) { - saveQueryToHistory(input, querySettings.queryMode); + saveQueryToHistory(input); } } dispatchResultVisibilityState(PaneVisibilityActionTypes.triggerExpand); }, [ executeQuery, + tracingLevelVerbosity, useMultiSchema, setLastUsedQueryAction, lastQueryExecutionSettings, - querySettingsFlag, querySettings, sendExecuteQuery, saveQueryToHistory, @@ -253,46 +247,38 @@ function QueryEditor(props: QueryEditorProps) { props.setShowPreview(false); }; - const handleGetExplainQueryClick = React.useCallback( - (settings: QuerySettings) => { - const {input} = executeQuery; - - setLastUsedQueryAction(QUERY_ACTIONS.explain); + const handleGetExplainQueryClick = React.useCallback(() => { + const {input} = executeQuery; - if (!isEqual(lastQueryExecutionSettings, settings)) { - resetBanner(); - setLastQueryExecutionSettings(settings); - } + setLastUsedQueryAction(QUERY_ACTIONS.explain); - const explainQuerySettings = querySettingsFlag - ? querySettings - : { - queryMode: querySettings.queryMode, - }; + if (!isEqual(lastQueryExecutionSettings, querySettings)) { + resetBanner(); + setLastQueryExecutionSettings(querySettings); + } - setResultType(RESULT_TYPES.EXPLAIN); - sendExplainQuery({ - query: input, - database: tenantName, - querySettings: explainQuerySettings, - }); - setIsResultLoaded(true); - setShowPreview(false); - dispatchResultVisibilityState(PaneVisibilityActionTypes.triggerExpand); - }, - [ - executeQuery, - lastQueryExecutionSettings, + setResultType(RESULT_TYPES.EXPLAIN); + sendExplainQuery({ + query: input, + database: tenantName, querySettings, - querySettingsFlag, - resetBanner, - sendExplainQuery, - setLastQueryExecutionSettings, - setLastUsedQueryAction, - setShowPreview, - tenantName, - ], - ); + tracingLevelVerbosity, + }); + setIsResultLoaded(true); + setShowPreview(false); + dispatchResultVisibilityState(PaneVisibilityActionTypes.triggerExpand); + }, [ + executeQuery, + lastQueryExecutionSettings, + querySettings, + resetBanner, + tracingLevelVerbosity, + sendExplainQuery, + setLastQueryExecutionSettings, + setLastUsedQueryAction, + setShowPreview, + tenantName, + ]); React.useEffect(() => { if (monacoHotKey === null) { @@ -302,9 +288,9 @@ function QueryEditor(props: QueryEditorProps) { switch (monacoHotKey) { case MONACO_HOT_KEY_ACTIONS.sendQuery: { if (lastUsedQueryAction === QUERY_ACTIONS.explain) { - handleGetExplainQueryClick(querySettings); + handleGetExplainQueryClick(); } else { - handleSendExecuteClick(querySettings); + handleSendExecuteClick(); } break; } @@ -318,7 +304,7 @@ function QueryEditor(props: QueryEditorProps) { endLineNumber: selection.getPosition().lineNumber, endColumn: selection.getPosition().column, }); - handleSendExecuteClick(querySettings, text); + handleSendExecuteClick(text); } break; } @@ -417,8 +403,6 @@ function QueryEditor(props: QueryEditorProps) { onExplainButtonClick={handleGetExplainQueryClick} explainIsLoading={explainQueryResult.isLoading} disabled={!executeQuery.input} - onUpdateQueryMode={(queryMode) => setQuerySettings({...querySettings, queryMode})} - querySettings={querySettings} highlightedAction={lastUsedQueryAction} /> ); diff --git a/src/containers/Tenant/Query/QueryEditorControls/QueryEditorControls.tsx b/src/containers/Tenant/Query/QueryEditorControls/QueryEditorControls.tsx index 42f2382d6..1105c1f4f 100644 --- a/src/containers/Tenant/Query/QueryEditorControls/QueryEditorControls.tsx +++ b/src/containers/Tenant/Query/QueryEditorControls/QueryEditorControls.tsx @@ -1,49 +1,18 @@ -import React from 'react'; - -import {ChevronDown, Gear, PlayFill} from '@gravity-ui/icons'; +import {Gear, PlayFill} from '@gravity-ui/icons'; import type {ButtonView} from '@gravity-ui/uikit'; -import {Button, DropdownMenu, Icon, Tooltip} from '@gravity-ui/uikit'; +import {Button, Icon, Tooltip} from '@gravity-ui/uikit'; -import {LabelWithPopover} from '../../../../components/LabelWithPopover'; import QuerySettingsDescription from '../../../../components/QuerySettingsDescription/QuerySettingsDescription'; -import {QUERY_SETTINGS, useSetting} from '../../../../lib'; -import type {QueryAction, QueryMode, QuerySettings} from '../../../../types/store/query'; +import type {QueryAction} from '../../../../types/store/query'; import {cn} from '../../../../utils/cn'; import {useChangedQuerySettings} from '../../../../utils/hooks/useChangedQuerySettings'; -import {QUERY_MODES, QUERY_MODES_TITLES} from '../../../../utils/query'; import {SaveQuery} from '../SaveQuery/SaveQuery'; import i18n from '../i18n'; import './QueryEditorControls.scss'; -const queryModeSelectorQa = 'query-mode-selector'; -const queryModeSelectorPopupQa = 'query-mode-selector-popup'; - const b = cn('ydb-query-editor-controls'); -const QueryModeSelectorOptions = { - [QUERY_MODES.script]: { - title: QUERY_MODES_TITLES[QUERY_MODES.script], - description: i18n('method-description.script'), - }, - [QUERY_MODES.scan]: { - title: QUERY_MODES_TITLES[QUERY_MODES.scan], - description: i18n('method-description.scan'), - }, - [QUERY_MODES.data]: { - title: QUERY_MODES_TITLES[QUERY_MODES.data], - description: i18n('method-description.data'), - }, - [QUERY_MODES.query]: { - title: QUERY_MODES_TITLES[QUERY_MODES.query], - description: i18n('method-description.query'), - }, - [QUERY_MODES.pg]: { - title: QUERY_MODES_TITLES[QUERY_MODES.pg], - description: i18n('method-description.pg'), - }, -} as const; - interface SettingsButtonProps { onClick: () => void; runIsLoading: boolean; @@ -85,59 +54,33 @@ const SettingsButton = ({onClick, runIsLoading}: SettingsButtonProps) => { }; interface QueryEditorControlsProps { - onRunButtonClick: (querySettings: QuerySettings) => void; + onRunButtonClick: () => void; onSettingsButtonClick: () => void; runIsLoading: boolean; - onExplainButtonClick: (querySettings: QuerySettings) => void; + onExplainButtonClick: () => void; explainIsLoading: boolean; disabled: boolean; - onUpdateQueryMode: (mode: QueryMode) => void; - querySettings: QuerySettings; highlightedAction: QueryAction; } export const QueryEditorControls = ({ onRunButtonClick, onSettingsButtonClick, - onUpdateQueryMode, runIsLoading, onExplainButtonClick, explainIsLoading, disabled, - querySettings, highlightedAction, }: QueryEditorControlsProps) => { - const [useQuerySettings] = useSetting(QUERY_SETTINGS); - const runView: ButtonView | undefined = highlightedAction === 'execute' ? 'action' : undefined; const explainView: ButtonView | undefined = highlightedAction === 'explain' ? 'action' : undefined; - const querySelectorMenuItems = React.useMemo(() => { - return Object.entries(QueryModeSelectorOptions).map(([mode, {title, description}]) => { - return { - text: ( - - ), - action: () => { - onUpdateQueryMode(mode as QueryMode); - }, - }; - }); - }, [onUpdateQueryMode]); - return (
- {useQuerySettings ? ( - - ) : ( -
- - - {`${i18n('controls.query-mode-selector_type')} ${ - QueryModeSelectorOptions[querySettings.queryMode].title - }`} - - - - } - /> -
- )} +
diff --git a/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx b/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx index f7f61a12a..6a39a1877 100644 --- a/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx +++ b/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx @@ -3,6 +3,7 @@ import React from 'react'; import {Dialog, Link as ExternalLink, Flex, TextInput} from '@gravity-ui/uikit'; import {Controller, useForm} from 'react-hook-form'; +import {TRACING_LEVEL_VERBOSITY_KEY} from '../../../../lib'; import { selectQueryAction, setQueryAction, @@ -11,6 +12,7 @@ import type {QuerySettings} from '../../../../types/store/query'; import {cn} from '../../../../utils/cn'; import { useQueryExecutionSettings, + useSetting, useTypedDispatch, useTypedSelector, } from '../../../../utils/hooks'; @@ -69,6 +71,8 @@ function QuerySettingsForm({initialValues, onSubmit, onClose}: QuerySettingsForm defaultValues: initialValues, }); + const [tracingLevelVerbosity] = useSetting(TRACING_LEVEL_VERBOSITY_KEY); + return (
@@ -76,7 +80,7 @@ function QuerySettingsForm({initialValues, onSubmit, onClose}: QuerySettingsForm -
+
- - -
- ( - - )} - /> -
-
+ {tracingLevelVerbosity && ( + + +
+ ( + + )} + /> +
+
+ )} -
+
(
diff --git a/src/containers/UserSettings/i18n/en.json b/src/containers/UserSettings/i18n/en.json index f2446c8a8..0e1c5b981 100644 --- a/src/containers/UserSettings/i18n/en.json +++ b/src/containers/UserSettings/i18n/en.json @@ -39,8 +39,8 @@ "settings.showDomainDatabase.title": "Show domain database", - "settings.useQuerySettings.title": "Use query settings", - "settings.useQuerySettings.description": "Use query settings", + "settings.tracingLevelVerbosity.title": "Enable tracing level select", + "settings.tracingLevelVerbosity.description": "Caution: Enabling this setting may break running of queries", "settings.queryUseMultiSchema.title": "Allow queries with multiple result sets", "settings.queryUseMultiSchema.description": "Use 'multi' schema for queries. It enables queries with multiple result sets. It returns nothing on versions 23-3 and older", diff --git a/src/containers/UserSettings/settings.tsx b/src/containers/UserSettings/settings.tsx index 217dca2f5..f734e64d7 100644 --- a/src/containers/UserSettings/settings.tsx +++ b/src/containers/UserSettings/settings.tsx @@ -8,10 +8,10 @@ import { ENABLE_AUTOCOMPLETE, INVERTED_DISKS_KEY, LANGUAGE_KEY, - QUERY_SETTINGS, QUERY_USE_MULTI_SCHEMA_KEY, SHOW_DOMAIN_DATABASE_KEY, THEME_KEY, + TRACING_LEVEL_VERBOSITY_KEY, USE_CLUSTER_BALANCER_AS_BACKEND_KEY, USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY, USE_PAGINATED_TABLES_KEY, @@ -113,10 +113,10 @@ export const showDomainDatabase: SettingProps = { title: i18n('settings.showDomainDatabase.title'), }; -export const useQuerySettings: SettingProps = { - settingKey: QUERY_SETTINGS, - title: i18n('settings.useQuerySettings.title'), - description: i18n('settings.useQuerySettings.description'), +export const tracingLevelVerbosity: SettingProps = { + settingKey: TRACING_LEVEL_VERBOSITY_KEY, + title: i18n('settings.tracingLevelVerbosity.title'), + description: i18n('settings.tracingLevelVerbosity.description'), }; export const queryUseMultiSchemaSetting: SettingProps = { @@ -167,7 +167,7 @@ export const experimentsSection: SettingsSection = { export const devSettingsSection: SettingsSection = { id: 'devSettingsSection', title: i18n('section.dev-setting'), - settings: [enableAutocompleteSetting, autocompleteOnEnterSetting], + settings: [enableAutocompleteSetting, autocompleteOnEnterSetting, tracingLevelVerbosity], }; export const aboutSettingsSection: SettingsSection = { diff --git a/src/services/settings.ts b/src/services/settings.ts index 9e77194e3..396dbbd2a 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -13,14 +13,15 @@ import { LAST_USED_QUERY_ACTION_KEY, PARTITIONS_HIDDEN_COLUMNS_KEY, QUERY_EXECUTION_SETTINGS_KEY, - QUERY_SETTINGS, QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY, QUERY_USE_MULTI_SCHEMA_KEY, SAVED_QUERIES_KEY, SHOW_DOMAIN_DATABASE_KEY, TENANT_INITIAL_PAGE_KEY, THEME_KEY, + TRACING_LEVEL_VERBOSITY_KEY, USE_CLUSTER_BALANCER_AS_BACKEND_KEY, + USE_DIRECTORY_OPERATIONS, USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY, USE_PAGINATED_TABLES_KEY, } from '../utils/constants'; @@ -48,7 +49,8 @@ export const DEFAULT_USER_SETTINGS = { [AUTOCOMPLETE_ON_ENTER]: true, [IS_HOTKEYS_HELP_HIDDEN_KEY]: false, [AUTO_REFRESH_INTERVAL]: 0, - [QUERY_SETTINGS]: false, + [USE_DIRECTORY_OPERATIONS]: false, + [TRACING_LEVEL_VERBOSITY_KEY]: false, [SHOW_DOMAIN_DATABASE_KEY]: false, [LAST_QUERY_EXECUTION_SETTINGS_KEY]: undefined, [QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY]: undefined, diff --git a/src/store/reducers/executeQuery.ts b/src/store/reducers/executeQuery.ts index 72458adbc..8bbf22dba 100644 --- a/src/store/reducers/executeQuery.ts +++ b/src/store/reducers/executeQuery.ts @@ -11,18 +11,12 @@ import type { } from '../../types/store/executeQuery'; import type { IQueryResult, - QueryMode, QueryRequestParams, QuerySettings, QuerySyntax, } from '../../types/store/query'; import {QUERIES_HISTORY_KEY} from '../../utils/constants'; -import { - QUERY_MODES, - QUERY_SYNTAX, - isQueryErrorResponse, - parseQueryAPIExecuteResponse, -} from '../../utils/query'; +import {QUERY_SYNTAX, isQueryErrorResponse, parseQueryAPIExecuteResponse} from '../../utils/query'; import {isNumeric} from '../../utils/utils'; import {createRequestActionTypes} from '../utils'; @@ -72,12 +66,9 @@ const executeQuery: Reducer = ( } case SAVE_QUERY_TO_HISTORY: { - const queryText = action.data.queryText; + const queryText = action.data; - // Do not save explicit yql syntax value for easier further support (use yql by default) - const syntax = action.data.mode === QUERY_MODES.pg ? QUERY_SYNTAX.pg : undefined; - - const newQueries = [...state.history.queries, {queryText, syntax}].slice( + const newQueries = [...state.history.queries, {queryText}].slice( state.history.queries.length >= MAXIMUM_QUERIES_IN_HISTORY ? 1 : 0, ); settingsManager.setUserSettingsValue(QUERIES_HISTORY_KEY, newQueries); @@ -144,12 +135,21 @@ const executeQuery: Reducer = ( interface SendQueryParams extends QueryRequestParams { querySettings?: Partial; schema?: Schemas; + // flag whether to send new tracing header or not + // default: not send + tracingLevelVerbosity?: boolean; } export const executeQueryApi = api.injectEndpoints({ endpoints: (build) => ({ executeQuery: build.mutation({ - queryFn: async ({query, database, querySettings = {}, schema = 'modern'}) => { + queryFn: async ({ + query, + database, + querySettings = {}, + schema = 'modern', + tracingLevelVerbosity, + }) => { let action: ExecuteActions = 'execute'; let syntax: QuerySyntax = QUERY_SYNTAX.yql; @@ -168,9 +168,10 @@ export const executeQueryApi = api.injectEndpoints({ action, syntax, stats: querySettings.statisticsMode, - tracingLevel: querySettings.tracingLevel - ? TracingLevelNumber[querySettings.tracingLevel] - : undefined, + tracingLevel: + querySettings.tracingLevel && tracingLevelVerbosity + ? TracingLevelNumber[querySettings.tracingLevel] + : undefined, transaction_mode: querySettings.isolationLevel, timeout: isNumeric(querySettings.timeout) ? Number(querySettings.timeout) * 1000 @@ -192,10 +193,10 @@ export const executeQueryApi = api.injectEndpoints({ overrideExisting: 'throw', }); -export const saveQueryToHistory = (queryText: string, mode: QueryMode) => { +export const saveQueryToHistory = (queryText: string) => { return { type: SAVE_QUERY_TO_HISTORY, - data: {queryText, mode}, + data: queryText, } as const; }; diff --git a/src/store/reducers/explainQuery/explainQuery.ts b/src/store/reducers/explainQuery/explainQuery.ts index 3078122b1..e3a4088c1 100644 --- a/src/store/reducers/explainQuery/explainQuery.ts +++ b/src/store/reducers/explainQuery/explainQuery.ts @@ -10,12 +10,15 @@ import {prepareExplainResponse} from './utils'; interface ExplainQueryParams extends QueryRequestParams { querySettings?: Partial; + // flag whether to send new tracing header or not + // default: not send + tracingLevelVerbosity?: boolean; } export const explainQueryApi = api.injectEndpoints({ endpoints: (build) => ({ explainQuery: build.mutation({ - queryFn: async ({query, database, querySettings}) => { + queryFn: async ({query, database, querySettings, tracingLevelVerbosity}) => { let action: ExplainActions = 'explain'; let syntax: QuerySyntax = QUERY_SYNTAX.yql; @@ -33,9 +36,10 @@ export const explainQueryApi = api.injectEndpoints({ action, syntax, stats: querySettings?.statisticsMode, - tracingLevel: querySettings?.tracingLevel - ? TracingLevelNumber[querySettings?.tracingLevel] - : undefined, + tracingLevel: + querySettings?.tracingLevel && tracingLevelVerbosity + ? TracingLevelNumber[querySettings?.tracingLevel] + : undefined, transaction_mode: querySettings?.isolationLevel, timeout: isNumeric(querySettings?.timeout) ? Number(querySettings?.timeout) * 1000 diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 0f9b3a33d..4a1d97964 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -159,4 +159,8 @@ export const AUTOCOMPLETE_ON_ENTER = 'autocompleteOnEnter'; export const IS_HOTKEYS_HELP_HIDDEN_KEY = 'isHotKeysHelpHidden'; -export const QUERY_SETTINGS = 'query_settings'; +export const USE_SEPARATE_DISKS_PAGES_KEY = 'useSeparateDisksPages'; + +export const USE_DIRECTORY_OPERATIONS = 'useDirectoryOperations'; + +export const TRACING_LEVEL_VERBOSITY_KEY = 'tracingLevelVerbosity'; diff --git a/src/utils/hooks/useQueryExecutionSettings.ts b/src/utils/hooks/useQueryExecutionSettings.ts index 107b529c5..48aea6103 100644 --- a/src/utils/hooks/useQueryExecutionSettings.ts +++ b/src/utils/hooks/useQueryExecutionSettings.ts @@ -1,8 +1,23 @@ import type {QuerySettings} from '../../types/store/query'; -import {QUERY_EXECUTION_SETTINGS_KEY} from '../constants'; +import { + DEFAULT_QUERY_SETTINGS, + QUERY_EXECUTION_SETTINGS_KEY, + TRACING_LEVEL_VERBOSITY_KEY, +} from '../constants'; import {useSetting} from './useSetting'; export const useQueryExecutionSettings = () => { - return useSetting(QUERY_EXECUTION_SETTINGS_KEY); + const [tracingLevelVerbosity] = useSetting(TRACING_LEVEL_VERBOSITY_KEY); + const [setting, setSetting] = useSetting(QUERY_EXECUTION_SETTINGS_KEY); + + return [ + { + ...setting, + tracingLevel: tracingLevelVerbosity + ? setting.tracingLevel + : DEFAULT_QUERY_SETTINGS.tracingLevel, + }, + setSetting, + ] as const; }; diff --git a/tests/suites/tenant/queryEditor/QueryEditor.ts b/tests/suites/tenant/queryEditor/QueryEditor.ts index 16f222eb3..1ed6a0aa9 100644 --- a/tests/suites/tenant/queryEditor/QueryEditor.ts +++ b/tests/suites/tenant/queryEditor/QueryEditor.ts @@ -4,80 +4,161 @@ import {BaseModel} from '../../../models/BaseModel'; import {selectContentTable} from '../../../utils/selectContentTable'; type QueryMode = 'YQL Script' | 'Scan'; -type ExplainResultType = 'Schema' | 'Simplified' | 'JSON' | 'AST'; - -const queryModeSelectorQa = 'query-mode-selector'; -const queryModeSelectorPopupQa = 'query-mode-selector-popup'; +type ExplainResultType = 'Schema' | 'JSON' | 'AST'; export class QueryEditor extends BaseModel { protected readonly editorTextArea: Locator; - protected readonly runButton: Locator; - protected readonly explainButton: Locator; + protected runButton: Locator; + protected explainButton: Locator; + protected readonly gearButton: Locator; + protected readonly indicatorIcon: Locator; + protected readonly settingsDialog: Locator; + protected readonly banner: Locator; constructor(page: Page) { super(page, page.locator('.query-editor')); this.editorTextArea = this.selector.locator('.query-editor__monaco textarea'); - this.runButton = this.selector.getByRole('button', {name: /Run/}); this.explainButton = this.selector.getByRole('button', {name: /Explain/}); + this.gearButton = this.selector.locator('.ydb-query-editor-controls__gear-button'); + this.indicatorIcon = this.selector.locator( + '.ydb-query-editor-controls__query-settings-icon', + ); + this.settingsDialog = this.page.locator('.ydb-query-settings-dialog'); + this.banner = this.page.locator('.ydb-query-settings-banner'); } + async run(query: string, mode: QueryMode) { - await this.selectQueryMode(mode); + await this.clickGearButton(); + await this.changeQueryMode(mode); + await this.clickSaveInSettingsDialog(); await this.editorTextArea.fill(query); await this.runButton.click(); } async explain(query: string, mode: QueryMode) { - await this.selectQueryMode(mode); + await this.clickGearButton(); + await this.changeQueryMode(mode); + await this.clickSaveInSettingsDialog(); await this.editorTextArea.fill(query); await this.explainButton.click(); } - // eslint-disable-next-line consistent-return + async gearButtonContainsText(text: string) { + return this.gearButton.locator(`text=${text}`).isVisible(); + } + + async isBannerVisible() { + return this.banner.isVisible(); + } + + async isBannerHidden() { + return this.banner.isHidden(); + } + + async isRunButtonEnabled() { + return this.runButton.isEnabled(); + } + + async isRunButtonDisabled() { + return this.runButton.isDisabled(); + } + + async isExplainButtonEnabled() { + return this.explainButton.isEnabled(); + } + + async isExplainButtonDisabled() { + return this.explainButton.isDisabled(); + } + + async clickRunButton() { + await this.runButton.click(); + } + + async clickExplainButton() { + await this.explainButton.click(); + } + async getExplainResult(type: ExplainResultType) { await this.selectExplainResultType(type); const resultArea = this.selector.locator('.ydb-query-explain-result__result'); switch (type) { - case 'Schema': { + case 'Schema': return resultArea.locator('.canvas-container'); - } - case 'Simplified': { - return resultArea.locator('.ydb-query-explain-simplified-plan'); - } - case 'JSON': { + case 'JSON': return resultArea.locator('.json-inspector'); - } - case 'AST': { + case 'AST': return resultArea.locator('.ydb-query-explain-ast'); - } } } getRunResultTable() { const runResult = this.selector.locator('.ydb-query-execute-result__result'); + return selectContentTable(runResult); + } - // Result table is present only on successful not empty results - const resultTable = selectContentTable(runResult); + async settingsDialogIsVisible() { + return this.settingsDialog.isVisible(); + } - return resultTable; + async settingsDialogIsNotVisible() { + return this.settingsDialog.isHidden(); } - protected async selectExplainResultType(type: ExplainResultType) { - const radio = this.selector.locator('.ydb-query-explain-result__controls .g-radio-button'); - await radio.getByLabel(type).click(); + async clickGearButton() { + await this.gearButton.click(); + } + + async clickCancelInSettingsDialog() { + await this.settingsDialog.getByRole('button', {name: /Cancel/}).click(); + } + + async clickSaveInSettingsDialog() { + await this.settingsDialog.getByRole('button', {name: /Save/}).click(); + } + + async changeQueryMode(mode: QueryMode) { + const dropdown = this.settingsDialog.locator( + '.ydb-query-settings-dialog__control-wrapper_queryMode', + ); + await dropdown.click(); + const popup = this.page.locator('.ydb-query-settings-select__popup'); + await popup.getByText(mode).first().click(); + await this.page.waitForTimeout(1000); } - protected async selectQueryMode(type: QueryMode) { - const queryModeSelector = this.selector.getByTestId(queryModeSelectorQa); + async changeIsolationLevel(level: string) { + const dropdown = this.settingsDialog.locator( + '.ydb-query-settings-dialog__control-wrapper_isolationLevel', + ); + await dropdown.click(); + const popup = this.page.locator('.ydb-query-settings-select__popup'); + await popup.getByText(level).first().click(); + await this.page.waitForTimeout(1000); + } + + async closeBanner() { + await this.banner.locator('button').click(); + } - await queryModeSelector.click(); + async isIndicatorIconVisible() { + return this.indicatorIcon.isVisible(); + } - const queryModeSelectorPopup = this.page.getByTestId(queryModeSelectorPopupQa); + async hoverGearButton() { + await this.gearButton.hover(); + } - await queryModeSelectorPopup.waitFor({state: 'visible'}); - await queryModeSelectorPopup.getByText(type).click(); + async setQuery(query: string) { + await this.editorTextArea.fill(query); + } + + protected async selectExplainResultType(type: ExplainResultType) { + const radio = this.selector.locator('.ydb-query-explain-result__controls .g-radio-button'); + await radio.getByLabel(type).click(); } } diff --git a/tests/suites/tenant/queryEditor/queryEditor.test.ts b/tests/suites/tenant/queryEditor/queryEditor.test.ts index e3d4d259a..9040dc076 100644 --- a/tests/suites/tenant/queryEditor/queryEditor.test.ts +++ b/tests/suites/tenant/queryEditor/queryEditor.test.ts @@ -19,21 +19,41 @@ test.describe('Test Query Editor', async () => { await tenantPage.goto(pageQueryParams); }); - test('Can run scipt', async ({page}) => { + test('Settings dialog opens on Gear click and closes on Cancel', async ({page}) => { + const queryEditor = new QueryEditor(page); + await queryEditor.clickGearButton(); + + await expect(queryEditor.settingsDialogIsVisible()).toBeTruthy(); + + await queryEditor.clickCancelInSettingsDialog(); + await expect(queryEditor.settingsDialogIsNotVisible()).toBeTruthy(); + }); + + test('Settings dialog saves changes and updates Gear button', async ({page}) => { + const queryEditor = new QueryEditor(page); + await queryEditor.clickGearButton(); + + await queryEditor.changeQueryMode('Scan'); + await queryEditor.clickSaveInSettingsDialog(); + + await expect(queryEditor.gearButtonContainsText('(1)')).toBeTruthy(); + }); + + test('Run button executes YQL script', async ({page}) => { const queryEditor = new QueryEditor(page); await queryEditor.run(testQuery, 'YQL Script'); await expect(queryEditor.getRunResultTable()).toBeVisible(); }); - test('Can run scan', async ({page}) => { + test('Run button executes Scan', async ({page}) => { const queryEditor = new QueryEditor(page); await queryEditor.run(testQuery, 'Scan'); await expect(queryEditor.getRunResultTable()).toBeVisible(); }); - test('Can get explain script', async ({page}) => { + test('Explain button executes YQL script explanation', async ({page}) => { const queryEditor = new QueryEditor(page); await queryEditor.explain(testQuery, 'YQL Script'); @@ -44,7 +64,7 @@ test.describe('Test Query Editor', async () => { await expect(explainJSON).toBeVisible(); }); - test('Can get explain scan', async ({page}) => { + test('Explain button executes Scan explanation', async ({page}) => { const queryEditor = new QueryEditor(page); await queryEditor.explain(testQuery, 'Scan'); @@ -57,4 +77,72 @@ test.describe('Test Query Editor', async () => { const explainAST = await queryEditor.getExplainResult('AST'); await expect(explainAST).toBeVisible(); }); + + test('Banner appears after executing script with changed settings', async ({page}) => { + const queryEditor = new QueryEditor(page); + + // Change a setting + await queryEditor.clickGearButton(); + await queryEditor.changeQueryMode('Scan'); + await queryEditor.clickSaveInSettingsDialog(); + + // Execute a script + await queryEditor.setQuery(testQuery); + await queryEditor.clickRunButton(); + + // Check if banner appears + await expect(queryEditor.isBannerVisible()).toBeTruthy(); + }); + + test('Indicator icon appears after closing banner', async ({page}) => { + const queryEditor = new QueryEditor(page); + + // Change a setting + await queryEditor.clickGearButton(); + await queryEditor.changeQueryMode('Scan'); + await queryEditor.clickSaveInSettingsDialog(); + + // Execute a script to make the banner appear + await queryEditor.setQuery(testQuery); + await queryEditor.clickRunButton(); + + // Close the banner + await queryEditor.closeBanner(); + + // Check tooltip on hover + await queryEditor.hoverGearButton(); + await expect(queryEditor.isIndicatorIconVisible()).toBeTruthy(); + }); + + test('Gear button shows number of changed settings', async ({page}) => { + const queryEditor = new QueryEditor(page); + await queryEditor.clickGearButton(); + + await queryEditor.changeQueryMode('Scan'); + await queryEditor.changeIsolationLevel('Serializable'); + await queryEditor.clickSaveInSettingsDialog(); + + await expect(queryEditor.gearButtonContainsText('(2)')).toBeTruthy(); + }); + + test('Run and Explain buttons are disabled when query is empty', async ({page}) => { + const queryEditor = new QueryEditor(page); + + await expect(queryEditor.isRunButtonDisabled()).toBeTruthy(); + await expect(queryEditor.isExplainButtonDisabled()).toBeTruthy(); + + await queryEditor.setQuery(testQuery); + + await expect(queryEditor.isRunButtonEnabled()).toBeTruthy(); + await expect(queryEditor.isExplainButtonEnabled()).toBeTruthy(); + }); + + test('Banner does not appear when executing script with default settings', async ({page}) => { + const queryEditor = new QueryEditor(page); + + await queryEditor.setQuery(testQuery); + await queryEditor.clickRunButton(); + + await expect(queryEditor.isBannerHidden()).toBeTruthy(); + }); });