mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-25 20:54:18 +00:00
Compare commits
4 Commits
copilot/su
...
users/saks
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56bc65f5fb | ||
|
|
6c2ad8b001 | ||
|
|
31385950dd | ||
|
|
6dce2632c8 |
@@ -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);
|
|
||||||
169
test/sql/scaleAndSettings/dataMasking.spec.ts
Normal file
169
test/sql/scaleAndSettings/dataMasking.spec.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { expect, Page, test } from "@playwright/test";
|
||||||
|
import { CommandBarButton, DataExplorer, ONE_MINUTE_MS, TestAccount } from "../../fx";
|
||||||
|
import { createTestSQLContainer, TestContainerContext } from "../../testData";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for Dynamic Data Masking (DDM) feature.
|
||||||
|
*
|
||||||
|
* Prerequisites:
|
||||||
|
* - Test account must have the EnableDynamicDataMasking capability enabled
|
||||||
|
* - If the capability is not enabled, the DataMaskingTab will not be visible and tests will be skipped
|
||||||
|
*
|
||||||
|
* Important Notes:
|
||||||
|
* - Once DDM is enabled on a container, it cannot be disabled (isPolicyEnabled cannot be set to false)
|
||||||
|
* - Tests focus on enabling DDM and modifying the masking policy configuration
|
||||||
|
*/
|
||||||
|
test.describe("Data Masking under Scale & Settings", () => {
|
||||||
|
let context: TestContainerContext = null!;
|
||||||
|
let explorer: DataExplorer = null!;
|
||||||
|
|
||||||
|
test.beforeAll("Create Test Database", async () => {
|
||||||
|
context = await createTestSQLContainer();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.beforeEach("Open Data Masking tab under Scale & Settings", async ({ page }) => {
|
||||||
|
explorer = await DataExplorer.open(page, TestAccount.SQL);
|
||||||
|
|
||||||
|
// Click Scale & Settings
|
||||||
|
await explorer.openScaleAndSettings(context);
|
||||||
|
|
||||||
|
// Check if Data Masking tab is available (requires EnableDynamicDataMasking capability)
|
||||||
|
const dataMaskingTab = explorer.frame.getByTestId("settings-tab-header/DataMaskingTab");
|
||||||
|
const isTabVisible = await dataMaskingTab.isVisible().catch(() => false);
|
||||||
|
|
||||||
|
if (!isTabVisible) {
|
||||||
|
test.skip(
|
||||||
|
true,
|
||||||
|
"Data Masking tab is not available. Test account may not have EnableDynamicDataMasking capability.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await dataMaskingTab.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll("Delete Test Database", async () => {
|
||||||
|
await context?.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Data Masking editor should be visible", async () => {
|
||||||
|
// Verify the Data Masking editor is visible
|
||||||
|
const dataMaskingEditor = explorer.frame.locator(".settingsV2Editor");
|
||||||
|
await expect(dataMaskingEditor).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Enable data masking policy with valid JSON", async ({ page }) => {
|
||||||
|
await clearDataMaskingEditorContent({ page });
|
||||||
|
|
||||||
|
// Type a valid data masking policy
|
||||||
|
// Note: Once DDM is enabled on a container, it cannot be disabled
|
||||||
|
const validPolicy = JSON.stringify(
|
||||||
|
{
|
||||||
|
includedPaths: [
|
||||||
|
{
|
||||||
|
path: "/email",
|
||||||
|
strategy: "Default",
|
||||||
|
startPosition: 0,
|
||||||
|
length: -1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
excludedPaths: [],
|
||||||
|
isPolicyEnabled: true,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.keyboard.type(validPolicy);
|
||||||
|
|
||||||
|
// Wait a moment for the changes to be processed
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Click Save button
|
||||||
|
const saveButton = explorer.commandBarButton(CommandBarButton.Save);
|
||||||
|
await expect(saveButton).toBeEnabled();
|
||||||
|
await saveButton.click();
|
||||||
|
|
||||||
|
// Verify success message
|
||||||
|
await expect(explorer.getConsoleHeaderStatus()).toContainText(
|
||||||
|
`Successfully updated container ${context.container.id}`,
|
||||||
|
{
|
||||||
|
timeout: 2 * ONE_MINUTE_MS,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Show validation error for invalid JSON", async ({ page }) => {
|
||||||
|
await clearDataMaskingEditorContent({ page });
|
||||||
|
|
||||||
|
// Type invalid JSON
|
||||||
|
await page.keyboard.type("{invalid json}");
|
||||||
|
|
||||||
|
// Wait a moment for validation
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Save button should be disabled due to invalid JSON
|
||||||
|
const saveButton = explorer.commandBarButton(CommandBarButton.Save);
|
||||||
|
await expect(saveButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Update data masking policy with multiple paths", async ({ page }) => {
|
||||||
|
await clearDataMaskingEditorContent({ page });
|
||||||
|
|
||||||
|
// Type a policy with multiple included paths and excluded paths
|
||||||
|
const multiPathPolicy = JSON.stringify(
|
||||||
|
{
|
||||||
|
includedPaths: [
|
||||||
|
{
|
||||||
|
path: "/email",
|
||||||
|
strategy: "Default",
|
||||||
|
startPosition: 0,
|
||||||
|
length: -1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/phoneNumber",
|
||||||
|
strategy: "Default",
|
||||||
|
startPosition: 0,
|
||||||
|
length: -1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
excludedPaths: ["/id", "/timestamp"],
|
||||||
|
isPolicyEnabled: true,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.keyboard.type(multiPathPolicy);
|
||||||
|
|
||||||
|
// Wait a moment for the changes to be processed
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Click Save button
|
||||||
|
const saveButton = explorer.commandBarButton(CommandBarButton.Save);
|
||||||
|
await expect(saveButton).toBeEnabled();
|
||||||
|
await saveButton.click();
|
||||||
|
|
||||||
|
// Verify success message
|
||||||
|
await expect(explorer.getConsoleHeaderStatus()).toContainText(
|
||||||
|
`Successfully updated container ${context.container.id}`,
|
||||||
|
{
|
||||||
|
timeout: 2 * ONE_MINUTE_MS,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to clear the data masking editor content.
|
||||||
|
*/
|
||||||
|
const clearDataMaskingEditorContent = async ({ page }: { page: Page }): Promise<void> => {
|
||||||
|
// Wait for the Monaco editor to be visible
|
||||||
|
await explorer.frame.waitForSelector(".settingsV2Editor", { state: "visible" });
|
||||||
|
const dataMaskingEditor = explorer.frame.locator(".settingsV2Editor");
|
||||||
|
await dataMaskingEditor.click();
|
||||||
|
|
||||||
|
// Clear existing content (Ctrl+A + Backspace does not work reliably with webkit)
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
await page.keyboard.press("Backspace");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user