mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-04 00:31:41 +00:00
Compare commits
8 Commits
users/sind
...
ashleyst/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08d3318a87 | ||
|
|
8b6d857ddb | ||
|
|
2598760a11 | ||
|
|
44d886b4a0 | ||
|
|
5aa6b0abe1 | ||
|
|
f24b0bcf1b | ||
|
|
56408a97d7 | ||
|
|
0df68c4967 |
@@ -2296,6 +2296,17 @@ a:link {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.monaco-editor .quick-input-list-label {
|
||||
/* Restore some of Monaco's default styles that are clobbered by our global styles */
|
||||
padding: 0;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.monaco-editor .quick-input-list .highlight {
|
||||
/* Padding in highlighted text within the quick input list breaks the flow of the text */
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
td a {
|
||||
color: #393939;
|
||||
}
|
||||
|
||||
25
src/Common/KeyboardShortcuts.ts
Normal file
25
src/Common/KeyboardShortcuts.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { KeyMap } from "react-hotkeys";
|
||||
|
||||
export const keyMap: KeyMap = {
|
||||
NEW_QUERY: {
|
||||
name: "New Query",
|
||||
sequence: "ctrl+j",
|
||||
action: "keydown",
|
||||
},
|
||||
CANCEL_QUERY: {
|
||||
name: "Cancel Query",
|
||||
sequence: "f8",
|
||||
action: "keydown",
|
||||
},
|
||||
DISCARD: {
|
||||
name: "Discard Changes",
|
||||
sequence: "ctrl+x",
|
||||
action: "keydown"
|
||||
}
|
||||
};
|
||||
|
||||
export type KeyboardShortcutName = keyof typeof keyMap;
|
||||
|
||||
export type KeyboardShortcutHandlers = Partial<{
|
||||
[key in KeyboardShortcutName]: (keyEvent?: KeyboardEvent) => void;
|
||||
}>;
|
||||
@@ -53,6 +53,7 @@ export type FabricMessageV2 =
|
||||
id: string;
|
||||
message: {
|
||||
connectionId: string;
|
||||
isVisible: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
@@ -72,7 +73,7 @@ export type FabricMessageV2 =
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "setToolbarStatus";
|
||||
type: "explorerVisible";
|
||||
message: {
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* React component for Command button component.
|
||||
*/
|
||||
import { KeyboardShortcutName } from "Common/KeyboardShortcuts";
|
||||
import * as React from "react";
|
||||
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
@@ -30,7 +31,7 @@ export interface CommandButtonComponentProps {
|
||||
/**
|
||||
* Click handler for command button click
|
||||
*/
|
||||
onCommandClick: (e: React.SyntheticEvent) => void;
|
||||
onCommandClick: (e: React.SyntheticEvent | KeyboardEvent) => void;
|
||||
|
||||
/**
|
||||
* Label for the button
|
||||
@@ -107,10 +108,16 @@ export interface CommandButtonComponentProps {
|
||||
* Vertical bar to divide buttons
|
||||
*/
|
||||
isDivider?: boolean;
|
||||
|
||||
/**
|
||||
* Aria-label for the button
|
||||
*/
|
||||
ariaLabel: string;
|
||||
|
||||
/**
|
||||
* A keyboard shortcut that can be used to activate this button.
|
||||
*/
|
||||
keyboardShortcut?: KeyboardShortcutName;
|
||||
}
|
||||
|
||||
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Spinner, SpinnerSize } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { loadMonaco, monaco } from "../../LazyMonaco";
|
||||
import { MonacoNamespace, loadMonaco, monaco } from "../../LazyMonaco";
|
||||
// import "./EditorReact.less";
|
||||
|
||||
interface EditorReactStates {
|
||||
@@ -21,6 +21,7 @@ export interface EditorReactProps {
|
||||
minimap?: monaco.editor.IEditorOptions["minimap"];
|
||||
scrollBeyondLastLine?: monaco.editor.IEditorOptions["scrollBeyondLastLine"];
|
||||
monacoContainerStyles?: React.CSSProperties;
|
||||
configureEditor?: (monaco: MonacoNamespace, editor: monaco.editor.IStandaloneCodeEditor) => void;
|
||||
}
|
||||
|
||||
export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> {
|
||||
@@ -69,7 +70,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
);
|
||||
}
|
||||
|
||||
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
|
||||
protected configureEditor(monaco: MonacoNamespace, editor: monaco.editor.IStandaloneCodeEditor) {
|
||||
this.editor = editor;
|
||||
const queryEditorModel = this.editor.getModel();
|
||||
if (!this.props.isReadOnly && this.props.onContentChanged) {
|
||||
@@ -87,12 +88,16 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.configureEditor) {
|
||||
this.props.configureEditor(monaco, this.editor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the monaco editor and attach to DOM
|
||||
*/
|
||||
private async createEditor(createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) {
|
||||
private async createEditor(createCallback: (monaco: MonacoNamespace, e: monaco.editor.IStandaloneCodeEditor) => void) {
|
||||
const options: monaco.editor.IStandaloneEditorConstructionOptions = {
|
||||
language: this.props.language,
|
||||
value: this.props.content,
|
||||
@@ -111,7 +116,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
|
||||
this.rootNode.innerHTML = "";
|
||||
const monaco = await loadMonaco();
|
||||
createCallback(monaco?.editor?.create(this.rootNode, options));
|
||||
createCallback(monaco, monaco?.editor?.create(this.rootNode, options));
|
||||
|
||||
if (this.rootNode.innerHTML) {
|
||||
this.setState({
|
||||
|
||||
@@ -754,6 +754,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
keyboardShortcut: "DISCARD",
|
||||
onCommandClick: this.onRevertClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
|
||||
@@ -3,3 +3,4 @@ export type { monaco };
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const loadMonaco = () => import(/* webpackChunkName: "lazy-monaco" */ "monaco-editor/esm/vs/editor/editor.api");
|
||||
export type MonacoNamespace = Awaited<ReturnType<typeof loadMonaco>>;
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
* and update any knockout observables passed from the parent.
|
||||
*/
|
||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||
import { keyMap } from "Common/KeyboardShortcuts";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { userContext } from "UserContext";
|
||||
import * as React from "react";
|
||||
import { GlobalHotKeys } from "react-hotkeys";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
|
||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||
@@ -105,8 +107,13 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
},
|
||||
};
|
||||
|
||||
const handlers = CommandBarUtil.createKeyboardHandlers(staticButtons.concat(contextButtons).concat(controlButtons));
|
||||
|
||||
return (
|
||||
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
|
||||
{/* Handles keyboard shortcuts for command bar buttons when focus is OUTSIDE monaco. Even though it's placed here in the DOM, it hooks keydown on 'document' */}
|
||||
<GlobalHotKeys keyMap={keyMap} handlers={handlers} allowChanges={true} />
|
||||
|
||||
<FluentCommandBar
|
||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
||||
|
||||
@@ -240,7 +240,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
|
||||
buttons.push(fullScreenButton);
|
||||
}
|
||||
|
||||
if (configContext.platform !== Platform.Emulator) {
|
||||
if (configContext.platform === Platform.Portal) {
|
||||
const label = "Feedback";
|
||||
const feedbackButtonOptions: CommandButtonComponentProps = {
|
||||
iconSrc: FeedbackIcon,
|
||||
@@ -354,6 +354,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
||||
id: "newQueryBtn",
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
iconAlt: label,
|
||||
keyboardShortcut: "NEW_QUERY",
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
||||
@@ -369,6 +370,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
||||
id: "newQueryBtn",
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
iconAlt: label,
|
||||
keyboardShortcut: "NEW_QUERY",
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
IDropdownOption,
|
||||
IDropdownStyles,
|
||||
} from "@fluentui/react";
|
||||
import { KeyboardShortcutHandlers } from "Common/KeyboardShortcuts";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import * as React from "react";
|
||||
import _ from "underscore";
|
||||
@@ -233,3 +234,16 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType,
|
||||
onRender: () => <ConnectionStatus container={container} poolId={poolId} />,
|
||||
};
|
||||
};
|
||||
|
||||
export const createKeyboardHandlers = (buttons: CommandButtonComponentProps[]): KeyboardShortcutHandlers => {
|
||||
const handlers: KeyboardShortcutHandlers = {};
|
||||
buttons.forEach((button) => {
|
||||
if (button.keyboardShortcut) {
|
||||
handlers[button.keyboardShortcut] = (e) => {
|
||||
button.onCommandClick(e);
|
||||
e.preventDefault();
|
||||
};
|
||||
}
|
||||
});
|
||||
return handlers;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import React from "react";
|
||||
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
@@ -40,97 +41,112 @@ function openCollectionTab(
|
||||
databases: ViewModels.Database[],
|
||||
initialDatabaseIndex = 0,
|
||||
) {
|
||||
for (let i = initialDatabaseIndex; i < databases.length; i++) {
|
||||
const database: ViewModels.Database = databases[i];
|
||||
if (!!action.databaseResourceId && database.id() !== action.databaseResourceId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const collectionActionHandler = (collections: ViewModels.Collection[]) => {
|
||||
if (!action.collectionResourceId && collections.length === 0) {
|
||||
subscription.dispose();
|
||||
openCollectionTab(action, databases, ++i);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let j = 0; j < collections.length; j++) {
|
||||
const collection: ViewModels.Collection = collections[j];
|
||||
if (!!action.collectionResourceId && collection.id() !== action.collectionResourceId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// select the collection
|
||||
collection.expandCollection();
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLDocuments]
|
||||
) {
|
||||
collection.onDocumentDBDocumentsClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.MongoDocuments ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoDocuments]
|
||||
) {
|
||||
collection.onMongoDBDocumentsClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.SchemaAnalyzer ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer]
|
||||
) {
|
||||
collection.onSchemaAnalyzerClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.TableEntities ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]
|
||||
) {
|
||||
collection.onTableEntitiesClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.Graph ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.Graph]
|
||||
) {
|
||||
collection.onGraphDocumentsClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.SQLQuery ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
|
||||
) {
|
||||
collection.onNewQueryClick(
|
||||
collection,
|
||||
undefined,
|
||||
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.ScaleSettings ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.ScaleSettings]
|
||||
) {
|
||||
collection.onSettingsClick();
|
||||
break;
|
||||
}
|
||||
}
|
||||
subscription.dispose();
|
||||
//if databases are not yet loaded, wait until loaded
|
||||
if (!databases || databases.length === 0) {
|
||||
const databaseActionHandler = (databases: ViewModels.Database[]) => {
|
||||
databasesUnsubscription();
|
||||
openCollectionTab(action, databases, 0);
|
||||
return;
|
||||
};
|
||||
const databasesUnsubscription = useDatabases.subscribe(databaseActionHandler, (state) => state.databases);
|
||||
} else {
|
||||
for (let i = initialDatabaseIndex; i < databases.length; i++) {
|
||||
const database: ViewModels.Database = databases[i];
|
||||
if (!!action.databaseResourceId && database.id() !== action.databaseResourceId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const subscription = database.collections.subscribe((collections) => collectionActionHandler(collections));
|
||||
if (database.collections && database.collections() && database.collections().length) {
|
||||
collectionActionHandler(database.collections());
|
||||
//expand database first if not expanded to load the collections
|
||||
if (!database.isDatabaseExpanded?.()) {
|
||||
database.expandDatabase?.();
|
||||
}
|
||||
|
||||
const collectionActionHandler = (collections: ViewModels.Collection[]) => {
|
||||
if (!action.collectionResourceId && collections.length === 0) {
|
||||
subscription.dispose();
|
||||
openCollectionTab(action, databases, ++i);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let j = 0; j < collections.length; j++) {
|
||||
const collection: ViewModels.Collection = collections[j];
|
||||
if (!!action.collectionResourceId && collection.id() !== action.collectionResourceId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// select the collection
|
||||
collection.expandCollection();
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLDocuments]
|
||||
) {
|
||||
collection.onDocumentDBDocumentsClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.MongoDocuments ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoDocuments]
|
||||
) {
|
||||
collection.onMongoDBDocumentsClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.SchemaAnalyzer ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer]
|
||||
) {
|
||||
collection.onSchemaAnalyzerClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.TableEntities ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]
|
||||
) {
|
||||
collection.onTableEntitiesClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.Graph ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.Graph]
|
||||
) {
|
||||
collection.onGraphDocumentsClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.SQLQuery ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
|
||||
) {
|
||||
collection.onNewQueryClick(
|
||||
collection,
|
||||
undefined,
|
||||
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.ScaleSettings ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.ScaleSettings]
|
||||
) {
|
||||
collection.onSettingsClick();
|
||||
break;
|
||||
}
|
||||
}
|
||||
subscription.dispose();
|
||||
};
|
||||
|
||||
const subscription = database.collections.subscribe((collections) => collectionActionHandler(collections));
|
||||
if (database.collections && database.collections() && database.collections().length) {
|
||||
collectionActionHandler(database.collections());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,15 +104,16 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
)}
|
||||
<Stack className="tabPaneContentContainer">
|
||||
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>
|
||||
COPILOT
|
||||
<EditorReact
|
||||
language={"sql"}
|
||||
content={query}
|
||||
isReadOnly={false}
|
||||
wordWrap={"on"}
|
||||
ariaLabel={"Editing Query"}
|
||||
lineNumbers={"on"}
|
||||
onContentChanged={(newQuery: string) => setQuery(newQuery)}
|
||||
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
|
||||
language={"sql"}
|
||||
content={query}
|
||||
isReadOnly={false}
|
||||
wordWrap={"on"}
|
||||
ariaLabel={"Editing Query"}
|
||||
lineNumbers={"on"}
|
||||
onContentChanged={(newQuery: string) => setQuery(newQuery)}
|
||||
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
|
||||
/>
|
||||
<QueryCopilotResults />
|
||||
</SplitterLayout>
|
||||
|
||||
@@ -6,16 +6,16 @@ import DiscardIcon from "../../../images/discard.svg";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants";
|
||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
import { deleteConflict } from "../../Common/dataAccess/deleteConflict";
|
||||
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
|
||||
import { queryConflicts } from "../../Common/dataAccess/queryConflicts";
|
||||
import { updateDocument } from "../../Common/dataAccess/updateDocument";
|
||||
import editable from "../../Common/EditableUtility";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
|
||||
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
|
||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
import { deleteConflict } from "../../Common/dataAccess/deleteConflict";
|
||||
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
|
||||
import { queryConflicts } from "../../Common/dataAccess/queryConflicts";
|
||||
import { updateDocument } from "../../Common/dataAccess/updateDocument";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
@@ -621,6 +621,7 @@ export default class ConflictsTab extends TabsBase {
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
keyboardShortcut: "DISCARD",
|
||||
onCommandClick: this.onDiscardClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
|
||||
@@ -921,6 +921,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
keyboardShortcut: "DISCARD",
|
||||
onCommandClick: this.onRevertNewDocumentClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -950,6 +951,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
keyboardShortcut: "DISCARD",
|
||||
onCommandClick: this.onRevertExisitingDocumentClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { FeedOptions, QueryOperationOptions } from "@azure/cosmos";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
import { MonacoNamespace, monaco } from "Explorer/LazyMonaco";
|
||||
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||
@@ -39,7 +41,6 @@ import { userContext } from "../../../UserContext";
|
||||
import * as QueryUtils from "../../../Utils/QueryUtils";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||
@@ -468,6 +469,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
buttons.push({
|
||||
iconSrc: CancelQueryIcon,
|
||||
iconAlt: label,
|
||||
keyboardShortcut: "CANCEL_QUERY",
|
||||
onCommandClick: () => this.queryAbortController.abort(),
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -584,6 +586,15 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
}
|
||||
|
||||
private getEditorAndQueryResult(): JSX.Element {
|
||||
const configureEditor = (monaco: MonacoNamespace, editor: monaco.editor.IStandaloneCodeEditor) => {
|
||||
editor.addAction({
|
||||
id: "execute-query",
|
||||
label: "Execute Query",
|
||||
keybindings: [monaco.KeyMod.Shift | monaco.KeyCode.Enter],
|
||||
run: () => this.onExecuteQueryClick(),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="tab-pane" id={this.props.tabId} role="tabpanel">
|
||||
@@ -608,7 +619,8 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
lineNumbers={"on"}
|
||||
onContentChanged={(newContent: string) => this.onChangeContent(newContent)}
|
||||
onContentSelected={(selectedContent: string) => this.onSelectedContent(selectedContent)}
|
||||
/>
|
||||
configureEditor={configureEditor}
|
||||
/>;
|
||||
</div>
|
||||
</Fragment>
|
||||
{this.props.isSampleCopilotActive ? (
|
||||
|
||||
@@ -238,6 +238,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
keyboardShortcut: "DISCARD",
|
||||
onCommandClick: this.onDiscard,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
|
||||
@@ -347,6 +347,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
keyboardShortcut: "DISCARD",
|
||||
onCommandClick: this.onDiscard,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
|
||||
@@ -40,11 +40,10 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
this.database = options.database;
|
||||
this.rid = options.rid || (this.collection && this.collection.rid) || "";
|
||||
this.tabKind = options.tabKind;
|
||||
this.tabTitle = ko.observable<string>(options.title);
|
||||
this.tabTitle = ko.observable<string>(this.getTitle(options));
|
||||
this.tabPath =
|
||||
ko.observable(options.tabPath ?? "") ||
|
||||
(this.collection &&
|
||||
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${this.tabTitle()}`));
|
||||
this.collection &&
|
||||
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${options.title}`);
|
||||
this.pendingNotification = ko.observable<DataModels.Notification>(undefined);
|
||||
this.onLoadStartKey = options.onLoadStartKey;
|
||||
this.closeTabButton = {
|
||||
@@ -143,6 +142,26 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
return (this.collection && this.collection.container) || (this.database && this.database.container);
|
||||
}
|
||||
|
||||
public getTitle(options: ViewModels.TabOptions): string {
|
||||
const coll = this.collection?.id();
|
||||
const db = this.database?.id();
|
||||
if (coll) {
|
||||
if (coll.length > 8) {
|
||||
return coll.slice(0, 5) + "…" + options.title;
|
||||
} else {
|
||||
return coll + "." + options.title;
|
||||
}
|
||||
} else if (db) {
|
||||
if (db.length > 8) {
|
||||
return db.slice(0, 5) + "…" + options.title;
|
||||
} else {
|
||||
return db + "." + options.title;
|
||||
}
|
||||
} else {
|
||||
return options.title;
|
||||
}
|
||||
}
|
||||
|
||||
/** Renders a Javascript object to be displayed inside Monaco Editor */
|
||||
public renderObjectForEditor(value: any, replacer: any, space: string | number): string {
|
||||
return JSON.stringify(value, replacer, space);
|
||||
|
||||
@@ -256,6 +256,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
...this,
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
keyboardShortcut: "DISCARD",
|
||||
onCommandClick: this.onDiscard,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
|
||||
@@ -4,9 +4,9 @@ import React, { Component } from "react";
|
||||
import DiscardIcon from "../../../images/discard.svg";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction";
|
||||
import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
@@ -109,6 +109,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
...this,
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
keyboardShortcut: "DISCARD",
|
||||
onCommandClick: this.onDiscard,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
|
||||
@@ -308,7 +308,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
collectionName: this.id(),
|
||||
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.rawDataModel.id + " - Items",
|
||||
tabTitle: "Items",
|
||||
});
|
||||
this.documentIds([]);
|
||||
|
||||
@@ -316,7 +316,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
partitionKey: this.partitionKey,
|
||||
documentIds: ko.observableArray<DocumentId>([]),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: this.rawDataModel.id + " - Items",
|
||||
title: "Items",
|
||||
collection: this,
|
||||
node: this,
|
||||
tabPath: `${this.databaseId}>${this.id()}>Documents`,
|
||||
|
||||
23
src/Main.tsx
23
src/Main.tsx
@@ -18,6 +18,7 @@ import "../externals/jquery.typeahead.min.js";
|
||||
// Image Dependencies
|
||||
import { Platform } from "ConfigContext";
|
||||
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
||||
import * as ReactHotkeys from "react-hotkeys";
|
||||
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||
import "../images/favicon.ico";
|
||||
@@ -61,6 +62,28 @@ import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
||||
|
||||
initializeIcons();
|
||||
|
||||
const tagsIgnoredByReactHotkeys = ["INPUT", "SELECT"];
|
||||
ReactHotkeys.configure({
|
||||
ignoreEventsCondition: (evt) => {
|
||||
// The default react-hotkeys behavior is to ignore events targetting a textarea, but we want the monaco editor's key events to bubble up
|
||||
// So, we configure it to ignore all events targetting a textarea except when the target is a monaco editor's text area
|
||||
|
||||
if (!(evt.target instanceof HTMLElement)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tagsIgnoredByReactHotkeys.includes(evt.target.tagName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (evt.target.tagName === "TEXTAREA" && !evt.target.matches(".monaco-editor textarea")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
})
|
||||
|
||||
const App: React.FunctionComponent = () => {
|
||||
const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true);
|
||||
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
|
||||
|
||||
@@ -51,6 +51,7 @@ interface FabricContext {
|
||||
connectionId: string;
|
||||
databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined;
|
||||
isReadOnly: boolean;
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
export type AdminFeedbackControlPolicy =
|
||||
|
||||
@@ -2,7 +2,6 @@ import { createUri } from "Common/UrlUtility";
|
||||
import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract";
|
||||
import { FABRIC_RPC_VERSION, FabricMessageV2 } from "Contracts/FabricMessagesContract";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
||||
@@ -90,6 +89,7 @@ async function configureFabric(): Promise<Explorer> {
|
||||
// These are the versions of Fabric that Data Explorer supports.
|
||||
const SUPPORTED_FABRIC_VERSIONS = [FABRIC_RPC_VERSION];
|
||||
|
||||
let firstContainerOpened = false;
|
||||
let explorer: Explorer;
|
||||
return new Promise<Explorer>((resolve) => {
|
||||
window.addEventListener(
|
||||
@@ -121,7 +121,10 @@ async function configureFabric(): Promise<Explorer> {
|
||||
await scheduleRefreshDatabaseResourceToken(true);
|
||||
resolve(explorer);
|
||||
await explorer.refreshAllDatabases();
|
||||
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
|
||||
if (userContext.fabricContext.isVisible && !firstContainerOpened) {
|
||||
firstContainerOpened = true;
|
||||
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "newContainer":
|
||||
@@ -132,8 +135,16 @@ async function configureFabric(): Promise<Explorer> {
|
||||
handleCachedDataMessage(data);
|
||||
break;
|
||||
}
|
||||
case "setToolbarStatus": {
|
||||
useCommandBar.getState().setIsHidden(data.message.visible === false);
|
||||
case "explorerVisible": {
|
||||
userContext.fabricContext.isVisible = data.message.visible;
|
||||
if (
|
||||
userContext.fabricContext.isVisible &&
|
||||
!firstContainerOpened &&
|
||||
userContext?.fabricContext?.databaseConnectionInfo?.databaseId !== undefined
|
||||
) {
|
||||
firstContainerOpened = true;
|
||||
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -327,12 +338,13 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer {
|
||||
return explorer;
|
||||
}
|
||||
|
||||
function createExplorerFabric(params: { connectionId: string }): Explorer {
|
||||
function createExplorerFabric(params: { connectionId: string; isVisible: boolean }): Explorer {
|
||||
updateUserContext({
|
||||
fabricContext: {
|
||||
connectionId: params.connectionId,
|
||||
databaseConnectionInfo: undefined,
|
||||
isReadOnly: true,
|
||||
isVisible: params.isVisible ?? true,
|
||||
},
|
||||
authType: AuthType.ConnectionString,
|
||||
databaseAccount: {
|
||||
|
||||
Reference in New Issue
Block a user