Compare commits

..

24 Commits

Author SHA1 Message Date
Justin Kolasa (from Dev Box)
f1484a72c8 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-15 16:56:07 -04:00
Nishtha Ahuja
7aeb682bea Connect with VScode on Quickstart Page (#2151)
Co-authored-by: nishthaAhujaa <nishtha17354@iiittd.ac.in>
2025-05-15 17:55:51 +05:30
SATYA SB
35051bace5 [Screen reader - Azure Cosmos DB-New Container]: Screen reader does not announce the associated label information for the 'Estimated monthly cost' info icon under 'New Container' blade. (#2091)
* [accessibility-3690553]:[Screen reader - Azure Cosmos DB-New Container]: Screen reader does not announce the associated label information for the 'Estimated monthly cost' info icon under 'New Container' blade.

* snap updated.

---------

Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2025-05-14 22:45:44 +05:30
Justin Kolasa (from Dev Box)
4444f66e91 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-14 10:02:56 -04:00
JustinKol
5fc53a7f89 [BUG] Prevent dialog from opening if VS Code is open (#2145)
* master pull

* Reverting .npmrc file

* Removed logging userContext

* Prettier run

* Added support for opening CosmosDB Account without clicking database tab

* Reverting change in settings.json

* Prettier run

* Added check if the link closed

* Added check if the link didn't closed

* Check if VS Code was opened, if not popup with download button link

* Prettier run

* Redirect to Download VS Code if not opened

* Added error message to VS Code timeout and redirect

* Fixing baseUrl from testing

* Increased timeout for when user is asked to open VS Code

* switched to iframe for redirects

* Fixed VS Code url

* Removed insider url

* Added log messages

* Added link to vCore data explorer dashboard

* Increased timeout to 2.5 secs to see if that helps with VS Code open popup

* Changed to dialog box

* Changed param name

* Increase startTime for extra popup

* Changed to dialog box only when no VS Code detected

* Fixed vscode url

* Changed title back to Open CosmosDB in VS Code

* Added text on required extensions

* Removed text on required extensions as it will prompt by default

* Fixed wording and Primary Button timeout

* Spelled out VS Code

* Removed console log of timeout

* Updated snapshots and lowered timeout

* Remove VS Code button from Gremlin

* Prettier run on CommandBarComponentButtonFactory

* Changed from referencing location to a link

* Prettier run

* Reverting back to popup for opening

* Updated unit test snapshots

* Added vscode: to Content Security Policy

* Reverting back to popup only if opening times out

* Corrected misspelled url

* Corrected url

* Added event listener to check if DE is in focus or not, to prevent showing dialog when VS Code is opened

* Prettier and url fix

* Moved closeDialog before removing event listener

* Changed handleFocus to a const rather than function

* Changed listener to document

* Decreased timeout time

* Reverting back to popup by default as too many factors are present using a timeout
2025-05-14 10:01:39 -04:00
Justin Kolasa (from Dev Box)
2180eba0a0 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-14 09:47:45 -04:00
SATYA SB
ed83bf47e4 [accessibility-3556762]:[Screen Reader- Azure Cosmos DB- Data Explorer]: Two H1 heading are defined on the page. (#2061)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2025-05-14 15:29:08 +05:30
SATYA SB
d657c4919e [Supporting the platform - Azure Cosmos DB - Data Explorer]: All the controls present under 'Data Explorer' page are truncated after setting the viewport to 320*256 pixel. (#2092)
* [accessibility-2278267]:[Supporting the platform - Azure Cosmos DB - Data Explorer]: All the controls present under 'Data Explorer' page are truncated after setting the viewport to 320*256 pixel.

* feat: implement zoom level hook and update components for responsive design.

* Format fixed.

* feat: add conditionalClass utility and refactor className assignments for improved readability.

---------

Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2025-05-14 10:16:19 +05:30
sunghyunkang1111
95d33356c3 Hide throughput bucket settings for nonshared, nondedicated throughput container (#2146) 2025-05-13 18:58:42 -05:00
sunghyunkang1111
1081432bbd Added AFEC check in userContext (#2140)
* Added AFEC check in userContext

* Update unit tests and fix Group to Bucket
2025-05-13 11:26:47 -05:00
Justin Kolasa (from Dev Box)
ea1f0e9b0c Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-13 11:37:09 -04:00
Nishtha Ahuja
44d815454c for manual isAutoscale is supposed to be false (#2141)
Co-authored-by: nishthaAhujaa <nishtha17354@iiittd.ac.in>
2025-05-13 20:04:12 +05:30
JustinKol
6d604490d3 URL Correction in VS Code link (#2142)
* master pull

* Reverting .npmrc file

* Removed logging userContext

* Prettier run

* Added support for opening CosmosDB Account without clicking database tab

* Reverting change in settings.json

* Prettier run

* Added check if the link closed

* Added check if the link didn't closed

* Check if VS Code was opened, if not popup with download button link

* Prettier run

* Redirect to Download VS Code if not opened

* Added error message to VS Code timeout and redirect

* Fixing baseUrl from testing

* Increased timeout for when user is asked to open VS Code

* switched to iframe for redirects

* Fixed VS Code url

* Removed insider url

* Added log messages

* Added link to vCore data explorer dashboard

* Increased timeout to 2.5 secs to see if that helps with VS Code open popup

* Changed to dialog box

* Changed param name

* Increase startTime for extra popup

* Changed to dialog box only when no VS Code detected

* Fixed vscode url

* Changed title back to Open CosmosDB in VS Code

* Added text on required extensions

* Removed text on required extensions as it will prompt by default

* Fixed wording and Primary Button timeout

* Spelled out VS Code

* Removed console log of timeout

* Updated snapshots and lowered timeout

* Remove VS Code button from Gremlin

* Prettier run on CommandBarComponentButtonFactory

* Changed from referencing location to a link

* Prettier run

* Reverting back to popup for opening

* Updated unit test snapshots

* Added vscode: to Content Security Policy

* Reverting back to popup only if opening times out

* Corrected misspelled url

* Corrected url
2025-05-13 10:26:54 -04:00
Justin Kolasa (from Dev Box)
f66b78aaf8 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-13 09:28:03 -04:00
asier-isayas
34edd96c76 Default New Global Secondary Index Panel to be sharded (#2138)
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-05-12 13:12:20 -04:00
JustinKol
7c0aae6ffa Open VS Code via Data Explorer button (#2116)
* master pull

* Reverting .npmrc file

* Removed logging userContext

* Prettier run

* Added support for opening CosmosDB Account without clicking database tab

* Reverting change in settings.json

* Prettier run

* Added check if the link closed

* Added check if the link didn't closed

* Check if VS Code was opened, if not popup with download button link

* Prettier run

* Redirect to Download VS Code if not opened

* Added error message to VS Code timeout and redirect

* Fixing baseUrl from testing

* Increased timeout for when user is asked to open VS Code

* switched to iframe for redirects

* Fixed VS Code url

* Removed insider url

* Added log messages

* Added link to vCore data explorer dashboard

* Increased timeout to 2.5 secs to see if that helps with VS Code open popup

* Changed to dialog box

* Changed param name

* Increase startTime for extra popup

* Changed to dialog box only when no VS Code detected

* Fixed vscode url

* Changed title back to Open CosmosDB in VS Code

* Added text on required extensions

* Removed text on required extensions as it will prompt by default

* Fixed wording and Primary Button timeout

* Spelled out VS Code

* Removed console log of timeout

* Updated snapshots and lowered timeout

* Remove VS Code button from Gremlin

* Prettier run on CommandBarComponentButtonFactory

* Changed from referencing location to a link

* Prettier run

* Reverting back to popup for opening

* Updated unit test snapshots

* Added vscode: to Content Security Policy

* Reverting back to popup only if opening times out
2025-05-12 10:55:06 -04:00
JustinKol
86e8bf3c80 Show unique keys in Settings for SQL api (#2136)
* master pull

* Added unique keys in Settings for SQL api

* Revert settings.json

* Reverting other PR changes that haven't merged

* Adding space back in

* Added unit tests
2025-05-09 12:37:56 -04:00
Dmitry Shilov
e98c9a83b8 fix: Add collapsible feature to Accordion in SettingsPane (#2125) 2025-05-09 12:13:17 +02:00
Dmitry Shilov
7d57a90d50 fix: Correct documentId method call in error logging for deletion failures (#2132) 2025-05-09 12:12:32 +02:00
Dmitry Shilov
0f896f556b feat: Enhance UploadItemsPane with error handling and status icons for file uploads (#2133) 2025-05-09 12:11:26 +02:00
Justin Kolasa (from Dev Box)
30182ed503 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-08 08:55:42 -04:00
Justin Kolasa (from Dev Box)
9bf8cffd29 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-05 11:29:18 -04:00
Justin Kolasa (from Dev Box)
cb92bdb003 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-02 11:22:49 -04:00
Justin Kolasa (from Dev Box)
5f4ea0e614 master pull 2025-04-22 08:50:29 -04:00
25 changed files with 498 additions and 56 deletions

4
.npmrc
View File

@@ -1,4 +1,6 @@
save-exact=true
registry=https://msdata.pkgs.visualstudio.com/_packaging/cosmosdbportal/npm/registry/
always-auth=true
allow-same-version=true
# Ignore peer dependency conflicts
force=true # TODO: Remove this when we update to React 17 or higher!

View File

@@ -24,5 +24,6 @@
"source.organizeImports": "explicit"
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "esbenp.prettier-vscode",
"sarif-viewer.connectToGithubCodeScanning": "off"
}

View File

@@ -4,13 +4,18 @@ import * as React from "react";
export interface TooltipProps {
children: string;
className?: string;
ariaLabelForTooltip?: string;
}
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children, className }: TooltipProps) => {
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({
children,
className,
ariaLabelForTooltip = children,
}: TooltipProps) => {
return (
<span className={className}>
<TooltipHost content={children}>
<Icon iconName="Info" ariaLabel={children} className="panelInfoIcon" tabIndex={0} />
<Icon iconName="Info" aria-label={ariaLabelForTooltip} className="panelInfoIcon" tabIndex={0} />
</TooltipHost>
</span>
);

View File

@@ -143,4 +143,39 @@ describe("SubSettingsComponent", () => {
expect(subSettingsComponentInstance.getTtlValue(TtlType.On)).toEqual(TtlOn);
expect(subSettingsComponentInstance.getTtlValue(TtlType.Off)).toEqual(TtlOff);
});
it("uniqueKey is visible", () => {
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableSQL" }],
},
} as DatabaseAccount,
});
const subSettingsComponent = new SubSettingsComponent(baseProps);
expect(subSettingsComponent.getUniqueKeyVisible()).toEqual(true);
});
it("uniqueKey not visible due to no keys", () => {
const props = {
...baseProps,
...(baseProps.collection.rawDataModel.uniqueKeyPolicy.uniqueKeys = []),
};
const subSettingsComponent = new SubSettingsComponent(props);
expect(subSettingsComponent.getUniqueKeyVisible()).toEqual(false);
});
it("uniqueKey not visible for API", () => {
const newContainer = new Explorer();
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableMongo" }],
},
} as DatabaseAccount,
});
const props = { ...baseProps, container: newContainer };
const subSettingsComponent = new SubSettingsComponent(props);
expect(subSettingsComponent.getUniqueKeyVisible()).toEqual(false);
});
});

View File

@@ -63,12 +63,16 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
private geospatialVisible: boolean;
private partitionKeyValue: string;
private partitionKeyName: string;
private uniqueKeyName: string;
private uniqueKeyValue: string;
constructor(props: SubSettingsComponentProps) {
super(props);
this.geospatialVisible = userContext.apiType === "SQL";
this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
this.partitionKeyValue = this.getPartitionKeyValue();
this.uniqueKeyName = "Unique keys";
this.uniqueKeyValue = this.getUniqueKeyValue();
}
componentDidMount(): void {
@@ -351,6 +355,28 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
public isLargePartitionKeyEnabled = (): boolean => this.props.collection.partitionKey?.version >= 2;
public isHierarchicalPartitionedContainer = (): boolean => this.props.collection.partitionKey?.kind === "MultiHash";
public getUniqueKeyVisible = (): boolean => {
return this.props.collection.rawDataModel.uniqueKeyPolicy?.uniqueKeys.length > 0 && userContext.apiType === "SQL";
};
private getUniqueKeyValue = (): string => {
const paths = this.props.collection.rawDataModel.uniqueKeyPolicy?.uniqueKeys?.[0]?.paths;
return paths?.join(", ") || "";
};
private getUniqueKeyComponent = (): JSX.Element => (
<Stack {...titleAndInputStackProps}>
{this.getUniqueKeyVisible() && (
<TextField
label={this.uniqueKeyName}
disabled
styles={getTextFieldStyles(undefined, undefined)}
defaultValue={this.uniqueKeyValue}
/>
)}
</Stack>
);
public render(): JSX.Element {
return (
<Stack {...subComponentStackProps}>
@@ -363,6 +389,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
{this.props.changeFeedPolicyVisible && this.getChangeFeedComponent()}
{this.getPartitionKeyComponent()}
{this.getUniqueKeyComponent()}
</Stack>
);
}

View File

@@ -231,6 +231,34 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
Non-hierarchically partitioned container.
</Text>
</Stack>
<Stack
tokens={
{
"childrenGap": 5,
}
}
>
<StyledTextFieldBase
defaultValue="/id"
disabled={true}
label="Unique keys"
styles={
{
"fieldGroup": {
"borderColor": "",
"height": 25,
"selectors": {
":disabled": {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
/>
</Stack>
</Stack>
`;
@@ -520,6 +548,34 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
Non-hierarchically partitioned container.
</Text>
</Stack>
<Stack
tokens={
{
"childrenGap": 5,
}
}
>
<StyledTextFieldBase
defaultValue="/id"
disabled={true}
label="Unique keys"
styles={
{
"fieldGroup": {
"borderColor": "",
"height": 25,
"selectors": {
":disabled": {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
/>
</Stack>
</Stack>
`;
@@ -769,6 +825,34 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
Non-hierarchically partitioned container.
</Text>
</Stack>
<Stack
tokens={
{
"childrenGap": 5,
}
}
>
<StyledTextFieldBase
defaultValue="/id"
disabled={true}
label="Unique keys"
styles={
{
"fieldGroup": {
"borderColor": "",
"height": 25,
"selectors": {
":disabled": {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
/>
</Stack>
</Stack>
`;
@@ -1083,6 +1167,34 @@ exports[`SubSettingsComponent renders 1`] = `
Non-hierarchically partitioned container.
</Text>
</Stack>
<Stack
tokens={
{
"childrenGap": 5,
}
}
>
<StyledTextFieldBase
defaultValue="/id"
disabled={true}
label="Unique keys"
styles={
{
"fieldGroup": {
"borderColor": "",
"height": 25,
"selectors": {
":disabled": {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
/>
</Stack>
</Stack>
`;
@@ -1371,5 +1483,33 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
Non-hierarchically partitioned container.
</Text>
</Stack>
<Stack
tokens={
{
"childrenGap": 5,
}
}
>
<StyledTextFieldBase
defaultValue="/id"
disabled={true}
label="Unique keys"
styles={
{
"fieldGroup": {
"borderColor": "",
"height": 25,
"selectors": {
":disabled": {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
/>
</Stack>
</Stack>
`;

View File

@@ -17,7 +17,15 @@ export const collection = {
includedPaths: [],
excludedPaths: [],
}),
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
rawDataModel: {
uniqueKeyPolicy: {
uniqueKeys: [
{
paths: ["/id"],
},
],
},
},
usageSizeInKB: ko.observable(100),
offer: ko.observable<DataModels.Offer>({
autoscaleMaxThroughput: undefined,

View File

@@ -71,8 +71,18 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyProperties": [
"partitionKey",
],
"rawDataModel": {
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
}
@@ -153,8 +163,18 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyProperties": [
"partitionKey",
],
"rawDataModel": {
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
}
@@ -274,8 +294,18 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyProperties": [
"partitionKey",
],
"rawDataModel": {
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
}
@@ -404,8 +434,18 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyProperties": [
"partitionKey",
],
"rawDataModel": {
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
}

View File

@@ -44,13 +44,18 @@ export const CostEstimateText: FunctionComponent<CostEstimateTextProps> = ({
const currencySign: string = getCurrencySign(serverId);
const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multiplier) : getPricePerRu(serverId, multiplier);
const estimatedMonthlyCost = "Estimated monthly cost";
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>{estimatedCostDisclaimer}</InfoTooltip>;
const iconWithEstimatedCostDisclaimer: JSX.Element = (
<InfoTooltip ariaLabelForTooltip={`${estimatedMonthlyCost} ${currency} ${estimatedCostDisclaimer}`}>
{estimatedCostDisclaimer}
</InfoTooltip>
);
if (isAutoscale) {
return (
<Text variant="small">
Estimated monthly cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
{estimatedMonthlyCost} ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
<b>
{currencySign + calculateEstimateNumber(monthlyPrice / 10)} -{" "}
{currencySign + calculateEstimateNumber(monthlyPrice)}{" "}

View File

@@ -345,13 +345,13 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
role="none"
>
<StyledIconBase
ariaLabel="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
aria-label="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
className="panelInfoIcon"
iconName="Info"
tabIndex={0}
>
<IconBase
ariaLabel="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
aria-label="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
className="panelInfoIcon"
iconName="Info"
styles={[Function]}
@@ -1358,13 +1358,13 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
role="none"
>
<StyledIconBase
ariaLabel="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
aria-label="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
className="panelInfoIcon"
iconName="Info"
tabIndex={0}
>
<IconBase
ariaLabel="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
aria-label="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
className="panelInfoIcon"
iconName="Info"
styles={[Function]}

View File

@@ -517,7 +517,6 @@ export function createPostgreButtons(container: Explorer): CommandButtonComponen
export function createVCoreMongoButtons(container: Explorer): CommandButtonComponentProps[] {
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.VCoreMongo);
const addVsCode = createOpenVsCodeDialogButton(container);
return [openVCoreMongoTerminalButton, addVsCode];
}

View File

@@ -608,7 +608,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
<RightPaneForm {...genericPaneProps}>
<div className={`paneMainContent ${styles.container}`}>
{!isFabricNative() && (
<Accordion className={`customAccordion ${styles.firstItem}`}>
<Accordion className={`customAccordion ${styles.firstItem}`} collapsible>
{shouldShowQueryPageOptions && (
<AccordionItem value="1">
<AccordionHeader>

View File

@@ -12,6 +12,7 @@ exports[`Settings Pane should render Default properly 1`] = `
>
<Accordion
className="customAccordion ___1uf6361_0000000 fz7g6wx"
collapsible={true}
>
<AccordionItem
value="1"
@@ -573,6 +574,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
>
<Accordion
className="customAccordion ___1uf6361_0000000 fz7g6wx"
collapsible={true}
>
<AccordionItem
value="7"

View File

@@ -356,7 +356,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
value=""
>
<div
className="ms-TextField is-required root-110"
className="ms-TextField is-required root-116"
>
<div
className="ms-TextField-wrapper"
@@ -647,7 +647,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
}
>
<label
className="ms-Label root-121"
className="ms-Label root-127"
htmlFor="TextField0"
id="TextFieldLabel2"
>
@@ -656,13 +656,13 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
</LabelBase>
</StyledLabelBase>
<div
className="ms-TextField-fieldGroup fieldGroup-111"
className="ms-TextField-fieldGroup fieldGroup-117"
>
<input
aria-invalid={false}
aria-labelledby="TextFieldLabel2"
autoFocus={true}
className="ms-TextField-field field-112"
className="ms-TextField-field field-118"
id="TextField0"
name="collectionIdConfirmation"
onBlur={[Function]}
@@ -2464,7 +2464,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
>
<button
aria-label="Create"
className="ms-Button ms-Button--primary root-122"
className="ms-Button ms-Button--primary root-128"
data-is-focusable={true}
data-test="Panel/OkButton"
id="sidePanelOkButton"
@@ -2477,14 +2477,14 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
type="submit"
>
<span
className="ms-Button-flexContainer flexContainer-123"
className="ms-Button-flexContainer flexContainer-129"
data-automationid="splitbuttonprimary"
>
<span
className="ms-Button-textContainer textContainer-124"
className="ms-Button-textContainer textContainer-130"
>
<span
className="ms-Button-label label-126"
className="ms-Button-label label-132"
id="id__5"
key="id__5"
>

View File

@@ -2,9 +2,13 @@ import {
DetailsList,
DetailsListLayoutMode,
DirectionalHint,
FontIcon,
IColumn,
SelectionMode,
TooltipHost,
getTheme,
mergeStyles,
mergeStyleSets,
} from "@fluentui/react";
import { Upload } from "Common/Upload/Upload";
import { UploadDetailsRecord } from "Contracts/ViewModels";
@@ -14,6 +18,36 @@ import { getErrorMessage } from "../../Tables/Utilities";
import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
const theme = getTheme();
const iconClass = mergeStyles({
verticalAlign: "middle",
maxHeight: "16px",
maxWidth: "16px",
});
const classNames = mergeStyleSets({
fileIconHeaderIcon: {
padding: 0,
fontSize: "16px",
},
fileIconCell: {
textAlign: "center",
selectors: {
"&:before": {
content: ".",
display: "inline-block",
verticalAlign: "middle",
height: "100%",
width: "0px",
visibility: "hidden",
},
},
},
error: [{ color: theme.semanticColors.errorIcon }, iconClass],
accept: [{ color: theme.semanticColors.successIcon }, iconClass],
warning: [{ color: theme.semanticColors.warningIcon }, iconClass],
});
export const UploadItemsPane: FunctionComponent = () => {
const [files, setFiles] = useState<FileList>();
const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]);
@@ -60,44 +94,94 @@ export const UploadItemsPane: FunctionComponent = () => {
};
const columns: IColumn[] = [
{
key: "icons",
name: "",
fieldName: "",
className: classNames.fileIconCell,
iconClassName: classNames.fileIconHeaderIcon,
isIconOnly: true,
minWidth: 16,
maxWidth: 16,
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
if (item.numFailed) {
const errorList = (
<ul
aria-label={"error list"}
style={{
margin: "5px 0",
paddingLeft: "20px",
listStyleType: "disc", // Explicitly set to use bullets (dots)
}}
>
{item.errors.map((error, i) => (
<li key={i} style={{ display: "list-item" }}>
{error}
</li>
))}
</ul>
);
return (
<TooltipHost
content={errorList}
id={`tooltip-${index}-${column.key}`}
directionalHint={DirectionalHint.bottomAutoEdge}
>
<FontIcon iconName="Error" className={classNames.error} aria-label="error" />
</TooltipHost>
);
} else if (item.numThrottled) {
return <FontIcon iconName="Warning" className={classNames.warning} aria-label="warning" />;
} else {
return <FontIcon iconName="Accept" className={classNames.accept} aria-label="accept" />;
}
},
},
{
key: "fileName",
name: "FILE NAME",
fieldName: "fileName",
minWidth: 140,
minWidth: 120,
maxWidth: 140,
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
const fieldContent = item.fileName;
return (
<TooltipHost
content={fieldContent}
id={`tooltip-${index}-${column.key}`}
directionalHint={DirectionalHint.bottomAutoEdge}
>
{fieldContent}
</TooltipHost>
);
},
},
{
key: "status",
name: "STATUS",
fieldName: "numSucceeded",
minWidth: 140,
minWidth: 120,
maxWidth: 140,
isRowHeader: true,
isResizable: true,
data: "string",
isPadded: true,
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
const fieldContent = `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
return (
<TooltipHost
content={fieldContent}
id={`tooltip-${index}-${column.key}`}
directionalHint={DirectionalHint.bottomAutoEdge}
>
{fieldContent}
</TooltipHost>
);
},
},
];
const _renderItemColumn = (item: UploadDetailsRecord, index: number, column: IColumn) => {
let fieldContent: string;
const tooltipId = `tooltip-${index}-${column.key}`;
switch (column.key) {
case "status":
fieldContent = `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
break;
default:
fieldContent = item.fileName;
}
return (
<TooltipHost content={fieldContent} id={tooltipId} directionalHint={DirectionalHint.rightCenter}>
{fieldContent}
</TooltipHost>
);
};
return (
<RightPaneForm {...props}>
<div className="paneMainContent">
@@ -115,7 +199,6 @@ export const UploadItemsPane: FunctionComponent = () => {
<DetailsList
items={uploadFileData}
columns={columns}
onRenderItemColumn={_renderItemColumn}
selectionMode={SelectionMode.none}
layoutMode={DetailsListLayoutMode.justified}
isHeaderVisible={true}

View File

@@ -30,8 +30,10 @@ import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboard
import { isFabric, isFabricMirrored, isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext";
import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
import { conditionalClass } from "Utils/StyleUtils";
import { Allotment, AllotmentHandle } from "allotment";
import { useSidePanel } from "hooks/useSidePanel";
import useZoomLevel from "hooks/useZoomLevel";
import { debounce } from "lodash";
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
@@ -104,6 +106,23 @@ const useSidebarStyles = makeStyles({
display: "flex",
},
},
accessibleContent: {
"@media (max-width: 420px)": {
overflow: "scroll",
},
},
minHeightResponsive: {
"@media (max-width: 420px)": {
minHeight: "400px",
},
},
accessibleContentZoom: {
overflow: "scroll",
},
minHeightZoom: {
minHeight: "400px",
},
});
interface GlobalCommandsProps {
@@ -275,6 +294,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
const [expandedSize, setExpandedSize] = React.useState(300);
const hasSidebar = userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo";
const allotment = useRef<AllotmentHandle>(null);
const isZoomed = useZoomLevel();
const expand = useCallback(() => {
if (!expanded) {
@@ -325,11 +345,23 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
return (
<div className="sidebarContainer">
<Allotment ref={allotment} onChange={onChange} onDragEnd={onDragEnd} className="resourceTreeAndTabs">
<Allotment
ref={allotment}
onChange={onChange}
onDragEnd={onDragEnd}
className={`resourceTreeAndTabs ${styles.accessibleContent} ${conditionalClass(
isZoomed,
styles.accessibleContentZoom,
)}`}
>
{/* Collections Tree - Start */}
{hasSidebar && (
// When collapsed, we force the pane to 24 pixels wide and make it non-resizable.
<Allotment.Pane minSize={24} preferredSize={250}>
<Allotment.Pane
className={`${styles.minHeightResponsive} ${conditionalClass(isZoomed, styles.minHeightZoom)}`}
minSize={24}
preferredSize={250}
>
<CosmosFluentProvider className={mergeClasses(styles.sidebar)}>
<div className={styles.sidebarContainer}>
{loading && (
@@ -385,7 +417,10 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
</CosmosFluentProvider>
</Allotment.Pane>
)}
<Allotment.Pane minSize={200}>
<Allotment.Pane
className={`${styles.minHeightResponsive} ${conditionalClass(isZoomed, styles.minHeightZoom)}`}
minSize={200}
>
<Tabs explorer={explorer} />
</Allotment.Pane>
</Allotment>

View File

@@ -291,10 +291,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
<form className="connectExplorerFormContainer">
<div className="splashScreenContainer">
<div className="splashScreen">
<h1 className="title" role="heading" aria-label={title}>
<h2 className="title" role="heading" aria-label={title}>
{title}
<FeaturePanelLauncher />
</h1>
</h2>
<div className="subtitle">{subtitle}</div>
{this.getSplashScreenButtons()}
{useCarousel.getState().showCoachMark && (

View File

@@ -144,6 +144,13 @@ export const useDocumentsTabStyles = makeStyles({
deleteProgressContent: {
paddingTop: tokens.spacingVerticalL,
},
smallScreenContent: {
"@media (max-width: 420px)": {
flexWrap: "wrap",
minHeight: "max-content",
padding: "4px",
},
},
});
export class DocumentsTabV2 extends TabsBase {
@@ -729,7 +736,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
} else if (result.statusCode >= 400) {
newFailed.push(result.documentId);
logConsoleError(
`Failed to delete document ${result.documentId.id} with status code ${result.statusCode}`,
`Failed to delete document ${result.documentId.id()} with status code ${result.statusCode}`,
);
}
});
@@ -2102,7 +2109,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
return (
<CosmosFluentProvider className={styles.container}>
<div data-test={"DocumentsTab"} className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
<div data-test={"DocumentsTab/Filter"} className={styles.filterRow}>
<div data-test={"DocumentsTab/Filter"} className={`${styles.filterRow} ${styles.smallScreenContent}`}>
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
<InputDataList
dropdownOptions={getFilterChoices()}

View File

@@ -15,7 +15,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
}
>
<div
className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29"
className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29 ___1ngl8o6_0000000 fz7mnu6 fl3egqs flhmrkm"
data-test="DocumentsTab/Filter"
>
<span>

View File

@@ -8,6 +8,8 @@ import RunQuery from "../../../../images/RunQuery.png";
import { QueryResults } from "../../../Contracts/ViewModels";
import { ErrorList } from "./ErrorList";
import { ResultsView } from "./ResultsView";
import useZoomLevel from "hooks/useZoomLevel";
import { conditionalClass } from "Utils/StyleUtils";
export interface ResultsViewProps {
isMongoDB: boolean;
@@ -23,11 +25,16 @@ interface QueryResultProps extends ResultsViewProps {
const ExecuteQueryCallToAction: React.FC = () => {
const styles = useQueryTabStyles();
const isZoomed = useZoomLevel();
return (
<div data-test="QueryTab/ResultsPane/ExecuteCTA" className={styles.executeCallToAction}>
<div>
<p>
<img src={RunQuery} aria-hidden="true" />
<img
className={`${styles.responsiveImg} ${conditionalClass(isZoomed, styles.zoomedImageSize)}`}
src={RunQuery}
aria-hidden="true"
/>
</p>
<p>Execute a query to see the results</p>
</div>

View File

@@ -25,6 +25,9 @@ export const useQueryTabStyles = makeStyles({
height: "100%",
display: "flex",
flexDirection: "column",
"@media (max-width: 420px)": {
overflow: "scroll",
},
},
queryResultsMessage: {
...shorthands.margin("5px"),
@@ -38,6 +41,9 @@ export const useQueryTabStyles = makeStyles({
display: "flex",
rowGap: "12px",
flexDirection: "column",
"@media (max-width: 420px)": {
height: "auto",
},
},
queryResultsTabContentContainer: {
display: "flex",
@@ -93,4 +99,12 @@ export const useQueryTabStyles = makeStyles({
display: "flex",
flexDirection: "row",
},
responsiveImg: {
"@media (max-width: 420px)": {
width: "50px",
},
},
zoomedImageSize: {
width: "60px",
},
});

View File

@@ -1122,6 +1122,9 @@ export default class Collection implements ViewModels.Collection {
stats.numSucceeded++;
} else if (response.statusCode === 429) {
documentsToAttempt.push(attemptedDocuments[index]);
} else if (response.statusCode === 409) {
stats.numFailed++;
stats.errors.push(`Document with id ${attemptedDocuments[index].id} already exists.`);
} else {
stats.numFailed++;
stats.errors.push(JSON.stringify(response.resourceBody));

View File

@@ -21,3 +21,11 @@ export function copyStyles(sourceDoc: Document, targetDoc: Document): void {
}
});
}
/**
* Conditionally returns a class name based on a boolean condition.
* If the condition is true, returns the `trueValue` class; otherwise, returns `falseValue` (or an empty string if not provided).
*/
export function conditionalClass(condition: boolean, trueValue: string, falseValue?: string): string {
return condition ? trueValue : falseValue || "";
}

20
src/hooks/useZoomLevel.ts Normal file
View File

@@ -0,0 +1,20 @@
import { useEffect, useState } from "react";
const useZoomLevel = (threshold: number = 2): boolean => {
const [isZoomed, setIsZoomed] = useState<boolean>(false);
useEffect(() => {
const checkZoom = () => {
const zoomLevel = window.devicePixelRatio;
setIsZoomed(zoomLevel >= threshold);
};
checkZoom();
window.addEventListener("resize", checkZoom);
return () => window.removeEventListener("resize", checkZoom);
}, [threshold]);
return isZoomed;
};
export default useZoomLevel;

View File

@@ -30,7 +30,7 @@
<clear />
<add name="X-Xss-Protection" value="1; mode=block" />
<add name="X-Content-Type-Options" value="nosniff" />
<add name="Content-Security-Policy" value="frame-ancestors 'self' portal.azure.com *.portal.azure.com portal.azure.us portal.azure.cn portal.microsoftazure.de df.onecloud.azure-test.net *.fabric.microsoft.com *.powerbi.com *.analysis-df.windows.net dataexplorer-preview.azurewebsites.net" />
<add name="Content-Security-Policy" value="frame-src 'vscode:' frame-ancestors 'self' portal.azure.com *.portal.azure.com portal.azure.us portal.azure.cn portal.microsoftazure.de df.onecloud.azure-test.net *.fabric.microsoft.com *.powerbi.com *.analysis-df.windows.net dataexplorer-preview.azurewebsites.net" />
</customHeaders>
<redirectHeaders>
<clear />