mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-22 03:04:07 +00:00
Compare commits
6 Commits
copilot/su
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31385950dd | ||
|
|
6dce2632c8 | ||
|
|
80ad5f10d4 | ||
|
|
f02611c90e | ||
|
|
9646dfcf04 | ||
|
|
90f3c3a79e |
@@ -46,6 +46,10 @@ export type DataExploreMessageV3 =
|
|||||||
params: {
|
params: {
|
||||||
updateType: "created" | "deleted" | "settings";
|
updateType: "created" | "deleted" | "settings";
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: FabricMessageTypes.RestoreContainer;
|
||||||
|
params: [];
|
||||||
};
|
};
|
||||||
export interface GetCosmosTokenMessageOptions {
|
export interface GetCosmosTokenMessageOptions {
|
||||||
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
||||||
|
|||||||
@@ -516,7 +516,7 @@ describe("CopyJobActionMenu", () => {
|
|||||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle complete action disabled state for online jobs", () => {
|
it("should disable complete action when job is being updated", () => {
|
||||||
const job = createMockJob({
|
const job = createMockJob({
|
||||||
Status: CopyJobStatusType.InProgress,
|
Status: CopyJobStatusType.InProgress,
|
||||||
Mode: CopyJobMigrationType.Online,
|
Mode: CopyJobMigrationType.Online,
|
||||||
@@ -530,8 +530,34 @@ describe("CopyJobActionMenu", () => {
|
|||||||
const completeButton = screen.getByText("Complete");
|
const completeButton = screen.getByText("Complete");
|
||||||
fireEvent.click(completeButton);
|
fireEvent.click(completeButton);
|
||||||
|
|
||||||
|
// Simulate dialog confirmation to trigger state update
|
||||||
|
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
|
||||||
|
onOkCallback();
|
||||||
|
|
||||||
fireEvent.click(actionButton);
|
fireEvent.click(actionButton);
|
||||||
expect(screen.getByText("Complete")).toBeInTheDocument();
|
const completeButtonAfterClick = screen.getByText("Complete").closest("button");
|
||||||
|
expect(completeButtonAfterClick).toBeInTheDocument();
|
||||||
|
expect(completeButtonAfterClick).toHaveAttribute("aria-disabled", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should disable complete action when any other action is being performed", () => {
|
||||||
|
const job = createMockJob({
|
||||||
|
Status: CopyJobStatusType.InProgress,
|
||||||
|
Mode: CopyJobMigrationType.Online,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<TestComponentWrapper job={job} />);
|
||||||
|
|
||||||
|
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||||
|
fireEvent.click(actionButton);
|
||||||
|
|
||||||
|
const pauseButton = screen.getByText("Pause");
|
||||||
|
fireEvent.click(pauseButton);
|
||||||
|
fireEvent.click(actionButton);
|
||||||
|
|
||||||
|
const completeButtonAfterClick = screen.getByText("Complete").closest("button");
|
||||||
|
expect(completeButtonAfterClick).toBeInTheDocument();
|
||||||
|
expect(completeButtonAfterClick).toHaveAttribute("aria-disabled", "true");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
|||||||
|
|
||||||
const getMenuItems = (): IContextualMenuProps["items"] => {
|
const getMenuItems = (): IContextualMenuProps["items"] => {
|
||||||
const isThisJobUpdating = updatingJobAction?.jobName === job.Name;
|
const isThisJobUpdating = updatingJobAction?.jobName === job.Name;
|
||||||
const updatingAction = updatingJobAction?.action;
|
|
||||||
|
|
||||||
const baseItems = [
|
const baseItems = [
|
||||||
{
|
{
|
||||||
@@ -105,7 +104,7 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
|||||||
text: ContainerCopyMessages.MonitorJobs.Actions.complete,
|
text: ContainerCopyMessages.MonitorJobs.Actions.complete,
|
||||||
iconProps: { iconName: "CheckMark" },
|
iconProps: { iconName: "CheckMark" },
|
||||||
onClick: () => showActionConfirmationDialog(job, CopyJobActions.complete),
|
onClick: () => showActionConfirmationDialog(job, CopyJobActions.complete),
|
||||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.complete,
|
disabled: isThisJobUpdating,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return filteredItems;
|
return filteredItems;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
AddGlobalSecondaryIndexPanelProps,
|
AddGlobalSecondaryIndexPanelProps,
|
||||||
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
|
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
|
import { isFabric, isFabricNative, openRestoreContainerDialog } from "Platform/Fabric/FabricUtil";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
@@ -35,6 +35,7 @@ import StoredProcedure from "./Tree/StoredProcedure";
|
|||||||
import Trigger from "./Tree/Trigger";
|
import Trigger from "./Tree/Trigger";
|
||||||
import UserDefinedFunction from "./Tree/UserDefinedFunction";
|
import UserDefinedFunction from "./Tree/UserDefinedFunction";
|
||||||
import { useSelectedNode } from "./useSelectedNode";
|
import { useSelectedNode } from "./useSelectedNode";
|
||||||
|
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||||
|
|
||||||
export interface CollectionContextMenuButtonParams {
|
export interface CollectionContextMenuButtonParams {
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
@@ -60,6 +61,17 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (isFabricNative() && !userContext.fabricContext?.isReadOnly) {
|
||||||
|
const features = extractFeatures();
|
||||||
|
if (features?.enableRestoreContainer) {
|
||||||
|
items.push({
|
||||||
|
iconSrc: AddCollectionIcon,
|
||||||
|
onClick: () => openRestoreContainerDialog(),
|
||||||
|
label: `Restore ${getCollectionName()}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!isFabricNative() && (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations)) {
|
if (!isFabricNative() && (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations)) {
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: DeleteDatabaseIcon,
|
iconSrc: DeleteDatabaseIcon,
|
||||||
|
|||||||
@@ -54,6 +54,6 @@
|
|||||||
.mainButtonsContainer {
|
.mainButtonsContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0 16px;
|
gap: 0 16px;
|
||||||
margin-bottom: 10px
|
margin: 40px auto
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +164,23 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
const container = explorer;
|
const container = explorer;
|
||||||
const subscriptions: Array<{ dispose: () => void }> = [];
|
const subscriptions: Array<{ dispose: () => void }> = [];
|
||||||
|
|
||||||
|
let title: string;
|
||||||
|
let subtitle: string;
|
||||||
|
|
||||||
|
switch (userContext.apiType) {
|
||||||
|
case "Postgres":
|
||||||
|
title = "Welcome to Azure Cosmos DB for PostgreSQL";
|
||||||
|
subtitle = "Get started with our sample datasets, documentation, and additional tools.";
|
||||||
|
break;
|
||||||
|
case "VCoreMongo":
|
||||||
|
title = "Welcome to Azure DocumentDB (with MongoDB compatibility)";
|
||||||
|
subtitle = "Get started with our sample datasets, documentation, and additional tools.";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
title = "Welcome to Azure Cosmos DB";
|
||||||
|
subtitle = "Globally distributed, multi-model database service for any scale";
|
||||||
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
{
|
{
|
||||||
@@ -902,10 +919,11 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.splashScreenContainer}>
|
<div className={styles.splashScreenContainer}>
|
||||||
<div className={styles.splashScreen}>
|
<div className={styles.splashScreen}>
|
||||||
<h2 className={styles.title} role="heading" aria-label="Welcome to Azure Cosmos DB">
|
<h2 className={styles.title} role="heading" aria-label={title}>
|
||||||
Welcome to Azure Cosmos DB<span className="activePatch"></span>
|
{title}
|
||||||
|
<span className="activePatch"></span>
|
||||||
</h2>
|
</h2>
|
||||||
<div className={styles.subtitle}>Globally distributed, multi-model database service for any scale</div>
|
<div className={styles.subtitle}>{subtitle}</div>
|
||||||
{getSplashScreenButtons()}
|
{getSplashScreenButtons()}
|
||||||
{useCarousel.getState().showCoachMark && (
|
{useCarousel.getState().showCoachMark && (
|
||||||
<Coachmark
|
<Coachmark
|
||||||
|
|||||||
@@ -105,9 +105,12 @@ const App = (): JSX.Element => {
|
|||||||
// Scenario-based health tracking: start ApplicationLoad and complete phases.
|
// Scenario-based health tracking: start ApplicationLoad and complete phases.
|
||||||
const { startScenario, completePhase } = useMetricScenario();
|
const { startScenario, completePhase } = useMetricScenario();
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
startScenario(MetricScenario.ApplicationLoad);
|
// Only start scenario after config is initialized to avoid race conditions
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// with message handlers that depend on configContext.platform
|
||||||
}, []);
|
if (config) {
|
||||||
|
startScenario(MetricScenario.ApplicationLoad);
|
||||||
|
}
|
||||||
|
}, [config, startScenario]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (explorer) {
|
if (explorer) {
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
import { initializeIcons } from "@fluentui/react";
|
|
||||||
import "bootstrap/dist/css/bootstrap.css";
|
|
||||||
import React from "react";
|
|
||||||
import * as ReactDOM from "react-dom";
|
|
||||||
import { configContext, initializeConfiguration } from "../ConfigContext";
|
|
||||||
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
|
|
||||||
import { GalleryTab } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
|
||||||
import {
|
|
||||||
NotebookViewerComponent,
|
|
||||||
NotebookViewerComponentProps,
|
|
||||||
} from "../Explorer/Controls/NotebookViewer/NotebookViewerComponent";
|
|
||||||
import * as FileSystemUtil from "../Explorer/Notebook/FileSystemUtil";
|
|
||||||
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
|
|
||||||
import * as GalleryUtils from "../Utils/GalleryUtils";
|
|
||||||
|
|
||||||
const onInit = async () => {
|
|
||||||
initializeIcons();
|
|
||||||
await initializeConfiguration();
|
|
||||||
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
|
|
||||||
const notebookViewerProps = GalleryUtils.getNotebookViewerProps(window.location.search);
|
|
||||||
let backNavigationText: string;
|
|
||||||
let onBackClick: () => void;
|
|
||||||
if (galleryViewerProps.selectedTab !== undefined) {
|
|
||||||
backNavigationText = GalleryUtils.getTabTitle(galleryViewerProps.selectedTab);
|
|
||||||
onBackClick = () =>
|
|
||||||
(window.location.href = `${configContext.hostedExplorerURL}gallery.html?tab=${
|
|
||||||
GalleryTab[galleryViewerProps.selectedTab]
|
|
||||||
}`);
|
|
||||||
}
|
|
||||||
const hideInputs = notebookViewerProps.hideInputs;
|
|
||||||
|
|
||||||
const notebookUrl = decodeURIComponent(notebookViewerProps.notebookUrl);
|
|
||||||
|
|
||||||
const galleryItemId = notebookViewerProps.galleryItemId;
|
|
||||||
let galleryItem: IGalleryItem;
|
|
||||||
|
|
||||||
if (galleryItemId) {
|
|
||||||
const junoClient = new JunoClient();
|
|
||||||
const galleryItemJunoResponse = await junoClient.getNotebookInfo(galleryItemId);
|
|
||||||
galleryItem = galleryItemJunoResponse.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The main purpose of hiding the prompt is to hide everything when hiding inputs.
|
|
||||||
// It is generally not very useful to just hide the prompt.
|
|
||||||
const hidePrompts = hideInputs;
|
|
||||||
|
|
||||||
render(notebookUrl, backNavigationText, hideInputs, hidePrompts, galleryItem, onBackClick);
|
|
||||||
};
|
|
||||||
|
|
||||||
const render = (
|
|
||||||
notebookUrl: string,
|
|
||||||
backNavigationText: string,
|
|
||||||
hideInputs?: boolean,
|
|
||||||
hidePrompts?: boolean,
|
|
||||||
galleryItem?: IGalleryItem,
|
|
||||||
onBackClick?: () => void,
|
|
||||||
) => {
|
|
||||||
const props: NotebookViewerComponentProps = {
|
|
||||||
junoClient: galleryItem ? new JunoClient() : undefined,
|
|
||||||
notebookUrl,
|
|
||||||
galleryItem,
|
|
||||||
backNavigationText,
|
|
||||||
hideInputs,
|
|
||||||
hidePrompts,
|
|
||||||
onBackClick: onBackClick,
|
|
||||||
onTagClick: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (galleryItem) {
|
|
||||||
document.title = FileSystemUtil.stripExtension(galleryItem.name, "ipynb");
|
|
||||||
}
|
|
||||||
|
|
||||||
const element = (
|
|
||||||
<>
|
|
||||||
<header>
|
|
||||||
<GalleryHeaderComponent />
|
|
||||||
</header>
|
|
||||||
<div style={{ marginLeft: 120, marginRight: 120 }}>
|
|
||||||
<NotebookViewerComponent {...props} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
ReactDOM.render(element, document.getElementById("notebookContent"));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Entry point
|
|
||||||
window.addEventListener("load", onInit);
|
|
||||||
@@ -105,6 +105,12 @@ const requestAndStoreAccessToken = async (): Promise<void> => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const openRestoreContainerDialog = (): void => {
|
||||||
|
if (isFabricNative()) {
|
||||||
|
sendCachedDataMessage(FabricMessageTypes.RestoreContainer, []);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check token validity and schedule a refresh if necessary
|
* Check token validity and schedule a refresh if necessary
|
||||||
* @param tokenTimestamp
|
* @param tokenTimestamp
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export type Features = {
|
|||||||
readonly disableConnectionStringLogin: boolean;
|
readonly disableConnectionStringLogin: boolean;
|
||||||
readonly enableContainerCopy: boolean;
|
readonly enableContainerCopy: boolean;
|
||||||
readonly enableCloudShell: boolean;
|
readonly enableCloudShell: boolean;
|
||||||
|
readonly enableRestoreContainer: boolean; // only for Fabric
|
||||||
|
|
||||||
// can be set via both flight and feature flag
|
// can be set via both flight and feature flag
|
||||||
autoscaleDefault: boolean;
|
autoscaleDefault: boolean;
|
||||||
@@ -111,6 +112,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
||||||
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
||||||
enableContainerCopy: "true" === get("enablecontainercopy"),
|
enableContainerCopy: "true" === get("enablecontainercopy"),
|
||||||
|
enableRestoreContainer: "true" === get("enablerestorecontainer"),
|
||||||
enableCloudShell: true,
|
enableCloudShell: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ describe("AuthorizationUtils", () => {
|
|||||||
partitionKeyDefault: false,
|
partitionKeyDefault: false,
|
||||||
partitionKeyDefault2: false,
|
partitionKeyDefault2: false,
|
||||||
notebooksDownBanner: false,
|
notebooksDownBanner: false,
|
||||||
|
enableRestoreContainer: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ let CONTAINER_ID: string;
|
|||||||
|
|
||||||
// Set up test database and container with data before all tests
|
// Set up test database and container with data before all tests
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
testContainer = await createTestSQLContainer(true);
|
testContainer = await createTestSQLContainer({ includeTestData: true });
|
||||||
DATABASE_ID = testContainer.database.id;
|
DATABASE_ID = testContainer.database.id;
|
||||||
CONTAINER_ID = testContainer.container.id;
|
CONTAINER_ID = testContainer.container.id;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user