Implement query copilot UI (#1452)
This commit is contained in:
parent
abff435e88
commit
aadbb50e7d
|
@ -0,0 +1,35 @@
|
||||||
|
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="40" height="40" fill="white"/>
|
||||||
|
<path d="M17 3.73926L20 4.04436V12.6862L15.2 13.8013L13.6 15.9967V19.4479C13.6 21.7669 14.8545 23.9045 16.879 25.0353L21.4036 27.5626L13.6 31.69L10.3559 32.1523L7.27902 30.4337C5.25445 29.3028 4 27.1652 4 24.8462V14.7591C4 12.4395 5.25512 10.3015 7.28055 9.17085L16.3174 4.12639L16.3121 4.13012L17 3.73926Z" fill="url(#paint0_radial_420_101763)"/>
|
||||||
|
<path d="M17 3.73926L20 4.04436V12.6862L15.2 13.8013L13.6 15.9967V19.4479C13.6 21.7669 14.8545 23.9045 16.879 25.0353L21.4036 27.5626L13.6 31.69L10.3559 32.1523L7.27902 30.4337C5.25445 29.3028 4 27.1652 4 24.8462V14.7591C4 12.4395 5.25512 10.3015 7.28055 9.17085L16.3174 4.12639L16.3121 4.13012L17 3.73926Z" fill="url(#paint1_linear_420_101763)"/>
|
||||||
|
<path d="M26.399 15.001L34.399 19.801L35.999 21.401V24.8452C35.999 27.1642 34.7446 29.3018 32.72 30.4327L23.12 35.7949C21.1804 36.8784 18.8177 36.8784 16.878 35.7949L7.27804 30.4327C7.09146 30.3284 6.91141 30.2157 6.73828 30.095L7.278 30.3965C9.21762 31.4799 11.5803 31.4799 13.52 30.3965L23.12 25.0342C25.1445 23.9033 26.399 21.7657 26.399 19.4467V15.001Z" fill="url(#paint2_radial_420_101763)"/>
|
||||||
|
<path d="M26.399 15.001L34.399 19.801L35.999 21.401V24.8452C35.999 27.1642 34.7446 29.3018 32.72 30.4327L23.12 35.7949C21.1804 36.8784 18.8177 36.8784 16.878 35.7949L7.27804 30.4327C7.09146 30.3284 6.91141 30.2157 6.73828 30.095L7.278 30.3965C9.21762 31.4799 11.5803 31.4799 13.52 30.3965L23.12 25.0342C25.1445 23.9033 26.399 21.7657 26.399 19.4467V15.001Z" fill="url(#paint3_linear_420_101763)"/>
|
||||||
|
<path d="M32.7191 9.17053L23.119 3.8117C21.1802 2.72943 18.819 2.72943 16.8802 3.8117L16.3151 4.1271C14.6239 5.31737 13.5996 7.26472 13.5996 9.36025V16.0461L16.8802 14.2149C18.819 13.1326 21.1802 13.1326 23.119 14.2149L32.7191 19.5737C34.6984 20.6786 35.9421 22.7456 35.9977 25.0039C35.999 24.9514 35.9996 24.8987 35.9996 24.8459V14.7588C35.9996 12.4392 34.7445 10.3011 32.7191 9.17053Z" fill="url(#paint4_radial_420_101763)"/>
|
||||||
|
<path d="M32.7191 9.17053L23.119 3.8117C21.1802 2.72943 18.819 2.72943 16.8802 3.8117L16.3151 4.1271C14.6239 5.31737 13.5996 7.26472 13.5996 9.36025V16.0461L16.8802 14.2149C18.819 13.1326 21.1802 13.1326 23.119 14.2149L32.7191 19.5737C34.6984 20.6786 35.9421 22.7456 35.9977 25.0039C35.999 24.9514 35.9996 24.8987 35.9996 24.8459V14.7588C35.9996 12.4392 34.7445 10.3011 32.7191 9.17053Z" fill="url(#paint5_linear_420_101763)"/>
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="paint0_radial_420_101763" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(17.5054 12.8837) rotate(112.544) scale(23.4519 19.3067)">
|
||||||
|
<stop offset="0.206732" stop-color="#45D586"/>
|
||||||
|
<stop offset="0.875628" stop-color="#128245"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="paint1_linear_420_101763" x1="15.0625" y1="29.6174" x2="13.787" y2="27.2436" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.9999" stop-color="#0F773F"/>
|
||||||
|
<stop offset="1" stop-color="#0078D4" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="paint2_radial_420_101763" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10.899 27.5214) rotate(-3.9995) scale(24.6197 15.4594)">
|
||||||
|
<stop offset="0.140029" stop-color="#FBFF47"/>
|
||||||
|
<stop offset="0.952721" stop-color="#54B228"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="paint3_linear_420_101763" x1="30.8932" y1="18.5508" x2="29.5491" y2="20.8921" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.9999" stop-color="#27770B"/>
|
||||||
|
<stop offset="1" stop-color="#8C66BA" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="paint4_radial_420_101763" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(30.632 19.3878) rotate(-138.33) scale(22.8015 13.0047)">
|
||||||
|
<stop stop-color="#95FEA0"/>
|
||||||
|
<stop offset="0.839255" stop-color="#10B7B7"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="paint5_linear_420_101763" x1="13.5996" y1="11.7134" x2="16.2131" y2="11.7134" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.9999" stop-color="#0A7B7B"/>
|
||||||
|
<stop offset="1" stop-color="#436DCD" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.0 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="334" height="154" viewBox="0 0 334 154" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="0.5" y="0.5" width="333" height="153" fill="#F3F2F1" stroke="#E1DFDD"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 188 B |
|
@ -428,3 +428,6 @@ export class JunoEndpoints {
|
||||||
public static readonly Prod = "https://tools.cosmos.azure.com";
|
public static readonly Prod = "https://tools.cosmos.azure.com";
|
||||||
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
|
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const QueryCopilotSampleDatabaseId = "CopilotSampleDb";
|
||||||
|
export const QueryCopilotSampleContainerId = "SampleContainer";
|
||||||
|
|
|
@ -57,7 +57,7 @@ const SharedDatabaseDefault: DataModels.IndexingPolicy = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const AllPropertiesIndexed: DataModels.IndexingPolicy = {
|
export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
|
||||||
indexingMode: "consistent",
|
indexingMode: "consistent",
|
||||||
automatic: true,
|
automatic: true,
|
||||||
includedPaths: [
|
includedPaths: [
|
||||||
|
|
|
@ -4,11 +4,11 @@ import React, { FunctionComponent, useState } from "react";
|
||||||
import { Areas, SavedQueries } from "../../../Common/Constants";
|
import { Areas, SavedQueries } from "../../../Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||||
import { Query } from "../../../Contracts/DataModels";
|
import { Query } from "../../../Contracts/DataModels";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
|
||||||
import { useTabs } from "../../../hooks/useTabs";
|
|
||||||
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";
|
||||||
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
||||||
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
|
import { useTabs } from "../../../hooks/useTabs";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab";
|
import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
|
@ -16,9 +16,13 @@ import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneFor
|
||||||
|
|
||||||
interface SaveQueryPaneProps {
|
interface SaveQueryPaneProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
|
queryToSave?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer }: SaveQueryPaneProps): JSX.Element => {
|
export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
|
||||||
|
explorer,
|
||||||
|
queryToSave,
|
||||||
|
}: SaveQueryPaneProps): JSX.Element => {
|
||||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||||
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
|
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
|
||||||
const [formError, setFormError] = useState<string>("");
|
const [formError, setFormError] = useState<string>("");
|
||||||
|
@ -36,7 +40,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryTab = useTabs.getState().activeTab as NewQueryTab;
|
const queryTab = useTabs.getState().activeTab as NewQueryTab;
|
||||||
const query: string = queryTab && queryTab.iTabAccessor.onSaveClickEvent();
|
const query: string = queryToSave || queryTab?.iTabAccessor.onSaveClickEvent();
|
||||||
|
|
||||||
if (!queryName || queryName.length === 0) {
|
if (!queryName || queryName.length === 0) {
|
||||||
setFormError("No query name specified");
|
setFormError("No query name specified");
|
||||||
|
@ -62,8 +66,8 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer
|
||||||
try {
|
try {
|
||||||
await explorer.queriesClient.saveQuery(queryParam);
|
await explorer.queriesClient.saveQuery(queryParam);
|
||||||
setLoadingFalse();
|
setLoadingFalse();
|
||||||
queryTab.tabTitle(queryParam.queryName);
|
queryTab?.tabTitle(queryParam.queryName);
|
||||||
queryTab.tabPath(`${queryTab.collection.databaseId}>${queryTab.collection.id()}>${queryParam.queryName}`);
|
queryTab?.tabPath(`${queryTab.collection.databaseId}>${queryTab.collection.id()}>${queryParam.queryName}`);
|
||||||
traceSuccess(
|
traceSuccess(
|
||||||
Action.SaveQuery,
|
Action.SaveQuery,
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import Explorer from "../Explorer";
|
||||||
|
import { QueryCopilotCarousel } from "./CopilotCarousel";
|
||||||
|
|
||||||
|
describe("Query Copilot Carousel snapshot test", () => {
|
||||||
|
it("should render when isOpen is true", () => {
|
||||||
|
const wrapper = shallow(<QueryCopilotCarousel isOpen={true} explorer={new Explorer()} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,275 @@
|
||||||
|
import {
|
||||||
|
DefaultButton,
|
||||||
|
ISeparatorStyles,
|
||||||
|
IconButton,
|
||||||
|
Image,
|
||||||
|
Link,
|
||||||
|
Modal,
|
||||||
|
PrimaryButton,
|
||||||
|
Separator,
|
||||||
|
Spinner,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
} from "@fluentui/react";
|
||||||
|
import { QueryCopilotSampleDatabaseId, StyleConstants } from "Common/Constants";
|
||||||
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
|
import { createCollection } from "Common/dataAccess/createCollection";
|
||||||
|
import * as DataModels from "Contracts/DataModels";
|
||||||
|
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel";
|
||||||
|
import { PromptCard } from "Explorer/QueryCopilot/PromptCard";
|
||||||
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
|
import { useCarousel } from "hooks/useCarousel";
|
||||||
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import YoutubePlaceholder from "../../../images/YoutubePlaceholder.svg";
|
||||||
|
|
||||||
|
interface QueryCopilotCarouselProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
explorer: Explorer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const separatorStyles: Partial<ISeparatorStyles> = {
|
||||||
|
root: {
|
||||||
|
selectors: {
|
||||||
|
"::before": {
|
||||||
|
background: StyleConstants.BaseMedium,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
padding: "16px 0",
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QueryCopilotCarousel: React.FC<QueryCopilotCarouselProps> = ({
|
||||||
|
isOpen,
|
||||||
|
explorer,
|
||||||
|
}: QueryCopilotCarouselProps): JSX.Element => {
|
||||||
|
const [page, setPage] = useState<number>(1);
|
||||||
|
const [isCreatingDatabase, setIsCreatingDatabase] = useState<boolean>(false);
|
||||||
|
const [selectedPrompt, setSelectedPrompt] = useState<number>(1);
|
||||||
|
|
||||||
|
const getHeaderText = (): string => {
|
||||||
|
switch (page) {
|
||||||
|
case 1:
|
||||||
|
return "What exactly is copilot?";
|
||||||
|
case 2:
|
||||||
|
return "Setting up your Sample database";
|
||||||
|
case 3:
|
||||||
|
return "Sample prompts to help you";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getQueryCopilotInitialInput = (): string => {
|
||||||
|
switch (selectedPrompt) {
|
||||||
|
case 1:
|
||||||
|
return "Write a query to return all records in this table";
|
||||||
|
case 2:
|
||||||
|
return "Write a query to return all records in this table created in the last thirty days";
|
||||||
|
case 3:
|
||||||
|
return 'Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"';
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSampleDatabase = async (): Promise<void> => {
|
||||||
|
const database = useDatabases.getState().findDatabaseWithId(QueryCopilotSampleDatabaseId);
|
||||||
|
if (database) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsCreatingDatabase(true);
|
||||||
|
const params: DataModels.CreateCollectionParams = {
|
||||||
|
createNewDatabase: true,
|
||||||
|
collectionId: "SampleContainer",
|
||||||
|
databaseId: QueryCopilotSampleDatabaseId,
|
||||||
|
databaseLevelThroughput: true,
|
||||||
|
autoPilotMaxThroughput: 1000,
|
||||||
|
offerThroughput: undefined,
|
||||||
|
indexingPolicy: AllPropertiesIndexed,
|
||||||
|
partitionKey: {
|
||||||
|
paths: ["/CategoryId"],
|
||||||
|
kind: "Hash",
|
||||||
|
version: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await createCollection(params);
|
||||||
|
await explorer.refreshAllDatabases();
|
||||||
|
const database = useDatabases.getState().findDatabaseWithId(QueryCopilotSampleDatabaseId);
|
||||||
|
// populate sample container with sample data
|
||||||
|
await database.loadCollections();
|
||||||
|
const collection = database.findCollectionWithId("SampleContainer");
|
||||||
|
collection.isSampleCollection = true;
|
||||||
|
const sampleGenerator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorer);
|
||||||
|
await sampleGenerator.populateContainerAsync(collection, "/CategoryId");
|
||||||
|
// auto-expand sample database + container and show teaching bubble
|
||||||
|
await database.expandDatabase();
|
||||||
|
collection.expandCollection();
|
||||||
|
useDatabases.getState().updateDatabase(database);
|
||||||
|
} catch (error) {
|
||||||
|
//TODO: show error in UI
|
||||||
|
handleError(error, "Query copilot quickstart");
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setIsCreatingDatabase(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContent = (): JSX.Element => {
|
||||||
|
switch (page) {
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<Stack style={{ marginTop: 8 }}>
|
||||||
|
<Text style={{ fontSize: 13 }}>
|
||||||
|
A couple of lines about copilot and the background about it. The idea is to have some text to give context
|
||||||
|
to the user.
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: 14, fontWeight: 600, marginTop: 16 }}>How do you use copilot</Text>
|
||||||
|
<Text style={{ fontSize: 13, marginTop: 8 }}>
|
||||||
|
To generate queries , just describe the query you want and copilot will generate the query for you.Watch
|
||||||
|
this video to learn more about how to use copilot.
|
||||||
|
</Text>
|
||||||
|
<Image src={YoutubePlaceholder} style={{ margin: "16px auto" }} />
|
||||||
|
<Text style={{ fontSize: 14, fontWeight: 600 }}>What is copilot good at</Text>
|
||||||
|
<Text style={{ fontSize: 13, marginTop: 8 }}>
|
||||||
|
A couple of lines about what copilot can do and its capablites with a link to{" "}
|
||||||
|
<Link href="" target="_blank">
|
||||||
|
documentation
|
||||||
|
</Link>{" "}
|
||||||
|
if possible.
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: 14, fontWeight: 600, marginTop: 16 }}>What are its limitations</Text>
|
||||||
|
<Text style={{ fontSize: 13, marginTop: 8 }}>
|
||||||
|
A couple of lines about what copilot cant do and its limitations.{" "}
|
||||||
|
<Link href="" target="_blank">
|
||||||
|
Link to documentation
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: 14, fontWeight: 600, marginTop: 16 }}>Disclaimer</Text>
|
||||||
|
<Text style={{ fontSize: 13, marginTop: 8 }}>
|
||||||
|
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "}
|
||||||
|
<Link href="" target="_blank">
|
||||||
|
Read preview terms
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
case 2:
|
||||||
|
return (
|
||||||
|
<Stack style={{ marginTop: 8 }}>
|
||||||
|
<Text style={{ fontSize: 13 }}>
|
||||||
|
Before you get started, we need to configure your sample database for you. Here is a summary of the
|
||||||
|
database being created for your reference. Configuration values can be updated using the settings icon in
|
||||||
|
the query builder.
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 24 }}>Database Id</Text>
|
||||||
|
<Text style={{ fontSize: 13 }}>CopilotSampleDb</Text>
|
||||||
|
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database throughput (autoscale)</Text>
|
||||||
|
<Text style={{ fontSize: 13 }}>Autoscale</Text>
|
||||||
|
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database Max RU/s</Text>
|
||||||
|
<Text>1000</Text>
|
||||||
|
<Text style={{ fontSize: 10, marginTop: 8 }}>
|
||||||
|
Your database throughput will automatically scale from{" "}
|
||||||
|
<strong>100 RU/s (10% of max RU/s) - 1000 RU/s</strong> based on usage.
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: 10, marginTop: 8 }}>
|
||||||
|
Estimated monthly cost (USD): <strong>$8.76 - $87.60</strong> (1 region, 100 - 1000 RU/s, $0.00012/RU)
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Container Id</Text>
|
||||||
|
<Text style={{ fontSize: 13 }}>SampleContainer</Text>
|
||||||
|
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Partition key</Text>
|
||||||
|
<Text style={{ fontSize: 13 }}>CategoryId</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
case 3:
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Text>To help you get started, here are some sample prompts to get you started</Text>
|
||||||
|
<Stack tokens={{ childrenGap: 12 }} style={{ marginTop: 16 }}>
|
||||||
|
<PromptCard
|
||||||
|
header="Write a query to return all records in this table"
|
||||||
|
description="This is a basic query which returns all records in the table "
|
||||||
|
onSelect={() => setSelectedPrompt(1)}
|
||||||
|
isSelected={selectedPrompt === 1}
|
||||||
|
/>
|
||||||
|
<PromptCard
|
||||||
|
header="Write a query to return all records in this table created in the last thirty days"
|
||||||
|
description="This builds on the previous query which returns all records in the table which were inserted in the last thirty days. You can also modify this query to return records based upon creation date"
|
||||||
|
onSelect={() => setSelectedPrompt(2)}
|
||||||
|
isSelected={selectedPrompt === 2}
|
||||||
|
/>
|
||||||
|
<PromptCard
|
||||||
|
header='Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"'
|
||||||
|
description='This builds on the previous query which returns all records in the table which were inserted in the last thirty days but which has the record owner as "contoso"'
|
||||||
|
onSelect={() => setSelectedPrompt(3)}
|
||||||
|
isSelected={selectedPrompt === 3}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Text style={{ fontSize: 13, marginTop: 32 }}>
|
||||||
|
Interested in learning more about how to write effective prompts. Please read this article for more
|
||||||
|
information.
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: 13, marginTop: 16 }}>
|
||||||
|
You can also access these prompts by selecting the Samples prompts button in the query builder page.
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: 13, marginTop: 16 }}>
|
||||||
|
Don't like any of the prompts? Just click Get Started and write your own prompt.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal styles={{ main: { width: 880 } }} isOpen={isOpen && page < 4}>
|
||||||
|
<Stack style={{ padding: 16 }}>
|
||||||
|
<Stack horizontal horizontalAlign="space-between">
|
||||||
|
<Text variant="xLarge">{getHeaderText()}</Text>
|
||||||
|
<IconButton
|
||||||
|
iconProps={{ iconName: "Cancel" }}
|
||||||
|
onClick={() => useCarousel.getState().setShowCopilotCarousel(false)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
{getContent()}
|
||||||
|
<Separator styles={separatorStyles} />
|
||||||
|
<Stack horizontal horizontalAlign="start" verticalAlign="center">
|
||||||
|
{page !== 1 && (
|
||||||
|
<DefaultButton
|
||||||
|
text="Previous"
|
||||||
|
style={{ marginRight: 8 }}
|
||||||
|
onClick={() => setPage(page - 1)}
|
||||||
|
disabled={isCreatingDatabase}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<PrimaryButton
|
||||||
|
text={page === 3 ? "Get started" : "Next"}
|
||||||
|
onClick={async () => {
|
||||||
|
if (page === 3) {
|
||||||
|
useCarousel.getState().setShowCopilotCarousel(false);
|
||||||
|
useTabs.getState().setQueryCopilotTabInitialInput(getQueryCopilotInitialInput());
|
||||||
|
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page === 2) {
|
||||||
|
await createSampleDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
setPage(page + 1);
|
||||||
|
}}
|
||||||
|
disabled={isCreatingDatabase}
|
||||||
|
/>
|
||||||
|
{isCreatingDatabase && <Spinner style={{ marginLeft: 8 }} />}
|
||||||
|
{isCreatingDatabase && <Text style={{ marginLeft: 8, color: "#0078D4" }}>Setting up your database...</Text>}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import { PromptCard } from "./PromptCard";
|
||||||
|
|
||||||
|
describe("Prompt card snapshot test", () => {
|
||||||
|
it("should render properly if isSelected is true", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<PromptCard header="TestHeader" description="TestDescription" isSelected={true} onSelect={() => undefined} />
|
||||||
|
);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render properly if isSelected is false", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<PromptCard header="TestHeader" description="TestDescription" isSelected={false} onSelect={() => undefined} />
|
||||||
|
);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { ChoiceGroup, Stack, Text } from "@fluentui/react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface PromptCardProps {
|
||||||
|
header: string;
|
||||||
|
description: string;
|
||||||
|
isSelected: boolean;
|
||||||
|
onSelect: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PromptCard: React.FC<PromptCardProps> = ({
|
||||||
|
header,
|
||||||
|
description,
|
||||||
|
isSelected,
|
||||||
|
onSelect,
|
||||||
|
}: PromptCardProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
horizontal
|
||||||
|
style={{
|
||||||
|
padding: "16px 0 16px 16px ",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
width: 650,
|
||||||
|
height: 100,
|
||||||
|
border: "1px solid #F3F2F1",
|
||||||
|
boxShadow: "0px 1.6px 3.6px rgba(0, 0, 0, 0.132), 0px 0.3px 0.9px rgba(0, 0, 0, 0.108)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack.Item grow={1}>
|
||||||
|
<Stack horizontal>
|
||||||
|
<div>
|
||||||
|
<Text style={{ fontSize: 13, color: "#00A2AD", background: "#F8FFF0" }}>Prompt</Text>
|
||||||
|
</div>
|
||||||
|
<Text style={{ fontSize: 13, marginLeft: 16 }}>{header}</Text>
|
||||||
|
</Stack>
|
||||||
|
<Text style={{ fontSize: 10, marginTop: 16 }}>{description}</Text>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item style={{ marginLeft: 16 }}>
|
||||||
|
<ChoiceGroup
|
||||||
|
styles={{ flexContainer: { width: 36 } }}
|
||||||
|
options={[{ key: "selected", text: "" }]}
|
||||||
|
selectedKey={isSelected ? "selected" : ""}
|
||||||
|
onChange={onSelect}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import Explorer from "../Explorer";
|
||||||
|
import { QueryCopilotTab } from "./QueryCopilotTab";
|
||||||
|
|
||||||
|
describe("Query copilot tab snapshot test", () => {
|
||||||
|
it("should render with initial input", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<QueryCopilotTab initialInput="Write a query to return all records in this table" explorer={new Explorer()} />
|
||||||
|
);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,164 @@
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
|
import { IconButton, Image, Link, Stack, Text, TextField } from "@fluentui/react";
|
||||||
|
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||||
|
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
|
||||||
|
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
|
||||||
|
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
||||||
|
import { queryDocuments } from "Common/dataAccess/queryDocuments";
|
||||||
|
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
|
||||||
|
import { QueryResults } from "Contracts/ViewModels";
|
||||||
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
|
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
||||||
|
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||||
|
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import SplitterLayout from "react-splitter-layout";
|
||||||
|
import CopilotIcon from "../../../images/Copilot.svg";
|
||||||
|
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
||||||
|
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
||||||
|
|
||||||
|
interface QueryCopilotTabProps {
|
||||||
|
initialInput: string;
|
||||||
|
explorer: Explorer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
||||||
|
initialInput,
|
||||||
|
explorer,
|
||||||
|
}: QueryCopilotTabProps): JSX.Element => {
|
||||||
|
const [userInput, setUserInput] = useState<string>(initialInput || "");
|
||||||
|
const [query, setQuery] = useState<string>("");
|
||||||
|
const [selectedQuery, setSelectedQuery] = useState<string>("");
|
||||||
|
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||||
|
const [queryIterator, setQueryIterator] = useState<MinimalQueryIterator>();
|
||||||
|
const [queryResults, setQueryResults] = useState<QueryResults>();
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||||
|
|
||||||
|
const generateQuery = (): string => {
|
||||||
|
switch (userInput) {
|
||||||
|
case "Write a query to return all records in this table":
|
||||||
|
return "SELECT * FROM c";
|
||||||
|
case "Write a query to return all records in this table created in the last thirty days":
|
||||||
|
return "SELECT * FROM c WHERE c._ts > (DATEDIFF(s, '1970-01-01T00:00:00Z', GETUTCDATE()) - 2592000) * 1000";
|
||||||
|
case `Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"`:
|
||||||
|
return `SELECT * FROM c WHERE c.owner = "Contoso" AND c._ts > (DATEDIFF(s, '1970-01-01T00:00:00Z', GETUTCDATE()) - 2592000) * 1000`;
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onExecuteQueryClick = async (): Promise<void> => {
|
||||||
|
const queryToExecute = selectedQuery || query;
|
||||||
|
const queryIterator = queryDocuments(QueryCopilotSampleDatabaseId, QueryCopilotSampleContainerId, queryToExecute, {
|
||||||
|
enableCrossPartitionQuery: shouldEnableCrossPartitionKey(),
|
||||||
|
} as FeedOptions);
|
||||||
|
setQueryIterator(queryIterator);
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
await queryDocumentsPerPage(0, queryIterator);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryDocumentsPerPage = async (firstItemIndex: number, queryIterator: MinimalQueryIterator): Promise<void> => {
|
||||||
|
try {
|
||||||
|
setIsExecuting(true);
|
||||||
|
const queryResults: QueryResults = await queryPagesUntilContentPresent(
|
||||||
|
firstItemIndex,
|
||||||
|
async (firstItemIndex: number) =>
|
||||||
|
queryDocumentsPage(QueryCopilotSampleContainerId, queryIterator, firstItemIndex)
|
||||||
|
);
|
||||||
|
|
||||||
|
setQueryResults(queryResults);
|
||||||
|
setErrorMessage("");
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = getErrorMessage(error);
|
||||||
|
setErrorMessage(errorMessage);
|
||||||
|
handleError(errorMessage, "executeQueryCopilotTab");
|
||||||
|
} finally {
|
||||||
|
setIsExecuting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
||||||
|
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
||||||
|
const executeQueryBtn = {
|
||||||
|
iconSrc: ExecuteQueryIcon,
|
||||||
|
iconAlt: executeQueryBtnLabel,
|
||||||
|
onCommandClick: () => onExecuteQueryClick(),
|
||||||
|
commandButtonLabel: executeQueryBtnLabel,
|
||||||
|
ariaLabel: executeQueryBtnLabel,
|
||||||
|
hasPopup: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveQueryBtn = {
|
||||||
|
iconSrc: SaveQueryIcon,
|
||||||
|
iconAlt: "Save Query",
|
||||||
|
onCommandClick: () =>
|
||||||
|
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={explorer} queryToSave={query} />),
|
||||||
|
commandButtonLabel: "Save Query",
|
||||||
|
ariaLabel: "Save Query",
|
||||||
|
hasPopup: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return [executeQueryBtn, saveQueryBtn];
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
useCommandBar.getState().setContextButtons(getCommandbarButtons());
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack className="tab-pane" style={{ padding: 24, width: "100%", height: "100%" }}>
|
||||||
|
<Stack horizontal verticalAlign="center">
|
||||||
|
<Image src={CopilotIcon} />
|
||||||
|
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%" }}>
|
||||||
|
<TextField
|
||||||
|
value={userInput}
|
||||||
|
onChange={(_, newValue) => setUserInput(newValue)}
|
||||||
|
style={{ lineHeight: 30 }}
|
||||||
|
styles={{ root: { width: "90%" } }}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
iconProps={{ iconName: "Send" }}
|
||||||
|
style={{ marginLeft: 8 }}
|
||||||
|
onClick={() => setQuery(generateQuery())}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Text style={{ marginTop: 8, marginBottom: 24, fontSize: 12 }}>
|
||||||
|
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "}
|
||||||
|
<Link href="" target="_blank">
|
||||||
|
Read preview terms
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Stack className="tabPaneContentContainer">
|
||||||
|
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
|
||||||
|
<EditorReact
|
||||||
|
language={"sql"}
|
||||||
|
content={query}
|
||||||
|
isReadOnly={false}
|
||||||
|
ariaLabel={"Editing Query"}
|
||||||
|
lineNumbers={"on"}
|
||||||
|
onContentChanged={(newQuery: string) => setQuery(newQuery)}
|
||||||
|
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
|
||||||
|
/>
|
||||||
|
<QueryResultSection
|
||||||
|
isMongoDB={false}
|
||||||
|
queryEditorContent={selectedQuery || query}
|
||||||
|
error={errorMessage}
|
||||||
|
queryResults={queryResults}
|
||||||
|
isExecuting={isExecuting}
|
||||||
|
executeQueryDocumentsPage={(firstItemIndex: number) => queryDocumentsPerPage(firstItemIndex, queryIterator)}
|
||||||
|
/>
|
||||||
|
</SplitterLayout>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,198 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Query Copilot Carousel snapshot test should render when isOpen is true 1`] = `
|
||||||
|
<Modal
|
||||||
|
isOpen={true}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"main": Object {
|
||||||
|
"width": 880,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"padding": 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
horizontalAlign="space-between"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
variant="xLarge"
|
||||||
|
>
|
||||||
|
What exactly is copilot?
|
||||||
|
</Text>
|
||||||
|
<CustomizedIconButton
|
||||||
|
iconProps={
|
||||||
|
Object {
|
||||||
|
"iconName": "Cancel",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginTop": 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": 13,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
A couple of lines about copilot and the background about it. The idea is to have some text to give context to the user.
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": 14,
|
||||||
|
"fontWeight": 600,
|
||||||
|
"marginTop": 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
How do you use copilot
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": 13,
|
||||||
|
"marginTop": 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
To generate queries , just describe the query you want and copilot will generate the query for you.Watch this video to learn more about how to use copilot.
|
||||||
|
</Text>
|
||||||
|
<Image
|
||||||
|
src=""
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"margin": "16px auto",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": 14,
|
||||||
|
"fontWeight": 600,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
What is copilot good at
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": 13,
|
||||||
|
"marginTop": 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
A couple of lines about what copilot can do and its capablites with a link to
|
||||||
|
|
||||||
|
<StyledLinkBase
|
||||||
|
href=""
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
documentation
|
||||||
|
</StyledLinkBase>
|
||||||
|
|
||||||
|
if possible.
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": 14,
|
||||||
|
"fontWeight": 600,
|
||||||
|
"marginTop": 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
What are its limitations
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": 13,
|
||||||
|
"marginTop": 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
A couple of lines about what copilot cant do and its limitations.
|
||||||
|
|
||||||
|
<StyledLinkBase
|
||||||
|
href=""
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Link to documentation
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": 14,
|
||||||
|
"fontWeight": 600,
|
||||||
|
"marginTop": 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Disclaimer
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": 13,
|
||||||
|
"marginTop": 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
|
||||||
|
|
||||||
|
<StyledLinkBase
|
||||||
|
href=""
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Read preview terms
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Separator
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"height": 1,
|
||||||
|
"padding": "16px 0",
|
||||||
|
"selectors": Object {
|
||||||
|
"::before": Object {
|
||||||
|
"background": undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
horizontalAlign="start"
|
||||||
|
verticalAlign="center"
|
||||||
|
>
|
||||||
|
<CustomizedPrimaryButton
|
||||||
|
disabled={false}
|
||||||
|
onClick={[Function]}
|
||||||
|
text="Next"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
`;
|
|
@ -0,0 +1,171 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Prompt card snapshot test should render properly if isSelected is false 1`] = `
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"border": "1px solid #F3F2F1",
|
||||||
|
"boxShadow": "0px 1.6px 3.6px rgba(0, 0, 0, 0.132), 0px 0.3px 0.9px rgba(0, 0, 0, 0.108)",
|
||||||
|
"boxSizing": "border-box",
|
||||||
|
"height": 100,
|
||||||
|
"padding": "16px 0 16px 16px ",
|
||||||
|
"width": 650,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem
|
||||||
|
grow={1}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": "#F8FFF0",
|
||||||
|
"color": "#00A2AD",
|
||||||
|
"fontSize": 13,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Prompt
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": 13,
|
||||||
|
"marginLeft": 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
TestHeader
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": 10,
|
||||||
|
"marginTop": 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
TestDescription
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginLeft": 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledChoiceGroup
|
||||||
|
onChange={[Function]}
|
||||||
|
options={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"key": "selected",
|
||||||
|
"text": "",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
selectedKey=""
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"flexContainer": Object {
|
||||||
|
"width": 36,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Prompt card snapshot test should render properly if isSelected is true 1`] = `
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"border": "1px solid #F3F2F1",
|
||||||
|
"boxShadow": "0px 1.6px 3.6px rgba(0, 0, 0, 0.132), 0px 0.3px 0.9px rgba(0, 0, 0, 0.108)",
|
||||||
|
"boxSizing": "border-box",
|
||||||
|
"height": 100,
|
||||||
|
"padding": "16px 0 16px 16px ",
|
||||||
|
"width": 650,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem
|
||||||
|
grow={1}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": "#F8FFF0",
|
||||||
|
"color": "#00A2AD",
|
||||||
|
"fontSize": 13,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Prompt
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": 13,
|
||||||
|
"marginLeft": 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
TestHeader
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": 10,
|
||||||
|
"marginTop": 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
TestDescription
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginLeft": 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledChoiceGroup
|
||||||
|
onChange={[Function]}
|
||||||
|
options={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"key": "selected",
|
||||||
|
"text": "",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
selectedKey="selected"
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"flexContainer": Object {
|
||||||
|
"width": 36,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
`;
|
|
@ -0,0 +1,124 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Query copilot tab snapshot test should render with initial input 1`] = `
|
||||||
|
<Stack
|
||||||
|
className="tab-pane"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": "100%",
|
||||||
|
"padding": 24,
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
verticalAlign="center"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontWeight": 600,
|
||||||
|
"marginLeft": 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Copilot
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginTop": 16,
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="center"
|
||||||
|
>
|
||||||
|
<StyledTextFieldBase
|
||||||
|
onChange={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"lineHeight": 30,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": "90%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="Write a query to return all records in this table"
|
||||||
|
/>
|
||||||
|
<CustomizedIconButton
|
||||||
|
iconProps={
|
||||||
|
Object {
|
||||||
|
"iconName": "Send",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginLeft": 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": 12,
|
||||||
|
"marginBottom": 24,
|
||||||
|
"marginTop": 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
|
||||||
|
|
||||||
|
<StyledLinkBase
|
||||||
|
href=""
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Read preview terms
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
<Stack
|
||||||
|
className="tabPaneContentContainer"
|
||||||
|
>
|
||||||
|
<t
|
||||||
|
customClassName=""
|
||||||
|
onDragEnd={null}
|
||||||
|
onDragStart={null}
|
||||||
|
onSecondaryPaneSizeChange={null}
|
||||||
|
percentage={false}
|
||||||
|
primaryIndex={0}
|
||||||
|
primaryMinSize={100}
|
||||||
|
secondaryMinSize={200}
|
||||||
|
vertical={true}
|
||||||
|
>
|
||||||
|
<EditorReact
|
||||||
|
ariaLabel="Editing Query"
|
||||||
|
content=""
|
||||||
|
isReadOnly={false}
|
||||||
|
language="sql"
|
||||||
|
lineNumbers="on"
|
||||||
|
onContentChanged={[Function]}
|
||||||
|
onContentSelected={[Function]}
|
||||||
|
/>
|
||||||
|
<QueryResultSection
|
||||||
|
error=""
|
||||||
|
executeQueryDocumentsPage={[Function]}
|
||||||
|
isExecuting={false}
|
||||||
|
isMongoDB={false}
|
||||||
|
queryEditorContent=""
|
||||||
|
/>
|
||||||
|
</t>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
`;
|
|
@ -14,19 +14,21 @@ import {
|
||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { TerminalKind } from "Contracts/ViewModels";
|
import { TerminalKind } from "Contracts/ViewModels";
|
||||||
|
import { SplashScreenButton } from "Explorer/SplashScreen/SplashScreenButton";
|
||||||
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { useCarousel } from "hooks/useCarousel";
|
import { useCarousel } from "hooks/useCarousel";
|
||||||
import { usePostgres } from "hooks/usePostgres";
|
import { usePostgres } from "hooks/usePostgres";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
|
||||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import ConnectIcon from "../../../images/Connect_color.svg";
|
import ConnectIcon from "../../../images/Connect_color.svg";
|
||||||
import ContainersIcon from "../../../images/Containers.svg";
|
import ContainersIcon from "../../../images/Containers.svg";
|
||||||
|
import CopilotIcon from "../../../images/Copilot.svg";
|
||||||
import LinkIcon from "../../../images/Link_blue.svg";
|
import LinkIcon from "../../../images/Link_blue.svg";
|
||||||
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
|
|
||||||
import NotebookColorIcon from "../../../images/Notebooks.svg";
|
import NotebookColorIcon from "../../../images/Notebooks.svg";
|
||||||
import PowerShellIcon from "../../../images/PowerShell.svg";
|
import PowerShellIcon from "../../../images/PowerShell.svg";
|
||||||
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
|
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
|
||||||
|
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
|
||||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
@ -54,10 +56,6 @@ export interface SplashScreenProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SplashScreen extends React.Component<SplashScreenProps> {
|
export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||||
private static readonly dataModelingUrl = "https://docs.microsoft.com/azure/cosmos-db/modeling-data";
|
|
||||||
private static readonly throughputEstimatorUrl = "https://cosmos.azure.com/capacitycalculator";
|
|
||||||
private static readonly failoverUrl = "https://docs.microsoft.com/azure/cosmos-db/high-availability";
|
|
||||||
|
|
||||||
private readonly container: Explorer;
|
private readonly container: Explorer;
|
||||||
private subscriptions: Array<{ dispose: () => void }>;
|
private subscriptions: Array<{ dispose: () => void }>;
|
||||||
|
|
||||||
|
@ -108,32 +106,52 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||||
this.setState({});
|
this.setState({});
|
||||||
};
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
private getSplashScreenButtons = (): JSX.Element => {
|
||||||
const mainItems = this.createMainItems();
|
if (userContext.features.enableCopilot && userContext.apiType === "SQL") {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="connectExplorerContainer">
|
<Stack style={{ width: "66%", cursor: "pointer", margin: "40px auto" }} tokens={{ childrenGap: 16 }}>
|
||||||
<form className="connectExplorerFormContainer">
|
<Stack horizontal tokens={{ childrenGap: 16 }}>
|
||||||
<div className="splashScreenContainer">
|
<SplashScreenButton
|
||||||
<div className="splashScreen">
|
imgSrc={QuickStartIcon}
|
||||||
<div
|
title={"Launch quick start"}
|
||||||
className="title"
|
description={"Launch a quick start tutorial to get started with sample data"}
|
||||||
aria-label={
|
onClick={() => {
|
||||||
userContext.apiType === "Postgres"
|
this.container.onNewCollectionClicked({ isQuickstart: true });
|
||||||
? "Welcome to Azure Cosmos DB for PostgreSQL"
|
traceOpen(Action.LaunchQuickstart, { apiType: userContext.apiType });
|
||||||
: "Welcome to Azure Cosmos DB"
|
}}
|
||||||
|
/>
|
||||||
|
<SplashScreenButton
|
||||||
|
imgSrc={ContainersIcon}
|
||||||
|
title={`New ${getCollectionName()}`}
|
||||||
|
description={"Create a new container for storage and throughput"}
|
||||||
|
onClick={() => {
|
||||||
|
this.container.onNewCollectionClicked();
|
||||||
|
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</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={() => useCarousel.getState().setShowCopilotCarousel(true)}
|
||||||
{userContext.apiType === "Postgres"
|
/>
|
||||||
? "Welcome to Azure Cosmos DB for PostgreSQL"
|
<SplashScreenButton
|
||||||
: "Welcome to Azure Cosmos DB"}
|
imgSrc={ConnectIcon}
|
||||||
<FeaturePanelLauncher />
|
title={"Connect"}
|
||||||
</div>
|
description={"Prefer using your own choice of tooling? Find the connection string you need to connect"}
|
||||||
<div className="subtitle">
|
onClick={() => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect)}
|
||||||
{userContext.apiType === "Postgres"
|
/>
|
||||||
? "Get started with our sample datasets, documentation, and additional tools."
|
</Stack>
|
||||||
: "Globally distributed, multi-model database service for any scale"}
|
</Stack>
|
||||||
</div>
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainItems = this.createMainItems();
|
||||||
|
return (
|
||||||
<div className="mainButtonsContainer">
|
<div className="mainButtonsContainer">
|
||||||
{userContext.apiType === "Postgres" &&
|
{userContext.apiType === "Postgres" &&
|
||||||
usePostgres.getState().showPostgreTeachingBubble &&
|
usePostgres.getState().showPostgreTeachingBubble &&
|
||||||
|
@ -158,8 +176,8 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Welcome! If you are new to Cosmos DB PGSQL and need help with getting started, here is where you
|
Welcome! If you are new to Cosmos DB PGSQL and need help with getting started, here is where you can find
|
||||||
can find sample data, query.
|
sample data, query.
|
||||||
</TeachingBubble>
|
</TeachingBubble>
|
||||||
)}
|
)}
|
||||||
{mainItems.map((item) => (
|
{mainItems.map((item) => (
|
||||||
|
@ -219,6 +237,34 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||||
</TeachingBubble>
|
</TeachingBubble>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="connectExplorerContainer">
|
||||||
|
<form className="connectExplorerFormContainer">
|
||||||
|
<div className="splashScreenContainer">
|
||||||
|
<div className="splashScreen">
|
||||||
|
<div
|
||||||
|
className="title"
|
||||||
|
aria-label={
|
||||||
|
userContext.apiType === "Postgres"
|
||||||
|
? "Welcome to Azure Cosmos DB for PostgreSQL"
|
||||||
|
: "Welcome to Azure Cosmos DB"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{userContext.apiType === "Postgres"
|
||||||
|
? "Welcome to Azure Cosmos DB for PostgreSQL"
|
||||||
|
: "Welcome to Azure Cosmos DB"}
|
||||||
|
<FeaturePanelLauncher />
|
||||||
|
</div>
|
||||||
|
<div className="subtitle">
|
||||||
|
{userContext.apiType === "Postgres"
|
||||||
|
? "Get started with our sample datasets, documentation, and additional tools."
|
||||||
|
: "Globally distributed, multi-model database service for any scale"}
|
||||||
|
</div>
|
||||||
|
{this.getSplashScreenButtons()}
|
||||||
{useCarousel.getState().showCoachMark && (
|
{useCarousel.getState().showCoachMark && (
|
||||||
<Coachmark
|
<Coachmark
|
||||||
target="#quickstartDescription"
|
target="#quickstartDescription"
|
||||||
|
@ -248,7 +294,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||||
</Coachmark>
|
</Coachmark>
|
||||||
)}
|
)}
|
||||||
{userContext.apiType === "Postgres" ? (
|
{userContext.apiType === "Postgres" ? (
|
||||||
<Stack horizontal style={{ margin: "0 auto", width: "84%" }} tokens={{ childrenGap: 32 }}>
|
<Stack horizontal style={{ margin: "0 auto", width: "84%" }} tokens={{ childrenGap: 16 }}>
|
||||||
<Stack style={{ width: "33%" }}>
|
<Stack style={{ width: "33%" }}>
|
||||||
<Text
|
<Text
|
||||||
variant="large"
|
variant="large"
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { Stack, Text } from "@fluentui/react";
|
||||||
|
import React from "react";
|
||||||
|
import { KeyCodes } from "../../Common/Constants";
|
||||||
|
|
||||||
|
interface SplashScreenButtonProps {
|
||||||
|
imgSrc: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
||||||
|
imgSrc,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
onClick,
|
||||||
|
}: SplashScreenButtonProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
horizontal
|
||||||
|
style={{
|
||||||
|
border: "1px solid #949494",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
boxShadow: "0 4px 4px rgba(0, 0, 0, 0.25)",
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: "32px 16px",
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
width: "100%",
|
||||||
|
minHeight: 150,
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
onKeyPress={(event: React.KeyboardEvent) => {
|
||||||
|
if (event.charCode === KeyCodes.Space || event.charCode === KeyCodes.Enter) {
|
||||||
|
onClick();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<img src={imgSrc} />
|
||||||
|
</div>
|
||||||
|
<Stack style={{ marginLeft: 16 }}>
|
||||||
|
<Text style={{ fontSize: 18, fontWeight: 600 }}>{title}</Text>
|
||||||
|
<Text style={{ fontSize: 13 }}>{description}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,495 @@
|
||||||
|
import {
|
||||||
|
DetailsList,
|
||||||
|
DetailsListLayoutMode,
|
||||||
|
IColumn,
|
||||||
|
Pivot,
|
||||||
|
PivotItem,
|
||||||
|
SelectionMode,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
} from "@fluentui/react";
|
||||||
|
import { HttpHeaders, NormalizedEventKey } from "Common/Constants";
|
||||||
|
import MongoUtility from "Common/MongoUtility";
|
||||||
|
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||||
|
import { QueryMetrics } from "Contracts/DataModels";
|
||||||
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
|
import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
import { useNotificationConsole } from "hooks/useNotificationConsole";
|
||||||
|
import React from "react";
|
||||||
|
import DownloadQueryMetrics from "../../../../images/DownloadQuery.svg";
|
||||||
|
import QueryEditorNext from "../../../../images/Query-Editor-Next.svg";
|
||||||
|
import RunQuery from "../../../../images/RunQuery.png";
|
||||||
|
import InfoColor from "../../../../images/info_color.svg";
|
||||||
|
import { QueryResults } from "../../../Contracts/ViewModels";
|
||||||
|
|
||||||
|
interface QueryResultProps {
|
||||||
|
isMongoDB: boolean;
|
||||||
|
queryEditorContent: string;
|
||||||
|
error: string;
|
||||||
|
isExecuting: boolean;
|
||||||
|
queryResults: QueryResults;
|
||||||
|
executeQueryDocumentsPage: (firstItemIndex: number) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QueryResultSection: React.FC<QueryResultProps> = ({
|
||||||
|
isMongoDB,
|
||||||
|
queryEditorContent,
|
||||||
|
error,
|
||||||
|
queryResults,
|
||||||
|
isExecuting,
|
||||||
|
executeQueryDocumentsPage,
|
||||||
|
}: QueryResultProps): JSX.Element => {
|
||||||
|
const queryMetrics = React.useRef(queryResults?.headers?.[HttpHeaders.queryMetrics]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const latestQueryMetrics = queryResults?.headers?.[HttpHeaders.queryMetrics];
|
||||||
|
if (latestQueryMetrics && Object.keys(latestQueryMetrics).length > 0) {
|
||||||
|
queryMetrics.current = latestQueryMetrics;
|
||||||
|
}
|
||||||
|
}, [queryResults]);
|
||||||
|
|
||||||
|
const onRender = (item: IDocument): JSX.Element => (
|
||||||
|
<>
|
||||||
|
<InfoTooltip>{`${item.toolTip}`}</InfoTooltip>
|
||||||
|
<Text style={{ paddingLeft: 10, margin: 0 }}>{`${item.metric}`}</Text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
const columns: IColumn[] = [
|
||||||
|
{
|
||||||
|
key: "column2",
|
||||||
|
name: "METRIC",
|
||||||
|
minWidth: 200,
|
||||||
|
data: String,
|
||||||
|
fieldName: "metric",
|
||||||
|
onRender,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "column3",
|
||||||
|
name: "VALUE",
|
||||||
|
minWidth: 200,
|
||||||
|
data: String,
|
||||||
|
fieldName: "value",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const maybeSubQuery = queryEditorContent && /.*\(.*SELECT.*\)/i.test(queryEditorContent);
|
||||||
|
const queryResultsString = queryResults
|
||||||
|
? isMongoDB
|
||||||
|
? MongoUtility.tojson(queryResults.documents, undefined, false)
|
||||||
|
: JSON.stringify(queryResults.documents, undefined, 4)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const onErrorDetailsClick = (): boolean => {
|
||||||
|
useNotificationConsole.getState().expandConsole();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onErrorDetailsKeyPress = (event: React.KeyboardEvent<HTMLAnchorElement>): boolean => {
|
||||||
|
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||||
|
onErrorDetailsClick();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDownloadQueryMetricsCsvClick = (): boolean => {
|
||||||
|
downloadQueryMetricsCsvData();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDownloadQueryMetricsCsvKeyPress = (event: React.KeyboardEvent<HTMLAnchorElement>): boolean => {
|
||||||
|
if (event.key === NormalizedEventKey.Space || NormalizedEventKey.Enter) {
|
||||||
|
downloadQueryMetricsCsvData();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadQueryMetricsCsvData = (): void => {
|
||||||
|
const csvData: string = generateQueryMetricsCsvData();
|
||||||
|
if (!csvData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navigator.msSaveBlob) {
|
||||||
|
// for IE and Edge
|
||||||
|
navigator.msSaveBlob(
|
||||||
|
new Blob([csvData], { type: "data:text/csv;charset=utf-8" }),
|
||||||
|
"PerPartitionQueryMetrics.csv"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const downloadLink: HTMLAnchorElement = document.createElement("a");
|
||||||
|
downloadLink.href = "data:text/csv;charset=utf-8," + encodeURI(csvData);
|
||||||
|
downloadLink.target = "_self";
|
||||||
|
downloadLink.download = "QueryMetricsPerPartition.csv";
|
||||||
|
|
||||||
|
// for some reason, FF displays the download prompt only when
|
||||||
|
// the link is added to the dom so we add and remove it
|
||||||
|
document.body.appendChild(downloadLink);
|
||||||
|
downloadLink.click();
|
||||||
|
downloadLink.remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAggregatedQueryMetrics = (): QueryMetrics => {
|
||||||
|
const aggregatedQueryMetrics = {
|
||||||
|
documentLoadTime: 0,
|
||||||
|
documentWriteTime: 0,
|
||||||
|
indexHitDocumentCount: 0,
|
||||||
|
outputDocumentCount: 0,
|
||||||
|
outputDocumentSize: 0,
|
||||||
|
indexLookupTime: 0,
|
||||||
|
retrievedDocumentCount: 0,
|
||||||
|
retrievedDocumentSize: 0,
|
||||||
|
vmExecutionTime: 0,
|
||||||
|
runtimeExecutionTimes: {
|
||||||
|
queryEngineExecutionTime: 0,
|
||||||
|
systemFunctionExecutionTime: 0,
|
||||||
|
userDefinedFunctionExecutionTime: 0,
|
||||||
|
},
|
||||||
|
totalQueryExecutionTime: 0,
|
||||||
|
} as QueryMetrics;
|
||||||
|
|
||||||
|
if (queryMetrics.current) {
|
||||||
|
Object.keys(queryMetrics.current).forEach((partitionKeyRangeId) => {
|
||||||
|
const queryMetricsPerPartition = queryMetrics.current[partitionKeyRangeId];
|
||||||
|
if (!queryMetricsPerPartition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
aggregatedQueryMetrics.documentLoadTime += queryMetricsPerPartition.documentLoadTime?.totalMilliseconds() || 0;
|
||||||
|
aggregatedQueryMetrics.documentWriteTime +=
|
||||||
|
queryMetricsPerPartition.documentWriteTime?.totalMilliseconds() || 0;
|
||||||
|
aggregatedQueryMetrics.indexHitDocumentCount += queryMetricsPerPartition.indexHitDocumentCount || 0;
|
||||||
|
aggregatedQueryMetrics.outputDocumentCount += queryMetricsPerPartition.outputDocumentCount || 0;
|
||||||
|
aggregatedQueryMetrics.outputDocumentSize += queryMetricsPerPartition.outputDocumentSize || 0;
|
||||||
|
aggregatedQueryMetrics.indexLookupTime += queryMetricsPerPartition.indexLookupTime?.totalMilliseconds() || 0;
|
||||||
|
aggregatedQueryMetrics.retrievedDocumentCount += queryMetricsPerPartition.retrievedDocumentCount || 0;
|
||||||
|
aggregatedQueryMetrics.retrievedDocumentSize += queryMetricsPerPartition.retrievedDocumentSize || 0;
|
||||||
|
aggregatedQueryMetrics.vmExecutionTime += queryMetricsPerPartition.vmExecutionTime?.totalMilliseconds() || 0;
|
||||||
|
aggregatedQueryMetrics.totalQueryExecutionTime +=
|
||||||
|
queryMetricsPerPartition.totalQueryExecutionTime?.totalMilliseconds() || 0;
|
||||||
|
aggregatedQueryMetrics.runtimeExecutionTimes.queryEngineExecutionTime +=
|
||||||
|
queryMetricsPerPartition.runtimeExecutionTimes?.queryEngineExecutionTime?.totalMilliseconds() || 0;
|
||||||
|
aggregatedQueryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime +=
|
||||||
|
queryMetricsPerPartition.runtimeExecutionTimes?.systemFunctionExecutionTime?.totalMilliseconds() || 0;
|
||||||
|
aggregatedQueryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime +=
|
||||||
|
queryMetricsPerPartition.runtimeExecutionTimes?.userDefinedFunctionExecutionTime?.totalMilliseconds() || 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return aggregatedQueryMetrics;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateQueryMetricsCsvData = (): string => {
|
||||||
|
if (queryMetrics.current) {
|
||||||
|
let csvData =
|
||||||
|
[
|
||||||
|
"Partition key range id",
|
||||||
|
"Retrieved document count",
|
||||||
|
"Retrieved document size (in bytes)",
|
||||||
|
"Output document count",
|
||||||
|
"Output document size (in bytes)",
|
||||||
|
"Index hit document count",
|
||||||
|
"Index lookup time (ms)",
|
||||||
|
"Document load time (ms)",
|
||||||
|
"Query engine execution time (ms)",
|
||||||
|
"System function execution time (ms)",
|
||||||
|
"User defined function execution time (ms)",
|
||||||
|
"Document write time (ms)",
|
||||||
|
].join(",") + "\n";
|
||||||
|
|
||||||
|
Object.keys(queryMetrics.current).forEach((partitionKeyRangeId) => {
|
||||||
|
const queryMetricsPerPartition = queryMetrics.current[partitionKeyRangeId];
|
||||||
|
csvData +=
|
||||||
|
[
|
||||||
|
partitionKeyRangeId,
|
||||||
|
queryMetricsPerPartition.retrievedDocumentCount,
|
||||||
|
queryMetricsPerPartition.retrievedDocumentSize,
|
||||||
|
queryMetricsPerPartition.outputDocumentCount,
|
||||||
|
queryMetricsPerPartition.outputDocumentSize,
|
||||||
|
queryMetricsPerPartition.indexHitDocumentCount,
|
||||||
|
queryMetricsPerPartition.indexLookupTime?.totalMilliseconds(),
|
||||||
|
queryMetricsPerPartition.documentLoadTime?.totalMilliseconds(),
|
||||||
|
queryMetricsPerPartition.runtimeExecutionTimes?.queryEngineExecutionTime?.totalMilliseconds(),
|
||||||
|
queryMetricsPerPartition.runtimeExecutionTimes?.systemFunctionExecutionTime?.totalMilliseconds(),
|
||||||
|
queryMetricsPerPartition.runtimeExecutionTimes?.userDefinedFunctionExecutionTime?.totalMilliseconds(),
|
||||||
|
queryMetricsPerPartition.documentWriteTime?.totalMilliseconds(),
|
||||||
|
].join(",") + "\n";
|
||||||
|
});
|
||||||
|
|
||||||
|
return csvData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFetchNextPageClick = async (): Promise<void> => {
|
||||||
|
const { firstItemIndex, itemCount } = queryResults;
|
||||||
|
await executeQueryDocumentsPage(firstItemIndex + itemCount - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateQueryStatsItems = (): IDocument[] => {
|
||||||
|
const items: IDocument[] = [
|
||||||
|
{
|
||||||
|
metric: "Request Charge",
|
||||||
|
value: `${queryResults.requestCharge} RUs`,
|
||||||
|
toolTip: "Request Charge",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: "Showing Results",
|
||||||
|
value: queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`,
|
||||||
|
toolTip: "Showing Results",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (userContext.apiType === "SQL") {
|
||||||
|
const aggregatedQueryMetrics = getAggregatedQueryMetrics();
|
||||||
|
items.push(
|
||||||
|
{
|
||||||
|
metric: "Retrieved document count",
|
||||||
|
value: aggregatedQueryMetrics.retrievedDocumentCount?.toString() || "",
|
||||||
|
toolTip: "Total number of retrieved documents",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: "Retrieved document size",
|
||||||
|
value: `${aggregatedQueryMetrics.retrievedDocumentSize?.toString() || 0} bytes`,
|
||||||
|
toolTip: "Total size of retrieved documents in bytes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: "Output document count",
|
||||||
|
value: aggregatedQueryMetrics.outputDocumentCount?.toString() || "",
|
||||||
|
toolTip: "Number of output documents",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: "Output document size",
|
||||||
|
value: `${aggregatedQueryMetrics.outputDocumentSize?.toString() || 0} bytes`,
|
||||||
|
toolTip: "Total size of output documents in bytes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: "Index hit document count",
|
||||||
|
value: aggregatedQueryMetrics.indexHitDocumentCount?.toString() || "",
|
||||||
|
toolTip: "Total number of documents matched by the filter",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: "Index lookup time",
|
||||||
|
value: `${aggregatedQueryMetrics.indexLookupTime?.toString() || 0} ms`,
|
||||||
|
toolTip: "Time spent in physical index layer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: "Document load time",
|
||||||
|
value: `${aggregatedQueryMetrics.documentLoadTime?.toString() || 0} ms`,
|
||||||
|
toolTip: "Time spent in loading documents",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: "Query engine execution time",
|
||||||
|
value: `${aggregatedQueryMetrics.runtimeExecutionTimes?.queryEngineExecutionTime?.toString() || 0} ms`,
|
||||||
|
toolTip:
|
||||||
|
"Time spent by the query engine to execute the query expression (excludes other execution times like load documents or write results)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: "System function execution time",
|
||||||
|
value: `${aggregatedQueryMetrics.runtimeExecutionTimes?.systemFunctionExecutionTime?.toString() || 0} ms`,
|
||||||
|
toolTip: "Total time spent executing system (built-in) functions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: "User defined function execution time",
|
||||||
|
value: `${
|
||||||
|
aggregatedQueryMetrics.runtimeExecutionTimes?.userDefinedFunctionExecutionTime?.toString() || 0
|
||||||
|
} ms`,
|
||||||
|
toolTip: "Total time spent executing user-defined functions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: "Document write time",
|
||||||
|
value: `${aggregatedQueryMetrics.documentWriteTime.toString() || 0} ms`,
|
||||||
|
toolTip: "Time spent to write query result set to response buffer",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryResults.roundTrips) {
|
||||||
|
items.push({
|
||||||
|
metric: "Round Trips",
|
||||||
|
value: queryResults.roundTrips?.toString(),
|
||||||
|
toolTip: "Number of round trips",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryResults.activityId) {
|
||||||
|
items.push({
|
||||||
|
metric: "Activity id",
|
||||||
|
value: queryResults.activityId,
|
||||||
|
toolTip: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack style={{ height: "100%" }}>
|
||||||
|
{isMongoDB && queryEditorContent.length === 0 && (
|
||||||
|
<div className="mongoQueryHelper">
|
||||||
|
Start by writing a Mongo query, for example: <strong>{"{'id':'foo'}"}</strong> or{" "}
|
||||||
|
<strong>
|
||||||
|
{"{ "}
|
||||||
|
{" }"}
|
||||||
|
</strong>{" "}
|
||||||
|
to get all the documents.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{maybeSubQuery && (
|
||||||
|
<div className="warningErrorContainer" aria-live="assertive">
|
||||||
|
<div className="warningErrorContent">
|
||||||
|
<span>
|
||||||
|
<img className="paneErrorIcon" src={InfoColor} alt="Error" />
|
||||||
|
</span>
|
||||||
|
<span className="warningErrorDetailsLinkContainer">
|
||||||
|
We have detected you may be using a subquery. Non-correlated subqueries are not currently supported.
|
||||||
|
<a href="https://docs.microsoft.com/en-us/azure/cosmos-db/sql-query-subquery">
|
||||||
|
Please see Cosmos sub query documentation for further information
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* <!-- Query Errors Tab - Start--> */}
|
||||||
|
{error && (
|
||||||
|
<div className="active queryErrorsHeaderContainer">
|
||||||
|
<span className="queryErrors" data-toggle="tab">
|
||||||
|
Errors
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* <!-- Query Errors Tab - End --> */}
|
||||||
|
{/* <!-- Query Results & Errors Content Container - Start--> */}
|
||||||
|
<div className="queryResultErrorContentContainer">
|
||||||
|
{!queryResults && !error && !isExecuting && (
|
||||||
|
<div className="queryEditorWatermark">
|
||||||
|
<p>
|
||||||
|
<img src={RunQuery} alt="Execute Query Watermark" />
|
||||||
|
</p>
|
||||||
|
<p className="queryEditorWatermarkText">Execute a query to see the results</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(queryResults || !!error) && (
|
||||||
|
<div className="queryResultsErrorsContent">
|
||||||
|
{!error && (
|
||||||
|
<Pivot aria-label="Successful execution" style={{ height: "100%" }}>
|
||||||
|
<PivotItem
|
||||||
|
headerText="Results"
|
||||||
|
headerButtonProps={{
|
||||||
|
"data-order": 1,
|
||||||
|
"data-title": "Results",
|
||||||
|
}}
|
||||||
|
style={{ height: "100%" }}
|
||||||
|
>
|
||||||
|
<div className="result-metadata">
|
||||||
|
<span>
|
||||||
|
<span>
|
||||||
|
{queryResults.itemCount > 0
|
||||||
|
? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}`
|
||||||
|
: `0 - 0`}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{queryResults.hasMoreResults && (
|
||||||
|
<>
|
||||||
|
<span className="queryResultDivider">|</span>
|
||||||
|
<span className="queryResultNextEnable">
|
||||||
|
<a onClick={() => onFetchNextPageClick()}>
|
||||||
|
<span>Load more</span>
|
||||||
|
<img className="queryResultnextImg" src={QueryEditorNext} alt="Fetch next page" />
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{queryResults && queryResultsString?.length > 0 && !error && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
paddingBottom: "100px",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditorReact
|
||||||
|
language={"json"}
|
||||||
|
content={queryResultsString}
|
||||||
|
isReadOnly={true}
|
||||||
|
ariaLabel={"Query results"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PivotItem>
|
||||||
|
<PivotItem
|
||||||
|
headerText="Query Stats"
|
||||||
|
headerButtonProps={{
|
||||||
|
"data-order": 2,
|
||||||
|
"data-title": "Query Stats",
|
||||||
|
}}
|
||||||
|
style={{ height: "100%", overflowY: "scroll" }}
|
||||||
|
>
|
||||||
|
{queryResults && !error && (
|
||||||
|
<div className="queryMetricsSummaryContainer">
|
||||||
|
<div className="queryMetricsSummary">
|
||||||
|
<h5>Query Statistics</h5>
|
||||||
|
<DetailsList
|
||||||
|
items={generateQueryStatsItems()}
|
||||||
|
columns={columns}
|
||||||
|
selectionMode={SelectionMode.none}
|
||||||
|
layoutMode={DetailsListLayoutMode.justified}
|
||||||
|
compact={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{userContext.apiType === "SQL" && (
|
||||||
|
<div className="downloadMetricsLinkContainer">
|
||||||
|
<a
|
||||||
|
id="downloadMetricsLink"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => onDownloadQueryMetricsCsvClick()}
|
||||||
|
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) =>
|
||||||
|
onDownloadQueryMetricsCsvKeyPress(event)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="downloadCsvImg"
|
||||||
|
src={DownloadQueryMetrics}
|
||||||
|
alt="download query metrics csv"
|
||||||
|
/>
|
||||||
|
<span>Per-partition query metrics (CSV)</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PivotItem>
|
||||||
|
</Pivot>
|
||||||
|
)}
|
||||||
|
{/* <!-- Query Errors Content - Start--> */}
|
||||||
|
{!!error && (
|
||||||
|
<div className="tab-pane active">
|
||||||
|
<div className="errorContent">
|
||||||
|
<span className="errorMessage">{error}</span>
|
||||||
|
<span className="errorDetailsLink">
|
||||||
|
<a
|
||||||
|
onClick={() => onErrorDetailsClick()}
|
||||||
|
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) => onErrorDetailsKeyPress(event)}
|
||||||
|
id="error-display"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="Error details link"
|
||||||
|
>
|
||||||
|
More details
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* <!-- Query Errors Content - End--> */}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,30 +1,22 @@
|
||||||
import { DetailsList, DetailsListLayoutMode, IColumn, Pivot, PivotItem, SelectionMode, Text } from "@fluentui/react";
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
|
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
import SplitterLayout from "react-splitter-layout";
|
import SplitterLayout from "react-splitter-layout";
|
||||||
import "react-splitter-layout/lib/index.css";
|
import "react-splitter-layout/lib/index.css";
|
||||||
import DownloadQueryMetrics from "../../../../images/DownloadQuery.svg";
|
|
||||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||||
import InfoColor from "../../../../images/info_color.svg";
|
|
||||||
import QueryEditorNext from "../../../../images/Query-Editor-Next.svg";
|
|
||||||
import RunQuery from "../../../../images/RunQuery.png";
|
|
||||||
import SaveQueryIcon from "../../../../images/save-cosmos.svg";
|
import SaveQueryIcon from "../../../../images/save-cosmos.svg";
|
||||||
import * as Constants from "../../../Common/Constants";
|
|
||||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||||
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
|
|
||||||
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
|
||||||
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
|
||||||
import * as HeadersUtility from "../../../Common/HeadersUtility";
|
import * as HeadersUtility from "../../../Common/HeadersUtility";
|
||||||
import { MinimalQueryIterator } from "../../../Common/IteratorUtilities";
|
import { MinimalQueryIterator } from "../../../Common/IteratorUtilities";
|
||||||
import { queryIterator } from "../../../Common/MongoProxyClient";
|
import { queryIterator } from "../../../Common/MongoProxyClient";
|
||||||
import MongoUtility from "../../../Common/MongoUtility";
|
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
|
||||||
import { Splitter } from "../../../Common/Splitter";
|
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
||||||
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
|
||||||
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 { useNotificationConsole } from "../../../hooks/useNotificationConsole";
|
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import * as QueryUtils from "../../../Utils/QueryUtils";
|
import * as QueryUtils from "../../../Utils/QueryUtils";
|
||||||
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
|
@ -43,7 +35,6 @@ export interface IDocument {
|
||||||
metric: string;
|
metric: string;
|
||||||
value: string;
|
value: string;
|
||||||
toolTip: string;
|
toolTip: string;
|
||||||
isQueryMetricsEnabled: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITabAccessor {
|
export interface ITabAccessor {
|
||||||
|
@ -74,30 +65,13 @@ export interface IQueryTabComponentProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IQueryTabStates {
|
interface IQueryTabStates {
|
||||||
queryMetrics: Map<string, DataModels.QueryMetrics>;
|
|
||||||
aggregatedQueryMetrics: DataModels.QueryMetrics;
|
|
||||||
activityId: string;
|
|
||||||
roundTrips: number;
|
|
||||||
toggleState: ToggleState;
|
toggleState: ToggleState;
|
||||||
isQueryMetricsEnabled: boolean;
|
|
||||||
showingDocumentsDisplayText: string;
|
|
||||||
requestChargeDisplayText: string;
|
|
||||||
initialEditorContent: string;
|
|
||||||
sqlQueryEditorContent: string;
|
sqlQueryEditorContent: string;
|
||||||
selectedContent: string;
|
selectedContent: string;
|
||||||
_executeQueryButtonTitle: string;
|
queryResults: ViewModels.QueryResults;
|
||||||
sqlStatementToExecute: string;
|
|
||||||
queryResults: string;
|
|
||||||
statusMessge: string;
|
|
||||||
statusIcon: string;
|
|
||||||
allResultsMetadata: ViewModels.QueryResultsMetadata[];
|
|
||||||
error: string;
|
error: string;
|
||||||
isTemplateReady: boolean;
|
|
||||||
_isSaveQueriesEnabled: boolean;
|
|
||||||
isExecutionError: boolean;
|
isExecutionError: boolean;
|
||||||
isExecuting: boolean;
|
isExecuting: boolean;
|
||||||
columns: IColumn[];
|
|
||||||
items: IDocument[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
|
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
|
||||||
|
@ -105,90 +79,39 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
public executeQueryButton: Button;
|
public executeQueryButton: Button;
|
||||||
public saveQueryButton: Button;
|
public saveQueryButton: Button;
|
||||||
public splitterId: string;
|
public splitterId: string;
|
||||||
public splitter: Splitter;
|
|
||||||
public isPreferredApiMongoDB: boolean;
|
public isPreferredApiMongoDB: boolean;
|
||||||
public resultsDisplay: string;
|
|
||||||
protected monacoSettings: ViewModels.MonacoEditorSettings;
|
|
||||||
protected _iterator: MinimalQueryIterator;
|
|
||||||
private _resourceTokenPartitionKey: string;
|
|
||||||
_partitionKey: DataModels.PartitionKey;
|
|
||||||
public maybeSubQuery: boolean;
|
|
||||||
public isCloseClicked: boolean;
|
public isCloseClicked: boolean;
|
||||||
public allItems: IDocument[];
|
private _iterator: MinimalQueryIterator;
|
||||||
public defaultQueryText: string;
|
|
||||||
|
|
||||||
constructor(props: IQueryTabComponentProps) {
|
constructor(props: IQueryTabComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
const columns: IColumn[] = [
|
|
||||||
{
|
|
||||||
key: "column2",
|
|
||||||
name: "METRIC",
|
|
||||||
minWidth: 200,
|
|
||||||
data: String,
|
|
||||||
fieldName: "metric",
|
|
||||||
onRender: this.onRenderColumnItem,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "column3",
|
|
||||||
name: "VALUE",
|
|
||||||
minWidth: 200,
|
|
||||||
data: String,
|
|
||||||
fieldName: "value",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (this.props.isPreferredApiMongoDB) {
|
|
||||||
this.defaultQueryText = props.queryText;
|
|
||||||
} else {
|
|
||||||
this.defaultQueryText = props.queryText !== void 0 ? props.queryText : "SELECT * FROM c";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
queryMetrics: new Map(),
|
|
||||||
aggregatedQueryMetrics: undefined,
|
|
||||||
activityId: "",
|
|
||||||
roundTrips: undefined,
|
|
||||||
toggleState: ToggleState.Result,
|
toggleState: ToggleState.Result,
|
||||||
isQueryMetricsEnabled: userContext.apiType === "SQL" || false,
|
sqlQueryEditorContent: props.queryText || "SELECT * FROM c",
|
||||||
showingDocumentsDisplayText: this.resultsDisplay,
|
|
||||||
requestChargeDisplayText: "",
|
|
||||||
initialEditorContent: this.defaultQueryText,
|
|
||||||
sqlQueryEditorContent: this.defaultQueryText,
|
|
||||||
selectedContent: "",
|
selectedContent: "",
|
||||||
_executeQueryButtonTitle: "Execute Query",
|
queryResults: undefined,
|
||||||
sqlStatementToExecute: this.defaultQueryText,
|
|
||||||
queryResults: "",
|
|
||||||
statusMessge: "",
|
|
||||||
statusIcon: "",
|
|
||||||
allResultsMetadata: [],
|
|
||||||
error: "",
|
error: "",
|
||||||
isTemplateReady: false,
|
|
||||||
_isSaveQueriesEnabled: userContext.apiType === "SQL" || userContext.apiType === "Gremlin",
|
|
||||||
isExecutionError: this.props.isExecutionError,
|
isExecutionError: this.props.isExecutionError,
|
||||||
isExecuting: false,
|
isExecuting: false,
|
||||||
columns: columns,
|
|
||||||
items: [],
|
|
||||||
};
|
};
|
||||||
this.isCloseClicked = false;
|
this.isCloseClicked = false;
|
||||||
this.splitterId = this.props.tabId + "_splitter";
|
this.splitterId = this.props.tabId + "_splitter";
|
||||||
this.queryEditorId = `queryeditor${this.props.tabId}`;
|
this.queryEditorId = `queryeditor${this.props.tabId}`;
|
||||||
this._partitionKey = props.partitionKey;
|
|
||||||
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
|
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
|
||||||
this.monacoSettings = new ViewModels.MonacoEditorSettings(this.props.monacoEditorSetting, false);
|
|
||||||
|
|
||||||
this.executeQueryButton = {
|
this.executeQueryButton = {
|
||||||
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
|
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
|
||||||
visible: true,
|
visible: true,
|
||||||
};
|
};
|
||||||
const sql = this.state.sqlQueryEditorContent;
|
|
||||||
this.maybeSubQuery = sql && /.*\(.*SELECT.*\)/i.test(sql);
|
|
||||||
|
|
||||||
|
const isSaveQueryBtnEnabled = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||||
this.saveQueryButton = {
|
this.saveQueryButton = {
|
||||||
enabled: this.state._isSaveQueriesEnabled,
|
enabled: isSaveQueryBtnEnabled,
|
||||||
visible: this.state._isSaveQueriesEnabled,
|
visible: isSaveQueryBtnEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._buildCommandBarOptions();
|
this.props.tabsBaseInstance.updateNavbarWithTabsButtons();
|
||||||
props.onTabAccessor({
|
props.onTabAccessor({
|
||||||
onTabClickEvent: this.onTabClick.bind(this),
|
onTabClickEvent: this.onTabClick.bind(this),
|
||||||
onSaveClickEvent: this.getCurrentEditorQuery.bind(this),
|
onSaveClickEvent: this.getCurrentEditorQuery.bind(this),
|
||||||
|
@ -196,165 +119,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onRenderColumnItem(item: IDocument): JSX.Element {
|
|
||||||
if (item.toolTip !== "") {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<InfoTooltip>{`${item.toolTip}`}</InfoTooltip>
|
|
||||||
<Text style={{ paddingLeft: 10, margin: 0 }}>{`${item.metric}`}</Text>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public generateDetailsList(): IDocument[] {
|
|
||||||
const items: IDocument[] = [];
|
|
||||||
const allItems: IDocument[] = [
|
|
||||||
{
|
|
||||||
metric: "Request Charge",
|
|
||||||
value: this.state.requestChargeDisplayText,
|
|
||||||
toolTip: "Request Charge",
|
|
||||||
isQueryMetricsEnabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metric: "Showing Results",
|
|
||||||
value: this.state.showingDocumentsDisplayText,
|
|
||||||
toolTip: "Showing Results",
|
|
||||||
isQueryMetricsEnabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metric: "Retrieved document count",
|
|
||||||
value:
|
|
||||||
this.state.aggregatedQueryMetrics.retrievedDocumentCount !== undefined
|
|
||||||
? this.state.aggregatedQueryMetrics.retrievedDocumentCount.toString()
|
|
||||||
: "",
|
|
||||||
toolTip: "Total number of retrieved documents",
|
|
||||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metric: "Retrieved document size",
|
|
||||||
value:
|
|
||||||
this.state.aggregatedQueryMetrics.retrievedDocumentSize !== undefined
|
|
||||||
? this.state.aggregatedQueryMetrics.retrievedDocumentSize.toString() + " bytes"
|
|
||||||
: "",
|
|
||||||
toolTip: "Total size of retrieved documents in bytes",
|
|
||||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metric: "Output document count",
|
|
||||||
value:
|
|
||||||
this.state.aggregatedQueryMetrics.outputDocumentCount !== undefined
|
|
||||||
? this.state.aggregatedQueryMetrics.outputDocumentCount.toString()
|
|
||||||
: "",
|
|
||||||
toolTip: "Number of output documents",
|
|
||||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metric: "Output document size",
|
|
||||||
value:
|
|
||||||
this.state.aggregatedQueryMetrics.outputDocumentSize !== undefined
|
|
||||||
? this.state.aggregatedQueryMetrics.outputDocumentSize.toString() + " bytes"
|
|
||||||
: "",
|
|
||||||
toolTip: "Total size of output documents in bytes",
|
|
||||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metric: "Index hit document count",
|
|
||||||
value:
|
|
||||||
this.state.aggregatedQueryMetrics.indexHitDocumentCount !== undefined
|
|
||||||
? this.state.aggregatedQueryMetrics.indexHitDocumentCount.toString()
|
|
||||||
: "",
|
|
||||||
toolTip: "Total number of documents matched by the filter",
|
|
||||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metric: "Index lookup time",
|
|
||||||
value:
|
|
||||||
this.state.aggregatedQueryMetrics.indexLookupTime !== undefined
|
|
||||||
? this.state.aggregatedQueryMetrics.indexLookupTime.toString() + " ms"
|
|
||||||
: "",
|
|
||||||
toolTip: "Time spent in physical index layer",
|
|
||||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metric: "Document load time",
|
|
||||||
value:
|
|
||||||
this.state.aggregatedQueryMetrics.documentLoadTime !== undefined
|
|
||||||
? this.state.aggregatedQueryMetrics.documentLoadTime.toString() + " ms"
|
|
||||||
: "",
|
|
||||||
toolTip: "Time spent in loading documents",
|
|
||||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metric: "Query engine execution time",
|
|
||||||
value:
|
|
||||||
this.state.aggregatedQueryMetrics.runtimeExecutionTimes.queryEngineExecutionTime !== undefined
|
|
||||||
? this.state.aggregatedQueryMetrics.runtimeExecutionTimes.queryEngineExecutionTime.toString() + " ms"
|
|
||||||
: "",
|
|
||||||
toolTip:
|
|
||||||
"Time spent by the query engine to execute the query expression (excludes other execution times like load documents or write results)",
|
|
||||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metric: "System function execution time",
|
|
||||||
value:
|
|
||||||
this.state.aggregatedQueryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime !== undefined
|
|
||||||
? this.state.aggregatedQueryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime.toString() + " ms"
|
|
||||||
: "",
|
|
||||||
toolTip: "Total time spent executing system (built-in) functions",
|
|
||||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metric: "User defined function execution time",
|
|
||||||
value:
|
|
||||||
this.state.aggregatedQueryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime !== undefined
|
|
||||||
? this.state.aggregatedQueryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime.toString() +
|
|
||||||
" ms"
|
|
||||||
: "",
|
|
||||||
toolTip: "Total time spent executing user-defined functions",
|
|
||||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metric: "Document write time",
|
|
||||||
value:
|
|
||||||
this.state.aggregatedQueryMetrics.documentWriteTime !== undefined
|
|
||||||
? this.state.aggregatedQueryMetrics.documentWriteTime.toString() + " ms"
|
|
||||||
: "",
|
|
||||||
toolTip: "Time spent to write query result set to response buffer",
|
|
||||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metric: "Round Trips",
|
|
||||||
value: this.state.roundTrips ? this.state.roundTrips.toString() : "",
|
|
||||||
toolTip: "",
|
|
||||||
isQueryMetricsEnabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metric: "Activity id",
|
|
||||||
value: this.state.activityId ? this.state.activityId : "",
|
|
||||||
toolTip: "",
|
|
||||||
isQueryMetricsEnabled: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
allItems.forEach((item) => {
|
|
||||||
if (item.metric === "Round Trips" || item.metric === "Activity id") {
|
|
||||||
if (item.metric === "Round Trips" && this.state.roundTrips !== undefined) {
|
|
||||||
items.push(item);
|
|
||||||
} else if (item.metric === "Activity id" && this.state.activityId !== undefined) {
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (item.isQueryMetricsEnabled) {
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
public onCloseClick(isClicked: boolean): void {
|
public onCloseClick(isClicked: boolean): void {
|
||||||
this.isCloseClicked = isClicked;
|
this.isCloseClicked = isClicked;
|
||||||
}
|
}
|
||||||
|
@ -372,14 +136,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
}
|
}
|
||||||
|
|
||||||
public onExecuteQueryClick = async (): Promise<void> => {
|
public onExecuteQueryClick = async (): Promise<void> => {
|
||||||
const sqlStatement = this.state.selectedContent || this.state.sqlQueryEditorContent;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
sqlStatementToExecute: sqlStatement,
|
|
||||||
allResultsMetadata: [],
|
|
||||||
queryResults: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
this._iterator = undefined;
|
this._iterator = undefined;
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await this._executeQueryDocumentsPage(0);
|
await this._executeQueryDocumentsPage(0);
|
||||||
|
@ -396,31 +152,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this.props.collection.container} />);
|
.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this.props.collection.container} />);
|
||||||
};
|
};
|
||||||
|
|
||||||
public async onFetchNextPageClick(): Promise<void> {
|
|
||||||
const allResultsMetadata = (this.state.allResultsMetadata && this.state.allResultsMetadata) || [];
|
|
||||||
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
|
|
||||||
const firstResultIndex: number = (metadata && Number(metadata.firstItemIndex)) || 1;
|
|
||||||
const itemCount: number = (metadata && Number(metadata.itemCount)) || 0;
|
|
||||||
|
|
||||||
await this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
//eslint-disable-next-line
|
|
||||||
public onErrorDetailsClick = (): boolean => {
|
|
||||||
useNotificationConsole.getState().expandConsole();
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
public onErrorDetailsKeyPress = (event: React.KeyboardEvent<HTMLAnchorElement>): boolean => {
|
|
||||||
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
|
||||||
this.onErrorDetailsClick();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
public toggleResult(): void {
|
public toggleResult(): void {
|
||||||
this.setState({
|
this.setState({
|
||||||
toggleState: ToggleState.Result,
|
toggleState: ToggleState.Result,
|
||||||
|
@ -452,54 +183,30 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
focusElement && focusElement.focus();
|
focusElement && focusElement.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public isResultToggled(): boolean {
|
private async _executeQueryDocumentsPage(firstItemIndex: number): Promise<void> {
|
||||||
return this.state.toggleState === ToggleState.Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public isMetricsToggled(): boolean {
|
|
||||||
return this.state.toggleState === ToggleState.QueryMetrics;
|
|
||||||
}
|
|
||||||
|
|
||||||
public onDownloadQueryMetricsCsvClick = (): boolean => {
|
|
||||||
this._downloadQueryMetricsCsvData();
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
public onDownloadQueryMetricsCsvKeyPress = (event: React.KeyboardEvent<HTMLAnchorElement>): boolean => {
|
|
||||||
if (event.key === NormalizedEventKey.Space || NormalizedEventKey.Enter) {
|
|
||||||
this._downloadQueryMetricsCsvData();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
//eslint-disable-next-line
|
|
||||||
private async _executeQueryDocumentsPage(firstItemIndex: number): Promise<any> {
|
|
||||||
this.setState({
|
|
||||||
error: "",
|
|
||||||
roundTrips: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this._iterator === undefined) {
|
if (this._iterator === undefined) {
|
||||||
if (this.isPreferredApiMongoDB) {
|
this._iterator = this.props.isPreferredApiMongoDB
|
||||||
this._initIteratorMongo();
|
? queryIterator(
|
||||||
} else {
|
this.props.collection.databaseId,
|
||||||
this._initIterator();
|
this.props.viewModelcollection,
|
||||||
}
|
this.state.selectedContent || this.state.sqlQueryEditorContent
|
||||||
|
)
|
||||||
|
: queryDocuments(
|
||||||
|
this.props.collection.databaseId,
|
||||||
|
this.props.collection.id(),
|
||||||
|
this.state.selectedContent || this.state.sqlQueryEditorContent,
|
||||||
|
{ enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey() } as FeedOptions
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this._queryDocumentsPage(firstItemIndex);
|
await this._queryDocumentsPage(firstItemIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _queryDocumentsPage(firstItemIndex: number): Promise<void> {
|
private async _queryDocumentsPage(firstItemIndex: number): Promise<void> {
|
||||||
let results: string;
|
|
||||||
|
|
||||||
this.props.tabsBaseInstance.isExecutionError(false);
|
this.props.tabsBaseInstance.isExecutionError(false);
|
||||||
this.setState({
|
this.setState({
|
||||||
isExecutionError: false,
|
isExecutionError: false,
|
||||||
});
|
});
|
||||||
this._resetAggregateQueryMetrics();
|
|
||||||
|
|
||||||
const queryDocuments = async (firstItemIndex: number) =>
|
const queryDocuments = async (firstItemIndex: number) =>
|
||||||
await queryDocumentsPage(this.props.collection && this.props.collection.id(), this._iterator, firstItemIndex);
|
await queryDocumentsPage(this.props.collection && this.props.collection.id(), this._iterator, firstItemIndex);
|
||||||
|
@ -513,44 +220,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
firstItemIndex,
|
firstItemIndex,
|
||||||
queryDocuments
|
queryDocuments
|
||||||
);
|
);
|
||||||
const allResultsMetadata = (this.state.allResultsMetadata && this.state.allResultsMetadata) || [];
|
this.setState({ queryResults });
|
||||||
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
|
|
||||||
const resultsMetadata: ViewModels.QueryResultsMetadata = {
|
|
||||||
hasMoreResults: queryResults.hasMoreResults,
|
|
||||||
itemCount: queryResults.itemCount,
|
|
||||||
firstItemIndex: queryResults.firstItemIndex,
|
|
||||||
lastItemIndex: queryResults.lastItemIndex,
|
|
||||||
};
|
|
||||||
this.state.allResultsMetadata.push(resultsMetadata);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
activityId: queryResults.activityId,
|
|
||||||
roundTrips: queryResults.roundTrips,
|
|
||||||
});
|
|
||||||
|
|
||||||
const documents = queryResults.documents;
|
|
||||||
if (this.isPreferredApiMongoDB) {
|
|
||||||
results = MongoUtility.tojson(documents, undefined, false);
|
|
||||||
} else {
|
|
||||||
results = this.props.tabsBaseInstance.renderObjectForEditor(documents, undefined, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resultsDisplay: string =
|
|
||||||
queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
showingDocumentsDisplayText: resultsDisplay,
|
|
||||||
requestChargeDisplayText: `${queryResults.requestCharge} RUs`,
|
|
||||||
queryResults: results,
|
|
||||||
});
|
|
||||||
|
|
||||||
this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]);
|
|
||||||
|
|
||||||
if (queryResults.itemCount === 0 && metadata !== undefined && metadata.itemCount >= 0) {
|
|
||||||
// we let users query for the next page because the SDK sometimes specifies there are more elements
|
|
||||||
// even though there aren't any so we should not update the prior query results.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.props.tabsBaseInstance.isExecutionError(true);
|
this.props.tabsBaseInstance.isExecutionError(true);
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -571,228 +241,10 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void {
|
|
||||||
if (!metricsMap) {
|
|
||||||
this.allItems = this.generateDetailsList();
|
|
||||||
this.setState({
|
|
||||||
items: this.allItems,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(metricsMap).forEach((key: string) => {
|
|
||||||
this.state.queryMetrics.set(key, metricsMap[key]);
|
|
||||||
});
|
|
||||||
|
|
||||||
this._aggregateQueryMetrics(this.state.queryMetrics);
|
|
||||||
this.allItems = this.generateDetailsList();
|
|
||||||
this.setState({
|
|
||||||
items: this.allItems,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _aggregateQueryMetrics(metricsMap: Map<string, DataModels.QueryMetrics>): DataModels.QueryMetrics {
|
|
||||||
if (!metricsMap) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const aggregatedMetrics: DataModels.QueryMetrics = this.state.aggregatedQueryMetrics;
|
|
||||||
metricsMap.forEach((queryMetrics) => {
|
|
||||||
if (queryMetrics) {
|
|
||||||
aggregatedMetrics.documentLoadTime =
|
|
||||||
this._normalize(queryMetrics.documentLoadTime.totalMilliseconds()) +
|
|
||||||
this._normalize(aggregatedMetrics.documentLoadTime);
|
|
||||||
aggregatedMetrics.documentWriteTime =
|
|
||||||
this._normalize(queryMetrics.documentWriteTime.totalMilliseconds()) +
|
|
||||||
this._normalize(aggregatedMetrics.documentWriteTime);
|
|
||||||
aggregatedMetrics.indexHitDocumentCount =
|
|
||||||
this._normalize(queryMetrics.indexHitDocumentCount) +
|
|
||||||
this._normalize(aggregatedMetrics.indexHitDocumentCount);
|
|
||||||
aggregatedMetrics.outputDocumentCount =
|
|
||||||
this._normalize(queryMetrics.outputDocumentCount) + this._normalize(aggregatedMetrics.outputDocumentCount);
|
|
||||||
aggregatedMetrics.outputDocumentSize =
|
|
||||||
this._normalize(queryMetrics.outputDocumentSize) + this._normalize(aggregatedMetrics.outputDocumentSize);
|
|
||||||
aggregatedMetrics.indexLookupTime =
|
|
||||||
this._normalize(queryMetrics.indexLookupTime.totalMilliseconds()) +
|
|
||||||
this._normalize(aggregatedMetrics.indexLookupTime);
|
|
||||||
aggregatedMetrics.retrievedDocumentCount =
|
|
||||||
this._normalize(queryMetrics.retrievedDocumentCount) +
|
|
||||||
this._normalize(aggregatedMetrics.retrievedDocumentCount);
|
|
||||||
aggregatedMetrics.retrievedDocumentSize =
|
|
||||||
this._normalize(queryMetrics.retrievedDocumentSize) +
|
|
||||||
this._normalize(aggregatedMetrics.retrievedDocumentSize);
|
|
||||||
aggregatedMetrics.vmExecutionTime =
|
|
||||||
this._normalize(queryMetrics.vmExecutionTime.totalMilliseconds()) +
|
|
||||||
this._normalize(aggregatedMetrics.vmExecutionTime);
|
|
||||||
aggregatedMetrics.totalQueryExecutionTime =
|
|
||||||
this._normalize(queryMetrics.totalQueryExecutionTime.totalMilliseconds()) +
|
|
||||||
this._normalize(aggregatedMetrics.totalQueryExecutionTime);
|
|
||||||
|
|
||||||
aggregatedMetrics.runtimeExecutionTimes.queryEngineExecutionTime =
|
|
||||||
this._normalize(queryMetrics.runtimeExecutionTimes.queryEngineExecutionTime.totalMilliseconds()) +
|
|
||||||
this._normalize(aggregatedMetrics.runtimeExecutionTimes.queryEngineExecutionTime);
|
|
||||||
aggregatedMetrics.runtimeExecutionTimes.systemFunctionExecutionTime =
|
|
||||||
this._normalize(queryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime.totalMilliseconds()) +
|
|
||||||
this._normalize(aggregatedMetrics.runtimeExecutionTimes.systemFunctionExecutionTime);
|
|
||||||
aggregatedMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime =
|
|
||||||
this._normalize(queryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime.totalMilliseconds()) +
|
|
||||||
this._normalize(aggregatedMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return aggregatedMetrics;
|
|
||||||
}
|
|
||||||
|
|
||||||
public _downloadQueryMetricsCsvData(): void {
|
|
||||||
const csvData: string = this._generateQueryMetricsCsvData();
|
|
||||||
if (!csvData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (navigator.msSaveBlob) {
|
|
||||||
// for IE and Edge
|
|
||||||
navigator.msSaveBlob(
|
|
||||||
new Blob([csvData], { type: "data:text/csv;charset=utf-8" }),
|
|
||||||
"PerPartitionQueryMetrics.csv"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const downloadLink: HTMLAnchorElement = document.createElement("a");
|
|
||||||
downloadLink.href = "data:text/csv;charset=utf-8," + encodeURI(csvData);
|
|
||||||
downloadLink.target = "_self";
|
|
||||||
downloadLink.download = "QueryMetricsPerPartition.csv";
|
|
||||||
|
|
||||||
// for some reason, FF displays the download prompt only when
|
|
||||||
// the link is added to the dom so we add and remove it
|
|
||||||
document.body.appendChild(downloadLink);
|
|
||||||
downloadLink.click();
|
|
||||||
downloadLink.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _initIterator(): void {
|
|
||||||
const options = QueryTabComponent.getIteratorOptions();
|
|
||||||
if (this._resourceTokenPartitionKey) {
|
|
||||||
options.partitionKey = this._resourceTokenPartitionKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._iterator = queryDocuments(
|
|
||||||
this.props.collection.databaseId,
|
|
||||||
this.props.collection.id(),
|
|
||||||
this.state.sqlStatementToExecute,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _initIteratorMongo(): Promise<MinimalQueryIterator> {
|
|
||||||
//eslint-disable-next-line
|
|
||||||
const options: any = {};
|
|
||||||
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
|
|
||||||
this._iterator = queryIterator(
|
|
||||||
this.props.collection.databaseId,
|
|
||||||
this.props.viewModelcollection,
|
|
||||||
this.state.sqlStatementToExecute
|
|
||||||
);
|
|
||||||
const mongoPromise: Promise<MinimalQueryIterator> = new Promise((resolve) => {
|
|
||||||
resolve(this._iterator);
|
|
||||||
});
|
|
||||||
return mongoPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
//eslint-disable-next-line
|
|
||||||
public static getIteratorOptions(collection?: ViewModels.Collection): any {
|
|
||||||
//eslint-disable-next-line
|
|
||||||
const options: any = {};
|
|
||||||
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _normalize(value: number): number {
|
|
||||||
if (!value) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _resetAggregateQueryMetrics(): void {
|
|
||||||
this.setState({
|
|
||||||
aggregatedQueryMetrics: {
|
|
||||||
clientSideMetrics: {},
|
|
||||||
documentLoadTime: undefined,
|
|
||||||
documentWriteTime: undefined,
|
|
||||||
indexHitDocumentCount: undefined,
|
|
||||||
outputDocumentCount: undefined,
|
|
||||||
outputDocumentSize: undefined,
|
|
||||||
indexLookupTime: undefined,
|
|
||||||
retrievedDocumentCount: undefined,
|
|
||||||
retrievedDocumentSize: undefined,
|
|
||||||
vmExecutionTime: undefined,
|
|
||||||
queryPreparationTimes: undefined,
|
|
||||||
runtimeExecutionTimes: {
|
|
||||||
queryEngineExecutionTime: undefined,
|
|
||||||
systemFunctionExecutionTime: undefined,
|
|
||||||
userDefinedFunctionExecutionTime: undefined,
|
|
||||||
},
|
|
||||||
totalQueryExecutionTime: undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _generateQueryMetricsCsvData(): string {
|
|
||||||
if (!this.state.queryMetrics) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryMetrics = this.state.queryMetrics;
|
|
||||||
let csvData = "";
|
|
||||||
const columnHeaders: string =
|
|
||||||
[
|
|
||||||
"Partition key range id",
|
|
||||||
"Retrieved document count",
|
|
||||||
"Retrieved document size (in bytes)",
|
|
||||||
"Output document count",
|
|
||||||
"Output document size (in bytes)",
|
|
||||||
"Index hit document count",
|
|
||||||
"Index lookup time (ms)",
|
|
||||||
"Document load time (ms)",
|
|
||||||
"Query engine execution time (ms)",
|
|
||||||
"System function execution time (ms)",
|
|
||||||
"User defined function execution time (ms)",
|
|
||||||
"Document write time (ms)",
|
|
||||||
].join(",") + "\n";
|
|
||||||
csvData = csvData + columnHeaders;
|
|
||||||
queryMetrics.forEach((queryMetric, partitionKeyRangeId) => {
|
|
||||||
const partitionKeyRangeData: string =
|
|
||||||
[
|
|
||||||
partitionKeyRangeId,
|
|
||||||
queryMetric.retrievedDocumentCount,
|
|
||||||
queryMetric.retrievedDocumentSize,
|
|
||||||
queryMetric.outputDocumentCount,
|
|
||||||
queryMetric.outputDocumentSize,
|
|
||||||
queryMetric.indexHitDocumentCount,
|
|
||||||
queryMetric.indexLookupTime && queryMetric.indexLookupTime.totalMilliseconds(),
|
|
||||||
queryMetric.documentLoadTime && queryMetric.documentLoadTime.totalMilliseconds(),
|
|
||||||
queryMetric.runtimeExecutionTimes &&
|
|
||||||
queryMetric.runtimeExecutionTimes.queryEngineExecutionTime &&
|
|
||||||
queryMetric.runtimeExecutionTimes.queryEngineExecutionTime.totalMilliseconds(),
|
|
||||||
queryMetric.runtimeExecutionTimes &&
|
|
||||||
queryMetric.runtimeExecutionTimes.systemFunctionExecutionTime &&
|
|
||||||
queryMetric.runtimeExecutionTimes.systemFunctionExecutionTime.totalMilliseconds(),
|
|
||||||
queryMetric.runtimeExecutionTimes &&
|
|
||||||
queryMetric.runtimeExecutionTimes.userDefinedFunctionExecutionTime &&
|
|
||||||
queryMetric.runtimeExecutionTimes.userDefinedFunctionExecutionTime.totalMilliseconds(),
|
|
||||||
queryMetric.documentWriteTime && queryMetric.documentWriteTime.totalMilliseconds(),
|
|
||||||
].join(",") + "\n";
|
|
||||||
csvData = csvData + partitionKeyRangeData;
|
|
||||||
});
|
|
||||||
|
|
||||||
return csvData;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||||
const buttons: CommandButtonComponentProps[] = [];
|
const buttons: CommandButtonComponentProps[] = [];
|
||||||
if (this.executeQueryButton.visible) {
|
if (this.executeQueryButton.visible) {
|
||||||
const label = this.state._executeQueryButtonTitle;
|
const label = this.state.selectedContent?.length > 0 ? "Execute Selection" : "Execute Query";
|
||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: ExecuteQueryIcon,
|
iconSrc: ExecuteQueryIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
|
@ -820,10 +272,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _buildCommandBarOptions(): void {
|
|
||||||
this.props.tabsBaseInstance.updateNavbarWithTabsButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
public onChangeContent(newContent: string): void {
|
public onChangeContent(newContent: string): void {
|
||||||
this.setState({
|
this.setState({
|
||||||
sqlQueryEditorContent: newContent,
|
sqlQueryEditorContent: newContent,
|
||||||
|
@ -848,13 +296,11 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
public onSelectedContent(selectedContent: string): void {
|
public onSelectedContent(selectedContent: string): void {
|
||||||
if (selectedContent.trim().length > 0) {
|
if (selectedContent.trim().length > 0) {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedContent: selectedContent,
|
selectedContent,
|
||||||
_executeQueryButtonTitle: "Execute Selection",
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedContent: "",
|
selectedContent: "",
|
||||||
_executeQueryButtonTitle: "Execute Query",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
|
@ -874,7 +320,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
<div className="queryEditor" style={{ height: "100%" }}>
|
<div className="queryEditor" style={{ height: "100%" }}>
|
||||||
<EditorReact
|
<EditorReact
|
||||||
language={"sql"}
|
language={"sql"}
|
||||||
content={this.state.initialEditorContent}
|
content={this.state.sqlQueryEditorContent}
|
||||||
isReadOnly={false}
|
isReadOnly={false}
|
||||||
ariaLabel={"Editing Query"}
|
ariaLabel={"Editing Query"}
|
||||||
lineNumbers={"on"}
|
lineNumbers={"on"}
|
||||||
|
@ -883,174 +329,14 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
<Fragment>
|
<QueryResultSection
|
||||||
{this.isPreferredApiMongoDB && this.state.sqlQueryEditorContent.length === 0 && (
|
isMongoDB={this.props.isPreferredApiMongoDB}
|
||||||
<div className="mongoQueryHelper">
|
queryEditorContent={this.state.sqlQueryEditorContent}
|
||||||
Start by writing a Mongo query, for example: <strong>{"{'id':'foo'}"}</strong> or{" "}
|
error={this.state.error}
|
||||||
<strong>
|
queryResults={this.state.queryResults}
|
||||||
{"{ "}
|
isExecuting={this.state.isExecuting}
|
||||||
{" }"}
|
executeQueryDocumentsPage={(firstItemIndex: number) => this._executeQueryDocumentsPage(firstItemIndex)}
|
||||||
</strong>{" "}
|
|
||||||
to get all the documents.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{this.maybeSubQuery && (
|
|
||||||
<div className="warningErrorContainer" aria-live="assertive">
|
|
||||||
<div className="warningErrorContent">
|
|
||||||
<span>
|
|
||||||
<img className="paneErrorIcon" src={InfoColor} alt="Error" />
|
|
||||||
</span>
|
|
||||||
<span className="warningErrorDetailsLinkContainer">
|
|
||||||
We have detected you may be using a subquery. Non-correlated subqueries are not currently
|
|
||||||
supported.
|
|
||||||
<a href="https://docs.microsoft.com/en-us/azure/cosmos-db/sql-query-subquery">
|
|
||||||
Please see Cosmos sub query documentation for further information
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* <!-- Query Errors Tab - Start--> */}
|
|
||||||
{!!this.state.error && (
|
|
||||||
<div className="active queryErrorsHeaderContainer">
|
|
||||||
<span className="queryErrors" data-toggle="tab">
|
|
||||||
Errors
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* <!-- Query Errors Tab - End --> */}
|
|
||||||
{/* <!-- Query Results & Errors Content Container - Start--> */}
|
|
||||||
<div className="queryResultErrorContentContainer">
|
|
||||||
{this.state.allResultsMetadata.length === 0 &&
|
|
||||||
!this.state.error &&
|
|
||||||
!this.state.queryResults &&
|
|
||||||
!this.props.tabsBaseInstance.isExecuting() && (
|
|
||||||
<div className="queryEditorWatermark">
|
|
||||||
<p>
|
|
||||||
<img src={RunQuery} alt="Execute Query Watermark" />
|
|
||||||
</p>
|
|
||||||
<p className="queryEditorWatermarkText">Execute a query to see the results</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{(this.state.allResultsMetadata.length > 0 || !!this.state.error || this.state.queryResults) && (
|
|
||||||
<div className="queryResultsErrorsContent">
|
|
||||||
{!this.state.error && (
|
|
||||||
<Pivot aria-label="Successful execution" style={{ height: "100%" }}>
|
|
||||||
<PivotItem
|
|
||||||
headerText="Results"
|
|
||||||
headerButtonProps={{
|
|
||||||
"data-order": 1,
|
|
||||||
"data-title": "Results",
|
|
||||||
}}
|
|
||||||
style={{ height: "100%" }}
|
|
||||||
>
|
|
||||||
<div className="result-metadata">
|
|
||||||
<span>
|
|
||||||
<span>{this.state.showingDocumentsDisplayText}</span>
|
|
||||||
</span>
|
|
||||||
{this.state.allResultsMetadata[this.state.allResultsMetadata.length - 1]
|
|
||||||
.hasMoreResults && (
|
|
||||||
<>
|
|
||||||
<span className="queryResultDivider">|</span>
|
|
||||||
<span className="queryResultNextEnable">
|
|
||||||
<a onClick={this.onFetchNextPageClick.bind(this)}>
|
|
||||||
<span>Load more</span>
|
|
||||||
<img className="queryResultnextImg" src={QueryEditorNext} alt="Fetch next page" />
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{this.state.queryResults &&
|
|
||||||
this.state.queryResults.length > 0 &&
|
|
||||||
this.state.allResultsMetadata.length > 0 &&
|
|
||||||
!this.state.error && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
paddingBottom: "100px",
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EditorReact
|
|
||||||
language={"json"}
|
|
||||||
content={this.state.queryResults}
|
|
||||||
isReadOnly={true}
|
|
||||||
ariaLabel={"Query results"}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</PivotItem>
|
|
||||||
<PivotItem
|
|
||||||
headerText="Query Stats"
|
|
||||||
headerButtonProps={{
|
|
||||||
"data-order": 2,
|
|
||||||
"data-title": "Query Stats",
|
|
||||||
}}
|
|
||||||
style={{ height: "100%", overflowY: "scroll" }}
|
|
||||||
>
|
|
||||||
{this.state.allResultsMetadata.length > 0 && !this.state.error && (
|
|
||||||
<div className="queryMetricsSummaryContainer">
|
|
||||||
<div className="queryMetricsSummary">
|
|
||||||
<h5>Query Statistics</h5>
|
|
||||||
<DetailsList
|
|
||||||
items={this.state.items}
|
|
||||||
columns={this.state.columns}
|
|
||||||
selectionMode={SelectionMode.none}
|
|
||||||
layoutMode={DetailsListLayoutMode.justified}
|
|
||||||
compact={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{this.state.isQueryMetricsEnabled && (
|
|
||||||
<div className="downloadMetricsLinkContainer">
|
|
||||||
<a
|
|
||||||
id="downloadMetricsLink"
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => this.onDownloadQueryMetricsCsvClick()}
|
|
||||||
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) =>
|
|
||||||
this.onDownloadQueryMetricsCsvKeyPress(event)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className="downloadCsvImg"
|
|
||||||
src={DownloadQueryMetrics}
|
|
||||||
alt="download query metrics csv"
|
|
||||||
/>
|
|
||||||
<span>Per-partition query metrics (CSV)</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</PivotItem>
|
|
||||||
</Pivot>
|
|
||||||
)}
|
|
||||||
{/* <!-- Query Errors Content - Start--> */}
|
|
||||||
{!!this.state.error && (
|
|
||||||
<div className="tab-pane active">
|
|
||||||
<div className="errorContent">
|
|
||||||
<span className="errorMessage">{this.state.error}</span>
|
|
||||||
<span className="errorDetailsLink">
|
|
||||||
<a
|
|
||||||
onClick={() => this.onErrorDetailsClick()}
|
|
||||||
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) =>
|
|
||||||
this.onErrorDetailsKeyPress(event)
|
|
||||||
}
|
|
||||||
id="error-display"
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label="Error details link"
|
|
||||||
>
|
|
||||||
More details
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* <!-- Query Errors Content - End--> */}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
</SplitterLayout>
|
</SplitterLayout>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { sendMessage } from "Common/MessageHandler";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
|
||||||
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
|
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
|
||||||
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
||||||
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
|
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
|
||||||
|
@ -68,6 +69,13 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
|
||||||
const focusTab = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
const focusTab = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
||||||
const tabId = tab ? tab.tabId : "connect";
|
const tabId = tab ? tab.tabId : "connect";
|
||||||
|
|
||||||
|
const getReactTabTitle = (): ko.Observable<string> => {
|
||||||
|
if (tabKind === ReactTabKind.QueryCopilot) {
|
||||||
|
return ko.observable("Query");
|
||||||
|
}
|
||||||
|
return ko.observable(ReactTabKind[tabKind]);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (active && focusTab.current) {
|
if (active && focusTab.current) {
|
||||||
focusTab.current.focus();
|
focusTab.current.focus();
|
||||||
|
@ -109,7 +117,7 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
|
||||||
<img className="loadingIcon" title="Loading" src={loadingIcon} alt="Loading" />
|
<img className="loadingIcon" title="Loading" src={loadingIcon} alt="Loading" />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="tabNavText">{useObservable(tab?.tabTitle || ko.observable(ReactTabKind[tabKind]))}</span>
|
<span className="tabNavText">{useObservable(tab?.tabTitle || getReactTabTitle())}</span>
|
||||||
{tabKind !== ReactTabKind.Home && (
|
{tabKind !== ReactTabKind.Home && (
|
||||||
<span className="tabIconSection">
|
<span className="tabIconSection">
|
||||||
<CloseButton tab={tab} active={active} hovering={hovering} tabKind={tabKind} />
|
<CloseButton tab={tab} active={active} hovering={hovering} tabKind={tabKind} />
|
||||||
|
@ -211,6 +219,8 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
||||||
return <SplashScreen explorer={explorer} />;
|
return <SplashScreen explorer={explorer} />;
|
||||||
case ReactTabKind.Quickstart:
|
case ReactTabKind.Quickstart:
|
||||||
return <QuickstartTab explorer={explorer} />;
|
return <QuickstartTab explorer={explorer} />;
|
||||||
|
case ReactTabKind.QueryCopilot:
|
||||||
|
return <QueryCopilotTab initialInput={useTabs.getState().queryCopilotTabInitialInput} explorer={explorer} />;
|
||||||
default:
|
default:
|
||||||
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
||||||
}
|
}
|
||||||
|
|
21
src/Main.tsx
21
src/Main.tsx
|
@ -1,13 +1,13 @@
|
||||||
// CSS Dependencies
|
// CSS Dependencies
|
||||||
import { initializeIcons } from "@fluentui/react";
|
import { initializeIcons } from "@fluentui/react";
|
||||||
import "bootstrap/dist/css/bootstrap.css";
|
|
||||||
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
||||||
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
|
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
|
||||||
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
|
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
import "bootstrap/dist/css/bootstrap.css";
|
||||||
import { useCarousel } from "hooks/useCarousel";
|
import { useCarousel } from "hooks/useCarousel";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import "../externals/jquery-ui.min.css";
|
import "../externals/jquery-ui.min.css";
|
||||||
import "../externals/jquery-ui.min.js";
|
import "../externals/jquery-ui.min.js";
|
||||||
import "../externals/jquery-ui.structure.min.css";
|
import "../externals/jquery-ui.structure.min.css";
|
||||||
|
@ -16,19 +16,20 @@ import "../externals/jquery.dataTables.min.css";
|
||||||
import "../externals/jquery.typeahead.min.css";
|
import "../externals/jquery.typeahead.min.css";
|
||||||
import "../externals/jquery.typeahead.min.js";
|
import "../externals/jquery.typeahead.min.js";
|
||||||
// Image Dependencies
|
// Image Dependencies
|
||||||
|
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
||||||
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
||||||
import "../images/favicon.ico";
|
|
||||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||||
|
import "../images/favicon.ico";
|
||||||
|
import "../less/TableStyles/CustomizeColumns.less";
|
||||||
|
import "../less/TableStyles/EntityEditor.less";
|
||||||
|
import "../less/TableStyles/fulldatatables.less";
|
||||||
|
import "../less/TableStyles/queryBuilder.less";
|
||||||
import "../less/documentDB.less";
|
import "../less/documentDB.less";
|
||||||
import "../less/forms.less";
|
import "../less/forms.less";
|
||||||
import "../less/infobox.less";
|
import "../less/infobox.less";
|
||||||
import "../less/menus.less";
|
import "../less/menus.less";
|
||||||
import "../less/messagebox.less";
|
import "../less/messagebox.less";
|
||||||
import "../less/resourceTree.less";
|
import "../less/resourceTree.less";
|
||||||
import "../less/TableStyles/CustomizeColumns.less";
|
|
||||||
import "../less/TableStyles/EntityEditor.less";
|
|
||||||
import "../less/TableStyles/fulldatatables.less";
|
|
||||||
import "../less/TableStyles/queryBuilder.less";
|
|
||||||
import "../less/tree.less";
|
import "../less/tree.less";
|
||||||
import { CollapsedResourceTree } from "./Common/CollapsedResourceTree";
|
import { CollapsedResourceTree } from "./Common/CollapsedResourceTree";
|
||||||
import { ResourceTreeContainer } from "./Common/ResourceTreeContainer";
|
import { ResourceTreeContainer } from "./Common/ResourceTreeContainer";
|
||||||
|
@ -50,16 +51,17 @@ import "./Explorer/Panes/PanelComponent.less";
|
||||||
import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
|
import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
|
||||||
import "./Explorer/SplashScreen/SplashScreen.less";
|
import "./Explorer/SplashScreen/SplashScreen.less";
|
||||||
import { Tabs } from "./Explorer/Tabs/Tabs";
|
import { Tabs } from "./Explorer/Tabs/Tabs";
|
||||||
import { useConfig } from "./hooks/useConfig";
|
|
||||||
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
|
||||||
import "./Libs/jquery";
|
import "./Libs/jquery";
|
||||||
import "./Shared/appInsights";
|
import "./Shared/appInsights";
|
||||||
|
import { useConfig } from "./hooks/useConfig";
|
||||||
|
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
||||||
|
|
||||||
initializeIcons();
|
initializeIcons();
|
||||||
|
|
||||||
const App: React.FunctionComponent = () => {
|
const App: React.FunctionComponent = () => {
|
||||||
const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true);
|
const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true);
|
||||||
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
|
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
|
||||||
|
const isCopilotCarouselOpen = useCarousel((state) => state.showCopilotCarousel);
|
||||||
|
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const explorer = useKnockoutExplorer(config?.platform);
|
const explorer = useKnockoutExplorer(config?.platform);
|
||||||
|
@ -122,6 +124,7 @@ const App: React.FunctionComponent = () => {
|
||||||
{<QuickstartCarousel isOpen={isCarouselOpen} />}
|
{<QuickstartCarousel isOpen={isCarouselOpen} />}
|
||||||
{<SQLQuickstartTutorial />}
|
{<SQLQuickstartTutorial />}
|
||||||
{<MongoQuickstartTutorial />}
|
{<MongoQuickstartTutorial />}
|
||||||
|
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -35,6 +35,7 @@ export type Features = {
|
||||||
readonly enableLegacyMongoShellV2: boolean;
|
readonly enableLegacyMongoShellV2: boolean;
|
||||||
readonly enableLegacyMongoShellV2Debug: boolean;
|
readonly enableLegacyMongoShellV2Debug: boolean;
|
||||||
readonly loadLegacyMongoShellFromBE: boolean;
|
readonly loadLegacyMongoShellFromBE: boolean;
|
||||||
|
readonly enableCopilot: boolean;
|
||||||
|
|
||||||
// can be set via both flight and feature flag
|
// can be set via both flight and feature flag
|
||||||
autoscaleDefault: boolean;
|
autoscaleDefault: boolean;
|
||||||
|
@ -102,6 +103,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||||
enableLegacyMongoShellV2: "true" === get("enablelegacymongoshellv2"),
|
enableLegacyMongoShellV2: "true" === get("enablelegacymongoshellv2"),
|
||||||
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
|
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
|
||||||
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
|
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
|
||||||
|
enableCopilot: "true" === get("enablecopilot"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,17 @@ import create, { UseStore } from "zustand";
|
||||||
interface CarouselState {
|
interface CarouselState {
|
||||||
shouldOpen: boolean;
|
shouldOpen: boolean;
|
||||||
showCoachMark: boolean;
|
showCoachMark: boolean;
|
||||||
|
showCopilotCarousel: boolean;
|
||||||
setShouldOpen: (shouldOpen: boolean) => void;
|
setShouldOpen: (shouldOpen: boolean) => void;
|
||||||
setShowCoachMark: (showCoachMark: boolean) => void;
|
setShowCoachMark: (showCoachMark: boolean) => void;
|
||||||
|
setShowCopilotCarousel: (showCopilotCarousel: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCarousel: UseStore<CarouselState> = create((set) => ({
|
export const useCarousel: UseStore<CarouselState> = create((set) => ({
|
||||||
shouldOpen: false,
|
shouldOpen: false,
|
||||||
showCoachMark: false,
|
showCoachMark: false,
|
||||||
|
showCopilotCarousel: false,
|
||||||
setShouldOpen: (shouldOpen: boolean) => set({ shouldOpen }),
|
setShouldOpen: (shouldOpen: boolean) => set({ shouldOpen }),
|
||||||
setShowCoachMark: (showCoachMark: boolean) => set({ showCoachMark }),
|
setShowCoachMark: (showCoachMark: boolean) => set({ showCoachMark }),
|
||||||
|
setShowCopilotCarousel: (showCopilotCarousel: boolean) => set({ showCopilotCarousel }),
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -10,6 +10,7 @@ interface TabsState {
|
||||||
activeTab: TabsBase | undefined;
|
activeTab: TabsBase | undefined;
|
||||||
activeReactTab: ReactTabKind | undefined;
|
activeReactTab: ReactTabKind | undefined;
|
||||||
networkSettingsWarning: string;
|
networkSettingsWarning: string;
|
||||||
|
queryCopilotTabInitialInput: string;
|
||||||
activateTab: (tab: TabsBase) => void;
|
activateTab: (tab: TabsBase) => void;
|
||||||
activateNewTab: (tab: TabsBase) => void;
|
activateNewTab: (tab: TabsBase) => void;
|
||||||
activateReactTab: (tabkind: ReactTabKind) => void;
|
activateReactTab: (tabkind: ReactTabKind) => void;
|
||||||
|
@ -22,12 +23,14 @@ interface TabsState {
|
||||||
openAndActivateReactTab: (tabKind: ReactTabKind) => void;
|
openAndActivateReactTab: (tabKind: ReactTabKind) => void;
|
||||||
closeReactTab: (tabKind: ReactTabKind) => void;
|
closeReactTab: (tabKind: ReactTabKind) => void;
|
||||||
setNetworkSettingsWarning: (warningMessage: string) => void;
|
setNetworkSettingsWarning: (warningMessage: string) => void;
|
||||||
|
setQueryCopilotTabInitialInput: (input: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ReactTabKind {
|
export enum ReactTabKind {
|
||||||
Connect,
|
Connect,
|
||||||
Home,
|
Home,
|
||||||
Quickstart,
|
Quickstart,
|
||||||
|
QueryCopilot,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||||
|
@ -36,6 +39,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||||
activeTab: undefined,
|
activeTab: undefined,
|
||||||
activeReactTab: ReactTabKind.Home,
|
activeReactTab: ReactTabKind.Home,
|
||||||
networkSettingsWarning: "",
|
networkSettingsWarning: "",
|
||||||
|
queryCopilotTabInitialInput: "",
|
||||||
activateTab: (tab: TabsBase): void => {
|
activateTab: (tab: TabsBase): void => {
|
||||||
if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) {
|
if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) {
|
||||||
set({ activeTab: tab, activeReactTab: undefined });
|
set({ activeTab: tab, activeReactTab: undefined });
|
||||||
|
@ -146,4 +150,5 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||||
set({ openedReactTabs: updatedOpenedReactTabs });
|
set({ openedReactTabs: updatedOpenedReactTabs });
|
||||||
},
|
},
|
||||||
setNetworkSettingsWarning: (warningMessage: string) => set({ networkSettingsWarning: warningMessage }),
|
setNetworkSettingsWarning: (warningMessage: string) => set({ networkSettingsWarning: warningMessage }),
|
||||||
|
setQueryCopilotTabInitialInput: (input: string) => set({ queryCopilotTabInitialInput: input }),
|
||||||
}));
|
}));
|
||||||
|
|
Loading…
Reference in New Issue