Migrate Copilot local persistence (toggle and prompt history) to new local storage infrastructure (#1948)
* Migrate copilot persistence to AppState * Migrate persistence of toggle and history to new infra * Save toggle value as boolean * Fix compile bug * Fix unit tests
This commit is contained in:
parent
d7647b2ecf
commit
91649d2f52
|
@ -28,6 +28,8 @@ import {
|
||||||
SuggestedPrompt,
|
SuggestedPrompt,
|
||||||
getSampleDatabaseSuggestedPrompts,
|
getSampleDatabaseSuggestedPrompts,
|
||||||
getSuggestedPrompts,
|
getSuggestedPrompts,
|
||||||
|
readPromptHistory,
|
||||||
|
savePromptHistory,
|
||||||
} from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
} from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { SubmitFeedback, allocatePhoenixContainer } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { SubmitFeedback, allocatePhoenixContainer } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
|
@ -136,9 +138,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
||||||
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
|
const [histories, setHistories] = useState<string[]>(() => readPromptHistory(userContext.databaseAccount));
|
||||||
const cachedHistories = cachedHistoriesString?.split("|");
|
|
||||||
const [histories, setHistories] = useState<string[]>(cachedHistories || []);
|
|
||||||
const suggestedPrompts: SuggestedPrompt[] = isSampleCopilotActive
|
const suggestedPrompts: SuggestedPrompt[] = isSampleCopilotActive
|
||||||
? getSampleDatabaseSuggestedPrompts()
|
? getSampleDatabaseSuggestedPrompts()
|
||||||
: getSuggestedPrompts();
|
: getSuggestedPrompts();
|
||||||
|
@ -172,7 +172,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||||
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
|
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
|
||||||
|
|
||||||
setHistories(newHistories);
|
setHistories(newHistories);
|
||||||
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join("|"));
|
savePromptHistory(userContext.databaseAccount, newHistories);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetMessageStates = (): void => {
|
const resetMessageStates = (): void => {
|
||||||
|
|
|
@ -1,10 +1,39 @@
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
|
import { CopilotSubComponentNames } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { AppStateComponentNames, StorePath } from "Shared/AppStatePersistenceUtility";
|
||||||
|
import { updateUserContext } from "UserContext";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { QueryCopilotTab } from "./QueryCopilotTab";
|
import { QueryCopilotTab } from "./QueryCopilotTab";
|
||||||
|
|
||||||
describe("Query copilot tab snapshot test", () => {
|
describe("Query copilot tab snapshot test", () => {
|
||||||
it("should render with initial input", () => {
|
it("should render with initial input", () => {
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
name: "name",
|
||||||
|
properties: undefined,
|
||||||
|
id: "",
|
||||||
|
location: "",
|
||||||
|
type: "",
|
||||||
|
kind: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadState = (path: StorePath) => {
|
||||||
|
if (
|
||||||
|
path.componentName === AppStateComponentNames.QueryCopilot &&
|
||||||
|
path.subComponentName === CopilotSubComponentNames.toggleStatus
|
||||||
|
) {
|
||||||
|
return { enabled: true };
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock("Shared/AppStatePersistenceUtility", () => ({
|
||||||
|
loadState,
|
||||||
|
}));
|
||||||
|
|
||||||
const wrapper = shallow(<QueryCopilotTab explorer={new Explorer()} />);
|
const wrapper = shallow(<QueryCopilotTab explorer={new Explorer()} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
||||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||||
|
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
||||||
|
@ -18,18 +19,13 @@ import SplitterLayout from "react-splitter-layout";
|
||||||
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
|
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
|
||||||
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
||||||
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
||||||
import * as StringUtility from "../../Shared/StringUtility";
|
|
||||||
|
|
||||||
export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
|
export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
|
||||||
const { query, setQuery, selectedQuery, setSelectedQuery, isGeneratingQuery } = useQueryCopilot();
|
const { query, setQuery, selectedQuery, setSelectedQuery, isGeneratingQuery } = useQueryCopilot();
|
||||||
|
|
||||||
const cachedCopilotToggleStatus: string = localStorage.getItem(
|
const [copilotActive, setCopilotActive] = useState<boolean>(() =>
|
||||||
`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`,
|
readCopilotToggleStatus(userContext.databaseAccount),
|
||||||
);
|
);
|
||||||
const copilotInitialActive: boolean = cachedCopilotToggleStatus
|
|
||||||
? StringUtility.toBoolean(cachedCopilotToggleStatus)
|
|
||||||
: true;
|
|
||||||
const [copilotActive, setCopilotActive] = useState<boolean>(copilotInitialActive);
|
|
||||||
const [tabActive, setTabActive] = useState<boolean>(true);
|
const [tabActive, setTabActive] = useState<boolean>(true);
|
||||||
|
|
||||||
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
||||||
|
@ -88,7 +84,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||||
|
|
||||||
const toggleCopilot = (toggle: boolean) => {
|
const toggleCopilot = (toggle: boolean) => {
|
||||||
setCopilotActive(toggle);
|
setCopilotActive(toggle);
|
||||||
localStorage.setItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`, toggle.toString());
|
saveCopilotToggleStatus(userContext.databaseAccount, toggle);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -4,8 +4,11 @@ import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
import { sampleDataClient } from "Common/SampleDataClient";
|
import { sampleDataClient } from "Common/SampleDataClient";
|
||||||
import { getPartitionKeyValue } from "Common/dataAccess/getPartitionKeyValue";
|
import { getPartitionKeyValue } from "Common/dataAccess/getPartitionKeyValue";
|
||||||
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
|
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
|
||||||
|
import { DatabaseAccount } from "Contracts/DataModels";
|
||||||
import DocumentId from "Explorer/Tree/DocumentId";
|
import DocumentId from "Explorer/Tree/DocumentId";
|
||||||
|
import { AppStateComponentNames, loadState, saveState } from "Shared/AppStatePersistenceUtility";
|
||||||
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
||||||
|
import * as StringUtility from "../../Shared/StringUtility";
|
||||||
|
|
||||||
export interface SuggestedPrompt {
|
export interface SuggestedPrompt {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -54,3 +57,110 @@ export const getSuggestedPrompts = (): SuggestedPrompt[] => {
|
||||||
{ id: 3, text: "Find the oldest item added to my collection" },
|
{ id: 3, text: "Find the oldest item added to my collection" },
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Prompt history persistence
|
||||||
|
export enum CopilotSubComponentNames {
|
||||||
|
promptHistory = "PromptHistory",
|
||||||
|
toggleStatus = "ToggleStatus",
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLegacyHistoryKey = (databaseAccount: DatabaseAccount): string =>
|
||||||
|
`${databaseAccount?.id}-queryCopilotHistories`;
|
||||||
|
const getLegacyToggleStatusKey = (databaseAccount: DatabaseAccount): string =>
|
||||||
|
`${databaseAccount?.id}-queryCopilotToggleStatus`;
|
||||||
|
|
||||||
|
// Migration only needs to run once
|
||||||
|
let hasMigrated = false;
|
||||||
|
// Migrate old prompt history to new format
|
||||||
|
export const migrateCopilotPersistence = (databaseAccount: DatabaseAccount): void => {
|
||||||
|
if (hasMigrated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = getLegacyHistoryKey(databaseAccount);
|
||||||
|
let item = localStorage.getItem(key);
|
||||||
|
if (item !== undefined && item !== null) {
|
||||||
|
const historyItems = item.split("|");
|
||||||
|
saveState(
|
||||||
|
{
|
||||||
|
componentName: AppStateComponentNames.QueryCopilot,
|
||||||
|
subComponentName: CopilotSubComponentNames.promptHistory,
|
||||||
|
globalAccountName: databaseAccount.name,
|
||||||
|
databaseName: undefined,
|
||||||
|
containerName: undefined,
|
||||||
|
},
|
||||||
|
historyItems,
|
||||||
|
);
|
||||||
|
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
key = getLegacyToggleStatusKey(databaseAccount);
|
||||||
|
item = localStorage.getItem(key);
|
||||||
|
if (item !== undefined && item !== null) {
|
||||||
|
saveState(
|
||||||
|
{
|
||||||
|
componentName: AppStateComponentNames.QueryCopilot,
|
||||||
|
subComponentName: CopilotSubComponentNames.toggleStatus,
|
||||||
|
globalAccountName: databaseAccount.name,
|
||||||
|
databaseName: undefined,
|
||||||
|
containerName: undefined,
|
||||||
|
},
|
||||||
|
StringUtility.toBoolean(item),
|
||||||
|
);
|
||||||
|
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMigrated = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readPromptHistory = (databaseAccount: DatabaseAccount): string[] => {
|
||||||
|
migrateCopilotPersistence(databaseAccount);
|
||||||
|
return (
|
||||||
|
(loadState({
|
||||||
|
componentName: AppStateComponentNames.QueryCopilot,
|
||||||
|
subComponentName: CopilotSubComponentNames.promptHistory,
|
||||||
|
globalAccountName: databaseAccount.name,
|
||||||
|
databaseName: undefined,
|
||||||
|
containerName: undefined,
|
||||||
|
}) as string[]) || []
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const savePromptHistory = (databaseAccount: DatabaseAccount, historyItems: string[]): void => {
|
||||||
|
saveState(
|
||||||
|
{
|
||||||
|
componentName: AppStateComponentNames.QueryCopilot,
|
||||||
|
subComponentName: CopilotSubComponentNames.promptHistory,
|
||||||
|
globalAccountName: databaseAccount.name,
|
||||||
|
databaseName: undefined,
|
||||||
|
containerName: undefined,
|
||||||
|
},
|
||||||
|
historyItems,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readCopilotToggleStatus = (databaseAccount: DatabaseAccount): boolean => {
|
||||||
|
migrateCopilotPersistence(databaseAccount);
|
||||||
|
return !!loadState({
|
||||||
|
componentName: AppStateComponentNames.QueryCopilot,
|
||||||
|
subComponentName: CopilotSubComponentNames.toggleStatus,
|
||||||
|
globalAccountName: databaseAccount.name,
|
||||||
|
databaseName: undefined,
|
||||||
|
containerName: undefined,
|
||||||
|
}) as boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveCopilotToggleStatus = (databaseAccount: DatabaseAccount, status: boolean): void => {
|
||||||
|
saveState(
|
||||||
|
{
|
||||||
|
componentName: AppStateComponentNames.QueryCopilot,
|
||||||
|
subComponentName: CopilotSubComponentNames.toggleStatus,
|
||||||
|
globalAccountName: databaseAccount.name,
|
||||||
|
databaseName: undefined,
|
||||||
|
containerName: undefined,
|
||||||
|
},
|
||||||
|
status,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -26,7 +26,7 @@ import {
|
||||||
import { AuthorizationTokenHeaderMetadata, QueryResults } from "Contracts/ViewModels";
|
import { AuthorizationTokenHeaderMetadata, QueryResults } from "Contracts/ViewModels";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
|
@ -36,7 +36,6 @@ import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
import * as StringUtility from "../../../Shared/StringUtility";
|
|
||||||
|
|
||||||
async function fetchWithTimeout(
|
async function fetchWithTimeout(
|
||||||
url: string,
|
url: string,
|
||||||
|
@ -361,9 +360,7 @@ export const QueryDocumentsPerPage = async (
|
||||||
correlationId: useQueryCopilot.getState().correlationId,
|
correlationId: useQueryCopilot.getState().correlationId,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const isCopilotActive = StringUtility.toBoolean(
|
const isCopilotActive = readCopilotToggleStatus(userContext.databaseAccount);
|
||||||
localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`),
|
|
||||||
);
|
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||||
correlationId: useQueryCopilot.getState().correlationId,
|
correlationId: useQueryCopilot.getState().correlationId,
|
||||||
|
|
|
@ -17,38 +17,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<QueryCopilotPromptbar
|
|
||||||
containerId="SampleContainer"
|
|
||||||
databaseId="CopilotSampleDB"
|
|
||||||
explorer={
|
|
||||||
Explorer {
|
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
|
||||||
"isTabsContentExpanded": [Function],
|
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
|
||||||
"onRefreshResourcesClick": [Function],
|
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
|
||||||
"queriesClient": QueriesClient {
|
|
||||||
"container": [Circular],
|
|
||||||
},
|
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toggleCopilot={[Function]}
|
|
||||||
/>
|
|
||||||
<Stack
|
<Stack
|
||||||
className="tabPaneContentContainer"
|
className="tabPaneContentContainer"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
// Definitions of State data
|
// Definitions of State data
|
||||||
|
|
||||||
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
||||||
import { deleteState, loadState, saveState, saveStateDebounced } from "Shared/AppStatePersistenceUtility";
|
import {
|
||||||
|
AppStateComponentNames,
|
||||||
|
deleteState,
|
||||||
|
loadState,
|
||||||
|
saveState,
|
||||||
|
saveStateDebounced,
|
||||||
|
} from "Shared/AppStatePersistenceUtility";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
|
||||||
const componentName = "DocumentsTab";
|
const componentName = AppStateComponentNames.DocumentsTab;
|
||||||
|
|
||||||
export enum SubComponentName {
|
export enum SubComponentName {
|
||||||
ColumnSizes = "ColumnSizes",
|
ColumnSizes = "ColumnSizes",
|
||||||
FilterHistory = "FilterHistory",
|
FilterHistory = "FilterHistory",
|
||||||
|
|
|
@ -2,12 +2,14 @@ import { fireEvent, render } from "@testing-library/react";
|
||||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||||
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||||
|
import { CopilotSubComponentNames } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import {
|
import {
|
||||||
IQueryTabComponentProps,
|
IQueryTabComponentProps,
|
||||||
QueryTabComponent,
|
QueryTabComponent,
|
||||||
QueryTabCopilotComponent,
|
QueryTabCopilotComponent,
|
||||||
} from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
} from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
||||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||||
|
import { AppStateComponentNames, StorePath } from "Shared/AppStatePersistenceUtility";
|
||||||
import { updateUserContext, userContext } from "UserContext";
|
import { updateUserContext, userContext } from "UserContext";
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
@ -16,6 +18,24 @@ import React from "react";
|
||||||
|
|
||||||
jest.mock("Explorer/Controls/Editor/EditorReact");
|
jest.mock("Explorer/Controls/Editor/EditorReact");
|
||||||
|
|
||||||
|
const loadState = (path: StorePath) => {
|
||||||
|
if (
|
||||||
|
path.componentName === AppStateComponentNames.QueryCopilot &&
|
||||||
|
path.subComponentName === CopilotSubComponentNames.toggleStatus
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock("Shared/AppStatePersistenceUtility", () => ({
|
||||||
|
loadState,
|
||||||
|
AppStateComponentNames: {
|
||||||
|
QueryCopilot: "QueryCopilot",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
describe("QueryTabComponent", () => {
|
describe("QueryTabComponent", () => {
|
||||||
const mockStore = useQueryCopilot.getState();
|
const mockStore = useQueryCopilot.getState();
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -50,6 +70,17 @@ describe("QueryTabComponent", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("copilot should be enabled by default when tab is active", () => {
|
it("copilot should be enabled by default when tab is active", () => {
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
name: "name",
|
||||||
|
properties: undefined,
|
||||||
|
id: "",
|
||||||
|
location: "",
|
||||||
|
type: "",
|
||||||
|
kind: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
useQueryCopilot.getState().setCopilotEnabled(true);
|
useQueryCopilot.getState().setCopilotEnabled(true);
|
||||||
useQueryCopilot.getState().setCopilotUserDBEnabled(true);
|
useQueryCopilot.getState().setCopilotUserDBEnabled(true);
|
||||||
const activeTab = new TabsBase({
|
const activeTab = new TabsBase({
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { monaco } from "Explorer/LazyMonaco";
|
||||||
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||||
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||||
|
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
||||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||||
|
@ -46,7 +47,6 @@ import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
|
||||||
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import * as StringUtility from "../../../Shared/StringUtility";
|
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import * as QueryUtils from "../../../Utils/QueryUtils";
|
import * as QueryUtils from "../../../Utils/QueryUtils";
|
||||||
|
@ -209,13 +209,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||||
|
|
||||||
private _queryCopilotActive(): boolean {
|
private _queryCopilotActive(): boolean {
|
||||||
if (this.props.copilotEnabled) {
|
if (this.props.copilotEnabled) {
|
||||||
const cachedCopilotToggleStatus: string = localStorage.getItem(
|
return readCopilotToggleStatus(userContext.databaseAccount);
|
||||||
`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`,
|
|
||||||
);
|
|
||||||
const copilotInitialActive: boolean = cachedCopilotToggleStatus
|
|
||||||
? StringUtility.toBoolean(cachedCopilotToggleStatus)
|
|
||||||
: true;
|
|
||||||
return copilotInitialActive;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -584,7 +578,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||||
private _toggleCopilot = (active: boolean) => {
|
private _toggleCopilot = (active: boolean) => {
|
||||||
this.setState({ copilotActive: active });
|
this.setState({ copilotActive: active });
|
||||||
useQueryCopilot.getState().setCopilotEnabledforExecution(active);
|
useQueryCopilot.getState().setCopilotEnabledforExecution(active);
|
||||||
localStorage.setItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`, active.toString());
|
saveCopilotToggleStatus(userContext.databaseAccount, active);
|
||||||
|
|
||||||
TelemetryProcessor.traceSuccess(active ? Action.ActivateQueryCopilot : Action.DeactivateQueryCopilot, {
|
TelemetryProcessor.traceSuccess(active ? Action.ActivateQueryCopilot : Action.DeactivateQueryCopilot, {
|
||||||
databaseName: this.props.collection.databaseId,
|
databaseName: this.props.collection.databaseId,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
AppStateComponentNames,
|
||||||
createKeyFromPath,
|
createKeyFromPath,
|
||||||
deleteState,
|
deleteState,
|
||||||
loadState,
|
loadState,
|
||||||
|
@ -20,7 +21,7 @@ jest.mock("Shared/StorageUtility", () => ({
|
||||||
|
|
||||||
describe("AppStatePersistenceUtility", () => {
|
describe("AppStatePersistenceUtility", () => {
|
||||||
const storePath = {
|
const storePath = {
|
||||||
componentName: "a",
|
componentName: AppStateComponentNames.DocumentsTab,
|
||||||
subComponentName: "b",
|
subComponentName: "b",
|
||||||
globalAccountName: "c",
|
globalAccountName: "c",
|
||||||
databaseName: "d",
|
databaseName: "d",
|
||||||
|
@ -176,10 +177,10 @@ describe("AppStatePersistenceUtility", () => {
|
||||||
|
|
||||||
it("should handle components that include special characters", () => {
|
it("should handle components that include special characters", () => {
|
||||||
const storePath = {
|
const storePath = {
|
||||||
componentName: "a/b/c",
|
componentName: AppStateComponentNames.DocumentsTab,
|
||||||
subComponentName: 'd"e"f',
|
subComponentName: 'd"e"f',
|
||||||
globalAccountName: "g:h",
|
globalAccountName: "g:hi{j",
|
||||||
databaseName: "i{j",
|
databaseName: "a/b/c",
|
||||||
containerName: "https://blahblah.document.azure.com:443/",
|
containerName: "https://blahblah.document.azure.com:443/",
|
||||||
};
|
};
|
||||||
const key = createKeyFromPath(storePath);
|
const key = createKeyFromPath(storePath);
|
||||||
|
@ -190,10 +191,9 @@ describe("AppStatePersistenceUtility", () => {
|
||||||
const expectSubstringsInValue = (value: string, subStrings: string[]): boolean =>
|
const expectSubstringsInValue = (value: string, subStrings: string[]): boolean =>
|
||||||
subStrings.every((subString) => value.includes(subString));
|
subStrings.every((subString) => value.includes(subString));
|
||||||
|
|
||||||
expect(expectSubstringsInValue(segments[1], ["a", "b", "c"])).toBe(true);
|
|
||||||
expect(expectSubstringsInValue(segments[2], ["d", "e", "f"])).toBe(true);
|
expect(expectSubstringsInValue(segments[2], ["d", "e", "f"])).toBe(true);
|
||||||
expect(expectSubstringsInValue(segments[3], ["g", "h"])).toBe(true);
|
expect(expectSubstringsInValue(segments[3], ["g", "hi", "j"])).toBe(true);
|
||||||
expect(expectSubstringsInValue(segments[4], ["i", "j"])).toBe(true);
|
expect(expectSubstringsInValue(segments[4], ["a", "b", "c"])).toBe(true);
|
||||||
expect(expectSubstringsInValue(segments[5], ["https", "blahblah", "document", "com", "443"])).toBe(true);
|
expect(expectSubstringsInValue(segments[5], ["https", "blahblah", "document", "com", "443"])).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
|
||||||
// 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 type ComponentName = "DocumentsTab";
|
export enum AppStateComponentNames {
|
||||||
|
DocumentsTab = "DocumentsTab",
|
||||||
|
QueryCopilot = "QueryCopilot",
|
||||||
|
}
|
||||||
|
|
||||||
export const PATH_SEPARATOR = "/"; // export for testing purposes
|
export const PATH_SEPARATOR = "/"; // export for testing purposes
|
||||||
const SCHEMA_VERSION = 1;
|
const SCHEMA_VERSION = 1;
|
||||||
|
|
||||||
|
@ -14,8 +18,9 @@ export interface StateData {
|
||||||
data: unknown;
|
data: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
type StorePath = {
|
// Export for testing purposes
|
||||||
componentName: string;
|
export type StorePath = {
|
||||||
|
componentName: AppStateComponentNames;
|
||||||
subComponentName?: string;
|
subComponentName?: string;
|
||||||
globalAccountName?: string;
|
globalAccountName?: string;
|
||||||
databaseName?: string;
|
databaseName?: string;
|
||||||
|
|
Loading…
Reference in New Issue