Allow query result view to be toggled from command bar (#1833)

* allow query result view to be toggled from command bar

also provides a default results view option that's stored in the
browser's local storage

* update SettingsPane test snapshot
This commit is contained in:
Ashley Stanton-Nurse 2024-06-05 12:16:28 -07:00 committed by GitHub
parent 7002da0b51
commit 9b12775151
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 172 additions and 11 deletions

View File

@ -31,7 +31,7 @@ export interface CommandButtonComponentProps {
/** /**
* Click handler for command button click * Click handler for command button click
*/ */
onCommandClick: (e: React.SyntheticEvent | KeyboardEvent) => void; onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent) => void;
/** /**
* Label for the button * Label for the button

View File

@ -60,14 +60,16 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined, imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
iconName: btn.iconName, iconName: btn.iconName,
}, },
onClick: (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => { onClick: btn.onCommandClick
btn.onCommandClick(ev); ? (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
let copilotEnabled = false; btn.onCommandClick(ev);
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) { let copilotEnabled = false;
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution; if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
} copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label, copilotEnabled }); }
}, TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label, copilotEnabled });
}
: undefined,
key: `${btn.commandButtonLabel}${index}`, key: `${btn.commandButtonLabel}${index}`,
text: label, text: label,
"data-test": label, "data-test": label,

View File

@ -9,12 +9,14 @@ import {
Toggle, Toggle,
} from "@fluentui/react"; } from "@fluentui/react";
import * as Constants from "Common/Constants"; import * as Constants from "Common/Constants";
import { SplitterDirection } from "Common/Splitter";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { configContext } from "ConfigContext"; import { configContext } from "ConfigContext";
import { import {
DefaultRUThreshold, DefaultRUThreshold,
LocalStorageUtility, LocalStorageUtility,
StorageKey, StorageKey,
getDefaultQueryResultsView,
getRUThreshold, getRUThreshold,
ruThresholdEnabled as isRUThresholdEnabled, ruThresholdEnabled as isRUThresholdEnabled,
} from "Shared/StorageUtility"; } from "Shared/StorageUtility";
@ -47,6 +49,9 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled), LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
); );
const [queryTimeout, setQueryTimeout] = useState<number>(LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout)); const [queryTimeout, setQueryTimeout] = useState<number>(LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout));
const [defaultQueryResultsView, setDefaultQueryResultsView] = useState<SplitterDirection>(
getDefaultQueryResultsView(),
);
const [automaticallyCancelQueryAfterTimeout, setAutomaticallyCancelQueryAfterTimeout] = useState<boolean>( const [automaticallyCancelQueryAfterTimeout, setAutomaticallyCancelQueryAfterTimeout] = useState<boolean>(
LocalStorageUtility.getEntryBoolean(StorageKey.AutomaticallyCancelQueryAfterTimeout), LocalStorageUtility.getEntryBoolean(StorageKey.AutomaticallyCancelQueryAfterTimeout),
); );
@ -121,6 +126,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism); LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString()); LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString());
LocalStorageUtility.setEntryString(StorageKey.CopilotSampleDBEnabled, copilotSampleDBEnabled.toString()); LocalStorageUtility.setEntryString(StorageKey.CopilotSampleDBEnabled, copilotSampleDBEnabled.toString());
LocalStorageUtility.setEntryString(StorageKey.DefaultQueryResultsView, defaultQueryResultsView);
if (shouldShowGraphAutoVizOption) { if (shouldShowGraphAutoVizOption) {
LocalStorageUtility.setEntryBoolean( LocalStorageUtility.setEntryBoolean(
@ -197,6 +203,11 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{ key: Constants.PriorityLevel.High, text: "High" }, { key: Constants.PriorityLevel.High, text: "High" },
]; ];
const defaultQueryResultsViewOptionList: IChoiceGroupOption[] = [
{ key: SplitterDirection.Vertical, text: "Vertical" },
{ key: SplitterDirection.Horizontal, text: "Horizontal" },
];
const handleOnPriorityLevelOptionChange = ( const handleOnPriorityLevelOptionChange = (
ev: React.FormEvent<HTMLInputElement>, ev: React.FormEvent<HTMLInputElement>,
option: IChoiceGroupOption, option: IChoiceGroupOption,
@ -234,6 +245,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
} }
}; };
const handleOnDefaultQueryResultsViewChange = (
ev: React.MouseEvent<HTMLElement>,
option: IChoiceGroupOption,
): void => {
setDefaultQueryResultsView(option.key as SplitterDirection);
};
const handleOnQueryRetryAttemptsSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => { const handleOnQueryRetryAttemptsSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => {
const retryAttempts = Number(newValue); const retryAttempts = Number(newValue);
if (!isNaN(retryAttempts)) { if (!isNaN(retryAttempts)) {
@ -438,6 +456,25 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
)} )}
</div> </div>
</div> </div>
<div className="settingsSection">
<div className="settingsSectionPart">
<div>
<legend id="defaultQueryResultsView" className="settingsSectionLabel legendLabel">
Default Query Results View
</legend>
<InfoTooltip>Select the default view to use when displaying query results.</InfoTooltip>
</div>
<div>
<ChoiceGroup
ariaLabelledBy="defaultQueryResultsView"
selectedKey={defaultQueryResultsView}
options={defaultQueryResultsViewOptionList}
styles={choiceButtonStyles}
onChange={handleOnDefaultQueryResultsViewChange}
/>
</div>
</div>
</div>
</> </>
)} )}
<div className="settingsSection"> <div className="settingsSection">

View File

@ -205,6 +205,67 @@ exports[`Settings Pane should render Default properly 1`] = `
</div> </div>
</div> </div>
</div> </div>
<div
className="settingsSection"
>
<div
className="settingsSectionPart"
>
<div>
<legend
className="settingsSectionLabel legendLabel"
id="defaultQueryResultsView"
>
Default Query Results View
</legend>
<InfoTooltip>
Select the default view to use when displaying query results.
</InfoTooltip>
</div>
<div>
<StyledChoiceGroup
ariaLabelledBy="defaultQueryResultsView"
onChange={[Function]}
options={
Array [
Object {
"key": "vertical",
"text": "Vertical",
},
Object {
"key": "horizontal",
"text": "Horizontal",
},
]
}
selectedKey="vertical"
styles={
Object {
"flexContainer": Array [
Object {
"selectors": Object {
".ms-ChoiceField": Object {
"marginTop": 0,
},
".ms-ChoiceField-wrapper label": Object {
"fontSize": 12,
"paddingTop": 0,
},
".ms-ChoiceFieldGroup root-133": Object {
"clear": "both",
},
},
},
],
"root": Object {
"clear": "both",
},
}
}
/>
</div>
</div>
</div>
<div <div
className="settingsSection" className="settingsSection"
> >

View File

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-console */ /* eslint-disable no-console */
import { FeedOptions, QueryOperationOptions } from "@azure/cosmos"; import { FeedOptions, QueryOperationOptions } from "@azure/cosmos";
import { SplitterDirection } from "Common/Splitter";
import { Platform, configContext } from "ConfigContext"; import { Platform, configContext } from "ConfigContext";
import { useDialog } from "Explorer/Controls/Dialog"; import { useDialog } from "Explorer/Controls/Dialog";
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal"; import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
@ -12,7 +13,13 @@ import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { KeyboardAction } from "KeyboardShortcuts"; import { KeyboardAction } from "KeyboardShortcuts";
import { QueryConstants } from "Shared/Constants"; import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility"; import {
LocalStorageUtility,
StorageKey,
getDefaultQueryResultsView,
getRUThreshold,
ruThresholdEnabled,
} from "Shared/StorageUtility";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { TabsState, useTabs } from "hooks/useTabs"; import { TabsState, useTabs } from "hooks/useTabs";
@ -25,6 +32,7 @@ import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
import DownloadQueryIcon from "../../../../images/DownloadQuery.svg"; import DownloadQueryIcon from "../../../../images/DownloadQuery.svg";
import CancelQueryIcon from "../../../../images/Entity_cancel.svg"; import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
import CheckIcon from "../../../../images/check-1.svg";
import SaveQueryIcon from "../../../../images/save-cosmos.svg"; import SaveQueryIcon from "../../../../images/save-cosmos.svg";
import { NormalizedEventKey } from "../../../Common/Constants"; import { NormalizedEventKey } from "../../../Common/Constants";
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
@ -103,6 +111,7 @@ interface IQueryTabStates {
cancelQueryTimeoutID: NodeJS.Timeout; cancelQueryTimeoutID: NodeJS.Timeout;
copilotActive: boolean; copilotActive: boolean;
currentTabActive: boolean; currentTabActive: boolean;
queryResultsView: SplitterDirection;
} }
export const QueryTabFunctionComponent = (props: IQueryTabComponentProps): any => { export const QueryTabFunctionComponent = (props: IQueryTabComponentProps): any => {
@ -147,6 +156,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
cancelQueryTimeoutID: undefined, cancelQueryTimeoutID: undefined,
copilotActive: this._queryCopilotActive(), copilotActive: this._queryCopilotActive(),
currentTabActive: true, currentTabActive: true,
queryResultsView: getDefaultQueryResultsView(),
}; };
this.isCloseClicked = false; this.isCloseClicked = false;
this.splitterId = this.props.tabId + "_splitter"; this.splitterId = this.props.tabId + "_splitter";
@ -508,9 +518,45 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
}); });
} }
buttons.push(this.createViewButtons());
return buttons; return buttons;
} }
private createViewButtons(): CommandButtonComponentProps {
const verticalButton: CommandButtonComponentProps = {
isSelected: this.state.queryResultsView === SplitterDirection.Vertical,
iconSrc: this.state.queryResultsView === SplitterDirection.Vertical ? CheckIcon : undefined,
commandButtonLabel: "Vertical",
ariaLabel: "Vertical",
onCommandClick: () => this._setViewLayout(SplitterDirection.Vertical),
hasPopup: false,
};
const horizontalButton: CommandButtonComponentProps = {
isSelected: this.state.queryResultsView === SplitterDirection.Horizontal,
iconSrc: this.state.queryResultsView === SplitterDirection.Horizontal ? CheckIcon : undefined,
commandButtonLabel: "Horizontal",
ariaLabel: "Horizontal",
onCommandClick: () => this._setViewLayout(SplitterDirection.Horizontal),
hasPopup: false,
};
return {
commandButtonLabel: "View",
ariaLabel: "View",
hasPopup: true,
children: [verticalButton, horizontalButton],
};
}
private _setViewLayout(direction: SplitterDirection): void {
this.setState({ queryResultsView: direction });
// We'll need to refresh the context buttons to update the selected state of the view buttons
setTimeout(() => {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}, 100);
}
private _toggleCopilot = (active: boolean) => { private _toggleCopilot = (active: boolean) => {
this.setState({ copilotActive: active }); this.setState({ copilotActive: active });
useQueryCopilot.getState().setCopilotEnabledforExecution(active); useQueryCopilot.getState().setCopilotEnabledforExecution(active);
@ -634,7 +680,12 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
></QueryCopilotPromptbar> ></QueryCopilotPromptbar>
)} )}
<div className="tabPaneContentContainer"> <div className="tabPaneContentContainer">
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}> <SplitterLayout
vertical={this.state.queryResultsView === SplitterDirection.Vertical}
primaryIndex={0}
primaryMinSize={100}
secondaryMinSize={200}
>
<Fragment> <Fragment>
<div className="queryEditor" style={{ height: "100%" }}> <div className="queryEditor" style={{ height: "100%" }}>
<EditorReact <EditorReact

View File

@ -1,3 +1,4 @@
import { SplitterDirection } from "Common/Splitter";
import * as LocalStorageUtility from "./LocalStorageUtility"; import * as LocalStorageUtility from "./LocalStorageUtility";
import * as SessionStorageUtility from "./SessionStorageUtility"; import * as SessionStorageUtility from "./SessionStorageUtility";
import * as StringUtility from "./StringUtility"; import * as StringUtility from "./StringUtility";
@ -27,6 +28,7 @@ export enum StorageKey {
GalleryCalloutDismissed, GalleryCalloutDismissed,
VisitedAccounts, VisitedAccounts,
PriorityLevel, PriorityLevel,
DefaultQueryResultsView,
} }
export const hasRUThresholdBeenConfigured = (): boolean => { export const hasRUThresholdBeenConfigured = (): boolean => {
@ -51,4 +53,12 @@ export const getRUThreshold = (): number => {
return DefaultRUThreshold; return DefaultRUThreshold;
}; };
export const getDefaultQueryResultsView = (): SplitterDirection => {
const defaultQueryResultsViewRaw = LocalStorageUtility.getEntryString(StorageKey.DefaultQueryResultsView);
if (defaultQueryResultsViewRaw === SplitterDirection.Horizontal) {
return SplitterDirection.Horizontal;
}
return SplitterDirection.Vertical;
};
export const DefaultRUThreshold = 5000; export const DefaultRUThreshold = 5000;