diff --git a/docs/docs/using-superset/exploring-data.mdx b/docs/docs/using-superset/exploring-data.mdx index 426561aaf27f..975965c6656b 100644 --- a/docs/docs/using-superset/exploring-data.mdx +++ b/docs/docs/using-superset/exploring-data.mdx @@ -451,6 +451,134 @@ to be drawn as zero across the entire day rather than only around the hours that Lastly, save your chart as Tutorial Resample and add it to the Tutorial Dashboard. Go to the tutorial dashboard to see the four charts side by side and compare the different outputs. +### SQL Lab query cost warning settings + +SQL Lab supports opt-in pre-run warnings for BigQuery and PostgreSQL. It can ask +for confirmation when estimated usage exceeds a connection's threshold or cannot +be assessed reliably. Warnings are off by default. They do not enforce a spending +or resource limit: an analyst can choose to continue, and existing permissions and +native database limits still apply. + +To enable warnings: + +1. Add `"SQLLAB_QUERY_COST_WARNINGS": True` and `"ESTIMATE_QUERY_COST": True` to the + existing `FEATURE_FLAGS` dictionary in `superset_config.py`, then restart + Superset and refresh SQL Lab. +2. Open **Settings > Database Connections**, edit the connection, and select + **Advanced > SQL Lab**. Expose the connection in SQL Lab if necessary. +3. Select **Enable query cost estimation**, then **Enable pre-run warnings** + and enter a positive threshold. Save the connection. No threshold is chosen + automatically; choose one appropriate for the connection's workload. + +The shipped cost checks use one rule per connection in `extra.query_cost_warning`, +containing `enabled`, `metric`, and `threshold`. BigQuery uses `bytes_processed`, +a positive whole number of bytes. PostgreSQL uses `total_cost`, a positive number +that can include decimals. Both numeric thresholds must be no greater than +9,007,199,254,740,991 so integer settings can be edited safely in the browser. +Planner-cost decimals use ordinary floating-point precision. This settings bound +does not limit database estimates, which are compared on the server. PostgreSQL +planner units are not runtime, money, or memory. BigQuery estimates describe usage, +not a guaranteed bill. These metrics are not converted into a common unit. + +BigQuery thresholds can be entered in Bytes, MB, or GB (decimal units: 1 MB = +1,000,000 bytes and 1 GB = 1,000,000,000 bytes). Changing the unit changes the +display while retaining the saved byte amount. Fractional MB or GB are accepted +only when they represent a positive whole number of bytes in the allowed range; +invalid amounts show validation instead of being rounded. PostgreSQL thresholds +stay in relative planner cost units and have no default amount. + +Disabling warnings retains valid saved values. Turning connection estimation off +also retains the rule and makes checking inactive. A broken rule can be replaced +with `{ "enabled": false }` to disable it without retaining invalid fields. + +Create, update, and import validate the rule against the resulting connection's +engine. + +The warning estimator uses BigQuery dry runs and PostgreSQL `EXPLAIN (FORMAT +JSON)` without `ANALYZE`. It compares raw values on the server. Valid zero +estimates are retained; missing, malformed, unknown, and masked estimates are +unavailable. A BigQuery lower bound must exceed the threshold to establish a +warning; a lower bound at or below it does not establish that usage is within the +threshold. An upper bound can provide a conservative warning. Estimates hidden by +BigQuery row-level security are unavailable. + +For BigQuery, a SQL row `LIMIT` does not cap bytes scanned. Use the warehouse's +native controls, such as `maximum_bytes_billed` where supported, for an actual +scan cap. The warning threshold is separate from those controls. Dry-run a small, +bounded query before executing it when verifying a connection's setup. + +The existing manual estimate button keeps its formatted display. Warning checks +are advisory and are intended for deliberate SQL Lab runs, not dashboards, +scheduled queries, automatic retries, CTAS/CVAS, or enforced warehouse limits. + +API clients can request an advisory check with `POST /api/v1/sqllab/estimate/`, setting +`check_warning` to `true` and passing the selected `sql`, `database_id`, `catalog`, +`schema`, `template_params`, and SQL Lab dropdown `query_limit`. The check applies +the execution row limits, including the extra row fetched to detect truncation. +The result returns `action: proceed` when this check does not need confirmation. +This is not a guarantee that the query is safe in every other way. Otherwise it +returns `action: confirm`, a `warning` or `unavailable` reason, a nonempty message, +and optional plain-text label/value details. For the cost checks, estimates and +thresholds are compared before display formatting. Precise estimates and upper +bounds equal to the threshold do not warn; lower bounds at equality still require +confirmation because usage could exceed the threshold. +BigQuery bounds are displayed as **At least** or **Up to**. Byte counts can exceed JavaScript's safe integer +range, so clients should display the server-provided details without reconverting +them to numbers. + +Checking supports one read-only SELECT. Templates, multiple statements, and +custom SQL transformations are unavailable. A known estimation timeout or an +unusable estimate also returns unavailable. Invalid rules, access failures, and +database or network errors remain errors. Omitting `check_warning` preserves the +manual estimator response. + +Database API and SQL Lab bootstrap responses include `query_warning_state`: +`unsupported`, `disabled`, `ready`, or `invalid`. Invalid saved settings are not +an opt-out: clients must preserve validation errors. Engine information exposes +`query_warning_supported` separately from the optional numeric +`query_cost_warning_metric`. `SQLLAB_QUERY_COST_WARNINGS` gates all checks; +`ESTIMATE_QUERY_COST` and connection cost estimation are prerequisites of the +numeric threshold check. + +When checking is enabled, a deliberate **Run** (including Run selection or its +keyboard shortcut) sends an assessment request before execution. This adds a +delay that depends on the query, database, and network. The check uses the +captured execution request. The current-statement shortcut uses the existing +statement range without changing your selection. While checking, +**Stop** in the Run area (or its keyboard shortcut) prevents that attempt from +executing; it does not forcibly cancel a database dry run. The **Query warning** +popup explains an above-threshold or unavailable estimate and offers **Cancel** +and **Run anyway**. Errors stop the attempt without executing it. + +Changing the SQL or selection, connection, catalog, schema, template parameters, +effective limit, asynchronous execution setting, or locally loaded warning +settings cancels a pending attempt. Switching editor tabs does too. Confirming +runs the captured request once. Remote policy changes become visible when +connection metadata is refreshed; the dialog does not detect them in advance. + +**Run query in a new tab** from query history uses the same warning flow. The +rerun retains its existing settings: SQL, connection, catalog, and schema come +from the historical query; the limit and template parameters come from the +active editor, and asynchronous execution follows the connection's current +setting. Cancelling leaves the new tab open without executing its query. Repeated +Run clicks while a check or confirmation is pending do not create another attempt. + +#### Engine extension point + +Only BigQuery and PostgreSQL have production warning checks. An engine spec can +explicitly opt into the shared flow through `get_query_warning_check()`, returning +a check with `validate(rule)`, `is_enabled(database)`, and +`assess(database, catalog, schema, prepared_sql, rule)`, or `None` when unsupported. +The shared settings validation requires a rule object and a strict boolean +`enabled`; the selected check validates its own fields and prerequisites. + +Both shipped integrations use `QueryCostThresholdCheck` with their engine-specific +estimation adapters. A future integration can supply a message-only check and its +own settings controls without changing Run or the confirmation dialog. Checks +return the same `proceed` or `confirm` contract, with display details as plain text. +Subclasses must explicitly opt in; inheriting an engine's manual estimator alone +does not establish warning support. + ### Stopping chart queries In Explore, a mouse, touch, pen, or Enter/Space key press that starts on **Stop** will not diff --git a/docs/static/feature-flags.json b/docs/static/feature-flags.json index e39581a4439c..02f234009784 100644 --- a/docs/static/feature-flags.json +++ b/docs/static/feature-flags.json @@ -246,6 +246,12 @@ "lifecycle": "testing", "description": "Temporary rollout / kill-switch gate for soft delete (off = legacy hard delete). An emergency stop, not a clean rollback: flipping ON->OFF resurrects already-soft-deleted rows. Retained through this release as the move-back lever; removed (along with its two gate points \u2014 BaseDAO.delete routing and the do_orm_execute visibility listener) once post-flip confidence is established." }, + { + "name": "SQLLAB_QUERY_COST_WARNINGS", + "default": false, + "lifecycle": "testing", + "description": "Optional advisory warnings before deliberately running SQL in SQL Lab. Requires explicit connection-level opt-in. Numeric checks also require ESTIMATE_QUERY_COST and connection cost estimation." + }, { "name": "SSH_TUNNELING", "default": false, diff --git a/docs/static/resources/openapi.json b/docs/static/resources/openapi.json index 028af7aa04bb..fbe8c5bde7c0 100644 --- a/docs/static/resources/openapi.json +++ b/docs/static/resources/openapi.json @@ -5448,6 +5448,12 @@ "is_managed_externally": { "type": "boolean" }, + "query_cost_warning": { + "readOnly": true + }, + "query_warning_state": { + "readOnly": true + }, "uuid": { "format": "uuid", "nullable": true, @@ -5547,6 +5553,12 @@ "id": { "type": "integer" }, + "query_cost_warning": { + "readOnly": true + }, + "query_warning_state": { + "readOnly": true + }, "uuid": { "format": "uuid", "nullable": true, @@ -7852,6 +7864,13 @@ "description": "OAuth2 authentication is enabled on this connection", "type": "boolean" }, + "query_cost_warning_metric": { + "nullable": true, + "type": "string" + }, + "query_warning_supported": { + "type": "boolean" + }, "supports_dynamic_catalog": { "description": "The database supports multiple catalogs in a single connection", "type": "boolean" @@ -7882,10 +7901,20 @@ "nullable": true, "type": "string" }, + "check_warning": { + "default": false, + "description": "Run the connection's advisory pre-execution check", + "type": "boolean" + }, "database_id": { "description": "The database id", "type": "integer" }, + "query_limit": { + "description": "The SQL Lab dropdown row limit", + "nullable": true, + "type": "integer" + }, "schema": { "description": "The database schema", "nullable": true, @@ -11879,6 +11908,19 @@ "id": { "description": "The database id", "type": "integer" + }, + "query_cost_warning": { + "nullable": true, + "type": "object" + }, + "query_warning_state": { + "enum": [ + "unsupported", + "disabled", + "ready", + "invalid" + ], + "type": "string" } }, "type": "object" @@ -36077,14 +36119,80 @@ "schema": { "properties": { "result": { - "type": "object" + "oneOf": [ + { + "items": { + "type": "object" + }, + "type": "array" + }, + { + "properties": { + "action": { + "enum": [ + "proceed" + ], + "type": "string" + } + }, + "required": [ + "action" + ], + "type": "object" + }, + { + "properties": { + "action": { + "enum": [ + "confirm" + ], + "type": "string" + }, + "details": { + "items": { + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "label", + "value" + ], + "type": "object" + }, + "type": "array" + }, + "message": { + "minLength": 1, + "type": "string" + }, + "reason": { + "enum": [ + "warning", + "unavailable" + ], + "type": "string" + } + }, + "required": [ + "action", + "reason", + "message" + ], + "type": "object" + } + ] } }, "type": "object" } } }, - "description": "Query estimation result" + "description": "Formatted manual estimate, or a warning decision when check_warning is true. The action is proceed when this check does not require confirmation. A confirm action includes a warning or unavailable reason, a nonempty message, and optional plain-text label/value details." }, "400": { "$ref": "#/components/responses/400" diff --git a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts index c55045a80e3d..37e49c6d21c3 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts +++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts @@ -52,6 +52,7 @@ export enum FeatureFlag { EnableTemplateProcessing = 'ENABLE_TEMPLATE_PROCESSING', EscapeMarkdownHtml = 'ESCAPE_MARKDOWN_HTML', EstimateQueryCost = 'ESTIMATE_QUERY_COST', + SqlLabQueryCostWarnings = 'SQLLAB_QUERY_COST_WARNINGS', FabApiKeyEnabled = 'FAB_API_KEY_ENABLED', FilterBarClosedByDefault = 'FILTERBAR_CLOSED_BY_DEFAULT', GlobalAsyncQueries = 'GLOBAL_ASYNC_QUERIES', diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.ts b/superset-frontend/src/SqlLab/actions/sqlLab.ts index 632fafb1eff5..e5b6cde2c994 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.ts +++ b/superset-frontend/src/SqlLab/actions/sqlLab.ts @@ -566,34 +566,31 @@ export function runQuery( }; } -export function runQueryFromSqlEditor( +/** Build one request for both the warning check and execution after confirmation. */ +export function buildSqlEditorQuery( database: Database | null, queryEditor: QueryEditor, defaultQueryLimit: number, tempTable?: string, ctas?: boolean, ctasMethod?: string, -): SqlLabThunkAction { - return function (dispatch: AppDispatch, getState: GetState) { - const qe = getUpToDateQuery(getState(), queryEditor, queryEditor.id); - const query: Query = { - id: nanoid(11), - dbId: qe.dbId, - sql: qe.selectedText || qe.sql, - sqlEditorId: qe.tabViewId ?? qe.id, - sqlEditorImmutableId: qe.immutableId, - tab: qe.name, - catalog: qe.catalog, - schema: qe.schema, - tempTable, - templateParams: qe.templateParams, - queryLimit: qe.queryLimit || defaultQueryLimit, - runAsync: database ? database.allow_run_async : false, - ctas, - ctas_method: ctasMethod, - updateTabState: !qe.selectedText, - }; - dispatch(runQuery(query)); +): Query { + return { + id: nanoid(11), + dbId: queryEditor.dbId, + sql: queryEditor.selectedText || queryEditor.sql, + sqlEditorId: queryEditor.tabViewId ?? queryEditor.id, + sqlEditorImmutableId: queryEditor.immutableId, + tab: queryEditor.name, + catalog: queryEditor.catalog, + schema: queryEditor.schema, + tempTable, + templateParams: queryEditor.templateParams, + queryLimit: queryEditor.queryLimit || defaultQueryLimit, + runAsync: database ? database.allow_run_async : false, + ctas, + ctas_method: ctasMethod, + updateTabState: !queryEditor.selectedText, }; } @@ -848,9 +845,11 @@ export function addNewQueryEditor(): SqlLabThunkAction { }; } +/** Clone with existing effective settings and optional deliberate-run intent. */ export function cloneQueryToNewTab( query: Query, autorun: boolean, + autorunSource?: QueryEditor['autorunSource'], ): SqlLabThunkAction { return function (dispatch: AppDispatch, getState: GetState) { const state = getState(); @@ -868,6 +867,7 @@ export function cloneQueryToNewTab( catalog: query.catalog, schema: query.schema ?? undefined, autorun, + ...(autorunSource && { autorunSource }), sql: query.sql, queryLimit: sourceQueryEditor.queryLimit, templateParams: sourceQueryEditor.templateParams, diff --git a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx index d97a264a2dee..96f101521ef4 100644 --- a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx +++ b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx @@ -153,7 +153,7 @@ const QueryTable = ({ }; const openQueryInNewTab = (query: QueryResponse) => { - dispatch(cloneQueryToNewTab(query, true)); + dispatch(cloneQueryToNewTab(query, true, 'history')); }; const statusAttributes = { diff --git a/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx b/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx index 59dceabec807..89c0f421d731 100644 --- a/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx +++ b/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx @@ -185,6 +185,39 @@ test('renders the caret icon with a color that contrasts with the primary button expect(caretIcon).toHaveStyle({ color: supersetTheme.colorTextLightSolid }); }); +test('pending checks use the existing Stop button without extra controls', () => { + const runQuery = jest.fn(); + const stopQuery = jest.fn(); + const { getByRole, queryByRole } = setup( + { queryState: 'pending', runQuery, stopQuery }, + mockStore(initialState), + ); + const stop = getByRole('button', { name: /Stop$/ }); + expect(stop).toBeEnabled(); + expect(queryByRole('button', { name: 'Cancel' })).not.toBeInTheDocument(); + fireEvent.click(stop); + expect(runQuery).not.toHaveBeenCalled(); + expect(stopQuery).toHaveBeenCalledTimes(1); +}); + +test('confirmation disables Run without adding extra toolbar labels', () => { + const { getByRole } = setup( + { awaitingConfirmation: true }, + mockStore(initialState), + ); + expect(getByRole('button', { name: /Run$/ })).toBeDisabled(); +}); + +test('Stop still stops an already running query', () => { + const stopQuery = jest.fn(); + const { getByRole } = setup( + { queryState: 'running', stopQuery }, + mockStore(initialState), + ); + fireEvent.click(getByRole('button', { name: /Stop$/ })); + expect(stopQuery).toHaveBeenCalledTimes(1); +}); + const renderGestureButton = (props: Partial) => { const runQuery = jest.fn(); const stopQuery = jest.fn(); diff --git a/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx b/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx index 79c6ef08f742..f032c032bab2 100644 --- a/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx +++ b/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx @@ -46,6 +46,7 @@ export interface RunQueryActionButtonProps { runQuery: () => void; stopQuery: () => void; overlayCreateAsMenu: ReactElement | null; + awaitingConfirmation?: boolean; } const buildTextAndIcon = ( @@ -73,7 +74,7 @@ const onClick = ( isStopAction: boolean, runQuery: () => void = () => undefined, stopQuery = () => {}, - logAction: (name: string, payload: Record) => void, + logAction: (name: string, payload: Record) => void, ): void => { const eventName = isStopAction ? LOG_ACTIONS_SQLLAB_STOP_QUERY @@ -110,6 +111,7 @@ const RunQueryActionButton = ({ overlayCreateAsMenu, runQuery, stopQuery, + awaitingConfirmation = false, }: RunQueryActionButtonProps) => { const theme = useTheme(); const logAction = useLogAction({ queryEditorId }); @@ -131,14 +133,18 @@ const RunQueryActionButton = ({ onClick(shouldShowStopBtn, runQuery, stopQuery, logAction), ); - const ButtonComponent: FC = overlayCreateAsMenu + const showCreateAsMenu = overlayCreateAsMenu && !awaitingConfirmation; + const ButtonComponent: FC = showCreateAsMenu ? (DropdownButton as FC) : Button; const sqlContent = selectedText || sql || ''; - const isDisabled = !sqlContent - ?.replace(/(\/\*[^*]*\*\/)|(\/\/[^*]*)|(--[^.].*)/gm, '') - .trim(); + const isDisabled = + !shouldShowStopBtn && + (awaitingConfirmation || + !sqlContent + ?.replace(/(\/\*[^*]*\*\/)|(\/\/[^*]*)|(--[^.].*)/gm, '') + .trim()); const stopButtonTooltipText = useMemo( () => @@ -174,7 +180,7 @@ const RunQueryActionButton = ({ : t('Run query (Ctrl + Return)'))) as string } cta - {...(overlayCreateAsMenu + {...(showCreateAsMenu ? { popupRender: () => overlayCreateAsMenu, icon: ( @@ -195,7 +201,7 @@ const RunQueryActionButton = ({ icon, })} > - {overlayCreateAsMenu && <>{icon}} + {showCreateAsMenu && <>{icon}} {text} diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/QueryCostWarningDialog.test.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/QueryCostWarningDialog.test.tsx new file mode 100644 index 000000000000..8492e379f1da --- /dev/null +++ b/superset-frontend/src/SqlLab/components/SqlEditor/QueryCostWarningDialog.test.tsx @@ -0,0 +1,103 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { fireEvent, render } from 'spec/helpers/testing-library'; +import QueryCostWarningDialog from './QueryCostWarningDialog'; +import type { QueryWarningConfirmation } from './useQueryCostWarning'; + +const confirmation: QueryWarningConfirmation = { + action: 'confirm', + reason: 'warning', + message: 'Above threshold', + details: [ + { + label: 'Estimated usage', + value: 'At least 9,007,199,254,740,993,123 bytes', + }, + { label: 'Warning threshold', value: '9,007,199,254,740,991 bytes' }, + ], +}; + +test('displays server detail strings exactly with explicit actions', () => { + const onCancel = jest.fn(); + const onContinue = jest.fn(); + const { getByText, getByRole } = render( + , + ); + expect(getByRole('dialog', { name: 'Query warning' })).toBeInTheDocument(); + expect( + getByText('At least 9,007,199,254,740,993,123 bytes'), + ).toBeInTheDocument(); + expect(getByText('9,007,199,254,740,991 bytes')).toBeInTheDocument(); + fireEvent.click(getByRole('button', { name: 'Cancel' })); + fireEvent.click(getByRole('button', { name: 'Run anyway' })); + expect(onCancel).toHaveBeenCalledTimes(1); + expect(onContinue).toHaveBeenCalledTimes(1); +}); + +test('upper-bound detail strings retain the server accuracy wording', () => { + const { getByText, queryByText } = render( + {}} + onContinue={() => {}} + />, + ); + expect(getByText('Up to 1,001 bytes')).toBeInTheDocument(); + expect(queryByText(/At least/)).not.toBeInTheDocument(); +}); + +test('unavailable assessments show their message without requiring numeric details', () => { + const { getByText, getByRole } = render( + {}} + onContinue={() => {}} + />, + ); + expect(getByText('Templates cannot be assessed')).toBeInTheDocument(); + expect(getByRole('button', { name: 'Run anyway' })).toBeInTheDocument(); +}); + +test('message and detail rows are plain text, including markup', () => { + const { getByText, container } = render( + Review this query', + details: [{ label: 'Notice', value: '' }], + }} + onCancel={() => {}} + onContinue={() => {}} + />, + ); + expect(getByText('Review this query')).toBeInTheDocument(); + expect(getByText('')).toBeInTheDocument(); + expect(container.querySelector('img')).toBeNull(); +}); diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/QueryCostWarningDialog.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/QueryCostWarningDialog.tsx new file mode 100644 index 000000000000..8bdf8e09d575 --- /dev/null +++ b/superset-frontend/src/SqlLab/components/SqlEditor/QueryCostWarningDialog.tsx @@ -0,0 +1,63 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { t } from '@apache-superset/core/translation'; +import { useTheme } from '@apache-superset/core/theme'; +import { ConfirmModal, Icons } from '@superset-ui/core/components'; +import type { QueryWarningConfirmation } from './useQueryCostWarning'; + +/** Display the server's plain-text assessment without interpreting its units. */ +export default function QueryCostWarningDialog({ + confirmation, + onCancel, + onContinue, +}: { + confirmation?: QueryWarningConfirmation; + onCancel: () => void; + onContinue: () => void; +}) { + const theme = useTheme(); + return ( +