mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-12-12 23:37:27 +00:00
Restore open collection tabs: Query, Documents, Settings (#2004)
* Persist query multiple query texts * Save multiple query tab histories * Save and restore states for QueryTab and DocumentsTab for SQL and Mongo * Enable Collection Scale/Settings restore * Persist documents tab current filter * Fix DocumentsTab conflict resolve mistake * Remove unused variable * Fix e2e test * Fix e2e localStorage reference * Try clearing local storage via playwright page * Clear local storage after opening page * Move restore flag behind feature flag. Whitelist restorable tabs in for Fabric. Restore e2e tests. * Fix typo * Fix: avoid setting undefined for preferredSize for the <Allotment.Pane> * Add comments * Move restore tabs after knockout configure step from Explorer constructor (which could be called multiple times)
This commit is contained in:
parent
80b926214b
commit
73d2686025
@ -9,6 +9,7 @@ export enum TabKind {
|
|||||||
Graph,
|
Graph,
|
||||||
SQLQuery,
|
SQLQuery,
|
||||||
ScaleSettings,
|
ScaleSettings,
|
||||||
|
MongoQuery,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,6 +52,8 @@ export interface OpenCollectionTab extends OpenTab {
|
|||||||
*/
|
*/
|
||||||
export interface OpenQueryTab extends OpenCollectionTab {
|
export interface OpenQueryTab extends OpenCollectionTab {
|
||||||
query: QueryInfo;
|
query: QueryInfo;
|
||||||
|
splitterDirection?: "vertical" | "horizontal";
|
||||||
|
queryViewSizePercent?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -115,7 +115,13 @@ export interface CollectionBase extends TreeNode {
|
|||||||
isSampleCollection?: boolean;
|
isSampleCollection?: boolean;
|
||||||
|
|
||||||
onDocumentDBDocumentsClick(): void;
|
onDocumentDBDocumentsClick(): void;
|
||||||
onNewQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
|
onNewQueryClick(
|
||||||
|
source: any,
|
||||||
|
event?: MouseEvent,
|
||||||
|
queryText?: string,
|
||||||
|
splitterDirection?: "horizontal" | "vertical",
|
||||||
|
queryViewSizePercent?: number,
|
||||||
|
): void;
|
||||||
expandCollection(): void;
|
expandCollection(): void;
|
||||||
collapseCollection(): void;
|
collapseCollection(): void;
|
||||||
getDatabase(): Database;
|
getDatabase(): Database;
|
||||||
@ -151,7 +157,13 @@ export interface Collection extends CollectionBase {
|
|||||||
onSettingsClick: () => Promise<void>;
|
onSettingsClick: () => Promise<void>;
|
||||||
|
|
||||||
onNewGraphClick(): void;
|
onNewGraphClick(): void;
|
||||||
onNewMongoQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
|
onNewMongoQueryClick(
|
||||||
|
source: any,
|
||||||
|
event?: MouseEvent,
|
||||||
|
queryText?: string,
|
||||||
|
splitterDirection?: "horizontal" | "vertical",
|
||||||
|
queryViewSizePercent?: number,
|
||||||
|
): void;
|
||||||
onNewMongoShellClick(): void;
|
onNewMongoShellClick(): void;
|
||||||
onNewStoredProcedureClick(source: Collection, event?: MouseEvent): void;
|
onNewStoredProcedureClick(source: Collection, event?: MouseEvent): void;
|
||||||
onNewUserDefinedFunctionClick(source: Collection, event?: MouseEvent): void;
|
onNewUserDefinedFunctionClick(source: Collection, event?: MouseEvent): void;
|
||||||
@ -311,6 +323,8 @@ export interface QueryTabOptions extends TabOptions {
|
|||||||
partitionKey?: DataModels.PartitionKey;
|
partitionKey?: DataModels.PartitionKey;
|
||||||
queryText?: string;
|
queryText?: string;
|
||||||
resourceTokenPartitionKey?: string;
|
resourceTokenPartitionKey?: string;
|
||||||
|
splitterDirection?: "horizontal" | "vertical";
|
||||||
|
queryViewSizePercent?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScriptTabOption extends TabOptions {
|
export interface ScriptTabOption extends TabOptions {
|
||||||
|
@ -1134,7 +1134,7 @@ export default class Explorer {
|
|||||||
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
||||||
userContext.authType === AuthType.ResourceToken
|
userContext.authType === AuthType.ResourceToken
|
||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
: this.refreshAllDatabases();
|
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow
|
||||||
}
|
}
|
||||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
||||||
|
import { configContext, Platform } from "ConfigContext";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
||||||
@ -56,6 +57,19 @@ function openCollectionTab(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
configContext.platform === Platform.Fabric &&
|
||||||
|
!(
|
||||||
|
// whitelist the tab kinds that are allowed to be opened in Fabric
|
||||||
|
(
|
||||||
|
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
|
||||||
|
action.tabKind === ActionContracts.TabKind.SQLQuery
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
//expand database first if not expanded to load the collections
|
//expand database first if not expanded to load the collections
|
||||||
if (!database.isDatabaseExpanded?.()) {
|
if (!database.isDatabaseExpanded?.()) {
|
||||||
database.expandDatabase?.();
|
database.expandDatabase?.();
|
||||||
@ -121,10 +135,28 @@ function openCollectionTab(
|
|||||||
action.tabKind === ActionContracts.TabKind.SQLQuery ||
|
action.tabKind === ActionContracts.TabKind.SQLQuery ||
|
||||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
|
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
|
||||||
) {
|
) {
|
||||||
|
const openQueryTabAction = action as ActionContracts.OpenQueryTab;
|
||||||
collection.onNewQueryClick(
|
collection.onNewQueryClick(
|
||||||
collection,
|
collection,
|
||||||
undefined,
|
undefined,
|
||||||
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties),
|
generateQueryText(openQueryTabAction, collection.partitionKeyProperties),
|
||||||
|
openQueryTabAction.splitterDirection,
|
||||||
|
openQueryTabAction.queryViewSizePercent,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
action.tabKind === ActionContracts.TabKind.MongoQuery ||
|
||||||
|
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoQuery]
|
||||||
|
) {
|
||||||
|
const openQueryTabAction = action as ActionContracts.OpenQueryTab;
|
||||||
|
collection.onNewMongoQueryClick(
|
||||||
|
collection,
|
||||||
|
undefined,
|
||||||
|
generateQueryText(openQueryTabAction, collection.partitionKeyProperties),
|
||||||
|
openQueryTabAction.splitterDirection,
|
||||||
|
openQueryTabAction.queryViewSizePercent,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -3,17 +3,11 @@
|
|||||||
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
||||||
import {
|
import {
|
||||||
AppStateComponentNames,
|
AppStateComponentNames,
|
||||||
deleteState,
|
deleteSubComponentState,
|
||||||
loadState,
|
readSubComponentState,
|
||||||
saveState,
|
saveSubComponentState,
|
||||||
saveStateDebounced,
|
|
||||||
} from "Shared/AppStatePersistenceUtility";
|
} from "Shared/AppStatePersistenceUtility";
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
|
|
||||||
const componentName = AppStateComponentNames.DocumentsTab;
|
|
||||||
|
|
||||||
export enum SubComponentName {
|
export enum SubComponentName {
|
||||||
ColumnSizes = "ColumnSizes",
|
ColumnSizes = "ColumnSizes",
|
||||||
@ -21,6 +15,7 @@ export enum SubComponentName {
|
|||||||
MainTabDivider = "MainTabDivider",
|
MainTabDivider = "MainTabDivider",
|
||||||
ColumnsSelection = "ColumnsSelection",
|
ColumnsSelection = "ColumnsSelection",
|
||||||
ColumnSort = "ColumnSort",
|
ColumnSort = "ColumnSort",
|
||||||
|
CurrentFilter = "CurrentFilter",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ColumnSizesMap = { [columnId: string]: WidthDefinition };
|
export type ColumnSizesMap = { [columnId: string]: WidthDefinition };
|
||||||
@ -30,84 +25,22 @@ export type TabDivider = { leftPaneWidthPercent: number };
|
|||||||
export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] };
|
export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] };
|
||||||
export type ColumnSort = { columnId: string; direction: "ascending" | "descending" };
|
export type ColumnSort = { columnId: string; direction: "ascending" | "descending" };
|
||||||
|
|
||||||
/**
|
// Wrap the ...SubComponentState functions for type safety
|
||||||
*
|
|
||||||
* @param subComponentName
|
export const readDocumentsTabSubComponentState = <T>(
|
||||||
* @param collection
|
|
||||||
* @param defaultValue Will be returned if persisted state is not found
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const readSubComponentState = <T>(
|
|
||||||
subComponentName: SubComponentName,
|
subComponentName: SubComponentName,
|
||||||
collection: ViewModels.CollectionBase,
|
collection: ViewModels.CollectionBase,
|
||||||
defaultValue: T,
|
defaultValue: T,
|
||||||
): T => {
|
): T => readSubComponentState<T>(AppStateComponentNames.DocumentsTab, subComponentName, collection, defaultValue);
|
||||||
const globalAccountName = userContext.databaseAccount?.name;
|
|
||||||
if (!globalAccountName) {
|
|
||||||
const message = "Database account name not found in userContext";
|
|
||||||
console.error(message);
|
|
||||||
TelemetryProcessor.traceFailure(Action.ReadPersistedTabState, { message, componentName });
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = loadState({
|
export const saveDocumentsTabSubComponentState = <T>(
|
||||||
componentName: componentName,
|
|
||||||
subComponentName,
|
|
||||||
globalAccountName,
|
|
||||||
databaseName: collection.databaseId,
|
|
||||||
containerName: collection.id(),
|
|
||||||
}) as T;
|
|
||||||
|
|
||||||
return state || defaultValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param subComponentName
|
|
||||||
* @param collection
|
|
||||||
* @param state State to save
|
|
||||||
* @param debounce true for high-frequency calls (e.g mouse drag events)
|
|
||||||
*/
|
|
||||||
export const saveSubComponentState = <T>(
|
|
||||||
subComponentName: SubComponentName,
|
subComponentName: SubComponentName,
|
||||||
collection: ViewModels.CollectionBase,
|
collection: ViewModels.CollectionBase,
|
||||||
state: T,
|
state: T,
|
||||||
debounce?: boolean,
|
debounce?: boolean,
|
||||||
): void => {
|
): void => saveSubComponentState<T>(AppStateComponentNames.DocumentsTab, subComponentName, collection, state, debounce);
|
||||||
const globalAccountName = userContext.databaseAccount?.name;
|
|
||||||
if (!globalAccountName) {
|
|
||||||
const message = "Database account name not found in userContext";
|
|
||||||
console.error(message);
|
|
||||||
TelemetryProcessor.traceFailure(Action.SavePersistedTabState, { message, componentName });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(debounce ? saveStateDebounced : saveState)(
|
export const deleteDocumentsTabSubComponentState = (
|
||||||
{
|
subComponentName: SubComponentName,
|
||||||
componentName: componentName,
|
collection: ViewModels.CollectionBase,
|
||||||
subComponentName,
|
) => deleteSubComponentState(AppStateComponentNames.DocumentsTab, subComponentName, collection);
|
||||||
globalAccountName,
|
|
||||||
databaseName: collection.databaseId,
|
|
||||||
containerName: collection.id(),
|
|
||||||
},
|
|
||||||
state,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteSubComponentState = (subComponentName: SubComponentName, collection: ViewModels.CollectionBase) => {
|
|
||||||
const globalAccountName = userContext.databaseAccount?.name;
|
|
||||||
if (!globalAccountName) {
|
|
||||||
const message = "Database account name not found in userContext";
|
|
||||||
console.error(message);
|
|
||||||
TelemetryProcessor.traceFailure(Action.DeletePersistedTabState, { message, componentName });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteState({
|
|
||||||
componentName: componentName,
|
|
||||||
subComponentName,
|
|
||||||
globalAccountName,
|
|
||||||
databaseName: collection.databaseId,
|
|
||||||
containerName: collection.id(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
@ -21,6 +21,7 @@ import { queryDocuments } from "Common/dataAccess/queryDocuments";
|
|||||||
import { readDocument } from "Common/dataAccess/readDocument";
|
import { readDocument } from "Common/dataAccess/readDocument";
|
||||||
import { updateDocument } from "Common/dataAccess/updateDocument";
|
import { updateDocument } from "Common/dataAccess/updateDocument";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
|
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
|
||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
@ -34,8 +35,9 @@ import {
|
|||||||
FilterHistory,
|
FilterHistory,
|
||||||
SubComponentName,
|
SubComponentName,
|
||||||
TabDivider,
|
TabDivider,
|
||||||
readSubComponentState,
|
deleteDocumentsTabSubComponentState,
|
||||||
saveSubComponentState,
|
readDocumentsTabSubComponentState,
|
||||||
|
saveDocumentsTabSubComponentState,
|
||||||
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
||||||
import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||||
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
||||||
@ -140,6 +142,8 @@ export class DocumentsTabV2 extends TabsBase {
|
|||||||
private title: string;
|
private title: string;
|
||||||
private resourceTokenPartitionKey: string;
|
private resourceTokenPartitionKey: string;
|
||||||
|
|
||||||
|
protected persistedState: OpenCollectionTab;
|
||||||
|
|
||||||
constructor(options: ViewModels.DocumentsTabOptions) {
|
constructor(options: ViewModels.DocumentsTabOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
@ -147,6 +151,13 @@ export class DocumentsTabV2 extends TabsBase {
|
|||||||
this.title = options.title;
|
this.title = options.title;
|
||||||
this.partitionKey = options.partitionKey;
|
this.partitionKey = options.partitionKey;
|
||||||
this.resourceTokenPartitionKey = options.resourceTokenPartitionKey;
|
this.resourceTokenPartitionKey = options.resourceTokenPartitionKey;
|
||||||
|
|
||||||
|
this.persistedState = {
|
||||||
|
actionType: ActionType.OpenCollectionTab,
|
||||||
|
tabKind: options.isPreferredApiMongoDB ? TabKind.MongoDocuments : TabKind.SQLDocuments,
|
||||||
|
databaseResourceId: options.collection.databaseId,
|
||||||
|
collectionResourceId: options.collection.id(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
@ -575,7 +586,10 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
onIsExecutingChange,
|
onIsExecutingChange,
|
||||||
isTabActive,
|
isTabActive,
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const [filterContent, setFilterContent] = useState<string>("");
|
const [filterContent, setFilterContent] = useState<string>(() =>
|
||||||
|
readDocumentsTabSubComponentState<string>(SubComponentName.CurrentFilter, _collection, ""),
|
||||||
|
);
|
||||||
|
|
||||||
const [documentIds, setDocumentIds] = useState<ExtendedDocumentId[]>([]);
|
const [documentIds, setDocumentIds] = useState<ExtendedDocumentId[]>([]);
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||||
const styles = useDocumentsTabStyles();
|
const styles = useDocumentsTabStyles();
|
||||||
@ -606,7 +620,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
// State
|
// State
|
||||||
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
|
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
|
||||||
readSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
|
readDocumentsTabSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
|
||||||
leftPaneWidthPercent: 35,
|
leftPaneWidthPercent: 35,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -621,7 +635,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
// User's filter history
|
// User's filter history
|
||||||
const [lastFilterContents, setLastFilterContents] = useState<FilterHistory>(() =>
|
const [lastFilterContents, setLastFilterContents] = useState<FilterHistory>(() =>
|
||||||
readSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
|
readDocumentsTabSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
|
||||||
);
|
);
|
||||||
|
|
||||||
// For progress bar for bulk delete (noSql)
|
// For progress bar for bulk delete (noSql)
|
||||||
@ -763,7 +777,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [selectedColumnIds, setSelectedColumnIds] = useState<string[]>(() => {
|
const [selectedColumnIds, setSelectedColumnIds] = useState<string[]>(() => {
|
||||||
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
|
const persistedColumnsSelection = readDocumentsTabSubComponentState<ColumnsSelection>(
|
||||||
SubComponentName.ColumnsSelection,
|
SubComponentName.ColumnsSelection,
|
||||||
_collection,
|
_collection,
|
||||||
undefined,
|
undefined,
|
||||||
@ -808,7 +822,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setKeyboardActions({
|
setKeyboardActions({
|
||||||
[KeyboardAction.CLEAR_SEARCH]: () => {
|
[KeyboardAction.CLEAR_SEARCH]: () => {
|
||||||
setFilterContent("");
|
updateFilterContent("");
|
||||||
refreshDocumentsGrid(true);
|
refreshDocumentsGrid(true);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@ -1645,7 +1659,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
// Column definition is a map<id, ColumnDefinition> to garantee uniqueness
|
// Column definition is a map<id, ColumnDefinition> to garantee uniqueness
|
||||||
const [columnDefinitions, setColumnDefinitions] = useState<ColumnDefinition[]>(() => {
|
const [columnDefinitions, setColumnDefinitions] = useState<ColumnDefinition[]>(() => {
|
||||||
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
|
const persistedColumnsSelection = readDocumentsTabSubComponentState<ColumnsSelection>(
|
||||||
SubComponentName.ColumnsSelection,
|
SubComponentName.ColumnsSelection,
|
||||||
_collection,
|
_collection,
|
||||||
undefined,
|
undefined,
|
||||||
@ -1956,7 +1970,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
const limitedLastFilterContents = lastFilterContents.slice(0, MAX_FILTER_HISTORY_COUNT);
|
const limitedLastFilterContents = lastFilterContents.slice(0, MAX_FILTER_HISTORY_COUNT);
|
||||||
|
|
||||||
setLastFilterContents(limitedLastFilterContents);
|
setLastFilterContents(limitedLastFilterContents);
|
||||||
saveSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents);
|
saveDocumentsTabSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents);
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshDocumentsGrid = useCallback(
|
const refreshDocumentsGrid = useCallback(
|
||||||
@ -2013,7 +2027,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
setSelectedColumnIds(newSelectedColumnIds);
|
setSelectedColumnIds(newSelectedColumnIds);
|
||||||
|
|
||||||
saveSubComponentState<ColumnsSelection>(SubComponentName.ColumnsSelection, _collection, {
|
saveDocumentsTabSubComponentState<ColumnsSelection>(SubComponentName.ColumnsSelection, _collection, {
|
||||||
selectedColumnIds: newSelectedColumnIds,
|
selectedColumnIds: newSelectedColumnIds,
|
||||||
columnDefinitions,
|
columnDefinitions,
|
||||||
});
|
});
|
||||||
@ -2063,6 +2077,15 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateFilterContent = (filter: string): void => {
|
||||||
|
if (filter === "" || filter === undefined) {
|
||||||
|
deleteDocumentsTabSubComponentState(SubComponentName.CurrentFilter, _collection);
|
||||||
|
} else {
|
||||||
|
saveDocumentsTabSubComponentState<string>(SubComponentName.CurrentFilter, _collection, filter, true);
|
||||||
|
}
|
||||||
|
setFilterContent(filter);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CosmosFluentProvider className={styles.container}>
|
<CosmosFluentProvider className={styles.container}>
|
||||||
<div className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
|
<div className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
|
||||||
@ -2077,7 +2100,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
}
|
}
|
||||||
title="Type a query predicate or choose one from the list."
|
title="Type a query predicate or choose one from the list."
|
||||||
value={filterContent}
|
value={filterContent}
|
||||||
onChange={(value) => setFilterContent(value)}
|
onChange={updateFilterContent}
|
||||||
onKeyDown={onFilterKeyDown}
|
onKeyDown={onFilterKeyDown}
|
||||||
bottomLink={{ text: "Learn more", url: DATA_EXPLORER_DOC_URL }}
|
bottomLink={{ text: "Learn more", url: DATA_EXPLORER_DOC_URL }}
|
||||||
/>
|
/>
|
||||||
@ -2103,7 +2126,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
<Allotment
|
<Allotment
|
||||||
onDragEnd={(sizes: number[]) => {
|
onDragEnd={(sizes: number[]) => {
|
||||||
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
||||||
saveSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
|
saveDocumentsTabSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
|
||||||
setTabStateData(tabStateData);
|
setTabStateData(tabStateData);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -42,9 +42,9 @@ import { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPan
|
|||||||
import {
|
import {
|
||||||
ColumnSizesMap,
|
ColumnSizesMap,
|
||||||
ColumnSort,
|
ColumnSort,
|
||||||
deleteSubComponentState,
|
deleteDocumentsTabSubComponentState,
|
||||||
readSubComponentState,
|
readDocumentsTabSubComponentState,
|
||||||
saveSubComponentState,
|
saveDocumentsTabSubComponentState,
|
||||||
SubComponentName,
|
SubComponentName,
|
||||||
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
||||||
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
@ -118,7 +118,11 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
const sortedRowsRef = React.useRef(null);
|
const sortedRowsRef = React.useRef(null);
|
||||||
|
|
||||||
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
|
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
|
||||||
const columnSizesMap: ColumnSizesMap = readSubComponentState(SubComponentName.ColumnSizes, collection, {});
|
const columnSizesMap: ColumnSizesMap = readDocumentsTabSubComponentState(
|
||||||
|
SubComponentName.ColumnSizes,
|
||||||
|
collection,
|
||||||
|
{},
|
||||||
|
);
|
||||||
const columnSizesPx: TableColumnSizingOptions = {};
|
const columnSizesPx: TableColumnSizingOptions = {};
|
||||||
selectedColumnIds.forEach((columnId) => {
|
selectedColumnIds.forEach((columnId) => {
|
||||||
if (
|
if (
|
||||||
@ -142,7 +146,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
sortDirection: "ascending" | "descending";
|
sortDirection: "ascending" | "descending";
|
||||||
sortColumn: TableColumnId | undefined;
|
sortColumn: TableColumnId | undefined;
|
||||||
}>(() => {
|
}>(() => {
|
||||||
const sort = readSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined);
|
const sort = readDocumentsTabSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined);
|
||||||
|
|
||||||
if (!sort) {
|
if (!sort) {
|
||||||
return {
|
return {
|
||||||
@ -174,7 +178,12 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as ColumnSizesMap);
|
}, {} as ColumnSizesMap);
|
||||||
|
|
||||||
saveSubComponentState<ColumnSizesMap>(SubComponentName.ColumnSizes, collection, persistentSizes, true);
|
saveDocumentsTabSubComponentState<ColumnSizesMap>(
|
||||||
|
SubComponentName.ColumnSizes,
|
||||||
|
collection,
|
||||||
|
persistentSizes,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
return newSizingOptions;
|
return newSizingOptions;
|
||||||
});
|
});
|
||||||
@ -186,11 +195,14 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
setColumnSort(event, columnId, direction);
|
setColumnSort(event, columnId, direction);
|
||||||
|
|
||||||
if (columnId === undefined || direction === undefined) {
|
if (columnId === undefined || direction === undefined) {
|
||||||
deleteSubComponentState(SubComponentName.ColumnSort, collection);
|
deleteDocumentsTabSubComponentState(SubComponentName.ColumnSort, collection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, { columnId, direction });
|
saveDocumentsTabSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, {
|
||||||
|
columnId,
|
||||||
|
direction,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes
|
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ActionType, TabKind } from "Contracts/ActionContracts";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import MongoUtility from "../../../Common/MongoUtility";
|
import MongoUtility from "../../../Common/MongoUtility";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
@ -20,7 +21,7 @@ export class NewMongoQueryTab extends NewQueryTab {
|
|||||||
private mongoQueryTabProps: IMongoQueryTabProps,
|
private mongoQueryTabProps: IMongoQueryTabProps,
|
||||||
) {
|
) {
|
||||||
super(options, mongoQueryTabProps);
|
super(options, mongoQueryTabProps);
|
||||||
this.queryText = "";
|
this.queryText = options.queryText ?? "";
|
||||||
this.iMongoQueryTabComponentProps = {
|
this.iMongoQueryTabComponentProps = {
|
||||||
collection: options.collection,
|
collection: options.collection,
|
||||||
isExecutionError: this.isExecutionError(),
|
isExecutionError: this.isExecutionError(),
|
||||||
@ -28,6 +29,8 @@ export class NewMongoQueryTab extends NewQueryTab {
|
|||||||
tabsBaseInstance: this,
|
tabsBaseInstance: this,
|
||||||
queryText: this.queryText,
|
queryText: this.queryText,
|
||||||
partitionKey: this.partitionKey,
|
partitionKey: this.partitionKey,
|
||||||
|
splitterDirection: options.splitterDirection,
|
||||||
|
queryViewSizePercent: options.queryViewSizePercent,
|
||||||
container: this.mongoQueryTabProps.container,
|
container: this.mongoQueryTabProps.container,
|
||||||
onTabAccessor: (instance: ITabAccessor): void => {
|
onTabAccessor: (instance: ITabAccessor): void => {
|
||||||
this.iTabAccessor = instance;
|
this.iTabAccessor = instance;
|
||||||
@ -35,6 +38,26 @@ export class NewMongoQueryTab extends NewQueryTab {
|
|||||||
isPreferredApiMongoDB: true,
|
isPreferredApiMongoDB: true,
|
||||||
monacoEditorSetting: "plaintext",
|
monacoEditorSetting: "plaintext",
|
||||||
viewModelcollection: this.mongoQueryTabProps.viewModelcollection,
|
viewModelcollection: this.mongoQueryTabProps.viewModelcollection,
|
||||||
|
onUpdatePersistedState: (state: {
|
||||||
|
queryText: string;
|
||||||
|
splitterDirection: string;
|
||||||
|
queryViewSizePercent: number;
|
||||||
|
}): void => {
|
||||||
|
this.persistedState = {
|
||||||
|
actionType: ActionType.OpenCollectionTab,
|
||||||
|
tabKind: TabKind.SQLQuery,
|
||||||
|
databaseResourceId: options.collection.databaseId,
|
||||||
|
collectionResourceId: options.collection.id(),
|
||||||
|
query: {
|
||||||
|
text: state.queryText,
|
||||||
|
},
|
||||||
|
splitterDirection: state.splitterDirection as "vertical" | "horizontal",
|
||||||
|
queryViewSizePercent: state.queryViewSizePercent,
|
||||||
|
};
|
||||||
|
if (this.triggerPersistState) {
|
||||||
|
this.triggerPersistState();
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
|
import { ActionType, OpenQueryTab, TabKind } from "Contracts/ActionContracts";
|
||||||
import { MessageTypes } from "Contracts/MessageTypes";
|
import { MessageTypes } from "Contracts/MessageTypes";
|
||||||
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
@ -26,6 +27,8 @@ export class NewQueryTab extends TabsBase {
|
|||||||
public iQueryTabComponentProps: IQueryTabComponentProps;
|
public iQueryTabComponentProps: IQueryTabComponentProps;
|
||||||
public iTabAccessor: ITabAccessor;
|
public iTabAccessor: ITabAccessor;
|
||||||
|
|
||||||
|
protected persistedState: OpenQueryTab;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
options: QueryTabOptions,
|
options: QueryTabOptions,
|
||||||
private props: IQueryTabProps,
|
private props: IQueryTabProps,
|
||||||
@ -39,12 +42,41 @@ export class NewQueryTab extends TabsBase {
|
|||||||
tabsBaseInstance: this,
|
tabsBaseInstance: this,
|
||||||
queryText: options.queryText,
|
queryText: options.queryText,
|
||||||
partitionKey: this.partitionKey,
|
partitionKey: this.partitionKey,
|
||||||
|
splitterDirection: options.splitterDirection,
|
||||||
|
queryViewSizePercent: options.queryViewSizePercent,
|
||||||
container: this.props.container,
|
container: this.props.container,
|
||||||
onTabAccessor: (instance: ITabAccessor): void => {
|
onTabAccessor: (instance: ITabAccessor): void => {
|
||||||
this.iTabAccessor = instance;
|
this.iTabAccessor = instance;
|
||||||
},
|
},
|
||||||
isPreferredApiMongoDB: false,
|
isPreferredApiMongoDB: false,
|
||||||
|
onUpdatePersistedState: (state: {
|
||||||
|
queryText: string;
|
||||||
|
splitterDirection: string;
|
||||||
|
queryViewSizePercent: number;
|
||||||
|
}): void => {
|
||||||
|
this.persistedState = {
|
||||||
|
actionType: ActionType.OpenCollectionTab,
|
||||||
|
tabKind: TabKind.SQLQuery,
|
||||||
|
databaseResourceId: options.collection.databaseId,
|
||||||
|
collectionResourceId: options.collection.id(),
|
||||||
|
query: {
|
||||||
|
text: state.queryText,
|
||||||
|
},
|
||||||
|
splitterDirection: state.splitterDirection as "vertical" | "horizontal",
|
||||||
|
queryViewSizePercent: state.queryViewSizePercent,
|
||||||
};
|
};
|
||||||
|
if (this.triggerPersistState) {
|
||||||
|
this.triggerPersistState();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// set initial state
|
||||||
|
this.iQueryTabComponentProps.onUpdatePersistedState({
|
||||||
|
queryText: options.queryText,
|
||||||
|
splitterDirection: options.splitterDirection,
|
||||||
|
queryViewSizePercent: options.queryViewSizePercent,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
|
@ -34,6 +34,7 @@ jest.mock("Shared/AppStatePersistenceUtility", () => ({
|
|||||||
AppStateComponentNames: {
|
AppStateComponentNames: {
|
||||||
QueryCopilot: "QueryCopilot",
|
QueryCopilot: "QueryCopilot",
|
||||||
},
|
},
|
||||||
|
readSubComponentState: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("QueryTabComponent", () => {
|
describe("QueryTabComponent", () => {
|
||||||
|
@ -18,13 +18,7 @@ import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
|||||||
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 {
|
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
||||||
LocalStorageUtility,
|
|
||||||
StorageKey,
|
|
||||||
getDefaultQueryResultsView,
|
|
||||||
getRUThreshold,
|
|
||||||
ruThresholdEnabled,
|
|
||||||
} from "Shared/StorageUtility";
|
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { Allotment } from "allotment";
|
import { Allotment } from "allotment";
|
||||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
@ -99,6 +93,13 @@ export interface IQueryTabComponentProps {
|
|||||||
copilotEnabled?: boolean;
|
copilotEnabled?: boolean;
|
||||||
isSampleCopilotActive?: boolean;
|
isSampleCopilotActive?: boolean;
|
||||||
copilotStore?: Partial<QueryCopilotState>;
|
copilotStore?: Partial<QueryCopilotState>;
|
||||||
|
splitterDirection?: "horizontal" | "vertical";
|
||||||
|
queryViewSizePercent?: number;
|
||||||
|
onUpdatePersistedState: (state: {
|
||||||
|
queryText: string;
|
||||||
|
splitterDirection: "vertical" | "horizontal";
|
||||||
|
queryViewSizePercent: number;
|
||||||
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IQueryTabStates {
|
interface IQueryTabStates {
|
||||||
@ -118,11 +119,13 @@ interface IQueryTabStates {
|
|||||||
queryResultsView: SplitterDirection;
|
queryResultsView: SplitterDirection;
|
||||||
errors?: QueryError[];
|
errors?: QueryError[];
|
||||||
modelMarkers?: monaco.editor.IMarkerData[];
|
modelMarkers?: monaco.editor.IMarkerData[];
|
||||||
|
queryViewSizePercent: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any => {
|
export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any => {
|
||||||
const styles = useQueryTabStyles();
|
const styles = useQueryTabStyles();
|
||||||
const copilotStore = useCopilotStore();
|
const copilotStore = useCopilotStore();
|
||||||
|
|
||||||
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
||||||
const queryTabProps = {
|
const queryTabProps = {
|
||||||
...props,
|
...props,
|
||||||
@ -132,12 +135,12 @@ export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any =>
|
|||||||
isSampleCopilotActive: isSampleCopilotActive,
|
isSampleCopilotActive: isSampleCopilotActive,
|
||||||
copilotStore: copilotStore,
|
copilotStore: copilotStore,
|
||||||
};
|
};
|
||||||
return <QueryTabComponentImpl styles={styles} {...queryTabProps}></QueryTabComponentImpl>;
|
return <QueryTabComponentImpl styles={styles} {...queryTabProps} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QueryTabComponent = (props: IQueryTabComponentProps): any => {
|
export const QueryTabComponent = (props: IQueryTabComponentProps): any => {
|
||||||
const styles = useQueryTabStyles();
|
const styles = useQueryTabStyles();
|
||||||
return <QueryTabComponentImpl styles={styles} {...props}></QueryTabComponentImpl>;
|
return <QueryTabComponentImpl styles={styles} {...{ ...props }} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
type QueryTabComponentImplProps = IQueryTabComponentProps & {
|
type QueryTabComponentImplProps = IQueryTabComponentProps & {
|
||||||
@ -146,6 +149,8 @@ type QueryTabComponentImplProps = IQueryTabComponentProps & {
|
|||||||
|
|
||||||
// Inner (legacy) class component. We only use this component via one of the two functional components above (since we need to use the `useQueryTabStyles` hook).
|
// Inner (legacy) class component. We only use this component via one of the two functional components above (since we need to use the `useQueryTabStyles` hook).
|
||||||
class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps, IQueryTabStates> {
|
class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps, IQueryTabStates> {
|
||||||
|
private static readonly DEBOUNCE_DELAY_MS = 1000;
|
||||||
|
|
||||||
public queryEditorId: string;
|
public queryEditorId: string;
|
||||||
public executeQueryButton: Button;
|
public executeQueryButton: Button;
|
||||||
public saveQueryButton: Button;
|
public saveQueryButton: Button;
|
||||||
@ -157,10 +162,10 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
private _iterator: MinimalQueryIterator;
|
private _iterator: MinimalQueryIterator;
|
||||||
private queryAbortController: AbortController;
|
private queryAbortController: AbortController;
|
||||||
queryEditor: React.RefObject<EditorReact>;
|
queryEditor: React.RefObject<EditorReact>;
|
||||||
|
private timeoutId: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
constructor(props: QueryTabComponentImplProps) {
|
constructor(props: QueryTabComponentImplProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.queryEditor = createRef<EditorReact>();
|
this.queryEditor = createRef<EditorReact>();
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
@ -176,7 +181,9 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
cancelQueryTimeoutID: undefined,
|
cancelQueryTimeoutID: undefined,
|
||||||
copilotActive: this._queryCopilotActive(),
|
copilotActive: this._queryCopilotActive(),
|
||||||
currentTabActive: true,
|
currentTabActive: true,
|
||||||
queryResultsView: getDefaultQueryResultsView(),
|
queryResultsView:
|
||||||
|
props.splitterDirection === "vertical" ? SplitterDirection.Vertical : SplitterDirection.Horizontal,
|
||||||
|
queryViewSizePercent: props.queryViewSizePercent,
|
||||||
};
|
};
|
||||||
this.isCloseClicked = false;
|
this.isCloseClicked = false;
|
||||||
this.splitterId = this.props.tabId + "_splitter";
|
this.splitterId = this.props.tabId + "_splitter";
|
||||||
@ -207,6 +214,23 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to save the query text in the query tab state
|
||||||
|
* Since it reads and writes to the same state, it is debounced
|
||||||
|
*/
|
||||||
|
private saveQueryTabStateDebounced = () => {
|
||||||
|
if (this.timeoutId) {
|
||||||
|
clearTimeout(this.timeoutId);
|
||||||
|
}
|
||||||
|
this.timeoutId = setTimeout(async () => {
|
||||||
|
this.props.onUpdatePersistedState({
|
||||||
|
queryText: this.state.sqlQueryEditorContent,
|
||||||
|
splitterDirection: this.state.queryResultsView,
|
||||||
|
queryViewSizePercent: this.state.queryViewSizePercent,
|
||||||
|
});
|
||||||
|
}, QueryTabComponentImpl.DEBOUNCE_DELAY_MS);
|
||||||
|
};
|
||||||
|
|
||||||
private _queryCopilotActive(): boolean {
|
private _queryCopilotActive(): boolean {
|
||||||
if (this.props.copilotEnabled) {
|
if (this.props.copilotEnabled) {
|
||||||
return readCopilotToggleStatus(userContext.databaseAccount);
|
return readCopilotToggleStatus(userContext.databaseAccount);
|
||||||
@ -567,7 +591,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
private _setViewLayout(direction: SplitterDirection): void {
|
private _setViewLayout(direction: SplitterDirection): void {
|
||||||
this.setState({ queryResultsView: direction });
|
this.setState({ queryResultsView: direction }, () => this.saveQueryTabStateDebounced());
|
||||||
|
|
||||||
// We'll need to refresh the context buttons to update the selected state of the view buttons
|
// We'll need to refresh the context buttons to update the selected state of the view buttons
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -599,13 +623,16 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
if (this.state.copilotActive) {
|
if (this.state.copilotActive) {
|
||||||
this.props.copilotStore?.setQuery(newContent);
|
this.props.copilotStore?.setQuery(newContent);
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
sqlQueryEditorContent: newContent,
|
sqlQueryEditorContent: newContent,
|
||||||
queryCopilotGeneratedQuery: "",
|
queryCopilotGeneratedQuery: "",
|
||||||
|
|
||||||
// Clear the markers when the user edits the document.
|
// Clear the markers when the user edits the document.
|
||||||
modelMarkers: [],
|
modelMarkers: [],
|
||||||
});
|
},
|
||||||
|
() => this.saveQueryTabStateDebounced(),
|
||||||
|
);
|
||||||
if (this.isPreferredApiMongoDB) {
|
if (this.isPreferredApiMongoDB) {
|
||||||
if (newContent.length > 0) {
|
if (newContent.length > 0) {
|
||||||
this.executeQueryButton = {
|
this.executeQueryButton = {
|
||||||
@ -704,8 +731,20 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
></QueryCopilotPromptbar>
|
></QueryCopilotPromptbar>
|
||||||
)}
|
)}
|
||||||
{/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */}
|
{/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */}
|
||||||
<Allotment key={vertical.toString()} vertical={vertical}>
|
<Allotment
|
||||||
<Allotment.Pane data-test="QueryTab/EditorPane">
|
key={vertical.toString()}
|
||||||
|
vertical={vertical}
|
||||||
|
onDragEnd={(sizes: number[]) => {
|
||||||
|
const queryViewSizePercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
||||||
|
this.setState({ queryViewSizePercent }, () => this.saveQueryTabStateDebounced());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Allotment.Pane
|
||||||
|
data-test="QueryTab/EditorPane"
|
||||||
|
preferredSize={
|
||||||
|
this.state.queryViewSizePercent !== undefined ? `${this.state.queryViewSizePercent}%` : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
<EditorReact
|
<EditorReact
|
||||||
ref={this.queryEditor}
|
ref={this.queryEditor}
|
||||||
className={this.props.styles.queryEditor}
|
className={this.props.styles.queryEditor}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { SettingsComponent } from "../Controls/Settings/SettingsComponent";
|
import { SettingsComponent } from "../Controls/Settings/SettingsComponent";
|
||||||
@ -10,6 +11,18 @@ export class SettingsTabV2 extends TabsBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CollectionSettingsTabV2 extends SettingsTabV2 {
|
export class CollectionSettingsTabV2 extends SettingsTabV2 {
|
||||||
|
protected persistedState: OpenCollectionTab;
|
||||||
|
|
||||||
|
constructor(options: ViewModels.TabOptions) {
|
||||||
|
super(options);
|
||||||
|
this.persistedState = {
|
||||||
|
actionType: ActionType.OpenCollectionTab,
|
||||||
|
tabKind: TabKind.ScaleSettings,
|
||||||
|
databaseResourceId: options.collection.databaseId,
|
||||||
|
collectionResourceId: options.collection.id(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public onActivate(): void {
|
public onActivate(): void {
|
||||||
super.onActivate();
|
super.onActivate();
|
||||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2);
|
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { OpenTab } from "Contracts/ActionContracts";
|
||||||
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
@ -30,6 +31,8 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||||||
protected _theme: string;
|
protected _theme: string;
|
||||||
public onLoadStartKey: number;
|
public onLoadStartKey: number;
|
||||||
|
|
||||||
|
protected persistedState: OpenTab | undefined = undefined; // Used to store state of tab for persistence
|
||||||
|
|
||||||
constructor(options: ViewModels.TabOptions) {
|
constructor(options: ViewModels.TabOptions) {
|
||||||
super();
|
super();
|
||||||
this.index = options.index;
|
this.index = options.index;
|
||||||
@ -55,6 +58,10 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called by useTabs to persist
|
||||||
|
public getPersistedState = (): OpenTab | null => this.persistedState;
|
||||||
|
public triggerPersistState: () => void = undefined;
|
||||||
|
|
||||||
public onCloseTabButtonClick(): void {
|
public onCloseTabButtonClick(): void {
|
||||||
useTabs.getState().closeTab(this);
|
useTabs.getState().closeTab(this);
|
||||||
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
|
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
|
||||||
|
@ -630,7 +630,13 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
|
public onNewQueryClick(
|
||||||
|
source: any,
|
||||||
|
event: MouseEvent,
|
||||||
|
queryText?: string,
|
||||||
|
splitterDirection?: "horizontal" | "vertical",
|
||||||
|
queryViewSizePercent?: number,
|
||||||
|
) {
|
||||||
const collection: ViewModels.Collection = source.collection || source;
|
const collection: ViewModels.Collection = source.collection || source;
|
||||||
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
||||||
const title = "Query " + id;
|
const title = "Query " + id;
|
||||||
@ -653,13 +659,21 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
queryText: queryText,
|
queryText: queryText,
|
||||||
partitionKey: collection.partitionKey,
|
partitionKey: collection.partitionKey,
|
||||||
onLoadStartKey: startKey,
|
onLoadStartKey: startKey,
|
||||||
|
splitterDirection,
|
||||||
|
queryViewSizePercent,
|
||||||
},
|
},
|
||||||
{ container: this.container },
|
{ container: this.container },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) {
|
public onNewMongoQueryClick(
|
||||||
|
source: any,
|
||||||
|
event: MouseEvent,
|
||||||
|
queryText?: string,
|
||||||
|
splitterDirection?: "horizontal" | "vertical",
|
||||||
|
queryViewSizePercent?: number,
|
||||||
|
) {
|
||||||
const collection: ViewModels.Collection = source.collection || source;
|
const collection: ViewModels.Collection = source.collection || source;
|
||||||
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
||||||
|
|
||||||
@ -681,6 +695,9 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
node: this,
|
node: this,
|
||||||
partitionKey: collection.partitionKey,
|
partitionKey: collection.partitionKey,
|
||||||
onLoadStartKey: startKey,
|
onLoadStartKey: startKey,
|
||||||
|
queryText,
|
||||||
|
splitterDirection,
|
||||||
|
queryViewSizePercent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
container: this.container,
|
container: this.container,
|
||||||
|
@ -38,6 +38,7 @@ export type Features = {
|
|||||||
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
||||||
readonly enablePriorityBasedExecution: boolean;
|
readonly enablePriorityBasedExecution: boolean;
|
||||||
readonly disableConnectionStringLogin: boolean;
|
readonly disableConnectionStringLogin: boolean;
|
||||||
|
readonly restoreTabs: boolean;
|
||||||
|
|
||||||
// can be set via both flight and feature flag
|
// can be set via both flight and feature flag
|
||||||
autoscaleDefault: boolean;
|
autoscaleDefault: boolean;
|
||||||
@ -108,6 +109,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||||
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
||||||
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
||||||
|
restoreTabs: "true" === get("restoretabs"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
|
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
|
||||||
// The component name whose state is being saved. Component name must not include special characters.
|
// The component name whose state is being saved. Component name must not include special characters.
|
||||||
export enum AppStateComponentNames {
|
export enum AppStateComponentNames {
|
||||||
DocumentsTab = "DocumentsTab",
|
DocumentsTab = "DocumentsTab",
|
||||||
MostRecentActivity = "MostRecentActivity",
|
MostRecentActivity = "MostRecentActivity",
|
||||||
QueryCopilot = "QueryCopilot",
|
QueryCopilot = "QueryCopilot",
|
||||||
|
DataExplorerAction = "DataExplorerAction",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subcomponent for DataExplorerAction
|
||||||
|
export const OPEN_TABS_SUBCOMPONENT_NAME = "OpenTabs";
|
||||||
|
|
||||||
export const PATH_SEPARATOR = "/"; // export for testing purposes
|
export const PATH_SEPARATOR = "/"; // export for testing purposes
|
||||||
const SCHEMA_VERSION = 1;
|
const SCHEMA_VERSION = 1;
|
||||||
|
|
||||||
@ -72,12 +80,18 @@ export const hasState = (path: StorePath): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// This is for high-frequency state changes
|
// This is for high-frequency state changes
|
||||||
let timeoutId: NodeJS.Timeout | undefined;
|
// Keep track of timeouts per path
|
||||||
|
const pathToTimeoutIdMap = new Map<string, NodeJS.Timeout>();
|
||||||
export const saveStateDebounced = (path: StorePath, state: unknown, debounceDelayMs = 1000): void => {
|
export const saveStateDebounced = (path: StorePath, state: unknown, debounceDelayMs = 1000): void => {
|
||||||
|
const key = createKeyFromPath(path);
|
||||||
|
const timeoutId = pathToTimeoutIdMap.get(key);
|
||||||
if (timeoutId) {
|
if (timeoutId) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
timeoutId = setTimeout(() => saveState(path, state), debounceDelayMs);
|
pathToTimeoutIdMap.set(
|
||||||
|
key,
|
||||||
|
setTimeout(() => saveState(path, state), debounceDelayMs),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ApplicationState {
|
interface ApplicationState {
|
||||||
@ -112,3 +126,93 @@ export const createKeyFromPath = (path: StorePath): string => {
|
|||||||
export const deleteAllStates = (): void => {
|
export const deleteAllStates = (): void => {
|
||||||
LocalStorageUtility.removeEntry(StorageKey.AppState);
|
LocalStorageUtility.removeEntry(StorageKey.AppState);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Convenience functions
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param subComponentName
|
||||||
|
* @param collection
|
||||||
|
* @param defaultValue Will be returned if persisted state is not found
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const readSubComponentState = <T>(
|
||||||
|
componentName: AppStateComponentNames,
|
||||||
|
subComponentName: string,
|
||||||
|
collection: ViewModels.CollectionBase | undefined,
|
||||||
|
defaultValue: T,
|
||||||
|
): T => {
|
||||||
|
const globalAccountName = userContext.databaseAccount?.name;
|
||||||
|
if (!globalAccountName) {
|
||||||
|
const message = "Database account name not found in userContext";
|
||||||
|
console.error(message);
|
||||||
|
TelemetryProcessor.traceFailure(Action.ReadPersistedTabState, { message, componentName });
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = loadState({
|
||||||
|
componentName: componentName,
|
||||||
|
subComponentName,
|
||||||
|
globalAccountName,
|
||||||
|
databaseName: collection ? collection.databaseId : "",
|
||||||
|
containerName: collection ? collection.id() : "",
|
||||||
|
}) as T;
|
||||||
|
|
||||||
|
return state || defaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param subComponentName
|
||||||
|
* @param collection
|
||||||
|
* @param state State to save
|
||||||
|
* @param debounce true for high-frequency calls (e.g mouse drag events)
|
||||||
|
*/
|
||||||
|
export const saveSubComponentState = <T>(
|
||||||
|
componentName: AppStateComponentNames,
|
||||||
|
subComponentName: string,
|
||||||
|
collection: ViewModels.CollectionBase | undefined,
|
||||||
|
state: T,
|
||||||
|
debounce?: boolean,
|
||||||
|
): void => {
|
||||||
|
const globalAccountName = userContext.databaseAccount?.name;
|
||||||
|
if (!globalAccountName) {
|
||||||
|
const message = "Database account name not found in userContext";
|
||||||
|
console.error(message);
|
||||||
|
TelemetryProcessor.traceFailure(Action.SavePersistedTabState, { message, componentName });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(debounce ? saveStateDebounced : saveState)(
|
||||||
|
{
|
||||||
|
componentName: componentName,
|
||||||
|
subComponentName,
|
||||||
|
globalAccountName,
|
||||||
|
databaseName: collection ? collection.databaseId : "",
|
||||||
|
containerName: collection ? collection.id() : "",
|
||||||
|
},
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteSubComponentState = (
|
||||||
|
componentName: AppStateComponentNames,
|
||||||
|
subComponentName: string,
|
||||||
|
collection: ViewModels.CollectionBase,
|
||||||
|
) => {
|
||||||
|
const globalAccountName = userContext.databaseAccount?.name;
|
||||||
|
if (!globalAccountName) {
|
||||||
|
const message = "Database account name not found in userContext";
|
||||||
|
console.error(message);
|
||||||
|
TelemetryProcessor.traceFailure(Action.DeletePersistedTabState, { message, componentName });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteState({
|
||||||
|
componentName: componentName,
|
||||||
|
subComponentName,
|
||||||
|
globalAccountName,
|
||||||
|
databaseName: collection.databaseId,
|
||||||
|
containerName: collection.id(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -7,6 +7,11 @@ import Explorer from "Explorer/Explorer";
|
|||||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||||
|
import {
|
||||||
|
AppStateComponentNames,
|
||||||
|
OPEN_TABS_SUBCOMPONENT_NAME,
|
||||||
|
readSubComponentState,
|
||||||
|
} from "Shared/AppStatePersistenceUtility";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||||
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
||||||
@ -80,6 +85,11 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
|||||||
await updateContextForCopilot(explorer);
|
await updateContextForCopilot(explorer);
|
||||||
await updateContextForSampleData(explorer);
|
await updateContextForSampleData(explorer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userContext.features.restoreTabs) {
|
||||||
|
restoreOpenTabs();
|
||||||
|
}
|
||||||
|
|
||||||
setExplorer(explorer);
|
setExplorer(explorer);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -816,3 +826,17 @@ async function updateContextForSampleData(explorer: Explorer): Promise<void> {
|
|||||||
interface SampledataconnectionResponse {
|
interface SampledataconnectionResponse {
|
||||||
connectionString: string;
|
connectionString: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const restoreOpenTabs = () => {
|
||||||
|
const openTabsState = readSubComponentState<(DataExplorerAction | undefined)[]>(
|
||||||
|
AppStateComponentNames.DataExplorerAction,
|
||||||
|
OPEN_TABS_SUBCOMPONENT_NAME,
|
||||||
|
undefined,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
openTabsState.forEach((openTabState) => {
|
||||||
|
if (openTabState) {
|
||||||
|
handleOpenAction(openTabState, useDatabases.getState().databases, this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import { clamp } from "@fluentui/react";
|
import { clamp } from "@fluentui/react";
|
||||||
|
import { OpenTab } from "Contracts/ActionContracts";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
|
import {
|
||||||
|
AppStateComponentNames,
|
||||||
|
OPEN_TABS_SUBCOMPONENT_NAME,
|
||||||
|
saveSubComponentState,
|
||||||
|
} from "Shared/AppStatePersistenceUtility";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { CollectionTabKind } from "../Contracts/ViewModels";
|
import { CollectionTabKind } from "../Contracts/ViewModels";
|
||||||
@ -36,6 +42,7 @@ export interface TabsState {
|
|||||||
selectLeftTab: () => void;
|
selectLeftTab: () => void;
|
||||||
selectRightTab: () => void;
|
selectRightTab: () => void;
|
||||||
closeActiveTab: () => void;
|
closeActiveTab: () => void;
|
||||||
|
persistTabsState: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ReactTabKind {
|
export enum ReactTabKind {
|
||||||
@ -73,7 +80,9 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
|||||||
},
|
},
|
||||||
activateNewTab: (tab: TabsBase): void => {
|
activateNewTab: (tab: TabsBase): void => {
|
||||||
set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab, activeReactTab: undefined }));
|
set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab, activeReactTab: undefined }));
|
||||||
|
tab.triggerPersistState = get().persistTabsState;
|
||||||
tab.onActivate();
|
tab.onActivate();
|
||||||
|
get().persistTabsState();
|
||||||
},
|
},
|
||||||
activateReactTab: (tabKind: ReactTabKind): void => {
|
activateReactTab: (tabKind: ReactTabKind): void => {
|
||||||
// Clear the selected node when switching to a react tab.
|
// Clear the selected node when switching to a react tab.
|
||||||
@ -130,6 +139,8 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
set({ openedTabs: updatedTabs });
|
set({ openedTabs: updatedTabs });
|
||||||
|
|
||||||
|
get().persistTabsState();
|
||||||
},
|
},
|
||||||
closeAllNotebookTabs: (hardClose): void => {
|
closeAllNotebookTabs: (hardClose): void => {
|
||||||
const isNotebook = (tabKind: CollectionTabKind): boolean => {
|
const isNotebook = (tabKind: CollectionTabKind): boolean => {
|
||||||
@ -226,4 +237,15 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
|||||||
state.closeTab(state.activeTab);
|
state.closeTab(state.activeTab);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
persistTabsState: () => {
|
||||||
|
const state = get();
|
||||||
|
const openTabsStates = state.openedTabs.map((tab) => tab.getPersistedState());
|
||||||
|
|
||||||
|
saveSubComponentState<OpenTab[]>(
|
||||||
|
AppStateComponentNames.DataExplorerAction,
|
||||||
|
OPEN_TABS_SUBCOMPONENT_NAME,
|
||||||
|
undefined,
|
||||||
|
openTabsStates,
|
||||||
|
);
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
Loading…
Reference in New Issue
Block a user