Compare commits

..

18 Commits

Author SHA1 Message Date
Senthamil Sindhu
ad165ae069 Add more logs for NPS 2024-01-23 14:55:00 -08:00
Senthamil Sindhu
913a96afec Fix error in code 2024-01-18 11:11:03 -08:00
Senthamil Sindhu
e26207e949 Address comments 2024-01-18 11:04:24 -08:00
Senthamil Sindhu
5d6273889d Remove lint error 2024-01-18 09:49:34 -08:00
Senthamil Sindhu
889cf77801 Update logic for NPS survey for existing accounts > 90 days 2024-01-18 09:22:46 -08:00
Laurent Nguyen
0975591945 Fabric: clean up RPC and support show/hide toolbar message (#1680)
* Use Promise for allResourceToken fabric message. Cleanup token message handling and add debounce.

* Improve rpc and update initalization flow

* Fix format

* Rev up message names for new version

* Refactor RPC with Fabric

* Build fix

* Fix build

* Fix format

* Update Message types

* Fix format

* Fix comments

* Fabric toolbar style and support to show/hide it (#1720)

* Add Fabric specific Toolbar design

* Add Fabric message to show/hide the Toolbar

* Fix CommandBarUtil formatting

* Update zustand state on setToolbarStatus to trigger a redraw of the command bar with updated visibility

---------

Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com>

* Fix format

---------

Co-authored-by: Vsevolod Kukol <sevoku@microsoft.com>
2024-01-18 17:13:04 +01:00
sunghyunkang1111
5d13463bdb Copilot settings (#1719)
* gating copilot with AFEC

* Add enable sample DB in settings

* Add enable sample DB in settings

* Add enable sample DB in settings

* PR comments
2024-01-09 13:32:20 -06:00
MokireddySampath
c4cceceafc Status attribute added for the message to be readout by screenreader (#1709) 2024-01-04 20:43:44 +05:30
MokireddySampath
532a453f5a Aria label has been updated to the button(Enable copilot/Disable copilot) (#1706) 2024-01-04 19:38:05 +05:30
MokireddySampath
9355a3ae04 Altt text and role has been added to the 'copilot icon' (#1705) 2024-01-04 19:37:50 +05:30
MokireddySampath
14456c2102 label has been added to the textfield of copilot promptbar (#1704) 2024-01-04 19:37:35 +05:30
MokireddySampath
0c9264e8b3 Bug 2819239:Screen reader does not announce the loading information which appears on invoking the 'Send' button. (#1703)
* Alert has been added and content also added accordign to the loader state

* Snapshot of tests have been updated
2024-01-04 19:37:16 +05:30
MokireddySampath
0dd1032357 Bug 2817823: [Programmatic access - Cosmos DB Query Copilot - Query]: Accessible name is not defined for the 'Like', 'Dislike' and 'Send' buttons present on the page. (#1700)
* text color of link has been changed to get the contrast ratio of atleaast 4.5:1

* screen reader names have been added to the buttons

* Update QueryCopilotPromptbar.tsx
2024-01-04 19:35:12 +05:30
MokireddySampath
5011d12f16 text color of link has been changed to get the contrast ratio of atleaast 4.5:1 (#1699) 2024-01-04 19:34:58 +05:30
MokireddySampath
a7e5ff2a9f status role has been added for the screen reader to announce the status message displayed (#1697) 2024-01-04 19:34:26 +05:30
MokireddySampath
ad1391f623 visual and arialabel were different which has been changed as required and tests have been updated (#1685) 2024-01-04 19:34:14 +05:30
MokireddySampath
a2a5407b15 Label has been added to the text field on selecting autoscale in throughputc (#1676) 2024-01-04 19:33:55 +05:30
Laurent Nguyen
e9181f19d7 Fix datatables issue and indicator not loading for Table API > Entities. Upgrade jquery. Fix right panel resize issue. (#1713)
* Fix datatables issue and indicator not loading for Table API > Entities

* Fix jquery and datatables compile issues. Add patch for datatables.net-colreorder error in types

* Fix side panel size. Fix bug resizing side panel.

* Update PanelContainerComponent unit test snapshot

* Fix commented code
2024-01-03 14:52:34 +01:00
53 changed files with 948 additions and 5163 deletions

View File

@@ -147,6 +147,7 @@
// CommandBar
@CommandBarButtonHeight: 40px;
@FabricCommandBarButtonHeight: 34px;
/**********************************************************************************
Portal Consts
@@ -164,7 +165,7 @@
@FabricFont: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
@FabricBoxBorderRadius: 8px;
@FabricBoxBorderShadow: 0 0 2px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14);
@FabricBoxBorderShadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px;
@FabricBoxMargin: 4px 3px 4px 3px;
@FabricAccentMediumHigh: #0c695a;

View File

@@ -2897,9 +2897,21 @@ a:link {
padding-left: 8px;
}
.settingsSectionInlineCheckbox {
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
align-items: center;
gap: 5px;
}
.settingsSectionLabel {
margin-bottom: @DefaultSpace;
margin-right: 5px;
.panelInfoIcon {
margin-left: 5px;
}
}
.pageOptionsPart {

View File

@@ -47,11 +47,15 @@ a:focus {
border-radius: @FabricBoxBorderRadius;
box-shadow: @FabricBoxBorderShadow;
margin: @FabricBoxMargin;
margin-top: 0px;
padding-top: 2px;
padding: 0px;
height: 40px;
}
.dividerContainer {
padding: @SmallSpace 0px @SmallSpace 0px;
height: @FabricCommandBarButtonHeight;
.flex-display();
span {

1406
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -55,8 +55,8 @@
"crossroads": "0.12.2",
"css-element-queries": "1.1.1",
"d3": "6.1.1",
"datatables.net-colreorder-dt": "1.5.1",
"datatables.net-dt": "1.10.19",
"datatables.net-colreorder-dt": "1.7.0",
"datatables.net-dt": "1.13.8",
"date-fns": "1.29.0",
"dayjs": "1.8.19",
"dom-to-image": "2.6.0",
@@ -71,13 +71,14 @@
"iframe-resizer-react": "1.1.0",
"immutable": "4.0.0-rc.12",
"is-ci": "2.0.0",
"jquery": "3.5.1",
"jquery-typeahead": "2.10.6",
"jquery-ui-dist": "1.12.1",
"jquery": "3.7.1",
"jquery-typeahead": "2.11.1",
"jquery-ui-dist": "1.13.2",
"knockout": "3.5.1",
"mkdirp": "1.0.4",
"monaco-editor": "0.44.0",
"ms": "2.1.3",
"patch-package": "8.0.0",
"p-retry": "4.6.2",
"plotly.js-cartesian-dist-min": "1.52.3",
"post-robot": "10.0.42",
@@ -114,11 +115,14 @@
"@types/codemirror": "0.0.56",
"@types/crossroads": "0.0.30",
"@types/d3": "5.9.2",
"@types/datatables.net": "1.10.28",
"@types/datatables.net-colreorder": "1.4.5",
"@types/dom-to-image": "2.6.2",
"@types/enzyme": "3.10.7",
"@types/enzyme-adapter-react-16": "1.0.6",
"@types/hasher": "0.0.31",
"@types/jest": "26.0.20",
"@types/jquery": "3.5.29",
"@types/node": "12.11.1",
"@types/post-robot": "10.0.1",
"@types/q": "1.5.1",
@@ -187,6 +191,7 @@
"webpack-dev-server": "4.15.1"
},
"scripts": {
"postinstall": "patch-package",
"start": "webpack serve --mode development",
"dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build",
"build:dataExplorer:ci": "npm run build:ci",
@@ -233,4 +238,4 @@
"printWidth": 120,
"endOfLine": "auto"
}
}
}

View File

@@ -0,0 +1,22 @@
diff --git a/node_modules/datatables.net-colreorder/types/types.d.ts b/node_modules/datatables.net-colreorder/types/types.d.ts
index e5dc283..1930c2b 100644
--- a/node_modules/datatables.net-colreorder/types/types.d.ts
+++ b/node_modules/datatables.net-colreorder/types/types.d.ts
@@ -7,7 +7,7 @@
/// <reference types="jquery" />
-import DataTables, {Api} from 'datatables.net';
+import DataTables, { Api } from 'datatables.net';
export default DataTables;
@@ -40,6 +40,8 @@ declare module 'datatables.net' {
/**
* Create a new ColReorder instance for the target DataTable
*/
+ // Ignore this error: error TS7013: Construct signature, which lacks return-type annotation, implicitly has an 'any' return type.
+ // @ts-ignore
new (dt: Api<any>, settings: boolean | ConfigColReorder);
/**

View File

@@ -40,9 +40,10 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
case Cosmos.ResourceType.item:
case Cosmos.ResourceType.pkranges:
// User resource tokens
// TODO userContext.fabricContext.databaseConnectionInfo can be undefined
headers[HttpHeaders.msDate] = new Date().toUTCString();
const resourceTokens = userContext.fabricDatabaseConnectionInfo.resourceTokens;
checkDatabaseResourceTokensValidity(userContext.fabricDatabaseConnectionInfo.resourceTokensTimestamp);
const resourceTokens = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp);
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
case Cosmos.ResourceType.none:
@@ -51,9 +52,11 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
case Cosmos.ResourceType.user:
case Cosmos.ResourceType.permission:
// User master tokens
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(MessageTypes.GetAuthorizationToken, [
requestInfo,
]);
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(
MessageTypes.GetAuthorizationToken,
[requestInfo],
userContext.fabricContext.connectionId,
);
console.log("Response from Fabric: ", authorizationToken);
headers[HttpHeaders.msDate] = authorizationToken.XDate;
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);

View File

@@ -27,15 +27,24 @@ export function handleCachedDataMessage(message: any): void {
runGarbageCollector();
}
/**
*
* @param messageType
* @param params
* @param scope Use this string to identify request Useful to distinguish response from different senders
* @param timeoutInMs
* @returns
*/
export function sendCachedDataMessage<TResponseDataModel>(
messageType: MessageTypes,
params: Object[],
scope?: string,
timeoutInMs?: number,
): Q.Promise<TResponseDataModel> {
let cachedDataPromise: CachedDataPromise<TResponseDataModel> = {
deferred: Q.defer<TResponseDataModel>(),
startTime: new Date(),
id: _.uniqueId(),
id: _.uniqueId(scope),
};
RequestMap[cachedDataPromise.id] = cachedDataPromise;
sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
@@ -47,6 +56,10 @@ export function sendCachedDataMessage<TResponseDataModel>(
);
}
/**
*
* @param data Overwrite the data property of the message
*/
export function sendMessage(data: any): void {
_sendMessage({
signature: "pcIframe",

View File

@@ -18,13 +18,13 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
if (
configContext.platform === Platform.Fabric &&
userContext.fabricDatabaseConnectionInfo &&
userContext.fabricDatabaseConnectionInfo.databaseId === databaseId
userContext.fabricContext &&
userContext.fabricContext.databaseConnectionInfo.databaseId === databaseId
) {
const collections: DataModels.Collection[] = [];
const promises: Promise<ContainerResponse>[] = [];
for (const collectionResourceId in userContext.fabricDatabaseConnectionInfo.resourceTokens) {
for (const collectionResourceId in userContext.fabricContext.databaseConnectionInfo.resourceTokens) {
// Dictionary key looks like this: dbs/SampleDB/colls/Container
const resourceIdObj = collectionResourceId.split("/");
const tokenDatabaseId = resourceIdObj[1];

View File

@@ -14,8 +14,8 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
let databases: DataModels.Database[];
const clearMessage = logConsoleProgress(`Querying databases`);
if (configContext.platform === Platform.Fabric && userContext.fabricDatabaseConnectionInfo?.resourceTokens) {
const tokensData = userContext.fabricDatabaseConnectionInfo;
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) {
const tokensData = userContext.fabricContext.databaseConnectionInfo;
const databaseIdsSet = new Set<string>(); // databaseId

View File

@@ -0,0 +1,42 @@
import { MessageTypes } from "./MessageTypes";
// This is the current version of these messages
export const DATA_EXPLORER_RPC_VERSION = "2";
// Data Explorer to Fabric
// TODO Remove when upgrading to Fabric v2
export type DataExploreMessageV1 =
| "ready"
| {
type: MessageTypes.GetAuthorizationToken;
id: string;
params: GetCosmosTokenMessageOptions[];
}
| {
type: MessageTypes.GetAllResourceTokens;
id: string;
};
// -----------------------------
export type DataExploreMessageV2 =
| {
type: MessageTypes.Ready;
id: string;
params: [string]; // version
}
| {
type: MessageTypes.GetAuthorizationToken;
id: string;
params: GetCosmosTokenMessageOptions[];
}
| {
type: MessageTypes.GetAllResourceTokens;
id: string;
};
export type GetCosmosTokenMessageOptions = {
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges";
resourceId: string;
};

View File

@@ -1,6 +1,12 @@
import { AuthorizationToken, MessageTypes } from "./MessageTypes";
import { AuthorizationToken } from "./MessageTypes";
export type FabricMessage =
// This is the version of these messages
export const FABRIC_RPC_VERSION = "2";
// Fabric to Data Explorer
// TODO Deprecated. Remove this section once DE is updated
export type FabricMessageV1 =
| {
type: "newContainer";
databaseName: string;
@@ -26,38 +32,52 @@ export type FabricMessage =
| {
type: "allResourceTokens";
message: {
id: string;
error: string | undefined;
endpoint: string | undefined;
databaseId: string | undefined;
resourceTokens: unknown | undefined;
resourceTokensTimestamp: number | undefined;
};
};
// -----------------------------
export type DataExploreMessage =
| "ready"
export type FabricMessageV2 =
| {
type: MessageTypes.TelemetryInfo;
data: {
action: "LoadDatabases";
actionModifier: "success" | "start";
defaultExperience: "SQL";
type: "newContainer";
databaseName: string;
}
| {
type: "initialize";
version: string;
id: string;
message: {
connectionId: string;
};
}
| {
type: MessageTypes.GetAuthorizationToken;
id: string;
params: GetCosmosTokenMessageOptions[];
type: "authorizationToken";
message: {
id: string;
error: string | undefined;
data: AuthorizationToken | undefined;
};
}
| {
type: MessageTypes.GetAllResourceTokens;
type: "allResourceTokens_v2";
message: {
id: string;
error: string | undefined;
data: FabricDatabaseConnectionInfo | undefined;
};
}
| {
type: "setToolbarStatus";
message: {
visible: boolean;
};
};
export type GetCosmosTokenMessageOptions = {
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges";
resourceId: string;
};
export type CosmosDBTokenResponse = {
token: string;
date: string;
@@ -66,12 +86,9 @@ export type CosmosDBTokenResponse = {
export type CosmosDBConnectionInfoResponse = {
endpoint: string;
databaseId: string;
resourceTokens: unknown;
resourceTokens: { [resourceId: string]: string };
};
export interface FabricDatabaseConnectionInfo {
endpoint: string;
databaseId: string;
resourceTokens: { [resourceId: string]: string };
export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse {
resourceTokensTimestamp: number;
}

View File

@@ -1,6 +1,12 @@
/**
* Messaging types used with Data Explorer <-> Portal communication,
* Hosted <-> Explorer communication and Data Explorer -> Fabric communication.
*
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
* WARNING: !!!!!!! YOU CAN ONLY ADD NEW TYPES TO THE END OF THIS ENUM !!!!!!!
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
*
* Enum are integers, so inserting or deleting a type will break the communication.
*/
export enum MessageTypes {
TelemetryInfo,
@@ -37,10 +43,9 @@ export enum MessageTypes {
DisplayNPSSurvey,
OpenVCoreMongoNetworkingBlade,
OpenVCoreMongoConnectionStringsBlade,
// Data Explorer -> Fabric communication
GetAuthorizationToken,
GetAllResourceTokens,
GetAuthorizationToken, // Data Explorer -> Fabric
GetAllResourceTokens, // Data Explorer -> Fabric
Ready, // Data Explorer -> Fabric
}
export interface AuthorizationToken {

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
* https://github.com/running-coder/jquery-typeahead/issues/156
* TODO: Replace this minimum definition by the official one when it comes out.
*/
/// <reference path="jquery.d.ts" />
/// <reference types="jquery" />
interface JQueryTypeaheadParam {
input: string;

View File

@@ -3,7 +3,7 @@
// Definitions by: Boris Yankov <https://github.com/borisyankov/>, John Reilly <https://github.com/johnnyreilly>
// Definitions: https://github.com/borisyankov/DefinitelyTyped
/// <reference path="jquery.d.ts"/>
/// <reference types="jquery"/>
declare namespace JQueryUI {
// Accordion //////////////////////////////////////////////////

File diff suppressed because it is too large Load Diff

View File

@@ -23,12 +23,12 @@ describe("ThroughputInput Pane", () => {
});
it("should switch mode properly", () => {
wrapper.find('[aria-label="Manual database throughput"]').simulate("change");
wrapper.find('[id="Manual-input"]').simulate("change");
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe(
"Container throughput (400 - unlimited RU/s)",
);
wrapper.find('[aria-label="Autoscale database throughput"]').simulate("change");
wrapper.find('[id="Autoscale-input"]').simulate("change");
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe("Container throughput (autoscale)");
});
});

View File

@@ -189,7 +189,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
<input
id="Autoscale-input"
className="throughputInputRadioBtn"
aria-label="Autoscale database throughput"
aria-label={`${getThroughputLabelText()} Autoscale`}
aria-required={true}
checked={isAutoscaleSelected}
type="radio"
@@ -204,7 +204,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
<input
id="Manual-input"
className="throughputInputRadioBtn"
aria-label="Manual database throughput"
aria-label={`${getThroughputLabelText()} Manual`}
checked={!isAutoscaleSelected}
type="radio"
aria-required={true}
@@ -276,6 +276,12 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
</Link>
.
</Text>
<Stack horizontal>
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }} aria-label="maxRUDescription">
{isDatabase ? "Database" : getCollectionName()} Required RU/s
</Text>
<InfoTooltip>{getAutoScaleTooltip()}</InfoTooltip>
</Stack>
<TooltipHost
directionalHint={DirectionalHint.topLeftEdge}
@@ -296,7 +302,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
min={SharedConstants.CollectionCreation.DefaultCollectionRUs400}
max={userContext.isTryCosmosDBSubscription ? Constants.TryCosmosExperience.maxRU : Infinity}
value={throughput.toString()}
aria-label="Max request units per second"
ariaLabel={`${isDatabase ? "Database" : getCollectionName()} Required RU/s`}
required={true}
errorMessage={throughputError}
/>

View File

@@ -678,7 +678,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
role="radiogroup"
>
<input
aria-label="Autoscale database throughput"
aria-label="Container throughput (autoscale) Autoscale"
aria-required={true}
checked={true}
className="throughputInputRadioBtn"
@@ -695,7 +695,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
Autoscale
</label>
<input
aria-label="Manual database throughput"
aria-label="Container throughput (autoscale) Manual"
aria-required={true}
checked={false}
className="throughputInputRadioBtn"

View File

@@ -3,9 +3,10 @@ import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { sendMessage } from "Common/MessageHandler";
import { Platform, configContext } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { getCopilotEnabled } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { IGalleryItem } from "Juno/JunoClient";
import { requestDatabaseResourceTokens } from "Platform/Fabric/FabricUtil";
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as ko from "knockout";
@@ -276,21 +277,32 @@ export default class Explorer {
const NINETY_DAYS_IN_MS = 7776000000;
const ONE_DAY_IN_MS = 86400000;
const THREE_DAYS_IN_MS = 259200000;
const isAccountNewerThanNinetyDays = isAccountNewerThanThresholdInMs(
userContext.databaseAccount?.systemData?.createdAt || "",
NINETY_DAYS_IN_MS,
);
const lastSubmitted: string = localStorage.getItem("lastSubmitted");
Logger.logInfo(`NPS Survey last shown date: ${lastSubmitted}`, "Explorer/openNPSSurveyDialog");
if (lastSubmitted !== null) {
Logger.logInfo(`NPS Survey last shown is not empty ${lastSubmitted}`, "Explorer/openNPSSurveyDialog");
let lastSubmittedDate: number = parseInt(lastSubmitted);
Logger.logInfo(`NPS Survey last shown is parsed ${lastSubmittedDate.toString()}`, "Explorer/openNPSSurveyDialog");
if (isNaN(lastSubmittedDate)) {
Logger.logInfo(
`NPS Survey last shown is not a number ${lastSubmittedDate.toString()}`,
"Explorer/openNPSSurveyDialog",
);
lastSubmittedDate = 0;
}
const nowMs: number = Date.now();
Logger.logInfo(`NPS Survey current date ${nowMs.toString()}`, "Explorer/openNPSSurveyDialog");
const millisecsSinceLastSubmitted = nowMs - lastSubmittedDate;
if (millisecsSinceLastSubmitted < NINETY_DAYS_IN_MS) {
Logger.logInfo(
`NPS Survey last shown is less than ninety days ${millisecsSinceLastSubmitted.toString()}`,
"Explorer/openNPSSurveyDialog",
);
return;
}
}
@@ -298,26 +310,32 @@ export default class Explorer {
// Try Cosmos DB subscription - survey shown to 100% of users at day 1 in Data Explorer.
if (userContext.isTryCosmosDBSubscription) {
if (isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS)) {
Logger.logInfo(
`Displaying NPS Survey for Try Cosmos DB ${userContext.apiType}`,
"Explorer/openNPSSurveyDialog",
);
this.sendNPSMessage();
}
} else {
// An existing account is older than 3 days but less than 90 days old. For existing account show to 100% of users in Data Explorer.
// Show survey when an existing account is older than 3 days
if (
!isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", THREE_DAYS_IN_MS) &&
isAccountNewerThanNinetyDays
!isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", THREE_DAYS_IN_MS)
) {
Logger.logInfo(
`Displaying NPS Survey for users with existing ${userContext.apiType} account older than 3 days`,
"Explorer/openNPSSurveyDialog",
);
this.sendNPSMessage();
} else {
// An existing account is greater than 90 days. For existing account show to random 33% of users in Data Explorer.
if (this.getRandomInt(100) < 33) {
this.sendNPSMessage();
}
}
}
}
private sendNPSMessage() {
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
Logger.logInfo(
`NPS Survey logging current date when survey is shown ${Date.now().toString()}`,
"Explorer/openNPSSurveyDialog",
);
localStorage.setItem("lastSubmitted", Date.now().toString());
}
@@ -383,9 +401,7 @@ export default class Explorer {
public onRefreshResourcesClick = (): void => {
if (configContext.platform === Platform.Fabric) {
// Requesting the tokens will trigger a refresh of the databases
// TODO: Once the id is returned from Fabric, we can await this call and then refresh the databases here
requestDatabaseResourceTokens();
scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases());
return;
}
@@ -1389,9 +1405,18 @@ export default class Explorer {
if (userContext.apiType !== "SQL" || !userContext.subscriptionId) {
return;
}
const copilotEnabled = await getCopilotEnabled();
useQueryCopilot.getState().setCopilotEnabled(copilotEnabled);
useQueryCopilot.getState().setCopilotUserDBEnabled(copilotEnabled);
const copilotEnabledPromise = getCopilotEnabled();
const copilotUserDBEnabledPromise = isCopilotFeatureRegistered(userContext.subscriptionId);
const [copilotEnabled, copilotUserDBEnabled] = await Promise.all([
copilotEnabledPromise,
copilotUserDBEnabledPromise,
]);
const copilotSampleDBEnabled = LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true";
useQueryCopilot.getState().setCopilotEnabled(copilotEnabled && copilotUserDBEnabled);
useQueryCopilot.getState().setCopilotUserDBEnabled(copilotUserDBEnabled);
useQueryCopilot
.getState()
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
}
public async refreshSampleData(): Promise<void> {

View File

@@ -1163,15 +1163,12 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
)}"`,
).then(
(documents: DataModels.DocumentId[]) => {
$.each(
documents,
(index: number, doc: { _graph_icon_property_value: string; icon: string; format: string }) => {
newIconsMap[doc["_graph_icon_property_value"]] = {
data: doc["icon"],
format: doc["format"],
};
},
);
$.each(documents, (index: number, doc: any) => {
newIconsMap[doc["_graph_icon_property_value"]] = {
data: doc["icon"],
format: doc["format"],
};
});
// Update graph configuration
this.setState({

View File

@@ -24,16 +24,21 @@ interface Props {
export interface CommandBarStore {
contextButtons: CommandButtonComponentProps[];
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void;
isHidden: boolean;
setIsHidden: (isHidden: boolean) => void;
}
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
contextButtons: [],
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
isHidden: false,
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
}));
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const selectedNodeState = useSelectedNode();
const buttons = useCommandBar((state) => state.contextButtons);
const isHidden = useCommandBar((state) => state.isHidden);
const backgroundColor = StyleConstants.BaseLight;
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
@@ -42,7 +47,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
? CommandBarComponentButtonFactory.createPostgreButtons(container)
: CommandBarComponentButtonFactory.createVCoreMongoButtons(container);
return (
<div className="commandBarContainer">
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
<FluentCommandBar
ariaLabel="Use left and right arrow keys to navigate between commands"
items={CommandBarUtil.convertButton(buttons, backgroundColor)}
@@ -91,7 +96,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
? {
root: {
backgroundColor: "transparent",
padding: "0px 14px 0px 14px",
padding: "2px 8px 0px 8px",
},
}
: {
@@ -101,7 +106,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
};
return (
<div className="commandBarContainer">
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
<FluentCommandBar
ariaLabel="Use left and right arrow keys to navigate between commands"
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}

View File

@@ -200,7 +200,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
{
iconSrc: SettingsIcon,
iconAlt: "Settings",
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane />),
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
commandButtonLabel: undefined,
ariaLabel: "Settings",
tooltipText: "Settings",

View File

@@ -25,7 +25,10 @@ import { MemoryTracker } from "./MemoryTrackerComponent";
* @param btns
*/
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
const buttonHeightPx =
configContext.platform == Platform.Fabric
? StyleConstants.FabricCommandBarButtonHeight
: StyleConstants.CommandBarButtonHeight;
const hoverColor =
configContext.platform == Platform.Fabric ? StyleConstants.FabricAccentLight : StyleConstants.AccentLight;
@@ -112,6 +115,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
splitButtonContainer: {
marginLeft: 5,
marginRight: 5,
height: buttonHeightPx,
},
},
className: btn.className,

View File

@@ -154,7 +154,7 @@ function openPane(action: ActionContracts.OpenPane, explorer: Explorer) {
action.paneKind === ActionContracts.PaneKind.GlobalSettings ||
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings]
) {
useSidePanel.getState().openSidePanel("Settings", <SettingsPane />);
useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={explorer} />);
}
}

View File

@@ -29,7 +29,7 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
};
}
omponentDidMount(): void {
componentDidMount(): void {
window.addEventListener("resize", () => this.setState({ height: this.getPanelHeight() }));
}
@@ -62,12 +62,12 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
customWidth={this.props.panelWidth ? this.props.panelWidth : "440px"}
headerClassName="panelHeader"
onRenderNavigationContent={this.props.onRenderNavigationContent}
isFooterAtBottom={true}
styles={{
navigation: { borderBottom: "1px solid #cccccc" },
content: { padding: 0, height: "100%" },
scrollableContent: { height: "100%" },
content: { padding: 0 },
header: { padding: "0 0 8px 34px" },
commands: { marginTop: 8 },
commands: { marginTop: 8, paddingTop: 0 },
}}
style={{ height: this.state.height }}
>
@@ -76,44 +76,7 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
);
}
public isFocusable(element: HTMLElement) {
return (
element.tabIndex >= 0 ||
(element instanceof HTMLAnchorElement && element.href) ||
(element instanceof HTMLAreaElement && element.href) ||
(element instanceof HTMLInputElement && !element.disabled) ||
(element instanceof HTMLSelectElement && !element.disabled) ||
(element instanceof HTMLTextAreaElement && !element.disabled) ||
(element instanceof HTMLButtonElement && !element.disabled)
);
}
private findFocusableParent = (element: HTMLElement) => {
while (element) {
if (this.isFocusable(element)) {
return element;
}
element = element.parentNode as HTMLElement;
}
return null;
};
private onDissmiss = (ev?: KeyboardEvent | React.SyntheticEvent<HTMLElement>): void => {
const elementIdToFocus = sessionStorage.getItem("focusedElementId") || null;
if (elementIdToFocus) {
const targetElement = document.getElementById(elementIdToFocus);
if (targetElement) {
const focusableParent = this.findFocusableParent(targetElement);
if (focusableParent) {
setTimeout(() => {
if (focusableParent) {
focusableParent.focus();
}
}, 100);
sessionStorage.removeItem("focusedElementId");
}
}
}
if (ev && (ev.target as HTMLElement).id === "notificationConsoleHeader") {
ev.preventDefault();
} else {

View File

@@ -6,7 +6,7 @@ import { SettingsPane } from "./SettingsPane";
describe("Settings Pane", () => {
it("should render Default properly", () => {
const wrapper = shallow(<SettingsPane />);
const wrapper = shallow(<SettingsPane explorer={null} />);
expect(wrapper).toMatchSnapshot();
});
@@ -18,7 +18,7 @@ describe("Settings Pane", () => {
},
} as DatabaseAccount,
});
const wrapper = shallow(<SettingsPane />);
const wrapper = shallow(<SettingsPane explorer={null} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -16,13 +16,20 @@ import * as StringUtility from "Shared/StringUtility";
import { userContext } from "UserContext";
import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react";
import Explorer from "../../Explorer";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export const SettingsPane: FunctionComponent = () => {
export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
explorer,
}: {
explorer: Explorer;
}): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const [refreshExplorer, setRefreshExplorer] = useState<boolean>(false);
const [pageOption, setPageOption] = useState<string>(
LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage) === Constants.Queries.unlimitedItemsPerPage
? Constants.Queries.UnlimitedPageOption
@@ -78,13 +85,17 @@ export const SettingsPane: FunctionComponent = () => {
? LocalStorageUtility.getEntryString(StorageKey.PriorityLevel)
: Constants.PriorityLevel.Default,
);
const [copilotSampleDBEnabled, setCopilotSampleDBEnabled] = useState<boolean>(
LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true",
);
const explorerVersion = configContext.gitSha;
const shouldShowQueryPageOptions = userContext.apiType === "SQL";
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
const handlerOnSubmit = () => {
const shouldShowCopilotSampleDBOption = userContext.apiType === "SQL" && useQueryCopilot.getState().copilotEnabled;
const handlerOnSubmit = async () => {
setIsExecuting(true);
LocalStorageUtility.setEntryNumber(
@@ -100,6 +111,7 @@ export const SettingsPane: FunctionComponent = () => {
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString());
LocalStorageUtility.setEntryString(StorageKey.CopilotSampleDBEnabled, copilotSampleDBEnabled.toString());
if (shouldShowGraphAutoVizOption) {
LocalStorageUtility.setEntryBoolean(
@@ -139,6 +151,7 @@ export const SettingsPane: FunctionComponent = () => {
logConsoleInfo(
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`,
);
refreshExplorer && (await explorer.refreshExplorer());
closeSidePanel();
};
@@ -218,6 +231,12 @@ export const SettingsPane: FunctionComponent = () => {
}
};
const handleSampleDatabaseChange = async (ev: React.MouseEvent<HTMLElement>, checked?: boolean): Promise<void> => {
setCopilotSampleDBEnabled(checked);
useQueryCopilot.getState().setCopilotSampleDBEnabled(checked);
setRefreshExplorer(!refreshExplorer);
};
const choiceButtonStyles = {
root: {
clear: "both",
@@ -434,7 +453,7 @@ export const SettingsPane: FunctionComponent = () => {
</div>
</div>
<div className="settingsSection">
<div className="settingsSectionPart">
<div className="settingsSectionPart settingsSectionInlineCheckbox">
<div className="settingsSectionLabel">
Enable container pagination
<InfoTooltip>
@@ -454,7 +473,7 @@ export const SettingsPane: FunctionComponent = () => {
</div>
{shouldShowCrossPartitionOption && (
<div className="settingsSection">
<div className="settingsSectionPart">
<div className="settingsSectionPart settingsSectionInlineCheckbox">
<div className="settingsSectionLabel">
Enable cross-partition query
<InfoTooltip>
@@ -545,6 +564,30 @@ export const SettingsPane: FunctionComponent = () => {
</div>
</div>
)}
{shouldShowCopilotSampleDBOption && (
<div className="settingsSection">
<div className="settingsSectionPart settingsSectionInlineCheckbox">
<div className="settingsSectionLabel">
Enable sample database
<InfoTooltip>
This is a sample database and collection with synthetic product data you can use to explore using
NoSQL queries and Copilot. This will appear as another database in the Data Explorer UI, and is
created by, and maintained by Microsoft at no cost to you.
</InfoTooltip>
</div>
<Checkbox
styles={{
label: { padding: 0 },
}}
className="padding"
ariaLabel="Enable sample db for Copilot"
checked={copilotSampleDBEnabled}
onChange={handleSampleDatabaseChange}
/>
</div>
</div>
)}
<div className="settingsSection">
<div className="settingsSectionPart">
<div className="settingsSectionLabel">Explorer Version</div>

View File

@@ -274,7 +274,7 @@ exports[`Settings Pane should render Default properly 1`] = `
className="settingsSection"
>
<div
className="settingsSectionPart"
className="settingsSectionPart settingsSectionInlineCheckbox"
>
<div
className="settingsSectionLabel"
@@ -303,7 +303,7 @@ exports[`Settings Pane should render Default properly 1`] = `
className="settingsSection"
>
<div
className="settingsSectionPart"
className="settingsSectionPart settingsSectionInlineCheckbox"
>
<div
className="settingsSectionLabel"
@@ -521,7 +521,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
className="settingsSection"
>
<div
className="settingsSectionPart"
className="settingsSectionPart settingsSectionInlineCheckbox"
>
<div
className="settingsSectionLabel"

View File

@@ -6,6 +6,7 @@ exports[`PaneContainerComponent test should be resize if notification console is
customWidth="440px"
headerClassName="panelHeader"
headerText="test"
isFooterAtBottom={true}
isLightDismiss={true}
isOpen={true}
onDismiss={[Function]}
@@ -18,9 +19,9 @@ exports[`PaneContainerComponent test should be resize if notification console is
Object {
"commands": Object {
"marginTop": 8,
"paddingTop": 0,
},
"content": Object {
"height": "100%",
"padding": 0,
},
"header": Object {
@@ -29,9 +30,6 @@ exports[`PaneContainerComponent test should be resize if notification console is
"navigation": Object {
"borderBottom": "1px solid #cccccc",
},
"scrollableContent": Object {
"height": "100%",
},
}
}
type={7}
@@ -48,6 +46,7 @@ exports[`PaneContainerComponent test should render with panel content and header
customWidth="440px"
headerClassName="panelHeader"
headerText="test"
isFooterAtBottom={true}
isLightDismiss={true}
isOpen={true}
onDismiss={[Function]}
@@ -60,9 +59,9 @@ exports[`PaneContainerComponent test should render with panel content and header
Object {
"commands": Object {
"marginTop": 8,
"paddingTop": 0,
},
"content": Object {
"height": "100%",
"padding": 0,
},
"header": Object {
@@ -71,9 +70,6 @@ exports[`PaneContainerComponent test should render with panel content and header
"navigation": Object {
"borderBottom": "1px solid #cccccc",
},
"scrollableContent": Object {
"height": "100%",
},
}
}
type={7}

View File

@@ -15,6 +15,7 @@ export const CopyPopup = ({
return showCopyPopup ? (
<Stack
role="status"
style={{
position: "fixed",
width: 345,

View File

@@ -4,6 +4,7 @@ exports[`Copy Popup snapshot test should render when showCopyPopup is false 1`]
exports[`Copy Popup snapshot test should render when showCopyPopup is true 1`] = `
<Stack
role="status"
style={
Object {
"background": "#FFFFFF",

View File

@@ -303,15 +303,29 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
resetButtonState();
};
const getAriaLabel = () => {
if (isGeneratingQuery === null) {
return " ";
} else if (isGeneratingQuery) {
return "Content is loading";
} else {
return "Content is updated";
}
};
React.useEffect(() => {
showTeachingBubble();
useTabs.getState().setIsQueryErrorThrown(false);
}, []);
return (
<Stack className="copilot-prompt-pane" styles={{ root: { backgroundColor: "#FAFAFA", padding: "16px 24px 0px" } }}>
<Stack
className="copilot-prompt-pane"
styles={{ root: { backgroundColor: "#FAFAFA", padding: "16px 24px 0px" } }}
id="copilot-textfield-label"
>
<Stack horizontal>
<Image src={CopilotIcon} style={{ width: 24, height: 24 }} />
<Image src={CopilotIcon} style={{ width: 24, height: 24 }} alt="Copilot" role="none" />
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
<IconButton
iconProps={{ imageProps: { src: errorIcon } }}
@@ -348,6 +362,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
disabled={isGeneratingQuery}
autoComplete="off"
placeholder="Ask a question in natural language and well generate the query for you."
aria-labelledby="copilot-textfield-label"
/>
{copilotTeachingBubbleVisible && (
<TeachingBubble
@@ -377,8 +392,11 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
disabled={isGeneratingQuery || !userPrompt.trim()}
style={{ marginLeft: 8 }}
onClick={() => startGenerateQueryProcess()}
aria-label="Send"
/>
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
<div role="alert" aria-label={getAriaLabel()}>
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
</div>
{showSamplePrompts && (
<Callout
styles={{ root: { minWidth: 400, maxWidth: "70vw" } }}
@@ -484,7 +502,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
<Stack style={{ margin: "8px 0" }}>
<Text style={{ fontSize: 12 }}>
AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "}
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank">
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank" style={{ color: "#0072c9" }}>
Read preview terms
</Link>
{showErrorMessageBar && (
@@ -552,6 +570,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
id="likeBtn"
style={{ marginLeft: 20 }}
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
aria-label="Like"
onClick={() => {
setShowCallout(!likeQuery);
setLikeQuery(!likeQuery);
@@ -571,6 +590,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
setDislikeQuery(!dislikeQuery);
setShowCallout(false);
}}
aria-label="Dislike"
/>
<Separator vertical style={{ color: "#EDEBE9" }} />
<CommandBarButton

View File

@@ -15,7 +15,12 @@ import { MinimalQueryIterator } from "Common/IteratorUtilities";
import { createUri } from "Common/UrlUtility";
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
import { configContext } from "ConfigContext";
import { ContainerConnectionInfo, CopilotEnabledConfiguration, IProvisionData } from "Contracts/DataModels";
import {
ContainerConnectionInfo,
CopilotEnabledConfiguration,
FeatureRegistration,
IProvisionData,
} from "Contracts/DataModels";
import { AuthorizationTokenHeaderMetadata, QueryResults } from "Contracts/ViewModels";
import { useDialog } from "Explorer/Controls/Dialog";
import Explorer from "Explorer/Explorer";
@@ -52,6 +57,28 @@ async function fetchWithTimeout(
return response;
}
export const isCopilotFeatureRegistered = async (subscriptionId: string): Promise<boolean> => {
const api_version = "2021-07-01";
const url = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.Features/featureProviders/Microsoft.DocumentDB/subscriptionFeatureRegistrations/MicrosoftCopilotForAzureInCDB?api-version=${api_version}`;
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token };
let response;
try {
response = await fetchWithTimeout(url, headers);
} catch (error) {
return false;
}
if (!response?.ok) {
return false;
}
const featureRegistration = (await response?.json()) as FeatureRegistration;
return featureRegistration?.properties?.state === "Registered";
};
export const getCopilotEnabled = async (): Promise<boolean> => {
const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`;
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();

View File

@@ -49,7 +49,7 @@ exports[`Footer snapshot test should not pass if no text 1`] = `
/>
</Stack>
<StyledTextFieldBase
disabled={false}
disabled={null}
multiline={true}
onChange={[Function]}
onKeyDown={[Function]}
@@ -74,7 +74,7 @@ exports[`Footer snapshot test should not pass if no text 1`] = `
value=""
/>
<CustomizedIconButton
disabled={false}
disabled={null}
iconProps={
Object {
"iconName": "Send",
@@ -134,7 +134,7 @@ exports[`Footer snapshot test should not pass text with non enter key 1`] = `
/>
</Stack>
<StyledTextFieldBase
disabled={false}
disabled={null}
multiline={true}
onChange={[Function]}
onKeyDown={[Function]}
@@ -159,7 +159,7 @@ exports[`Footer snapshot test should not pass text with non enter key 1`] = `
value=""
/>
<CustomizedIconButton
disabled={false}
disabled={null}
iconProps={
Object {
"iconName": "Send",
@@ -219,7 +219,7 @@ exports[`Footer snapshot test should open sample prompts on button click 1`] = `
/>
</Stack>
<StyledTextFieldBase
disabled={false}
disabled={null}
multiline={true}
onChange={[Function]}
onKeyDown={[Function]}
@@ -244,7 +244,7 @@ exports[`Footer snapshot test should open sample prompts on button click 1`] = `
value=""
/>
<CustomizedIconButton
disabled={false}
disabled={null}
iconProps={
Object {
"iconName": "Send",
@@ -304,7 +304,7 @@ exports[`Footer snapshot test should pass text with enter key 1`] = `
/>
</Stack>
<StyledTextFieldBase
disabled={false}
disabled={null}
multiline={true}
onChange={[Function]}
onKeyDown={[Function]}
@@ -329,7 +329,7 @@ exports[`Footer snapshot test should pass text with enter key 1`] = `
value="test message"
/>
<CustomizedIconButton
disabled={false}
disabled={null}
iconProps={
Object {
"iconName": "Send",
@@ -389,7 +389,7 @@ exports[`Footer snapshot test should pass text with icon button 1`] = `
/>
</Stack>
<StyledTextFieldBase
disabled={false}
disabled={null}
multiline={true}
onChange={[Function]}
onKeyDown={[Function]}
@@ -414,7 +414,7 @@ exports[`Footer snapshot test should pass text with icon button 1`] = `
value="test message"
/>
<CustomizedIconButton
disabled={false}
disabled={null}
iconProps={
Object {
"iconName": "Send",
@@ -474,7 +474,7 @@ exports[`Footer snapshot test should update user input 1`] = `
/>
</Stack>
<StyledTextFieldBase
disabled={false}
disabled={null}
multiline={true}
onChange={[Function]}
onKeyDown={[Function]}
@@ -499,7 +499,7 @@ exports[`Footer snapshot test should update user input 1`] = `
value=""
/>
<CustomizedIconButton
disabled={false}
disabled={null}
iconProps={
Object {
"iconName": "Send",

View File

@@ -148,23 +148,25 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
/>
</Stack>
<Stack horizontal tokens={{ childrenGap: 16 }}>
<SplashScreenButton
imgSrc={CopilotIcon}
title={"Query faster with Copilot"}
description={
"Copilot is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
}
onClick={() => {
const copilotVersion = userContext.features.copilotVersion;
if (copilotVersion === "v1.0") {
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
} else if (copilotVersion === "v2.0") {
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
sampleCollection.onNewQueryClick(sampleCollection, undefined);
{useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotSampleDBEnabled && (
<SplashScreenButton
imgSrc={CopilotIcon}
title={"Query faster with Copilot"}
description={
"Copilot is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
}
traceOpen(Action.OpenQueryCopilotFromSplashScreen, { apiType: userContext.apiType });
}}
/>
onClick={() => {
const copilotVersion = userContext.features.copilotVersion;
if (copilotVersion === "v1.0") {
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
} else if (copilotVersion === "v2.0") {
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
sampleCollection.onNewQueryClick(sampleCollection, undefined);
}
traceOpen(Action.OpenQueryCopilotFromSplashScreen, { apiType: userContext.apiType });
}}
/>
)}
<SplashScreenButton
imgSrc={ConnectIcon}
title={"Connect"}

View File

@@ -1,6 +1,8 @@
import * as ko from "knockout";
import * as _ from "underscore";
import * as DataTable from "datatables.net-dt";
import loadingIndicator3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as Constants from "../Constants";
import * as Entities from "../Entities";
@@ -94,7 +96,7 @@ function createDataTable(
});
}
tableEntityListViewModel.table = DataTableBuilder.createDataTable($dataTable, <DataTables.Settings>{
tableEntityListViewModel.table = DataTableBuilder.createDataTable($dataTable, <DataTable.Config>{
// WARNING!!! SECURITY: If you add new columns, make sure you encode them if they are user strings from Azure (see encodeText)
// so that they don't get interpreted as HTML in our page.
colReorder: true,
@@ -116,7 +118,7 @@ function createDataTable(
sPrevious: "<",
sLast: ">>",
},
sProcessing: '<img style="width: 28px; height: 6px; " src="images/LoadingIndicator_3Squares.gif">',
sProcessing: `<img style="width: 28px; height: 6px; " src="${loadingIndicator3Squares}">`,
oAria: {
sSortAscending: "",
sSortDescending: "",
@@ -345,7 +347,7 @@ function updateSelectionStatus(oSettings: any): void {
// TODO consider centralizing this "post-command" logic into some sort of Command Manager entity.
// See VSO:166520: "[Storage Explorer] Consider adding a 'command manager' to track command post-effects."
function updateDataTableFocus(queryTablesTabId: string): void {
var $activeElement: JQuery = $(document.activeElement);
var $activeElement: JQuery<Element> = $(document.activeElement);
var isFocusLost: boolean = $activeElement.is("body"); // When focus is lost, "body" becomes the active element.
var storageExplorerFrameHasFocus: boolean = document.hasFocus();
var operationManager = tableEntityListViewModelMap[queryTablesTabId].operationManager;

View File

@@ -1,3 +1,4 @@
import * as DataTable from "datatables.net-dt";
import * as Utilities from "../Utilities";
/**
@@ -8,7 +9,7 @@ import * as Utilities from "../Utilities";
* @param{$dataTableElem} JQuery data table element
* @param{$settings} Settings to use when creating the data table
*/
export function createDataTable($dataTableElem: JQuery, settings: any): DataTables.DataTable {
export function createDataTable($dataTableElem: JQuery, settings: any): DataTable.Api<HTMLElement> {
return $dataTableElem.DataTable(applyDefaultRendering(settings));
}
@@ -18,14 +19,14 @@ export function createDataTable($dataTableElem: JQuery, settings: any): DataTabl
* @param{settings} The settings to check
* @return The given settings with all columns having a rendering function
*/
function applyDefaultRendering(settings: any): DataTables.SettingsLegacy {
var tableColumns: DataTables.ColumnLegacy[] = null;
function applyDefaultRendering(settings: DataTable.Config): any {
var tableColumns: any[] = null;
if (settings.aoColumns) {
tableColumns = settings.aoColumns;
} else if (settings.aoColumnDefs) {
if (settings.columns) {
tableColumns = settings.columns;
} else if (settings.columnDefs) {
// for tables we use aoColumnDefs instead of aoColumns
tableColumns = settings.aoColumnDefs;
tableColumns = settings.columnDefs;
}
// either the settings had no columns defined, or they were called

View File

@@ -1,11 +1,11 @@
import ko from "knockout";
import * as DataTableOperations from "./DataTableOperations";
import * as Constants from "../Constants";
import * as Entities from "../Entities";
import * as Utilities from "../Utilities";
import * as DataTableOperations from "./DataTableOperations";
import TableCommands from "./TableCommands";
import TableEntityListViewModel from "./TableEntityListViewModel";
import * as Utilities from "../Utilities";
import * as Entities from "../Entities";
/*
* Base class for data table row selection.
@@ -13,9 +13,9 @@ import * as Entities from "../Entities";
export default class DataTableOperationManager {
private _tableEntityListViewModel: TableEntityListViewModel;
private _tableCommands: TableCommands;
private dataTable: JQuery;
private dataTable: JQuery<Element>;
constructor(table: JQuery, viewModel: TableEntityListViewModel, tableCommands: TableCommands) {
constructor(table: JQuery<Element>, viewModel: TableEntityListViewModel, tableCommands: TableCommands) {
this.dataTable = table;
this._tableEntityListViewModel = viewModel;
this._tableCommands = tableCommands;
@@ -25,7 +25,7 @@ export default class DataTableOperationManager {
}
private click = (event: JQueryEventObject) => {
var elem: JQuery = $(event.currentTarget);
var elem: JQuery<Element> = $(event.currentTarget);
this.updateLastSelectedItem(elem, event.shiftKey);
if (Utilities.isEnvironmentCtrlPressed(event)) {
@@ -48,7 +48,7 @@ export default class DataTableOperationManager {
if (isUpArrowKey || isDownArrowKey) {
var lastSelectedItem: Entities.ITableEntity = this._tableEntityListViewModel.lastSelectedItem;
var dataTableRows: JQuery = $(Constants.htmlSelectors.dataTableAllRowsSelector);
var dataTableRows: JQuery<Element> = $(Constants.htmlSelectors.dataTableAllRowsSelector);
var maximumIndex = dataTableRows.length - 1;
// If can't find an index for lastSelectedItem, then either no item is previously selected or it goes across page.
@@ -60,7 +60,7 @@ export default class DataTableOperationManager {
: -1;
var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1;
var safeIndex: number = Utilities.ensureBetweenBounds(nextIndex, 0, maximumIndex);
var selectedRowElement: JQuery = dataTableRows.eq(safeIndex);
var selectedRowElement: JQuery<Element> = dataTableRows.eq(safeIndex);
if (selectedRowElement) {
if (event.shiftKey) {
@@ -143,13 +143,13 @@ export default class DataTableOperationManager {
return handled;
}
private getEntityIdentity($elem: JQuery): Entities.ITableEntityIdentity {
private getEntityIdentity($elem: JQuery<Element>): Entities.ITableEntityIdentity {
return {
RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr),
};
}
private updateLastSelectedItem($elem: JQuery, isShiftSelect: boolean) {
private updateLastSelectedItem($elem: JQuery<Element>, isShiftSelect: boolean) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
var entity = this._tableEntityListViewModel.getItemFromCurrentPage(
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey),
@@ -162,7 +162,7 @@ export default class DataTableOperationManager {
}
}
private applySingleSelection($elem: JQuery) {
private applySingleSelection($elem: JQuery<Element>) {
if ($elem) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
@@ -179,7 +179,7 @@ export default class DataTableOperationManager {
);
}
private applyCtrlSelection($elem: JQuery): void {
private applyCtrlSelection($elem: JQuery<Element>): void {
var koSelected: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
? this._tableEntityListViewModel.selected
: null;
@@ -200,7 +200,7 @@ export default class DataTableOperationManager {
}
}
private applyShiftSelection($elem: JQuery): void {
private applyShiftSelection($elem: JQuery<Element>): void {
var anchorItem = this._tableEntityListViewModel.lastSelectedAnchorItem;
// If anchor item doesn't exist, use the first available item of current page instead
@@ -228,7 +228,7 @@ export default class DataTableOperationManager {
}
}
private applyContextMenuSelection($elem: JQuery) {
private applyContextMenuSelection($elem: JQuery<Element>) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
if (

View File

@@ -1,3 +1,4 @@
import * as DataTables from "datatables.net";
import Q from "q";
import _ from "underscore";
import * as QueryBuilderConstants from "../Constants";
@@ -13,7 +14,7 @@ export function getRowSelector(selectorSchema: Entities.IProperty[]): string {
return QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector + selector;
}
export function isRowVisible(dataTableScrollBodyQuery: JQuery, element: HTMLElement): boolean {
export function isRowVisible(dataTableScrollBodyQuery: JQuery<Element>, element: Element): boolean {
let isVisible = false;
if (dataTableScrollBodyQuery.length && element) {
@@ -26,16 +27,18 @@ export function isRowVisible(dataTableScrollBodyQuery: JQuery, element: HTMLElem
return isVisible;
}
export function scrollToRowIfNeeded(dataTableRows: JQuery, currentIndex: number, isScrollUp: boolean): void {
export function scrollToRowIfNeeded(dataTableRows: JQuery<Element>, currentIndex: number, isScrollUp: boolean): void {
if (dataTableRows.length) {
const dataTableScrollBodyQuery: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector),
selectedRowElement: HTMLElement = dataTableRows.get(currentIndex);
const dataTableScrollBodyQuery: JQuery<Element> = $(
QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector,
),
selectedRowElement: Element = dataTableRows.get(currentIndex);
if (dataTableScrollBodyQuery.length && selectedRowElement) {
const isVisible: boolean = isRowVisible(dataTableScrollBodyQuery, selectedRowElement);
if (!isVisible) {
const selectedRowQuery: JQuery = $(selectedRowElement),
const selectedRowQuery: JQuery<Element> = $(selectedRowElement),
scrollPosition: number = dataTableScrollBodyQuery.scrollTop(),
selectedElementPosition: number = selectedRowQuery.position().top;
let newScrollPosition = 0;
@@ -54,8 +57,8 @@ export function scrollToRowIfNeeded(dataTableRows: JQuery, currentIndex: number,
}
export function scrollToTopIfNeeded(): void {
const $dataTableRows: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector),
$dataTableScrollBody: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector);
const $dataTableRows: JQuery<Element> = $(QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector),
$dataTableScrollBody: JQuery<Element> = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector);
if ($dataTableRows.length && $dataTableScrollBody.length) {
$dataTableScrollBody.scrollTop(0);
@@ -71,7 +74,7 @@ export function setPaginationButtonEventHandlers(): void {
.attr("role", "button");
}
export function filterColumns(table: DataTables.DataTable, settings: boolean[]): void {
export function filterColumns(table: DataTables.Api<HTMLElement>, settings: boolean[]): void {
settings &&
settings.forEach((value: boolean, index: number) => {
table.column(index).visible(value, false);
@@ -84,7 +87,7 @@ export function filterColumns(table: DataTables.DataTable, settings: boolean[]):
* If no current order is specified, reorder the columns based on intial order.
*/
export function reorderColumns(
table: DataTables.DataTable,
table: DataTables.Api<HTMLElement>,
targetOrder: number[],
currentOrder?: number[],
//eslint-disable-next-line
@@ -108,7 +111,9 @@ export function reorderColumns(
? calculateTransformationOrder(currentOrder, targetOrder)
: targetOrder;
try {
$.fn.dataTable.ColReorder(table).fnOrder(transformationOrder);
// TODO: This possibly does not work with the new version of datatables.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
($.fn.dataTable as any).ColReorder(table).fnOrder(transformationOrder);
} catch (err) {
return Q.reject(err);
}
@@ -116,9 +121,9 @@ export function reorderColumns(
return Q.resolve(null);
}
export function resetColumns(table: DataTables.DataTable): void {
$.fn.dataTable.ColReorder(table).fnReset();
}
// export function resetColumns(table: DataTables.DataTable): void {
// $.fn.dataTable.ColReorder(table).fnReset();
// }
/**
* A table's initial order is described in the form of a natural ascending order.
@@ -133,8 +138,10 @@ export function getInitialOrder(columnsCount: number): number[] {
* Initial order: I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8}
* Current order: C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8}
*/
export function getCurrentOrder(table: DataTables.DataTable): number[] {
return $.fn.dataTable.ColReorder(table).fnOrder();
export function getCurrentOrder(table: DataTables.Api<HTMLElement>): number[] {
// TODO: This possibly does not work with the new version of datatables.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return ($.fn.dataTable as any).ColReorder(table).fnOrder();
}
/**
@@ -178,8 +185,8 @@ export function calculateTransformationOrder(currentOrder: number[], targetOrder
return transformationOrder;
}
export function getDataTableHeaders(table: DataTables.DataTable): string[] {
const columns: DataTables.ColumnsMethods = table.columns();
export function getDataTableHeaders(table: DataTables.Api<HTMLElement>): string[] {
const columns = table.columns();
let headers: string[] = [];
if (columns) {
// table.columns() return ColumnsMethods which is an array of arrays

View File

@@ -1,14 +1,15 @@
import * as ko from "knockout";
import * as _ from "underscore";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import CacheBase from "./CacheBase";
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as DataTables from "datatables.net";
import * as CommonConstants from "../../../Common/Constants";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as Constants from "../Constants";
import * as Entities from "../Entities";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
import CacheBase from "./CacheBase";
// This is the format of the data we will have to pass to Datatable render callback,
// and property names are defined by Datatable as well.
@@ -27,7 +28,7 @@ abstract class DataTableViewModel {
public items = ko.observableArray<Entities.ITableEntity>();
public selected = ko.observableArray<Entities.ITableEntity>();
public table: DataTables.DataTable;
public table: DataTables.Api<HTMLElement>;
// The anchor item is for shift selection. i.e., select all items between anchor item and a give item.
public lastSelectedAnchorItem: Entities.ITableEntity;

View File

@@ -1,3 +1,4 @@
import * as DataTables from "datatables.net";
import * as ko from "knockout";
import Q from "q";
import * as _ from "underscore";
@@ -56,7 +57,7 @@ function _parse(err: any): ErrorDataModel[] {
function _getInnerErrors(message: string): any[] {
/*
The backend error message has an inner-message which is a stringified object.
The backend error message has an inner-message which is a stringified object.
For SQL errors, the "errors" property is an array of SqlErrorDataModel.
Example:
"Message: {"Errors":["Resource with specified id or name already exists"]}\r\nActivityId: 80005000008d40b6a, Request URI: /apps/19000c000c0a0005/services/mctestdocdbprod-MasterService-0-00066ab9937/partitions/900005f9000e676fb8/replicas/13000000000955p"
@@ -131,7 +132,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
return [{ key: Constants.EntityKeyNames.RowKey, value: rowKey }];
}
public reloadTable(useSetting: boolean = true, resetHeaders: boolean = true): DataTables.DataTable {
public reloadTable(useSetting: boolean = true, resetHeaders: boolean = true): DataTables.Api<Element> {
this.clearCache();
this.clearSelection();
this.isCancelled = false;

View File

@@ -1,3 +1,4 @@
import * as DataTable from "datatables.net-dt";
import * as ko from "knockout";
import { KeyCodes } from "../../../Common/Constants";
import { userContext } from "../../../UserContext";
@@ -643,7 +644,7 @@ export default class QueryBuilderViewModel {
return groupViewModels;
};
public runQuery = (): DataTables.DataTable => {
public runQuery = (): DataTable.Api<Element> => {
return this._queryViewModel.runQuery();
};

View File

@@ -1,9 +1,10 @@
import * as DataTables from "datatables.net";
import * as ko from "knockout";
import React from "react";
import * as _ from "underscore";
import { KeyCodes } from "../../../Common/Constants";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { TableQuerySelectPanel } from "../../Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { getQuotedCqlIdentifier } from "../CqlUtilities";
@@ -158,7 +159,7 @@ export default class QueryViewModel {
notify: "always",
});
public runQuery = (): DataTables.DataTable => {
public runQuery = (): DataTables.Api<Element> => {
let filter = this.setFilter();
if (filter && userContext.apiType !== "Cassandra") {
filter = filter.replace(/"/g, "'");
@@ -176,7 +177,7 @@ export default class QueryViewModel {
return this._tableEntityListViewModel.reloadTable(/*useSetting*/ false, /*resetHeaders*/ false);
};
public clearQuery = (): DataTables.DataTable => {
public clearQuery = (): DataTables.Api<Element> => {
this.queryText();
this.topValue();
this.selectText();

View File

@@ -281,7 +281,7 @@ export class CassandraAPIDataClient extends TableDataClient {
query,
paginationToken,
},
beforeSend: this.setAuthorizationHeader,
beforeSend: this.setAuthorizationHeader as any,
cache: false,
});
shouldNotify &&
@@ -423,7 +423,7 @@ export class CassandraAPIDataClient extends TableDataClient {
keyspaceId: collection.databaseId,
tableId: collection.id(),
},
beforeSend: this.setAuthorizationHeader,
beforeSend: this.setAuthorizationHeader as any,
cache: false,
})
.then(
@@ -463,7 +463,7 @@ export class CassandraAPIDataClient extends TableDataClient {
keyspaceId: collection.databaseId,
tableId: collection.id(),
},
beforeSend: this.setAuthorizationHeader,
beforeSend: this.setAuthorizationHeader as any,
cache: false,
})
.then(
@@ -496,7 +496,7 @@ export class CassandraAPIDataClient extends TableDataClient {
resourceId: resourceId,
query: query,
},
beforeSend: this.setAuthorizationHeader,
beforeSend: this.setAuthorizationHeader as any,
cache: false,
}).then(
(data: any) => {

View File

@@ -224,7 +224,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
};
public onSaveQueryClick = (): void => {
sessionStorage.setItem("focusedElementId", "savequery");
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />);
};
@@ -395,7 +394,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
const label = "Save Query";
buttons.push({
iconSrc: SaveQueryIcon,
id: "savequery",
iconAlt: label,
onCommandClick: this.onSaveQueryClick,
commandButtonLabel: label,
@@ -446,7 +444,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
this._toggleCopilot(!this.state.copilotActive);
},
commandButtonLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot",
ariaLabel: "Copilot",
ariaLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot",
hasPopup: false,
};
buttons.push(toggleCopilotButton);

View File

@@ -769,7 +769,10 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const dataRootNode = buildDataTree();
const isSampleDataEnabled =
useQueryCopilot().copilotEnabled && userContext.sampleDataConnectionInfo && userContext.apiType === "SQL";
useQueryCopilot().copilotEnabled &&
useQueryCopilot().copilotSampleDBEnabled &&
userContext.sampleDataConnectionInfo &&
userContext.apiType === "SQL";
const sampleDataResourceTokenCollection = useDatabases((state) => state.sampleDataResourceTokenCollection);
return (

View File

@@ -1,33 +1,44 @@
import { sendCachedDataMessage } from "Common/MessageHandler";
import { FabricDatabaseConnectionInfo } from "Contracts/FabricContract";
import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract";
import { MessageTypes } from "Contracts/MessageTypes";
import Explorer from "Explorer/Explorer";
import { updateUserContext } from "UserContext";
import { updateUserContext, userContext } from "UserContext";
import { logConsoleError } from "Utils/NotificationConsoleUtils";
const TOKEN_VALIDITY_MS = (3600 - 600) * 1000; // 1 hour minus 10 minutes to be safe
const DEBOUNCE_DELAY_MS = 1000 * 20; // 20 second
let timeoutId: NodeJS.Timeout;
// Prevents multiple parallel requests
let isRequestPending = false;
// Prevents multiple parallel requests during DEBOUNCE_DELAY_MS
let lastRequestTimestamp: number = undefined;
export const requestDatabaseResourceTokens = (): void => {
if (isRequestPending) {
const requestDatabaseResourceTokens = async (): Promise<void> => {
if (lastRequestTimestamp !== undefined && lastRequestTimestamp + DEBOUNCE_DELAY_MS > Date.now()) {
return;
}
// TODO Make Fabric return the message id so we can handle this promise
isRequestPending = true;
sendCachedDataMessage<FabricDatabaseConnectionInfo>(MessageTypes.GetAllResourceTokens, []);
};
lastRequestTimestamp = Date.now();
try {
const fabricDatabaseConnectionInfo = await sendCachedDataMessage<FabricDatabaseConnectionInfo>(
MessageTypes.GetAllResourceTokens,
[],
userContext.fabricContext.connectionId,
);
export const handleRequestDatabaseResourceTokensResponse = (
explorer: Explorer,
fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo,
): void => {
isRequestPending = false;
updateUserContext({ fabricDatabaseConnectionInfo });
scheduleRefreshDatabaseResourceToken();
explorer.refreshAllDatabases();
if (!userContext.databaseAccount.properties.documentEndpoint) {
userContext.databaseAccount.properties.documentEndpoint = fabricDatabaseConnectionInfo.endpoint;
}
updateUserContext({
fabricContext: { ...userContext.fabricContext, databaseConnectionInfo: fabricDatabaseConnectionInfo },
databaseAccount: { ...userContext.databaseAccount },
});
scheduleRefreshDatabaseResourceToken();
} catch (error) {
logConsoleError(error);
throw error;
} finally {
lastRequestTimestamp = undefined;
}
};
/**
@@ -35,19 +46,24 @@ export const handleRequestDatabaseResourceTokensResponse = (
* @param tokenTimestamp
* @returns
*/
export const scheduleRefreshDatabaseResourceToken = (): void => {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
timeoutId = undefined;
}
export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Promise<void> => {
return new Promise((resolve) => {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
timeoutId = undefined;
}
timeoutId = setTimeout(() => {
requestDatabaseResourceTokens();
}, TOKEN_VALIDITY_MS);
timeoutId = setTimeout(
() => {
requestDatabaseResourceTokens().then(resolve);
},
refreshNow ? 0 : TOKEN_VALIDITY_MS,
);
});
};
export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): void => {
if (tokenTimestamp + TOKEN_VALIDITY_MS < Date.now()) {
requestDatabaseResourceTokens();
scheduleRefreshDatabaseResourceToken(true);
}
};

View File

@@ -11,6 +11,7 @@ export enum StorageKey {
MaxWaitTimeInSeconds,
AutomaticallyCancelQueryAfterTimeout,
ContainerPaginationEnabled,
CopilotSampleDBEnabled,
CustomItemPerPage,
DatabaseAccountId,
EncryptedKeyToken,

View File

@@ -1,4 +1,4 @@
import { FabricDatabaseConnectionInfo } from "Contracts/FabricContract";
import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract";
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
@@ -47,8 +47,13 @@ export interface VCoreMongoConnectionParams {
connectionString: string;
}
interface FabricContext {
connectionId: string;
databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined;
}
interface UserContext {
readonly fabricDatabaseConnectionInfo?: FabricDatabaseConnectionInfo;
readonly fabricContext?: FabricContext;
readonly authType?: AuthType;
readonly masterKey?: string;
readonly subscriptionId?: string;

View File

@@ -1,11 +1,10 @@
import { createUri } from "Common/UrlUtility";
import { FabricDatabaseConnectionInfo, FabricMessage } from "Contracts/FabricContract";
import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract";
import { FABRIC_RPC_VERSION, FabricMessageV2 } from "Contracts/FabricMessagesContract";
import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { useSelectedNode } from "Explorer/useSelectedNode";
import {
handleRequestDatabaseResourceTokensResponse,
scheduleRefreshDatabaseResourceToken,
} from "Platform/Fabric/FabricUtil";
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { ReactTabKind, useTabs } from "hooks/useTabs";
@@ -88,6 +87,9 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
}
async function configureFabric(): Promise<Explorer> {
// These are the versions of Fabric that Data Explorer supports.
const SUPPORTED_FABRIC_VERSIONS = [FABRIC_RPC_VERSION];
let explorer: Explorer;
return new Promise<Explorer>((resolve) => {
window.addEventListener(
@@ -101,38 +103,37 @@ async function configureFabric(): Promise<Explorer> {
return;
}
const data: FabricMessage = event.data?.data;
const data: FabricMessageV2 = event.data?.data;
if (!data) {
return;
}
switch (data.type) {
case "initialize": {
const fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo = {
endpoint: data.message.endpoint,
databaseId: data.message.databaseId,
resourceTokens: data.message.resourceTokens as { [resourceId: string]: string },
resourceTokensTimestamp: data.message.resourceTokensTimestamp,
};
explorer = await createExplorerFabric(fabricDatabaseConnectionInfo);
resolve(explorer);
const fabricVersion = data.version;
if (!SUPPORTED_FABRIC_VERSIONS.includes(fabricVersion)) {
// TODO Surface error to user
console.error(`Unsupported Fabric version: ${fabricVersion}`);
return;
}
explorer.refreshAllDatabases().then(() => {
openFirstContainer(explorer, fabricDatabaseConnectionInfo.databaseId);
});
scheduleRefreshDatabaseResourceToken();
explorer = createExplorerFabric(data.message);
await scheduleRefreshDatabaseResourceToken(true);
resolve(explorer);
await explorer.refreshAllDatabases();
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
break;
}
case "newContainer":
explorer.onNewCollectionClicked();
break;
case "authorizationToken": {
case "authorizationToken":
case "allResourceTokens_v2": {
handleCachedDataMessage(data);
break;
}
case "allResourceTokens": {
// TODO call handleCachedDataMessage when Fabric echoes message id back
handleRequestDatabaseResourceTokensResponse(explorer, data.message as FabricDatabaseConnectionInfo);
case "setToolbarStatus": {
useCommandBar.getState().setIsHidden(data.message.visible === false);
break;
}
default:
@@ -143,7 +144,11 @@ async function configureFabric(): Promise<Explorer> {
false,
);
sendReadyMessage();
sendMessage({
type: MessageTypes.Ready,
id: "ready",
params: [DATA_EXPLORER_RPC_VERSION],
});
});
}
@@ -319,9 +324,12 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer {
return explorer;
}
function createExplorerFabric(fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo): Explorer {
function createExplorerFabric(params: { connectionId: string }): Explorer {
updateUserContext({
fabricDatabaseConnectionInfo,
fabricContext: {
connectionId: params.connectionId,
databaseConnectionInfo: undefined,
},
authType: AuthType.ConnectionString,
databaseAccount: {
id: "",
@@ -330,7 +338,7 @@ function createExplorerFabric(fabricDatabaseConnectionInfo: FabricDatabaseConnec
name: "Mounted",
kind: AccountKind.Default,
properties: {
documentEndpoint: fabricDatabaseConnectionInfo.endpoint,
documentEndpoint: undefined,
},
},
});

View File

@@ -10,6 +10,7 @@ import { ContainerInfo } from "../Contracts/DataModels";
export interface QueryCopilotState {
copilotEnabled: boolean;
copilotUserDBEnabled: boolean;
copilotSampleDBEnabled: boolean;
generatedQuery: string;
likeQuery: boolean;
userPrompt: string;
@@ -50,6 +51,7 @@ export interface QueryCopilotState {
setCopilotEnabled: (copilotEnabled: boolean) => void;
setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => void;
setCopilotSampleDBEnabled: (copilotSampleDBEnabled: boolean) => void;
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => void;
closeFeedbackModal: () => void;
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) => void;
@@ -96,6 +98,7 @@ type QueryCopilotStore = UseStore<QueryCopilotState>;
export const useQueryCopilot: QueryCopilotStore = create((set) => ({
copilotEnabled: false,
copilotUserDBEnabled: false,
copilotSampleDBEnabled: false,
generatedQuery: "",
likeQuery: false,
userPrompt: "",
@@ -104,7 +107,7 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
correlationId: "",
query: "SELECT * FROM c",
selectedQuery: "",
isGeneratingQuery: false,
isGeneratingQuery: null,
isGeneratingExplanation: false,
isExecuting: false,
dislikeQuery: undefined,
@@ -145,6 +148,7 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
setCopilotEnabled: (copilotEnabled: boolean) => set({ copilotEnabled }),
setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => set({ copilotUserDBEnabled }),
setCopilotSampleDBEnabled: (copilotSampleDBEnabled: boolean) => set({ copilotSampleDBEnabled }),
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
closeFeedbackModal: () => set({ showFeedbackModal: false }),