mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-24 19:31:36 +00:00
Compare commits
8 Commits
accessibil
...
before_opt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bd905403b | ||
|
|
bd592d07af | ||
|
|
644f5941ec | ||
|
|
9fb006a996 | ||
|
|
c2b98c3e23 | ||
|
|
76d49d86d4 | ||
|
|
7893b89bf7 | ||
|
|
5945e3cb6b |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -83,7 +83,7 @@ jobs:
|
|||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build:contracts
|
- run: npm run build:contracts
|
||||||
- name: Restore Build Cache
|
- name: Restore Build Cache
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: .cache
|
path: .cache
|
||||||
key: ${{ runner.os }}-build-cache
|
key: ${{ runner.os }}-build-cache
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), https://cosmos.azure.com/, and the [Cosmos DB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator)
|
UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), https://cosmos.azure.com/, and the [Cosmos DB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator)
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ export enum CapacityMode {
|
|||||||
Serverless = "Serverless",
|
Serverless = "Serverless",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum WorkloadType {
|
||||||
|
Learning = "Learning",
|
||||||
|
DevelopmentTesting = "Development/Testing",
|
||||||
|
Production = "Production",
|
||||||
|
None = "None",
|
||||||
|
}
|
||||||
// flight names returned from the portal are always lowercase
|
// flight names returned from the portal are always lowercase
|
||||||
export class Flights {
|
export class Flights {
|
||||||
public static readonly SettingsV2 = "settingsv2";
|
public static readonly SettingsV2 = "settingsv2";
|
||||||
@@ -119,6 +125,7 @@ export class AfecFeatures {
|
|||||||
|
|
||||||
export class TagNames {
|
export class TagNames {
|
||||||
public static defaultExperience: string = "defaultExperience";
|
public static defaultExperience: string = "defaultExperience";
|
||||||
|
public static WorkloadType: string = "hidden-workload-type";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MongoDBAccounts {
|
export class MongoDBAccounts {
|
||||||
|
|||||||
34
src/Common/DatabaseAccountUtility.test.ts
Normal file
34
src/Common/DatabaseAccountUtility.test.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { WorkloadType } from "Common/Constants";
|
||||||
|
import { getWorkloadType } from "Common/DatabaseAccountUtility";
|
||||||
|
import { DatabaseAccount, Tags } from "Contracts/DataModels";
|
||||||
|
import { updateUserContext } from "UserContext";
|
||||||
|
|
||||||
|
describe("Database Account Utility", () => {
|
||||||
|
describe("Workload Type", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
tags: {} as Tags,
|
||||||
|
} as DatabaseAccount,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Workload Type should return Learning", () => {
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
tags: {
|
||||||
|
"hidden-workload-type": WorkloadType.Learning,
|
||||||
|
} as Tags,
|
||||||
|
} as DatabaseAccount,
|
||||||
|
});
|
||||||
|
|
||||||
|
const workloadType: WorkloadType = getWorkloadType();
|
||||||
|
expect(workloadType).toBe(WorkloadType.Learning);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Workload Type should return None", () => {
|
||||||
|
const workloadType: WorkloadType = getWorkloadType();
|
||||||
|
expect(workloadType).toBe(WorkloadType.None);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { TagNames, WorkloadType } from "Common/Constants";
|
||||||
|
import { Tags } from "Contracts/DataModels";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
function isVirtualNetworkFilterEnabled() {
|
function isVirtualNetworkFilterEnabled() {
|
||||||
@@ -15,3 +17,12 @@ function isPrivateEndpointConnectionsEnabled() {
|
|||||||
export function isPublicInternetAccessAllowed(): boolean {
|
export function isPublicInternetAccessAllowed(): boolean {
|
||||||
return !isVirtualNetworkFilterEnabled() && !isIpRulesEnabled() && !isPrivateEndpointConnectionsEnabled();
|
return !isVirtualNetworkFilterEnabled() && !isIpRulesEnabled() && !isPrivateEndpointConnectionsEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getWorkloadType(): WorkloadType {
|
||||||
|
const tags: Tags = userContext?.databaseAccount?.tags;
|
||||||
|
const workloadType: WorkloadType = tags && (tags[TagNames.WorkloadType] as WorkloadType);
|
||||||
|
if (!workloadType) {
|
||||||
|
return WorkloadType.None;
|
||||||
|
}
|
||||||
|
return workloadType;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface ArmEntity {
|
|||||||
location: string;
|
location: string;
|
||||||
type: string;
|
type: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
|
tags?: Tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatabaseAccount extends ArmEntity {
|
export interface DatabaseAccount extends ArmEntity {
|
||||||
@@ -663,3 +664,5 @@ export interface FeatureRegistration {
|
|||||||
state: string;
|
state: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Tags = { [key: string]: string };
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export enum MessageTypes {
|
|||||||
OpenPostgreSQLPasswordReset,
|
OpenPostgreSQLPasswordReset,
|
||||||
OpenPostgresNetworkingBlade,
|
OpenPostgresNetworkingBlade,
|
||||||
OpenCosmosDBNetworkingBlade,
|
OpenCosmosDBNetworkingBlade,
|
||||||
DisplayNPSSurvey,
|
DisplayNPSSurvey, // unused
|
||||||
OpenVCoreMongoNetworkingBlade,
|
OpenVCoreMongoNetworkingBlade,
|
||||||
OpenVCoreMongoConnectionStringsBlade,
|
OpenVCoreMongoConnectionStringsBlade,
|
||||||
GetAuthorizationToken, // unused. Can be removed if the portal uses the same list of enums.
|
GetAuthorizationToken, // unused. Can be removed if the portal uses the same list of enums.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
|
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
|
||||||
|
import { getWorkloadType } from "Common/DatabaseAccountUtility";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
@@ -34,10 +35,15 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
setIsThroughputCapExceeded,
|
setIsThroughputCapExceeded,
|
||||||
onCostAcknowledgeChange,
|
onCostAcknowledgeChange,
|
||||||
}: ThroughputInputProps) => {
|
}: ThroughputInputProps) => {
|
||||||
|
const defaultThroughput: number =
|
||||||
|
isFreeTier ||
|
||||||
|
isQuickstart ||
|
||||||
|
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(getWorkloadType())
|
||||||
|
? AutoPilotUtils.autoPilotThroughput1K
|
||||||
|
: AutoPilotUtils.autoPilotThroughput4K;
|
||||||
|
|
||||||
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
|
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
|
||||||
const [throughput, setThroughput] = useState<number>(
|
const [throughput, setThroughput] = useState<number>(defaultThroughput);
|
||||||
isFreeTier || isQuickstart ? AutoPilotUtils.autoPilotThroughput1K : AutoPilotUtils.autoPilotThroughput4K,
|
|
||||||
);
|
|
||||||
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
|
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
|
||||||
const [throughputError, setThroughputError] = useState<string>("");
|
const [throughputError, setThroughputError] = useState<string>("");
|
||||||
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
|
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
|
||||||
@@ -47,7 +53,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
|
|
||||||
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||||
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
|
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// throughput cap check for the initial state
|
// throughput cap check for the initial state
|
||||||
let totalThroughput = 0;
|
let totalThroughput = 0;
|
||||||
@@ -157,9 +162,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
|
|
||||||
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
|
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
|
||||||
if (mode === "Autoscale") {
|
if (mode === "Autoscale") {
|
||||||
const defaultThroughput = isFreeTier
|
|
||||||
? AutoPilotUtils.autoPilotThroughput1K
|
|
||||||
: AutoPilotUtils.autoPilotThroughput4K;
|
|
||||||
setThroughput(defaultThroughput);
|
setThroughput(defaultThroughput);
|
||||||
setIsAutoScaleSelected(true);
|
setIsAutoScaleSelected(true);
|
||||||
setThroughputValue(defaultThroughput);
|
setThroughputValue(defaultThroughput);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import { PhoenixClient } from "../Phoenix/PhoenixClient";
|
|||||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { isAccountNewerThanThresholdInMs, updateUserContext, userContext } from "../UserContext";
|
import { updateUserContext, userContext } from "../UserContext";
|
||||||
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||||
import { stringToBlob } from "../Utils/BlobUtils";
|
import { stringToBlob } from "../Utils/BlobUtils";
|
||||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||||
@@ -278,37 +278,6 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public openNPSSurveyDialog(): void {
|
|
||||||
if (!Platform.Portal || !["Postgres", "SQL", "Mongo"].includes(userContext.apiType)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ONE_DAY_IN_MS = 86400000;
|
|
||||||
const SEVEN_DAYS_IN_MS = 604800000;
|
|
||||||
|
|
||||||
// Try Cosmos DB subscription - survey shown to 100% of users at day 1 in Data Explorer.
|
|
||||||
if (userContext.isTryCosmosDBSubscription) {
|
|
||||||
if (isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS)) {
|
|
||||||
Logger.logInfo(
|
|
||||||
`Sending message to Portal to check if NPS Survey can be displayed in Try Cosmos DB ${userContext.apiType}`,
|
|
||||||
"Explorer/openNPSSurveyDialog",
|
|
||||||
);
|
|
||||||
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Show survey when an existing account is older than 7 days
|
|
||||||
if (
|
|
||||||
!isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", SEVEN_DAYS_IN_MS)
|
|
||||||
) {
|
|
||||||
Logger.logInfo(
|
|
||||||
`Sending message to Portal to check if NPS Survey can be displayed for existing ${userContext.apiType} account older than 7 days`,
|
|
||||||
"Explorer/openNPSSurveyDialog",
|
|
||||||
);
|
|
||||||
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async openCESCVAFeedbackBlade(): Promise<void> {
|
public async openCESCVAFeedbackBlade(): Promise<void> {
|
||||||
sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade });
|
sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade });
|
||||||
Logger.logInfo(
|
Logger.logInfo(
|
||||||
|
|||||||
@@ -174,15 +174,26 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
|
||||||
const explorerVersion = configContext.gitSha;
|
const explorerVersion = configContext.gitSha;
|
||||||
|
const isEmulator = configContext.platform === Platform.Emulator;
|
||||||
const shouldShowQueryPageOptions = userContext.apiType === "SQL";
|
const shouldShowQueryPageOptions = userContext.apiType === "SQL";
|
||||||
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
|
const showRetrySettings =
|
||||||
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
|
(userContext.apiType === "SQL" || userContext.apiType === "Tables" || userContext.apiType === "Gremlin") &&
|
||||||
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
|
!isEmulator;
|
||||||
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
|
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin" && !isEmulator;
|
||||||
|
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
||||||
|
const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
||||||
|
const showEnableEntraIdRbac =
|
||||||
|
userContext.apiType === "SQL" &&
|
||||||
|
userContext.authType === AuthType.AAD &&
|
||||||
|
configContext.platform !== Platform.Fabric &&
|
||||||
|
!isEmulator;
|
||||||
|
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled() && !isEmulator;
|
||||||
const shouldShowCopilotSampleDBOption =
|
const shouldShowCopilotSampleDBOption =
|
||||||
userContext.apiType === "SQL" &&
|
userContext.apiType === "SQL" &&
|
||||||
useQueryCopilot.getState().copilotEnabled &&
|
useQueryCopilot.getState().copilotEnabled &&
|
||||||
useDatabases.getState().sampleDataResourceTokenCollection;
|
useDatabases.getState().sampleDataResourceTokenCollection &&
|
||||||
|
!isEmulator;
|
||||||
|
|
||||||
const handlerOnSubmit = async () => {
|
const handlerOnSubmit = async () => {
|
||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
|
|
||||||
@@ -541,39 +552,37 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
{userContext.apiType === "SQL" &&
|
{showEnableEntraIdRbac && (
|
||||||
userContext.authType === AuthType.AAD &&
|
<AccordionItem value="2">
|
||||||
configContext.platform !== Platform.Fabric && (
|
<AccordionHeader>
|
||||||
<AccordionItem value="2">
|
<div className={styles.header}>Enable Entra ID RBAC</div>
|
||||||
<AccordionHeader>
|
</AccordionHeader>
|
||||||
<div className={styles.header}>Enable Entra ID RBAC</div>
|
<AccordionPanel>
|
||||||
</AccordionHeader>
|
<div className={styles.settingsSectionContainer}>
|
||||||
<AccordionPanel>
|
<div className={styles.settingsSectionDescription}>
|
||||||
<div className={styles.settingsSectionContainer}>
|
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra ID
|
||||||
<div className={styles.settingsSectionDescription}>
|
RBAC.
|
||||||
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
|
<a
|
||||||
ID RBAC.
|
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
|
||||||
<a
|
target="_blank"
|
||||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
>
|
||||||
rel="noopener noreferrer"
|
{" "}
|
||||||
>
|
Learn more{" "}
|
||||||
{" "}
|
</a>
|
||||||
Learn more{" "}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<ChoiceGroup
|
|
||||||
ariaLabelledBy="enableDataPlaneRBACOptions"
|
|
||||||
options={dataPlaneRBACOptionsList}
|
|
||||||
styles={choiceButtonStyles}
|
|
||||||
selectedKey={enableDataPlaneRBACOption}
|
|
||||||
onChange={handleOnDataPlaneRBACOptionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
<ChoiceGroup
|
||||||
</AccordionItem>
|
ariaLabelledBy="enableDataPlaneRBACOptions"
|
||||||
)}
|
options={dataPlaneRBACOptionsList}
|
||||||
{userContext.apiType === "SQL" && (
|
styles={choiceButtonStyles}
|
||||||
|
selectedKey={enableDataPlaneRBACOption}
|
||||||
|
onChange={handleOnDataPlaneRBACOptionChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
)}
|
||||||
|
{userContext.apiType === "SQL" && !isEmulator && (
|
||||||
<>
|
<>
|
||||||
<AccordionItem value="3">
|
<AccordionItem value="3">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
@@ -671,7 +680,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{(userContext.apiType === "SQL" || userContext.apiType === "Tables" || userContext.apiType === "Gremlin") && (
|
{showRetrySettings && (
|
||||||
<AccordionItem value="6">
|
<AccordionItem value="6">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div className={styles.header}>Retry Settings</div>
|
<div className={styles.header}>Retry Settings</div>
|
||||||
@@ -744,29 +753,30 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
|
{!isEmulator && (
|
||||||
<AccordionItem value="7">
|
<AccordionItem value="7">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<div className={styles.header}>Enable container pagination</div>
|
<div className={styles.header}>Enable container pagination</div>
|
||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
<AccordionPanel>
|
<AccordionPanel>
|
||||||
<div className={styles.settingsSectionContainer}>
|
<div className={styles.settingsSectionContainer}>
|
||||||
<div className={styles.settingsSectionDescription}>
|
<div className={styles.settingsSectionDescription}>
|
||||||
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
styles={{
|
||||||
|
label: { padding: 0 },
|
||||||
|
}}
|
||||||
|
className="padding"
|
||||||
|
ariaLabel="Enable container pagination"
|
||||||
|
checked={containerPaginationEnabled}
|
||||||
|
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
|
||||||
|
label="Enable container pagination"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Checkbox
|
</AccordionPanel>
|
||||||
styles={{
|
</AccordionItem>
|
||||||
label: { padding: 0 },
|
)}
|
||||||
}}
|
|
||||||
className="padding"
|
|
||||||
ariaLabel="Enable container pagination"
|
|
||||||
checked={containerPaginationEnabled}
|
|
||||||
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
|
|
||||||
label="Enable container pagination"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
{shouldShowCrossPartitionOption && (
|
{shouldShowCrossPartitionOption && (
|
||||||
<AccordionItem value="8">
|
<AccordionItem value="8">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
|
|||||||
import { Allotment, AllotmentHandle } from "allotment";
|
import { Allotment, AllotmentHandle } from "allotment";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
const useSidebarStyles = makeStyles({
|
const useSidebarStyles = makeStyles({
|
||||||
sidebar: {
|
sidebar: {
|
||||||
@@ -109,6 +109,7 @@ interface GlobalCommand {
|
|||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
keyboardAction?: KeyboardAction;
|
keyboardAction?: KeyboardAction;
|
||||||
|
ref?: React.RefObject<HTMLButtonElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||||
@@ -118,6 +119,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
// However, that messes with the Menu positioning, so we need to get a reference to the 'div' to pass to the Menu.
|
// However, that messes with the Menu positioning, so we need to get a reference to the 'div' to pass to the Menu.
|
||||||
// We can't use a ref though, because it would be set after the Menu is rendered, so we use a state value to force a re-render.
|
// We can't use a ref though, because it would be set after the Menu is rendered, so we use a state value to force a re-render.
|
||||||
const [globalCommandButton, setGlobalCommandButton] = useState<HTMLElement | null>(null);
|
const [globalCommandButton, setGlobalCommandButton] = useState<HTMLElement | null>(null);
|
||||||
|
const primaryFocusableRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
const actions = useMemo<GlobalCommand[]>(() => {
|
const actions = useMemo<GlobalCommand[]>(() => {
|
||||||
if (
|
if (
|
||||||
@@ -177,6 +179,16 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
);
|
);
|
||||||
}, [actions, setKeyboardActions]);
|
}, [actions, setKeyboardActions]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (primaryFocusableRef.current) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
primaryFocusableRef.current.focus();
|
||||||
|
}, 0);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!primaryAction) {
|
if (!primaryAction) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -184,7 +196,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.globalCommandsContainer} data-test="GlobalCommands">
|
<div className={styles.globalCommandsContainer} data-test="GlobalCommands">
|
||||||
{actions.length === 1 ? (
|
{actions.length === 1 ? (
|
||||||
<Button icon={primaryAction.icon} onClick={onPrimaryActionClick}>
|
<Button icon={primaryAction.icon} onClick={onPrimaryActionClick} ref={primaryFocusableRef}>
|
||||||
{primaryAction.label}
|
{primaryAction.label}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
@@ -194,7 +206,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
<div ref={setGlobalCommandButton}>
|
<div ref={setGlobalCommandButton}>
|
||||||
<SplitButton
|
<SplitButton
|
||||||
menuButton={{ ...triggerProps, "aria-label": "More commands" }}
|
menuButton={{ ...triggerProps, "aria-label": "More commands" }}
|
||||||
primaryActionButton={{ onClick: onPrimaryActionClick }}
|
primaryActionButton={{ onClick: onPrimaryActionClick, ref: primaryFocusableRef }}
|
||||||
className={styles.globalCommandsSplitButton}
|
className={styles.globalCommandsSplitButton}
|
||||||
icon={primaryAction.icon}
|
icon={primaryAction.icon}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
|
|||||||
</span>
|
</span>
|
||||||
<span className="tabNavText">{tabTitle}</span>
|
<span className="tabNavText">{tabTitle}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="tabIconSection" aria-hidden="true">
|
<span className="tabIconSection">
|
||||||
<CloseButton tab={tab} active={active} hovering={hovering} tabKind={tabKind} ariaLabel={tabTitle} />
|
<CloseButton tab={tab} active={active} hovering={hovering} tabKind={tabKind} ariaLabel={tabTitle} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -247,7 +247,7 @@ function TabPane({ tab, active }: { tab: Tab; active: boolean }) {
|
|||||||
if (tab) {
|
if (tab) {
|
||||||
if ("render" in tab) {
|
if ("render" in tab) {
|
||||||
return (
|
return (
|
||||||
<div id={tab.tabId} data-test={`Tab:${tab.tabId}`} {...attrs}>
|
<div data-test={`Tab:${tab.tabId}`} {...attrs}>
|
||||||
{tab.render()}
|
{tab.render()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (explorer) {
|
if (explorer) {
|
||||||
applyExplorerBindings(explorer);
|
applyExplorerBindings(explorer);
|
||||||
explorer.openNPSSurveyDialog();
|
|
||||||
}
|
}
|
||||||
}, [explorer]);
|
}, [explorer]);
|
||||||
|
|
||||||
@@ -182,6 +181,11 @@ async function configureFabric(): Promise<Explorer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openFirstContainer = async (explorer: Explorer, databaseName: string, collectionName?: string) => {
|
const openFirstContainer = async (explorer: Explorer, databaseName: string, collectionName?: string) => {
|
||||||
|
if (useTabs.getState().openedTabs.length > 0) {
|
||||||
|
// Don't open any tabs if there are already tabs open
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Expand database first
|
// Expand database first
|
||||||
databaseName = sessionStorage.getItem("openDatabaseName") ?? databaseName;
|
databaseName = sessionStorage.getItem("openDatabaseName") ?? databaseName;
|
||||||
const database = useDatabases.getState().databases.find((db) => db.id() === databaseName);
|
const database = useDatabases.getState().databases.find((db) => db.id() === databaseName);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const msRestNodeAuth = require("@azure/ms-rest-nodeauth");
|
const { AzureCliCredential } = require("@azure/identity");
|
||||||
const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb");
|
const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb");
|
||||||
const ms = require("ms");
|
const ms = require("ms");
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ function friendlyTime(date) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const credentials = await msRestNodeAuth.AzureCliCredentials.create();
|
const credentials = new AzureCliCredential();
|
||||||
const client = new CosmosDBManagementClient(credentials, subscriptionId);
|
const client = new CosmosDBManagementClient(credentials, subscriptionId);
|
||||||
const accounts = await client.databaseAccounts.list(resourceGroupName);
|
const accounts = await client.databaseAccounts.list(resourceGroupName);
|
||||||
for (const account of accounts) {
|
for (const account of accounts) {
|
||||||
|
|||||||
Reference in New Issue
Block a user