Compare commits

...

8 Commits

Author SHA1 Message Date
Satyapriya Bai
31ff90e2f9 Removed unwanted code. 2024-06-20 00:00:47 +05:30
Satyapriya Bai
fe0457bcb0 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into new-accessibility-2262594 2024-06-19 23:58:47 +05:30
sunghyunkang1111
62ab0e3e60 Fix codeql issues (#1875) 2024-06-19 10:12:38 -05:00
SATYA SB
d199311633 Ensures ARIA attributes are allowed for an element's role. (#1846)
* [accessibility-3048277]:[Programmatic Access - Azure Cosmos DB - Data Explorer>New Container]: Ensures ARIA attributes are allowed for an element's role

* updated PartitionKeyPane

---------

Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2024-06-19 16:02:09 +05:30
Satyapriya Bai
9c1d2a300e updated Test. 2024-06-19 15:26:41 +05:30
Satyapriya Bai
81a4c8039b [accessibility-2262594]:[Keyboard Navigation - Azure Cosmos DB - Data Explorer]: Keyboard focus is not retaining back to 'more' button after closing 'Delete container' dialog. 2024-06-19 15:12:44 +05:30
sunghyunkang1111
bf225f91c4 Remove notification for sample collection loading (#1874) 2024-06-18 12:15:06 -05:00
jawelton74
4d0b1a6db8 Switch accountrestrictions call to use new backend in MPAC. (#1868) 2024-06-18 09:41:24 -07:00
16 changed files with 83 additions and 34 deletions

View File

@@ -133,6 +133,7 @@ export enum MongoBackendEndpointType {
export class BackendApi {
public static readonly GenerateToken: string = "GenerateToken";
public static readonly PortalSettings: string = "PortalSettings";
public static readonly AccountRestrictions: string = "AccountRestrictions";
}
export class PortalBackendEndpoints {

View File

@@ -1,6 +1,7 @@
import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
import Q from "q";
import * as _ from "underscore";
import * as Logger from "../Common/Logger";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { getDataExplorerWindow } from "../Utils/WindowUtils";
import * as Constants from "./Constants";
@@ -96,10 +97,18 @@ const _sendMessage = (message: any): void => {
const portalChildWindow = getDataExplorerWindow(window) || window;
if (portalChildWindow === window) {
// Current window is a child of portal, send message to portal window
portalChildWindow.parent.postMessage(message, portalChildWindow.document.referrer || "*");
if (portalChildWindow.document.referrer) {
portalChildWindow.parent.postMessage(message, portalChildWindow.document.referrer);
} else {
Logger.logError("Iframe failed to send message to portal", "MessageHandler");
}
} else {
// Current window is not a child of portal, send message to the child window instead (which is data explorer)
portalChildWindow.postMessage(message, portalChildWindow.location.origin || "*");
if (portalChildWindow.location.origin) {
portalChildWindow.postMessage(message, portalChildWindow.location.origin);
} else {
Logger.logError("Iframe failed to send message to data explorer", "MessageHandler");
}
}
}
};

View File

@@ -2,7 +2,6 @@ import { CosmosClient } from "@azure/cosmos";
import { sampleDataClient } from "Common/SampleDataClient";
import { userContext } from "UserContext";
import * as DataModels from "../../Contracts/DataModels";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
@@ -31,7 +30,6 @@ export async function readCollectionInternal(
collectionId: string,
): Promise<DataModels.Collection> {
let collection: DataModels.Collection;
const clearMessage = logConsoleProgress(`Querying container ${collectionId}`);
try {
const response = await cosmosClient.database(databaseId).container(collectionId).read();
collection = response.resource as DataModels.Collection;
@@ -39,6 +37,5 @@ export async function readCollectionInternal(
handleError(error, "ReadCollection", `Error while querying container ${collectionId}`);
throw error;
}
clearMessage();
return collection;
}

View File

@@ -52,12 +52,15 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) {
items.push({
iconSrc: DeleteDatabaseIcon,
onClick: () =>
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) =>
useSidePanel
.getState()
.openSidePanel(
"Delete " + getDatabaseName(),
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
<DeleteDatabaseConfirmationPanel
lastFocusedElement={lastFocusedElement}
refreshDatabases={() => container.refreshAllDatabases()}
/>,
),
label: `Delete ${getDatabaseName()}`,
styleClass: "deleteDatabaseMenuItem",
@@ -132,13 +135,16 @@ export const createCollectionContextMenuButton = (
if (configContext.platform !== Platform.Fabric) {
items.push({
iconSrc: DeleteCollectionIcon,
onClick: () => {
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
useSelectedNode.getState().setSelectedNode(selectedCollection);
useSidePanel
.getState()
.openSidePanel(
"Delete " + getCollectionName(),
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
<DeleteCollectionConfirmationPane
lastFocusedElement={lastFocusedElement}
refreshDatabases={() => container.refreshAllDatabases()}
/>,
);
},
label: `Delete ${getCollectionName()}`,

View File

@@ -21,7 +21,7 @@ import { useCallback } from "react";
export interface TreeNodeMenuItem {
label: string;
onClick: () => void;
onClick: (value?: React.RefObject<HTMLElement>) => void;
iconSrc?: string;
isDisabled?: boolean;
styleClass?: string;
@@ -73,6 +73,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
treeNodeId,
}: TreeNodeComponentProps): JSX.Element => {
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const contextMenuRef = React.useRef<HTMLButtonElement>(null);
const getSortedChildren = (treeNode: TreeNode): TreeNode[] => {
if (!treeNode || !treeNode.children) {
@@ -139,7 +140,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
data-test={`TreeNode/ContextMenuItem:${menuItem.label}`}
disabled={menuItem.isDisabled}
key={menuItem.label}
onClick={menuItem.onClick}
onClick={() => menuItem.onClick(contextMenuRef)}
>
{menuItem.label}
</MenuItem>
@@ -163,6 +164,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
aria-label="More options"
data-test="TreeNode/ContextMenuTrigger"
appearance="subtle"
ref={contextMenuRef}
icon={<MoreHorizontal20Regular />}
/>
</MenuTrigger>

View File

@@ -1560,14 +1560,14 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
<MenuList>
<MenuItem
data-test="TreeNode/ContextMenuItem:enabledItem"
onClick={[MockFunction enabledItemClick]}
onClick={[Function]}
>
enabledItem
</MenuItem>
<MenuItem
data-test="TreeNode/ContextMenuItem:disabledItem"
disabled={true}
onClick={[MockFunction disabledItemClick]}
onClick={[Function]}
>
disabledItem
</MenuItem>
@@ -1604,7 +1604,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
<MenuItem
data-test="TreeNode/ContextMenuItem:enabledItem"
key="enabledItem"
onClick={[MockFunction enabledItemClick]}
onClick={[Function]}
>
enabledItem
</MenuItem>
@@ -1612,7 +1612,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
data-test="TreeNode/ContextMenuItem:disabledItem"
disabled={true}
key="disabledItem"
onClick={[MockFunction disabledItemClick]}
onClick={[Function]}
>
disabledItem
</MenuItem>

View File

@@ -162,7 +162,7 @@ export const addRootChildToGraph = (
* @param value
*/
export const escapeDoubleQuotes = (value: string): string => {
return value === undefined ? value : value.replace(/"/g, '\\"');
return value === undefined ? value : value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
};
/**
@@ -186,5 +186,5 @@ export const getQuotedPropValue = (ip: ViewModels.InputPropertyValue): string =>
* @param value
*/
export const escapeSingleQuotes = (value: string): string => {
return value === undefined ? value : value.replace(/'/g, "\\'");
return value === undefined ? value : value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
};

View File

@@ -576,9 +576,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</TooltipHost>
</Stack>
<Text variant="small" aria-label="pkDescription">
{this.getPartitionKeySubtext()}
</Text>
<Text variant="small">{this.getPartitionKeySubtext()}</Text>
<input
type="text"

View File

@@ -264,7 +264,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
</TooltipHost>
</Stack>
<Text variant="small" aria-label="pkDescription">
<Text variant="small">
{getPartitionKeySubtext(userContext.features.partitionKeyDefault, userContext.apiType)}
</Text>

View File

@@ -52,7 +52,9 @@ describe("Delete Collection Confirmation Pane", () => {
describe("shouldRecordFeedback()", () => {
it("should return true if last collection and database does not have shared throughput else false", () => {
const wrapper = shallow(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
const wrapper = shallow(
<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} lastFocusedElement={undefined} />,
);
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
const database = { id: ko.observable("testDB") } as Database;
@@ -109,7 +111,9 @@ describe("Delete Collection Confirmation Pane", () => {
});
it("should call delete collection", () => {
const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
const wrapper = mount(
<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} lastFocusedElement={undefined} />,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
@@ -126,7 +130,9 @@ describe("Delete Collection Confirmation Pane", () => {
});
it("should record feedback", async () => {
const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
const wrapper = mount(
<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} lastFocusedElement={undefined} />,
);
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
wrapper
.find("#confirmCollectionId")

View File

@@ -12,17 +12,19 @@ import { getCollectionName } from "Utils/APITypeUtils";
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
import { useSidePanel } from "hooks/useSidePanel";
import { useTabs } from "hooks/useTabs";
import React, { FunctionComponent, useState } from "react";
import React, { FunctionComponent, useEffect, useState } from "react";
import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface DeleteCollectionConfirmationPaneProps {
refreshDatabases: () => Promise<void>;
lastFocusedElement: React.MutableRefObject<HTMLElement>;
}
export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({
refreshDatabases,
lastFocusedElement,
}: DeleteCollectionConfirmationPaneProps) => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
@@ -35,7 +37,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
const collectionName = getCollectionName().toLocaleLowerCase();
const paneTitle = "Delete " + collectionName;
const lastItemElement = lastFocusedElement?.current;
const onSubmit = async (): Promise<void> => {
const collection = useSelectedNode.getState().findSelectedCollection();
if (!collection || inputCollectionName !== collection.id()) {
@@ -111,6 +113,14 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
};
const confirmContainer = `Confirm by typing the ${collectionName.toLowerCase()} id`;
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${collectionName}?`;
useEffect(() => {
return () => {
if (lastItemElement) {
lastItemElement.focus();
}
};
}, [lastItemElement]);
return (
<RightPaneForm {...props}>
<div className="panelFormWrapper">

View File

@@ -49,7 +49,9 @@ describe("Delete Database Confirmation Pane", () => {
});
it("shouldRecordFeedback() should return true if last non empty database or is last database that has shared throughput", () => {
const wrapper = shallow(<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} />);
const wrapper = shallow(
<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} lastFocusedElement={undefined} />,
);
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true);
useDatabases.getState().addDatabases([database]);
@@ -59,7 +61,9 @@ describe("Delete Database Confirmation Pane", () => {
});
it("Should call delete database", () => {
const wrapper = mount(<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} />);
const wrapper = mount(
<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} lastFocusedElement={undefined} />,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
@@ -74,7 +78,9 @@ describe("Delete Database Confirmation Pane", () => {
});
it("should record feedback", async () => {
const wrapper = mount(<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} />);
const wrapper = mount(
<DeleteDatabaseConfirmationPanel refreshDatabases={() => undefined} lastFocusedElement={undefined} />,
);
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
wrapper
.find("#confirmDatabaseId")

View File

@@ -13,7 +13,7 @@ import { getDatabaseName } from "Utils/APITypeUtils";
import { logConsoleError } from "Utils/NotificationConsoleUtils";
import { useSidePanel } from "hooks/useSidePanel";
import { useTabs } from "hooks/useTabs";
import React, { FunctionComponent, useState } from "react";
import React, { FunctionComponent, useEffect, useState } from "react";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
@@ -21,10 +21,12 @@ import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm
interface DeleteDatabaseConfirmationPanelProps {
refreshDatabases: () => Promise<void>;
lastFocusedElement: React.MutableRefObject<HTMLElement>;
}
export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseConfirmationPanelProps> = ({
refreshDatabases,
lastFocusedElement,
}: DeleteDatabaseConfirmationPanelProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const isLastNonEmptyDatabase = useDatabases((state) => state.isLastNonEmptyDatabase);
@@ -34,7 +36,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
const [databaseInput, setDatabaseInput] = useState<string>("");
const [databaseFeedbackInput, setDatabaseFeedbackInput] = useState<string>("");
const selectedDatabase: Database = useDatabases.getState().findSelectedDatabase();
const lastItemElement = lastFocusedElement?.current;
const submit = async (): Promise<void> => {
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
setFormError(
@@ -126,6 +128,13 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
};
const confirmDatabase = `Confirm by typing the ${getDatabaseName()} id`;
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${getDatabaseName()}?`;
useEffect(() => {
return () => {
if (lastItemElement) {
lastItemElement.focus();
}
};
}, [lastItemElement]);
return (
<RightPaneForm {...props}>
{!formError && <PanelInfoErrorComponent {...errorProps} />}

View File

@@ -223,7 +223,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
</StyledTooltipHostBase>
</Stack>
<Text
aria-label="pkDescription"
variant="small"
/>
<input

View File

@@ -51,13 +51,18 @@ export const fetchEncryptedToken_ToBeDeprecated = async (connectionString: strin
export const isAccountRestrictedForConnectionStringLogin = async (connectionString: string): Promise<boolean> => {
const headers = new Headers();
headers.append(HttpHeaders.connectionString, connectionString);
const url = configContext.BACKEND_ENDPOINT + "/api/guest/accountrestrictions/checkconnectionstringlogin";
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings)
? configContext.PORTAL_BACKEND_ENDPOINT
: configContext.BACKEND_ENDPOINT;
const url = backendEndpoint + "/api/guest/accountrestrictions/checkconnectionstringlogin";
const response = await fetch(url, { headers, method: "POST" });
if (!response.ok) {
throw response;
}
return (await response.text()) === "True";
return (await response.text()).toLowerCase() === "true";
};
export const ConnectExplorer: React.FunctionComponent<Props> = ({

View File

@@ -164,6 +164,7 @@ export function useNewPortalBackendEndpoint(backendApi: string): boolean {
PortalBackendEndpoints.Mpac,
PortalBackendEndpoints.Prod,
],
[BackendApi.AccountRestrictions]: [PortalBackendEndpoints.Development, PortalBackendEndpoints.Mpac],
};
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {