From 7ffd12c05cbd6b5ceef5625f66116218add81d78 Mon Sep 17 00:00:00 2001 From: "J.C. Zhong" Date: Fri, 22 Nov 2024 15:26:33 -0800 Subject: [PATCH] feat: add table sample rate to user settings (#1521) * feat: add table sample rate to user settings * fix linter * add comment --- package.json | 2 +- querybook/config/user_setting.yaml | 6 +++ .../DataDocQueryCell/DataDocQueryCell.tsx | 13 ++++--- .../StatementResult.tsx | 4 +- .../DataDocTableSamplingInfo.tsx | 4 +- .../QueryCellTitle/QueryCellTitle.tsx | 8 +--- .../QueryComposer/QueryComposer.tsx | 2 +- .../components/QueryExecution/QueryError.tsx | 20 ++++------ .../QueryExecution/SamplingToolTip.tsx | 5 +-- .../QueryRunButton/QueryRunButton.tsx | 32 +++++++-------- .../components/Search/SearchOverview.tsx | 7 +--- .../UserSettingsMenu/UserSettingsMenu.tsx | 28 +++++++++---- querybook/webapp/lib/public-config.ts | 39 +++++++++++++++++++ querybook/webapp/ui/Select/Select.tsx | 5 ++- 14 files changed, 110 insertions(+), 65 deletions(-) create mode 100644 querybook/webapp/lib/public-config.ts diff --git a/package.json b/package.json index b12bc9117..f41744c41 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "querybook", - "version": "3.37.0", + "version": "3.37.1", "description": "A Big Data Webapp", "private": true, "scripts": { diff --git a/querybook/config/user_setting.yaml b/querybook/config/user_setting.yaml index 56c56f551..c41d8aac0 100644 --- a/querybook/config/user_setting.yaml +++ b/querybook/config/user_setting.yaml @@ -50,6 +50,12 @@ show_full_view: - disabled helper: Instead of modal, show full view when opening table/execution/snippet +table_sample_rate: + default: '' + tab: general + options: [] + helper: The sample rate will be pre-selected when a table supports sampling. + editor_font_size: default: medium tab: editor diff --git a/querybook/webapp/components/DataDocQueryCell/DataDocQueryCell.tsx b/querybook/webapp/components/DataDocQueryCell/DataDocQueryCell.tsx index 7fd64323b..707b612bf 100644 --- a/querybook/webapp/components/DataDocQueryCell/DataDocQueryCell.tsx +++ b/querybook/webapp/components/DataDocQueryCell/DataDocQueryCell.tsx @@ -24,7 +24,6 @@ import { QuerySnippetInsertionModal } from 'components/QuerySnippetInsertionModa import { TemplatedQueryView } from 'components/TemplateQueryView/TemplatedQueryView'; import { TranspileQueryModal } from 'components/TranspileQueryModal/TranspileQueryModal'; import { UDFForm } from 'components/UDFForm/UDFForm'; -import PublicConfig from 'config/querybook_public_config.yaml'; import { ComponentType, ElementType } from 'const/analytics'; import { IDataQueryCellMeta, @@ -37,6 +36,7 @@ import { SurveySurfaceType } from 'const/survey'; import { triggerSurvey } from 'hooks/ui/useSurveyTrigger'; import { trackClick } from 'lib/analytics'; import CodeMirror from 'lib/codemirror'; +import { isAIFeatureEnabled } from 'lib/public-config'; import { getQueryAsExplain } from 'lib/sql-helper/sql-lexer'; import { DEFAULT_ROW_LIMIT } from 'lib/sql-helper/sql-limiter'; import { getPossibleTranspilers } from 'lib/templated-query/transpile'; @@ -65,8 +65,6 @@ import { ErrorQueryCell } from './ErrorQueryCell'; import './DataDocQueryCell.scss'; -const AIAssistantConfig = PublicConfig.ai_assistant; - const ON_CHANGE_DEBOUNCE_MS = 500; const FORMAT_QUERY_SHORTCUT = getShortcutSymbols( KeyMap.queryEditor.formatQuery.key @@ -221,8 +219,11 @@ class DataDocQueryCellComponent extends React.PureComponent { } public get sampleRate() { - // -1 for tables don't support sampling, 0 for default sample rate (which means disable sampling) - return this.hasSamplingTables ? this.state.meta.sample_rate ?? 0 : -1; + // -1 for tables don't support sampling + const sampleRate = this.hasSamplingTables + ? this.state.meta.sample_rate + : -1; + return sampleRate; } @decorate(memoizeOne) @@ -809,7 +810,7 @@ class DataDocQueryCellComponent extends React.PureComponent { {this.getAdditionalDropDownButtonDOM()} - {AIAssistantConfig.enabled && isEditable && ( + {isAIFeatureEnabled() && isEditable && ( (Full Result, diff --git a/querybook/webapp/components/DataDocTableSamplingInfo/DataDocTableSamplingInfo.tsx b/querybook/webapp/components/DataDocTableSamplingInfo/DataDocTableSamplingInfo.tsx index 2e4de3a98..552a7c7e2 100644 --- a/querybook/webapp/components/DataDocTableSamplingInfo/DataDocTableSamplingInfo.tsx +++ b/querybook/webapp/components/DataDocTableSamplingInfo/DataDocTableSamplingInfo.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { QueryComparison } from 'components/TranspileQueryModal/QueryComparison'; -import PublicConfig from 'config/querybook_public_config.yaml'; import { ISamplingTables } from 'const/datadoc'; import { useResource } from 'hooks/useResource'; +import { TABLE_SAMPLING_CONFIG } from 'lib/public-config'; import { formatError } from 'lib/utils/error'; import { QueryTransformResource } from 'resource/queryTransform'; import { Link } from 'ui/Link/Link'; @@ -27,7 +27,7 @@ export const DataDocTableSamplingInfo: React.FC = ({ onHide, }) => { const sampleUserGuideLink = - PublicConfig.table_sampling?.sample_user_guide_link ?? ''; + TABLE_SAMPLING_CONFIG.sample_user_guide_link ?? ''; const { data: sampledQuery, diff --git a/querybook/webapp/components/QueryCellTitle/QueryCellTitle.tsx b/querybook/webapp/components/QueryCellTitle/QueryCellTitle.tsx index b1edc97f1..41e693188 100644 --- a/querybook/webapp/components/QueryCellTitle/QueryCellTitle.tsx +++ b/querybook/webapp/components/QueryCellTitle/QueryCellTitle.tsx @@ -1,17 +1,15 @@ import React, { useCallback, useEffect, useState } from 'react'; -import PublicConfig from 'config/querybook_public_config.yaml'; import { AICommandType } from 'const/aiAssistant'; import { ComponentType, ElementType } from 'const/analytics'; import { useAISocket } from 'hooks/useAISocket'; import { trackClick } from 'lib/analytics'; +import { isAIFeatureEnabled } from 'lib/public-config'; import { IconButton } from 'ui/Button/IconButton'; import { ResizableTextArea } from 'ui/ResizableTextArea/ResizableTextArea'; import './QueryCellTitle.scss'; -const AIAssistantConfig = PublicConfig.ai_assistant; - interface IQueryCellTitleProps { cellId: number; value: string; @@ -30,9 +28,7 @@ export const QueryCellTitle: React.FC = ({ forceSaveQuery, }) => { const titleGenerationEnabled = - AIAssistantConfig.enabled && - AIAssistantConfig.query_title_generation.enabled && - query; + isAIFeatureEnabled('query_title_generation') && query; const [title, setTitle] = useState(''); const socket = useAISocket(AICommandType.SQL_TITLE, ({ data }) => { diff --git a/querybook/webapp/components/QueryComposer/QueryComposer.tsx b/querybook/webapp/components/QueryComposer/QueryComposer.tsx index da7e76c06..38d930437 100644 --- a/querybook/webapp/components/QueryComposer/QueryComposer.tsx +++ b/querybook/webapp/components/QueryComposer/QueryComposer.tsx @@ -167,7 +167,7 @@ const useTableSampleRate = ( samplingTables: Record ) => { const sampleRate = useSelector( - (state: IStoreState) => state.adhocQuery[environmentId]?.sampleRate ?? 0 + (state: IStoreState) => state.adhocQuery[environmentId]?.sampleRate ); const setSampleRate = useCallback( (newSampleRate: number) => diff --git a/querybook/webapp/components/QueryExecution/QueryError.tsx b/querybook/webapp/components/QueryExecution/QueryError.tsx index 4b85ab55f..d15ae8daf 100644 --- a/querybook/webapp/components/QueryExecution/QueryError.tsx +++ b/querybook/webapp/components/QueryExecution/QueryError.tsx @@ -3,7 +3,6 @@ import { useDispatch, useSelector } from 'react-redux'; import { AutoFixButton } from 'components/AIAssistant/AutoFixButton'; import { ErrorSuggestion } from 'components/DataDocStatementExecution/ErrorSuggestion'; -import PublicConfig from 'config/querybook_public_config.yaml'; import { IQueryEngine } from 'const/queryEngine'; import { IQueryError, @@ -11,6 +10,7 @@ import { IStatementExecution, QueryExecutionErrorType, } from 'const/queryExecution'; +import { isAIFeatureEnabled } from 'lib/public-config'; import { getQueryLinePosition, IToken, @@ -31,8 +31,6 @@ import { ExecutedQueryCell } from './ExecutedQueryCell'; import './QueryError.scss'; -const AIAssistantConfig = PublicConfig.ai_assistant; - interface IProps { queryEngine: IQueryEngine; queryError: IQueryError; @@ -186,15 +184,13 @@ export const QueryError: React.FunctionComponent = ({ {errorTitle} - {!readonly && - AIAssistantConfig.enabled && - AIAssistantConfig.query_auto_fix.enabled && ( - - )} + {!readonly && isAIFeatureEnabled('query_auto_fix') && ( + + )} ); diff --git a/querybook/webapp/components/QueryExecution/SamplingToolTip.tsx b/querybook/webapp/components/QueryExecution/SamplingToolTip.tsx index fadaee97c..e41979f5c 100644 --- a/querybook/webapp/components/QueryExecution/SamplingToolTip.tsx +++ b/querybook/webapp/components/QueryExecution/SamplingToolTip.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useState } from 'react'; import { SamplingInfoButton } from 'components/QueryRunButton/QueryRunButton'; -import PublicConfig from 'config/querybook_public_config.yaml'; import { IQueryExecution, QueryExecutionStatus } from 'const/queryExecution'; +import { TABLE_SAMPLING_CONFIG } from 'lib/public-config'; import { Message } from 'ui/Message/Message'; import { AccentText } from 'ui/StyledText/StyledText'; @@ -21,8 +21,7 @@ export const SamplingTooltip: React.FC = ({ hasSamplingTables, sampleRate, }) => { - const { enabled, sampling_tool_tip_delay: delay } = - PublicConfig.table_sampling; + const { enabled, sampling_tool_tip_delay: delay } = TABLE_SAMPLING_CONFIG; const [showSamplingTip, setShowSamplingTip] = useState(false); diff --git a/querybook/webapp/components/QueryRunButton/QueryRunButton.tsx b/querybook/webapp/components/QueryRunButton/QueryRunButton.tsx index 0ce56e8a5..0ed94d019 100644 --- a/querybook/webapp/components/QueryRunButton/QueryRunButton.tsx +++ b/querybook/webapp/components/QueryRunButton/QueryRunButton.tsx @@ -3,11 +3,14 @@ import * as React from 'react'; import { useState } from 'react'; import { useSelector } from 'react-redux'; -import PublicConfig from 'config/querybook_public_config.yaml'; import { IQueryEngine, QueryEngineStatus } from 'const/queryEngine'; import { queryEngineStatusToIconStatus } from 'const/queryStatusIcon'; import { TooltipDirection } from 'const/tooltip'; import { MIN_ENGINE_TO_SHOW_FILTER } from 'const/uiConfig'; +import { + getTableSamplingRateOptions, + TABLE_SAMPLING_CONFIG, +} from 'lib/public-config'; import { ALLOW_UNLIMITED_QUERY, DEFAULT_ROW_LIMIT, @@ -17,6 +20,7 @@ import { getShortcutSymbols, KeyMap } from 'lib/utils/keyboard'; import { stopPropagation } from 'lib/utils/noop'; import { formatNumber } from 'lib/utils/number'; import { queryEngineStatusByIdEnvSelector } from 'redux/queryEngine/selector'; +import { IStoreState } from 'redux/store/types'; import { AsyncButton, IAsyncButtonHandles } from 'ui/AsyncButton/AsyncButton'; import { IconButton } from 'ui/Button/IconButton'; import { Dropdown } from 'ui/Dropdown/Dropdown'; @@ -287,29 +291,23 @@ const QueryLimitSelector: React.FC<{ ); }; -const TABLE_SAMPLING_CONFIG = PublicConfig.table_sampling ?? { - enabled: false, - sample_rates: [], - default_sample_rate: 0, -}; -const sampleRateOptions = [{ label: 'none', value: 0 }].concat( - TABLE_SAMPLING_CONFIG.sample_rates.map((value) => ({ - label: value + '%', - value, - })) -); -const DEFAULT_SAMPLE_RATE = TABLE_SAMPLING_CONFIG.default_sample_rate; const TableSamplingSelector: React.FC<{ - sampleRate: number; + sampleRate: number | undefined; setSampleRate: (sampleRate: number) => void; tooltipPos: TooltipDirection; onTableSamplingInfoClick: () => void; }> = ({ sampleRate, setSampleRate, tooltipPos, onTableSamplingInfoClick }) => { + const sampleRateOptions = React.useMemo(getTableSamplingRateOptions, []); + const userDefaultTableSampleRate = useSelector( + (state: IStoreState) => state.user.computedSettings['table_sample_rate'] + ); + React.useEffect(() => { - if (!sampleRateOptions.some((option) => option.value === sampleRate)) { - setSampleRate(DEFAULT_SAMPLE_RATE); + // If it is a new cell without the sample rate selected, use the default sample rate from user settings + if (sampleRate === undefined) { + setSampleRate(parseFloat(userDefaultTableSampleRate)); } - }, [sampleRate, setSampleRate]); + }, [sampleRate, setSampleRate, userDefaultTableSampleRate]); const selectedSampleRateText = React.useMemo(() => { if (sampleRate > 0) { diff --git a/querybook/webapp/components/Search/SearchOverview.tsx b/querybook/webapp/components/Search/SearchOverview.tsx index 67b07eab6..6f4676d3c 100644 --- a/querybook/webapp/components/Search/SearchOverview.tsx +++ b/querybook/webapp/components/Search/SearchOverview.tsx @@ -6,7 +6,6 @@ import CreatableSelect from 'react-select/creatable'; import { UserAvatar } from 'components/UserBadge/UserAvatar'; import { UserSelect } from 'components/UserSelect/UserSelect'; -import PublicConfig from 'config/querybook_public_config.yaml'; import { ComponentType, ElementType } from 'const/analytics'; import { IBoardPreview, @@ -19,6 +18,7 @@ import { useShallowSelector } from 'hooks/redux/useShallowSelector'; import { useSurveyTrigger } from 'hooks/ui/useSurveyTrigger'; import { useTrackView } from 'hooks/useTrackView'; import { trackClick, trackView } from 'lib/analytics'; +import { isAIFeatureEnabled } from 'lib/public-config'; import { titleize } from 'lib/utils'; import { getCurrentEnv } from 'lib/utils/query-string'; import { @@ -69,8 +69,6 @@ import { TableSelect } from './TableSelect'; import './SearchOverview.scss'; -const AIAssistantConfig = PublicConfig.ai_assistant; - const userReactSelectStyle = makeReactSelectStyle( true, miniAsyncReactSelectStyles @@ -314,8 +312,7 @@ export const SearchOverview: React.FC = ({ autoFocus /> {searchType === SearchType.Table && - AIAssistantConfig.enabled && - AIAssistantConfig.table_vector_search.enabled && ( + isAIFeatureEnabled('table_vector_search') && (
Natural Language Search diff --git a/querybook/webapp/components/UserSettingsMenu/UserSettingsMenu.tsx b/querybook/webapp/components/UserSettingsMenu/UserSettingsMenu.tsx index 4b599a924..b3b67b84d 100644 --- a/querybook/webapp/components/UserSettingsMenu/UserSettingsMenu.tsx +++ b/querybook/webapp/components/UserSettingsMenu/UserSettingsMenu.tsx @@ -2,8 +2,12 @@ import React, { useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { UserSettingsTab } from 'components/EnvironmentAppRouter/modalRoute/UserSettingsMenuRoute'; -import PublicConfig from 'config/querybook_public_config.yaml'; import userSettingConfig from 'config/user_setting.yaml'; +import { + getTableSamplingRateOptions, + isAIFeatureEnabled, + TABLE_SAMPLING_CONFIG, +} from 'lib/public-config'; import { titleize } from 'lib/utils'; import { availableEnvironmentsSelector } from 'redux/environment/selector'; import { notificationServiceSelector } from 'redux/notificationService/selector'; @@ -14,8 +18,6 @@ import { makeSelectOptions, Select } from 'ui/Select/Select'; import './UserSettingsMenu.scss'; -const AIAssistantConfig = PublicConfig.ai_assistant; - export const UserSettingsMenu: React.FC<{ tab: UserSettingsTab }> = ({ tab, }) => { @@ -41,9 +43,7 @@ export const UserSettingsMenu: React.FC<{ tab: UserSettingsTab }> = ({ Object.entries(userSettingConfig).filter(([key, value]) => { if (key === 'sql_complete') { return ( - AIAssistantConfig.enabled && - AIAssistantConfig.sql_complete.enabled && - value.tab === tab + isAIFeatureEnabled('sql_complete') && value.tab === tab ); } return value.tab === tab; @@ -81,6 +81,9 @@ export const UserSettingsMenu: React.FC<{ tab: UserSettingsTab }> = ({ return makeSelectOptions( notifiers.map((notifier) => notifier.name) ); + } else if (key === 'table_sample_rate') { + const options = getTableSamplingRateOptions(); + return makeSelectOptions(options); } return makeSelectOptions(userSettingConfig[key].options); }, @@ -100,9 +103,18 @@ export const UserSettingsMenu: React.FC<{ tab: UserSettingsTab }> = ({ [userSettingByKey, setUserSettings, getRawKey] ); + const getValueByKey = (key: string) => { + let defaultValue = userSettingConfig[key].default; + + if (key === 'table_sample_rate') { + defaultValue = TABLE_SAMPLING_CONFIG.default_sample_rate.toString(); + } + + return userSettingByKey[getRawKey(key)] ?? defaultValue; + }; + const makeFieldByKey = (key: string) => { - const value = - userSettingByKey[getRawKey(key)] ?? userSettingConfig[key].default; + const value = getValueByKey(key); const formField = ( <> diff --git a/querybook/webapp/lib/public-config.ts b/querybook/webapp/lib/public-config.ts new file mode 100644 index 000000000..59b26f61e --- /dev/null +++ b/querybook/webapp/lib/public-config.ts @@ -0,0 +1,39 @@ +import PublicConfig from 'config/querybook_public_config.yaml'; + +export const isAIFeatureEnabled = ( + featureKey?: + | 'query_title_generation' + | 'query_generation' + | 'query_auto_fix' + | 'table_vector_search' + | 'sql_complete' +): boolean => { + const aiAssistantConfig = PublicConfig.ai_assistant; + if (!featureKey) { + return aiAssistantConfig.enabled; + } + return aiAssistantConfig.enabled && aiAssistantConfig[featureKey].enabled; +}; + +export const TABLE_SAMPLING_CONFIG = PublicConfig.table_sampling ?? { + enabled: false, + sample_rates: [], + default_sample_rate: 0, + sample_user_guide_link: '', + sampling_tool_tip_delay: 0, +}; + +export const getTableSamplingRateOptions = () => { + const sampleRates = TABLE_SAMPLING_CONFIG.sample_rates; + + // add the none option + if (!sampleRates.includes(0)) { + sampleRates.unshift(0); + } + + return sampleRates.map((rate) => ({ + key: rate, + value: rate, + label: rate === 0 ? 'none' : rate + '%', + })); +}; diff --git a/querybook/webapp/ui/Select/Select.tsx b/querybook/webapp/ui/Select/Select.tsx index 46a829b54..25efd7a0e 100644 --- a/querybook/webapp/ui/Select/Select.tsx +++ b/querybook/webapp/ui/Select/Select.tsx @@ -72,7 +72,8 @@ export const Select: React.FunctionComponent = ({ export type IOptions = Array< | { key: any; - value: string; + label?: string; + value: string | number; hidden?: boolean; } | string @@ -92,7 +93,7 @@ export function makeSelectOptions(options: IOptions) { key={option.key} hidden={option.hidden} > - {option.value} + {option.label ?? option.value} ) );