Compare commits

..

2 Commits

Author SHA1 Message Date
Sakshi Gupta
e9151bcaf0 Fix formatting in Utils.test.ts 2025-12-29 13:13:23 +05:30
Sakshi Gupta
72debd0778 added a dark theme toggle button on Copyjobs next to refresh button and covered full feature 2025-12-29 12:57:26 +05:30
43 changed files with 341 additions and 469 deletions

View File

@@ -406,7 +406,11 @@ body {
width: 440px;
min-height: 565px;
}
.dataExplorerLoaderforcopyJobs{
width: 100%;
min-height: 565px;
right: 0;
}
.dataExplorerTabLoaderContainer {
left: initial;
top: initial;

View File

@@ -1,4 +1,5 @@
import { Overlay, Spinner, SpinnerSize } from "@fluentui/react";
import { useThemeStore } from "hooks/useTheme";
import React from "react";
interface LoadingOverlayProps {
@@ -7,6 +8,7 @@ interface LoadingOverlayProps {
}
const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) => {
const isDarkMode = useThemeStore((state) => state.isDarkMode);
if (!isLoading) {
return null;
}
@@ -15,7 +17,7 @@ const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) =>
<Overlay
styles={{
root: {
backgroundColor: "rgba(255,255,255,0.9)",
backgroundColor: isDarkMode ? "rgba(32, 31, 30, 0.9)" : "rgba(255,255,255,0.9)",
zIndex: 9999,
display: "flex",
alignItems: "center",
@@ -23,7 +25,11 @@ const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) =>
},
}}
>
<Spinner size={SpinnerSize.large} label={label} styles={{ label: { fontWeight: 600 } }} />
<Spinner
size={SpinnerSize.large}
label={label}
styles={{ label: { fontWeight: 600, color: isDarkMode ? "#ffffff" : "#323130" } }}
/>
</Overlay>
);
};

View File

@@ -11,3 +11,14 @@
gap: 8px;
align-items: center;
}
/* Override dark mode inherit for pagination icons */
body.isDarkMode .pager-container .ms-Button .ms-Button-icon,
body.isDarkMode .pager-container .ms-Button i {
color: var(--colorBrandForeground1);
}
body.isDarkMode .pager-container .ms-Button:disabled .ms-Button-icon,
body.isDarkMode .pager-container .ms-Button:disabled i {
color: var(--colorNeutralForegroundDisabled);
}

View File

@@ -31,6 +31,7 @@ const iconButtonStyles = {
outline: "none",
},
};
const textStyle: React.CSSProperties = { color: "var(--colorNeutralForeground1)" };
const Pager: React.FC<PagerProps> = ({
startIndex,
@@ -59,7 +60,7 @@ const Pager: React.FC<PagerProps> = ({
return (
<div className={className || "pager-container"}>
{showItemCount && (
<Text>
<Text style={textStyle}>
Showing {startIndex + 1} - {endIndex} of {totalCount} items
</Text>
)}
@@ -82,7 +83,7 @@ const Pager: React.FC<PagerProps> = ({
disabled={disabled || currentPage === 1}
styles={iconButtonStyles}
/>
<Text>
<Text style={textStyle}>
Page {currentPage} of {totalPages}
</Text>
<IconButton

View File

@@ -1,24 +1,28 @@
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import React from "react";
import { StyleConstants } from "../../../Common/StyleConstants";
import { useThemeStore } from "../../../hooks/useTheme";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import * as CommandBarUtil from "../../Menus/CommandBar/CommandBarUtil";
import { getThemeTokens } from "../../Theme/ThemeUtil";
import { ContainerCopyProps } from "../Types/CopyJobTypes";
import { getCommandBarButtons } from "./Utils";
const backgroundColor = StyleConstants.BaseLight;
const rootStyle = {
root: {
backgroundColor: backgroundColor,
},
};
const CopyJobCommandBar: React.FC<ContainerCopyProps> = ({ explorer }) => {
const isDarkMode = useThemeStore((state) => state.isDarkMode);
const themeTokens = getThemeTokens(isDarkMode);
const backgroundColor = themeTokens.colorNeutralBackground1;
const rootStyle = {
root: {
backgroundColor: backgroundColor,
},
};
const commandBarItems: CommandButtonComponentProps[] = getCommandBarButtons(explorer);
const controlButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(commandBarItems, backgroundColor);
return (
<div className="commandBarContainer">
<div className="commandBarContainer" style={{ backgroundColor }}>
<FluentCommandBar
ariaLabel="Use left and right arrow keys to navigate between commands"
styles={rootStyle}

View File

@@ -82,9 +82,9 @@ describe("CommandBar Utils", () => {
it("should include feedback button when platform is Portal", () => {
const buttons = getCommandBarButtons(mockExplorer);
expect(buttons.length).toBe(3);
expect(buttons.length).toBe(4);
const feedbackButton = buttons[2];
const feedbackButton = buttons[3];
expect(feedbackButton).toBeDefined();
expect(feedbackButton.ariaLabel).toBe("Provide feedback on copy jobs");
expect(feedbackButton.tooltipText).toBe("Feedback");
@@ -107,7 +107,7 @@ describe("CommandBar Utils", () => {
const { getCommandBarButtons: getCommandBarButtonsEmulator } = await import("./Utils");
const buttons = getCommandBarButtonsEmulator(mockExplorer);
expect(buttons.length).toBe(2);
expect(buttons.length).toBe(3);
});
it("should call openCreateCopyJobPanel when create button is clicked", () => {
@@ -131,7 +131,7 @@ describe("CommandBar Utils", () => {
it("should call openContainerCopyFeedbackBlade when feedback button is clicked", () => {
const buttons = getCommandBarButtons(mockExplorer);
const feedbackButton = buttons[2];
const feedbackButton = buttons[3];
feedbackButton.onCommandClick({} as React.SyntheticEvent);
@@ -148,7 +148,10 @@ describe("CommandBar Utils", () => {
expect(buttons[1].iconAlt).toBe("Refresh");
expect(buttons[2].iconSrc).toBeDefined();
expect(buttons[2].iconAlt).toBe("Feedback");
expect(buttons[2].iconAlt).toBe("Dark Theme");
expect(buttons[3].iconSrc).toBeDefined();
expect(buttons[3].iconAlt).toBe("Feedback");
});
it("should handle null MonitorCopyJobsRefState ref gracefully", () => {
@@ -202,12 +205,13 @@ describe("CommandBar Utils", () => {
});
});
it("should maintain button order: create, refresh, feedback", () => {
it("should maintain button order: create, refresh, themeToggle, feedback", () => {
const buttons = getCommandBarButtons(mockExplorer);
expect(buttons[0].tooltipText).toBe("Create Copy Job");
expect(buttons[1].tooltipText).toBe("Refresh");
expect(buttons[2].tooltipText).toBe("Feedback");
expect(buttons[2].tooltipText).toBe("Dark Theme");
expect(buttons[3].tooltipText).toBe("Feedback");
});
});
@@ -229,7 +233,7 @@ describe("CommandBar Utils", () => {
buttons[1].onCommandClick({} as React.SyntheticEvent);
expect(mockRefreshJobList).toHaveBeenCalled();
buttons[2].onCommandClick({} as React.SyntheticEvent);
buttons[3].onCommandClick({} as React.SyntheticEvent);
expect(mockOpenContainerCopyFeedbackBlade).toHaveBeenCalled();
});
});

View File

@@ -1,7 +1,10 @@
import AddIcon from "../../../../images/Add.svg";
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import MoonIcon from "../../../../images/MoonIcon.svg";
import RefreshIcon from "../../../../images/refresh-cosmos.svg";
import SunIcon from "../../../../images/SunIcon.svg";
import { configContext, Platform } from "../../../ConfigContext";
import { useThemeStore } from "../../../hooks/useTheme";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import * as Actions from "../Actions/CopyJobActions";
@@ -11,6 +14,7 @@ import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes";
function getCopyJobBtns(explorer: Explorer): CopyJobCommandBarBtnType[] {
const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref);
const isDarkMode = useThemeStore.getState().isDarkMode;
const buttons: CopyJobCommandBarBtnType[] = [
{
key: "createCopyJob",
@@ -26,7 +30,15 @@ function getCopyJobBtns(explorer: Explorer): CopyJobCommandBarBtnType[] {
ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel,
onClick: () => monitorCopyJobsRef?.refreshJobList(),
},
{
key: "themeToggle",
iconSrc: isDarkMode ? SunIcon : MoonIcon,
label: isDarkMode ? "Light Theme" : "Dark Theme",
ariaLabel: isDarkMode ? "Switch to Light Theme" : "Switch to Dark Theme",
onClick: () => useThemeStore.getState().toggleTheme(),
},
];
if (configContext.platform === Platform.Portal) {
buttons.push({
key: "feedback",

View File

@@ -12,7 +12,12 @@ import useToggle from "./hooks/useToggle";
const managedIdentityTooltip = (
<Text>
{ContainerCopyMessages.addManagedIdentity.tooltip.content} &nbsp;
<Link href={ContainerCopyMessages.addManagedIdentity.tooltip.href} target="_blank" rel="noopener noreferrer">
<Link
style={{ color: "var(--colorBrandForeground1)" }}
href={ContainerCopyMessages.addManagedIdentity.tooltip.href}
target="_blank"
rel="noopener noreferrer"
>
{ContainerCopyMessages.addManagedIdentity.tooltip.hrefText}
</Link>
</Text>
@@ -26,7 +31,7 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
return (
<Stack className="addManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<Text>
<Text className="themeText">
{ContainerCopyMessages.addManagedIdentity.description}&ensp;
<Link href={ContainerCopyMessages.addManagedIdentity.descriptionHref} target="_blank" rel="noopener noreferrer">
{ContainerCopyMessages.addManagedIdentity.descriptionHrefText}

View File

@@ -13,7 +13,12 @@ import useToggle from "./hooks/useToggle";
const TooltipContent = (
<Text>
{ContainerCopyMessages.readPermissionAssigned.tooltip.content} &nbsp;
<Link href={ContainerCopyMessages.readPermissionAssigned.tooltip.href} target="_blank" rel="noopener noreferrer">
<Link
style={{ color: "var(--colorBrandForeground1)" }}
href={ContainerCopyMessages.readPermissionAssigned.tooltip.href}
target="_blank"
rel="noopener noreferrer"
>
{ContainerCopyMessages.readPermissionAssigned.tooltip.hrefText}
</Link>
</Text>

View File

@@ -47,8 +47,8 @@ const PermissionGroup: React.FC<PermissionGroupConfig> = ({ title, description,
tokens={{ childrenGap: 15 }}
styles={{
root: {
background: "#fafafa",
border: "1px solid #e1e1e1",
background: "var(--colorNeutralBackground2)",
border: "1px solid var(--colorNeutralStroke1)",
borderRadius: 8,
padding: 16,
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
@@ -56,11 +56,11 @@ const PermissionGroup: React.FC<PermissionGroupConfig> = ({ title, description,
}}
>
<Stack tokens={{ childrenGap: 5 }}>
<Text variant="medium" style={{ fontWeight: 600 }}>
<Text variant="medium" style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>
{title}
</Text>
{description && (
<Text variant="small" styles={{ root: { color: "#605E5C" } }}>
<Text variant="small" styles={{ root: { color: "var(--colorNeutralForeground2)" } }}>
{description}
</Text>
)}
@@ -100,7 +100,7 @@ const AssignPermissions = () => {
return (
<Stack className="assignPermissionsContainer" tokens={{ childrenGap: 20 }}>
<Text variant="medium">
<Text variant="medium" style={{ color: "var(--colorNeutralForeground1)" }}>
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription(
copyJobState?.source?.account?.name || "",

View File

@@ -12,7 +12,12 @@ import useToggle from "./hooks/useToggle";
const managedIdentityTooltip = (
<Text>
{ContainerCopyMessages.defaultManagedIdentity.tooltip.content} &nbsp;
<Link href={ContainerCopyMessages.defaultManagedIdentity.tooltip.href} target="_blank" rel="noopener noreferrer">
<Link
style={{ color: "var(--colorBrandForeground1)" }}
href={ContainerCopyMessages.defaultManagedIdentity.tooltip.href}
target="_blank"
rel="noopener noreferrer"
>
{ContainerCopyMessages.defaultManagedIdentity.tooltip.hrefText}
</Link>
</Text>

View File

@@ -13,7 +13,12 @@ import InfoTooltip from "../Components/InfoTooltip";
const tooltipContent = (
<Text>
{ContainerCopyMessages.pointInTimeRestore.tooltip.content} &nbsp;
<Link href={ContainerCopyMessages.pointInTimeRestore.tooltip.href} target="_blank" rel="noopener noreferrer">
<Link
style={{ color: "var(--colorBrandForeground1)" }}
href={ContainerCopyMessages.pointInTimeRestore.tooltip.href}
target="_blank"
rel="noopener noreferrer"
>
{ContainerCopyMessages.pointInTimeRestore.tooltip.hrefText}
</Link>
</Text>

View File

@@ -5,7 +5,7 @@ exports[`AddManagedIdentity Snapshot Tests renders initial state correctly 1`] =
class="ms-Stack addManagedIdentityContainer css-109"
>
<span
class="css-110"
class="themeText css-110"
>
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you dont have to store any credentials in code.
@@ -92,7 +92,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
class="ms-Stack addManagedIdentityContainer css-109"
>
<span
class="css-110"
class="themeText css-110"
>
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you dont have to store any credentials in code.
@@ -192,13 +192,13 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
</div>
</div>
<span
class="css-124"
class="themeText css-124"
style="font-weight: 600;"
>
Enable system assigned managed identity
</span>
<span
class="css-110"
class="themeText css-110"
>
Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button.
</span>
@@ -261,7 +261,7 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
class="ms-Stack addManagedIdentityContainer css-109"
>
<span
class="css-110"
class="themeText css-110"
>
A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you dont have to store any credentials in code.
@@ -345,13 +345,13 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
style="max-width: 450px;"
>
<span
class="css-124"
class="themeText css-124"
style="font-weight: 600;"
>
Enable system assigned managed identity
</span>
<span
class="css-110"
class="themeText css-110"
>
Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button.
</span>

View File

@@ -22,10 +22,10 @@ const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(
style={{ maxWidth: 450 }}
>
<LoadingOverlay isLoading={isLoading} label={ContainerCopyMessages.popoverOverlaySpinnerLabel} />
<Text variant="mediumPlus" style={{ fontWeight: 600 }}>
<Text variant="mediumPlus" className="themeText" style={{ fontWeight: 600 }}>
{title}
</Text>
<Text>{children}</Text>
<Text className="themeText">{children}</Text>
<Stack horizontal tokens={{ childrenGap: 20 }}>
<PrimaryButton text={"Yes"} onClick={onPrimary} disabled={isLoading} />
<DefaultButton text="No" onClick={onCancel} disabled={isLoading} />

View File

@@ -7,11 +7,11 @@ exports[`PopoverMessage Component Edge Cases should handle empty string title 1`
style="max-width: 450px;"
>
<span
class="css-110"
class="themeText css-110"
style="font-weight: 600;"
/>
<span
class="css-111"
class="themeText css-111"
>
<div>
Test content
@@ -74,7 +74,7 @@ exports[`PopoverMessage Component Edge Cases should handle null children 1`] = `
style="max-width: 450px;"
>
<span
class="css-110"
class="themeText css-110"
style="font-weight: 600;"
>
Test Title
@@ -136,7 +136,7 @@ exports[`PopoverMessage Component Edge Cases should handle undefined children 1`
style="max-width: 450px;"
>
<span
class="css-110"
class="themeText css-110"
style="font-weight: 600;"
>
Test Title
@@ -198,13 +198,13 @@ exports[`PopoverMessage Component Edge Cases should handle very long title 1`] =
style="max-width: 450px;"
>
<span
class="css-110"
class="themeText css-110"
style="font-weight: 600;"
>
This is a very long title that might cause layout issues or text wrapping in the popover component
</span>
<span
class="css-111"
class="themeText css-111"
>
<div>
Test content
@@ -269,13 +269,13 @@ exports[`PopoverMessage Component Rendering should render correctly when visible
style="max-width: 450px;"
>
<span
class="css-110"
class="themeText css-110"
style="font-weight: 600;"
>
Test Title
</span>
<span
class="css-111"
class="themeText css-111"
>
<div>
Test content
@@ -338,13 +338,13 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
style="max-width: 450px;"
>
<span
class="css-110"
class="themeText css-110"
style="font-weight: 600;"
>
Test Title
</span>
<span
class="css-111"
class="themeText css-111"
>
<div>
<p>
@@ -412,13 +412,13 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
style="max-width: 450px;"
>
<span
class="css-110"
class="themeText css-110"
style="font-weight: 600;"
>
Custom Title
</span>
<span
class="css-111"
class="themeText css-111"
>
<div>
Test content
@@ -485,13 +485,13 @@ exports[`PopoverMessage Component Rendering should render correctly with loading
data-testid="loading-overlay"
/>
<span
class="css-110"
class="themeText css-110"
style="font-weight: 600;"
>
Test Title
</span>
<span
class="css-111"
class="themeText css-111"
>
<div>
Test content

View File

@@ -41,7 +41,7 @@ const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapp
return (
<Stack className="addCollectionPanelWrapper">
<Stack.Item className="addCollectionPanelHeader">
<Text>{ContainerCopyMessages.createNewContainerSubHeading}</Text>
<Text className="themeText">{ContainerCopyMessages.createNewContainerSubHeading}</Text>
</Stack.Item>
<Stack.Item className="addCollectionPanelBody">
<AddCollectionPanel explorer={explorer} isCopyJobFlow={true} onSubmitSuccess={handleAddCollectionSuccess} />

View File

@@ -9,7 +9,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot 1`]
class="ms-StackItem addCollectionPanelHeader css-110"
>
<span
class="css-111"
class="themeText css-111"
>
Select the properties for your container.
</span>
@@ -50,7 +50,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
class="ms-StackItem addCollectionPanelHeader css-110"
>
<span
class="css-111"
class="themeText css-111"
>
Select the properties for your container.
</span>
@@ -91,7 +91,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
class="ms-StackItem addCollectionPanelHeader css-110"
>
<span
class="css-111"
class="themeText css-111"
>
Select the properties for your container.
</span>
@@ -132,7 +132,7 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
class="ms-StackItem addCollectionPanelHeader css-110"
>
<span
class="css-111"
class="themeText css-111"
>
Select the properties for your container.
</span>

View File

@@ -1,6 +1,6 @@
/* eslint-disable react/prop-types */
/* eslint-disable react/display-name */
import { Checkbox, Stack } from "@fluentui/react";
import { Checkbox, ICheckboxStyles, Stack } from "@fluentui/react";
import React from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
@@ -9,8 +9,25 @@ interface MigrationTypeCheckboxProps {
onChange: (_ev?: React.FormEvent, checked?: boolean) => void;
}
export const MigrationTypeCheckbox: React.FC<MigrationTypeCheckboxProps> = React.memo(({ checked, onChange }) => (
<Stack horizontal horizontalAlign="space-between" className="migrationTypeRow">
<Checkbox label={ContainerCopyMessages.migrationTypeCheckboxLabel} checked={checked} onChange={onChange} />
</Stack>
));
const checkboxStyles: ICheckboxStyles = {
text: { color: "var(--colorNeutralForeground1)" },
checkbox: { borderColor: "var(--colorNeutralStroke1)" },
root: {
selectors: {
":hover .ms-Checkbox-text": { color: "var(--colorNeutralForeground1)" },
},
},
};
export const MigrationTypeCheckbox: React.FC<MigrationTypeCheckboxProps> = React.memo(({ checked, onChange }) => {
return (
<Stack horizontal horizontalAlign="space-between" className="migrationTypeRow">
<Checkbox
label={ContainerCopyMessages.migrationTypeCheckboxLabel}
checked={checked}
onChange={onChange}
styles={checkboxStyles}
/>
</Stack>
);
});

View File

@@ -21,7 +21,7 @@ const SelectAccount = React.memo(() => {
return (
<Stack data-test="Panel:SelectAccountContainer" className="selectAccountContainer" tokens={{ childrenGap: 15 }}>
<Text>{ContainerCopyMessages.selectAccountDescription}</Text>
<Text className="themeText">{ContainerCopyMessages.selectAccountDescription}</Text>
<SubscriptionDropdown />

View File

@@ -6,7 +6,7 @@ exports[`SelectAccount Component Rendering should render correctly with snapshot
data-test="Panel:SelectAccountContainer"
>
<span
class="css-110"
class="themeText css-110"
>
Please select a source account from which to copy.
</span>

View File

@@ -48,7 +48,7 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
return (
<Stack className="selectSourceAndTargetContainers" tokens={{ childrenGap: 25 }}>
<span>{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
<span className="themeText">{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
<DatabaseContainerSection
heading={ContainerCopyMessages.sourceContainerSubHeading}
databaseOptions={sourceDatabaseOptions}

View File

@@ -41,7 +41,11 @@ export const DatabaseContainerSection = ({
onChange={containerOnChange}
/>
{handleOnDemandCreateContainer && (
<ActionButton className="create-container-link-btn" onClick={() => handleOnDemandCreateContainer()}>
<ActionButton
className="create-container-link-btn"
style={{ color: "var(--colorBrandForeground1)" }}
onClick={() => handleOnDemandCreateContainer()}
>
{ContainerCopyMessages.createContainerButtonLabel}
</ActionButton>
)}

View File

@@ -1,5 +1,6 @@
import { DetailsList, DetailsListLayoutMode, IColumn, Stack, Text } from "@fluentui/react";
import React, { memo } from "react";
import { useThemeStore } from "../../../../hooks/useTheme";
import ContainerCopyMessages from "../../ContainerCopyMessages";
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
import { CopyJobType } from "../../Types/CopyJobTypes";
@@ -63,6 +64,19 @@ const getCopyJobDetailsListColumns = (): IColumn[] => {
};
const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
const isDarkMode = useThemeStore((state) => state.isDarkMode);
const errorMessageStyle: React.CSSProperties = {
whiteSpace: "pre-wrap",
...(isDarkMode && {
whiteSpace: "pre-wrap",
backgroundColor: "var(--colorNeutralBackground2)",
color: "var(--colorNeutralForeground1)",
padding: "10px",
borderRadius: "4px",
}),
};
const selectedContainers = [
{
sourceContainerName: job?.Source?.containerName || "N/A",
@@ -77,10 +91,10 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
<Stack className="copyJobDetailsContainer" tokens={{ childrenGap: 15 }} data-testid="copy-job-details">
{job.Error ? (
<Stack.Item data-testid="error-stack" style={sectionCss.verticalAlign}>
<Text className="bold" style={sectionCss.headingText}>
<Text className="bold themeText" style={sectionCss.headingText}>
{ContainerCopyMessages.errorTitle}
</Text>
<Text as="pre" style={{ whiteSpace: "pre-wrap" }}>
<Text as="pre" style={errorMessageStyle}>
{job.Error.message}
</Text>
</Stack.Item>
@@ -88,16 +102,16 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
<Stack.Item data-testid="selectedcollection-stack">
<Stack tokens={{ childrenGap: 15 }}>
<Stack.Item style={sectionCss.verticalAlign}>
<Text className="bold">{ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime}</Text>
<Text>{job.LastUpdatedTime}</Text>
<Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.lastUpdatedTime}</Text>
<Text className="themeText">{job.LastUpdatedTime}</Text>
</Stack.Item>
<Stack.Item style={sectionCss.verticalAlign}>
<Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text>
<Text>{job.Source?.remoteAccountName}</Text>
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
<Text className="themeText">{job.Source?.remoteAccountName}</Text>
</Stack.Item>
<Stack.Item style={sectionCss.verticalAlign}>
<Text className="bold">{ContainerCopyMessages.MonitorJobs.Columns.mode}</Text>
<Text>{job.Mode}</Text>
<Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.mode}</Text>
<Text className="themeText">{job.Mode}</Text>
</Stack.Item>
</Stack>
</Stack.Item>

View File

@@ -1,30 +1,14 @@
import { FontIcon, getTheme, mergeStyles, mergeStyleSets, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
import { FontIcon, mergeStyles, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
import PropTypes from "prop-types";
import React from "react";
import ContainerCopyMessages from "../../ContainerCopyMessages";
import { CopyJobStatusType } from "../../Enums/CopyJobEnums";
const theme = getTheme();
const iconClass = mergeStyles({
fontSize: "16px",
marginRight: "8px",
});
const classNames = mergeStyleSets({
[CopyJobStatusType.Pending]: [{ color: theme.semanticColors.bodySubtext }, iconClass],
[CopyJobStatusType.InProgress]: [{ color: theme.palette.themePrimary }, iconClass],
[CopyJobStatusType.Running]: [{ color: theme.palette.themePrimary }, iconClass],
[CopyJobStatusType.Partitioning]: [{ color: theme.palette.themePrimary }, iconClass],
[CopyJobStatusType.Paused]: [{ color: theme.palette.themePrimary }, iconClass],
[CopyJobStatusType.Skipped]: [{ color: theme.semanticColors.bodySubtext }, iconClass],
[CopyJobStatusType.Cancelled]: [{ color: theme.semanticColors.bodySubtext }, iconClass],
[CopyJobStatusType.Failed]: [{ color: theme.semanticColors.errorIcon }, iconClass],
[CopyJobStatusType.Faulted]: [{ color: theme.semanticColors.errorIcon }, iconClass],
[CopyJobStatusType.Completed]: [{ color: theme.semanticColors.successIcon }, iconClass],
unknown: [{ color: theme.semanticColors.bodySubtext }, iconClass],
});
const iconMap: Partial<Record<CopyJobStatusType, string>> = {
[CopyJobStatusType.Pending]: "Clock",
[CopyJobStatusType.Paused]: "CirclePause",
@@ -35,6 +19,17 @@ const iconMap: Partial<Record<CopyJobStatusType, string>> = {
[CopyJobStatusType.Completed]: "CompletedSolid",
};
// Icon colors for different statuses
const statusIconColors: Partial<Record<CopyJobStatusType, string>> = {
[CopyJobStatusType.Failed]: "var(--colorPaletteRedForeground1)",
[CopyJobStatusType.Faulted]: "var(--colorPaletteRedForeground1)",
[CopyJobStatusType.Completed]: "#107c10", // Green color for success
[CopyJobStatusType.InProgress]: "var(--colorBrandForeground1)",
[CopyJobStatusType.Running]: "var(--colorBrandForeground1)",
[CopyJobStatusType.Partitioning]: "var(--colorBrandForeground1)",
[CopyJobStatusType.Paused]: "var(--colorBrandForeground1)",
};
export interface CopyJobStatusWithIconProps {
status: CopyJobStatusType;
}
@@ -47,19 +42,17 @@ const CopyJobStatusWithIcon: React.FC<CopyJobStatusWithIconProps> = React.memo((
CopyJobStatusType.InProgress,
CopyJobStatusType.Partitioning,
].includes(status);
const iconColor = statusIconColors[status] || "var(--colorNeutralForeground2)";
const iconStyle = mergeStyles(iconClass, { color: iconColor });
return (
<Stack horizontal verticalAlign="center">
{isSpinnerStatus ? (
<Spinner size={SpinnerSize.small} style={{ marginRight: "8px" }} />
) : (
<FontIcon
aria-label={status}
iconName={iconMap[status] || "UnknownSolid"}
className={classNames[status] || classNames.unknown}
/>
<FontIcon aria-label={status} iconName={iconMap[status] || "UnknownSolid"} className={iconStyle} />
)}
<Text>{statusText}</Text>
<Text className="themeText">{statusText}</Text>
</Stack>
);
});

View File

@@ -15,6 +15,8 @@ import {
} from "@fluentui/react";
import React, { useEffect } from "react";
import Pager from "../../../../Common/Pager";
import { useThemeStore } from "../../../../hooks/useTheme";
import { getThemeTokens } from "../../../Theme/ThemeUtil";
import { openCopyJobDetailsPanel } from "../../Actions/CopyJobActions";
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
import { getColumns } from "./CopyJobColumns";
@@ -26,13 +28,15 @@ interface CopyJobsListProps {
}
const styles = {
container: { height: "calc(100vh - 25em)" } as React.CSSProperties,
container: { height: "100%" } as React.CSSProperties,
stackItem: { position: "relative", marginBottom: "20px" } as React.CSSProperties,
};
const PAGE_SIZE = 10;
const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pageSize = PAGE_SIZE }) => {
const isDarkMode = useThemeStore((state) => state.isDarkMode);
const themeTokens = getThemeTokens(isDarkMode);
const [startIndex, setStartIndex] = React.useState(0);
const [sortedJobs, setSortedJobs] = React.useState<CopyJobType[]>(jobs);
const [sortedColumnKey, setSortedColumnKey] = React.useState<string | undefined>(undefined);
@@ -87,11 +91,28 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
enableShimmer={false}
constrainMode={ConstrainMode.unconstrained}
layoutMode={DetailsListLayoutMode.justified}
onRenderDetailsHeader={(props, defaultRender) => (
<Sticky stickyPosition={StickyPositionType.Header} isScrollSynced>
{defaultRender({ ...props })}
</Sticky>
)}
onRenderDetailsHeader={(props, defaultRender) => {
const bgColor = themeTokens.colorNeutralBackground3;
const textColor = themeTokens.colorNeutralForeground1;
return (
<Sticky stickyPosition={StickyPositionType.Header} isScrollSynced stickyBackgroundColor={bgColor}>
<div style={{ backgroundColor: bgColor }}>
{defaultRender({
...props,
styles: {
root: {
backgroundColor: bgColor,
selectors: {
".ms-DetailsHeader-cellTitle": { color: textColor },
".ms-DetailsHeader-cellName": { color: textColor },
},
},
},
})}
</div>
</Sticky>
);
}}
/>
</ScrollablePane>
</Stack.Item>

View File

@@ -13,7 +13,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders InProgress with spin
/>
</div>
<span
class="css-112"
class="themeText css-112"
>
Running
</span>
@@ -33,7 +33,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders Partitioning with sp
/>
</div>
<span
class="css-112"
class="themeText css-112"
>
Running
</span>
@@ -53,7 +53,7 @@ exports[`CopyJobStatusWithIcon Spinner Status Types renders Running with spinner
/>
</div>
<span
class="css-112"
class="themeText css-112"
>
Running
</span>
@@ -66,7 +66,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
>
<i
aria-label="Cancelled"
class="ms-Icon root-105 css-118 mocked-style-Cancelled"
class="ms-Icon root-105 css-118 mocked-styles"
data-icon-name="StatusErrorFull"
role="img"
style="font-family: "FabricMDL2Icons-4";"
@@ -74,7 +74,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
</i>
<span
class="css-112"
class="themeText css-112"
>
Cancelled
</span>
@@ -87,7 +87,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
>
<i
aria-label="Completed"
class="ms-Icon root-105 css-120 mocked-style-Completed"
class="ms-Icon root-105 css-120 mocked-styles"
data-icon-name="CompletedSolid"
role="img"
style="font-family: "FabricMDL2Icons-5";"
@@ -95,7 +95,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
</i>
<span
class="css-112"
class="themeText css-112"
>
Completed
</span>
@@ -108,7 +108,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
>
<i
aria-label="Failed"
class="ms-Icon root-105 css-118 mocked-style-Failed"
class="ms-Icon root-105 css-118 mocked-styles"
data-icon-name="StatusErrorFull"
role="img"
style="font-family: "FabricMDL2Icons-4";"
@@ -116,7 +116,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
</i>
<span
class="css-112"
class="themeText css-112"
>
Failed
</span>
@@ -129,7 +129,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
>
<i
aria-label="Faulted"
class="ms-Icon root-105 css-118 mocked-style-Faulted"
class="ms-Icon root-105 css-118 mocked-styles"
data-icon-name="StatusErrorFull"
role="img"
style="font-family: "FabricMDL2Icons-4";"
@@ -137,7 +137,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
</i>
<span
class="css-112"
class="themeText css-112"
>
Failed
</span>
@@ -150,7 +150,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
>
<i
aria-label="Paused"
class="ms-Icon root-105 css-114 mocked-style-Paused"
class="ms-Icon root-105 css-114 mocked-styles"
data-icon-name="CirclePause"
role="img"
style="font-family: "FabricMDL2Icons-11";"
@@ -158,7 +158,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
</i>
<span
class="css-112"
class="themeText css-112"
>
Paused
</span>
@@ -171,7 +171,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
>
<i
aria-label="Pending"
class="ms-Icon root-105 css-111 mocked-style-Pending"
class="ms-Icon root-105 css-111 mocked-styles"
data-icon-name="Clock"
role="img"
style="font-family: "FabricMDL2Icons-2";"
@@ -179,7 +179,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
</i>
<span
class="css-112"
class="themeText css-112"
>
Queued
</span>
@@ -192,7 +192,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
>
<i
aria-label="Skipped"
class="ms-Icon root-105 css-116 mocked-style-Skipped"
class="ms-Icon root-105 css-116 mocked-styles"
data-icon-name="StatusCircleBlock2"
role="img"
style="font-family: "FabricMDL2Icons-9";"
@@ -200,7 +200,7 @@ exports[`CopyJobStatusWithIcon Static Icon Status Types - Snapshot Tests renders
</i>
<span
class="css-112"
class="themeText css-112"
>
Cancelled
</span>

View File

@@ -1,6 +1,30 @@
@import "../../../less/Common/Constants.less";
// Common theme-aware classes
.themeText {
color: var(--colorNeutralForeground1);
}
.themeTextSecondary {
color: var(--colorNeutralForeground2);
}
.themeLinkText {
color: var(--colorBrandForeground1);
}
.themeBackground {
background-color: var(--colorNeutralBackground1);
}
.themeBackgroundSecondary {
background-color: var(--colorNeutralBackground2);
}
#containerCopyWrapper {
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
.centerContent {
justify-content: center;
align-items: center;
@@ -9,20 +33,30 @@
.noCopyJobsMessage {
font-weight: 600;
margin: 0 auto;
color: @FocusColor;
color: var(--colorNeutralForeground2);
}
button.createCopyJobButton {
color: @LinkColor;
color: var(--colorBrandForeground1);
}
}
}
.createCopyJobScreensContainer {
height: 100%;
padding: 1em 1.5em;
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
.pointInTimeRestoreContainer, .onlineCopyContainer {
position: relative;
}
.toggle-label {
color: var(--colorNeutralForeground1);
}
.accordionHeaderText {
color: var(--colorNeutralForeground1);
}
label {
padding: 0;
@@ -71,7 +105,7 @@
}
.foreground {
z-index: 10;
background-color: #f9f9f9;
background-color: var(--colorNeutralBackground2);
padding: 20px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
transform: translate(0%, -9%);
@@ -83,11 +117,12 @@
.create-container-link-btn {
padding: 0;
height: 25px;
color: @LinkColor;
color: var(--colorBrandForeground1);
&:focus {
outline: none;
}
}
/* Create collection panel */
@@ -105,7 +140,6 @@
width: 100%;
max-width: 100%;
margin: 0 auto;
.ms-DetailsList {
width: 100%;
@@ -114,33 +148,33 @@
padding: @DefaultSpace 20px;
font-weight: 600;
font-size: @DefaultFontSize;
color: @BaseHigh;
background-color: @BaseLow;
border-bottom: @ButtonBorderWidth solid @BaseMedium;
color: var(--colorNeutralForeground1);
background-color: var(--colorNeutralBackground2);
border-bottom: @ButtonBorderWidth solid var(--colorNeutralStroke1);
&:hover {
background-color: @BaseMediumLow;
background-color: var(--colorNeutralBackground3);
}
}
}
.ms-DetailsRow {
border-bottom: @ButtonBorderWidth solid @BaseMedium;
border-bottom: @ButtonBorderWidth solid var(--colorNeutralStroke1);
&:hover {
background-color: @BaseMediumLow;
background-color: var(--colorNeutralBackground2);
}
.ms-DetailsRow-cell {
padding: @MediumSpace 20px;
font-size: @DefaultFontSize;
color: @BaseHigh;
color: var(--colorNeutralForeground1);
min-height: 48px;
display: flex;
align-items: center;
.jobNameLink {
color: @LinkColor;
color: var(--colorBrandForeground1);
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
@@ -168,7 +202,7 @@
}
.ms-DetailsRow-cell {
font-size: @DefaultFontSize;
color: @BaseHigh;
color: var(--colorNeutralForeground1);
}
}
}

View File

@@ -155,12 +155,7 @@ export class ComputedPropertiesComponent extends React.Component<
</Link>
&#160; about how to define computed properties and how to use them.
</Text>
<div
className="settingsV2Editor"
tabIndex={0}
ref={this.computedPropertiesDiv}
data-test="computed-properties-editor"
></div>
<div className="settingsV2Editor" tabIndex={0} ref={this.computedPropertiesDiv}></div>
</Stack>
);
}

View File

@@ -302,8 +302,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
);
private geoSpatialConfigTypeChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: GeospatialConfigType.Geography, text: "Geography", ariaLabel: "geography-option" },
{ key: GeospatialConfigType.Geometry, text: "Geometry", ariaLabel: "geometry-option" },
{ key: GeospatialConfigType.Geography, text: "Geography" },
{ key: GeospatialConfigType.Geometry, text: "Geometry" },
];
private getGeoSpatialComponent = (): JSX.Element => (

View File

@@ -103,7 +103,10 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
offText="Inactive"
checked={bucket.maxThroughputPercentage !== 100}
onChange={(event, checked) => onToggle(bucket.id, checked)}
styles={{ root: { marginBottom: 0 }, text: { fontSize: 12 } }}
styles={{
root: { marginBottom: 0 },
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
}}
></Toggle>
</Stack>
))}

View File

@@ -31,7 +31,6 @@ exports[`ComputedPropertiesComponent renders 1`] = `
</Text>
<div
className="settingsV2Editor"
data-test="computed-properties-editor"
tabIndex={0}
/>
</Stack>

View File

@@ -167,12 +167,10 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
options={
[
{
"ariaLabel": "geography-option",
"key": "Geography",
"text": "Geography",
},
{
"ariaLabel": "geometry-option",
"key": "Geometry",
"text": "Geometry",
},
@@ -654,12 +652,10 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
options={
[
{
"ariaLabel": "geography-option",
"key": "Geography",
"text": "Geography",
},
{
"ariaLabel": "geometry-option",
"key": "Geometry",
"text": "Geometry",
},
@@ -1228,12 +1224,10 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
options={
[
{
"ariaLabel": "geography-option",
"key": "Geography",
"text": "Geography",
},
{
"ariaLabel": "geometry-option",
"key": "Geometry",
"text": "Geometry",
},
@@ -1766,12 +1760,10 @@ exports[`SubSettingsComponent renders 1`] = `
options={
[
{
"ariaLabel": "geography-option",
"key": "Geography",
"text": "Geography",
},
{
"ariaLabel": "geometry-option",
"key": "Geometry",
"text": "Geometry",
},
@@ -2338,12 +2330,10 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
options={
[
{
"ariaLabel": "geography-option",
"key": "Geography",
"text": "Geography",
},
{
"ariaLabel": "geometry-option",
"key": "Geometry",
"text": "Geometry",
},

View File

@@ -53,6 +53,7 @@ type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexTyp
const labelStyles = {
root: {
fontSize: 12,
color: "var(--colorNeutralForeground1)",
},
};
@@ -63,6 +64,8 @@ const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldSt
field: {
fontSize: 12,
padding: "0 8px",
backgroundColor: "var(--colorNeutralBackground1)",
color: "var(--colorNeutralForeground1)",
},
};

View File

@@ -853,7 +853,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isSynapseLinkEnabled() && (
<Stack className="panelGroupSpacing">
<Text variant="small">
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
Azure Synapse Link is required for creating an analytical store{" "}
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br />
<Link

View File

@@ -475,6 +475,11 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
className="panelGroupSpacing"
>
<Text
style={
{
"color": "var(--colorNeutralForeground1)",
}
}
variant="small"
>
Azure Synapse Link is required for creating an analytical store

View File

@@ -2,7 +2,7 @@ import React from "react";
import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares.gif";
export const PanelLoadingScreen: React.FunctionComponent = () => (
<div id="loadingScreen" className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer">
<div id="loadingScreen" className="dataExplorerLoaderContainer dataExplorerLoaderforcopyJobs">
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} />
</div>
);

View File

@@ -205,7 +205,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
tooltip="Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB. You can perform multiple upload operations for larger data sets."
/>
{uploadFileData?.length > 0 && (
<div className="fileUploadSummaryContainer" data-test="file-upload-status">
<div className="fileUploadSummaryContainer">
<b style={{ color: "var(--colorNeutralForeground1)" }}>File upload status</b>
<DetailsList
items={uploadFileData}

View File

@@ -438,7 +438,6 @@ body.isDarkMode {
button {
&:not(.ms-Button):not(.ms-IconButton) {
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
&:hover {

View File

@@ -326,7 +326,6 @@ type PanelOpenOptions = {
export enum CommandBarButton {
Save = "Save",
ExecuteQuery = "Execute Query",
UploadItem = "Upload Item",
}
/** Helper class that provides locator methods for DataExplorer components, on top of a Frame */

View File

@@ -1,17 +1,7 @@
import { expect, test } from "@playwright/test";
import { existsSync, unlinkSync, writeFileSync } from "fs";
import path from "path";
import { CommandBarButton, DataExplorer, DocumentsTab, ONE_MINUTE_MS, TestAccount } from "../fx";
import {
createTestSQLContainer,
itemsPerPartition,
partitionCount,
retry,
setPartitionKeys,
TestContainerContext,
TestData,
} from "../testData";
import { DataExplorer, DocumentsTab, TestAccount } from "../fx";
import { retry, setPartitionKeys } from "../testData";
import { documentTestCases } from "./testCases";
let explorer: DataExplorer = null!;
@@ -105,108 +95,3 @@ for (const { name, databaseId, containerId, documents } of documentTestCases) {
}
});
}
test.describe.serial("Upload Item", () => {
let context: TestContainerContext = null!;
const uploadDocumentFilePath: string = path.join(__dirname, "uploadDocument.json");
test.beforeEach("Create Test Database and Open documents tab", async ({ page }) => {
context = await createTestSQLContainer();
explorer = await DataExplorer.open(page, TestAccount.SQL);
const containerNode = await explorer.waitForContainerNode(context.database.id, context.container.id);
await containerNode.expand();
const containerMenuNode = await explorer.waitForContainerItemsNode(context.database.id, context.container.id);
await containerMenuNode.element.click();
});
test.afterEach("Delete Test Database and uploadDocument.json", async () => {
if (existsSync(uploadDocumentFilePath)) {
unlinkSync(uploadDocumentFilePath);
}
await context?.dispose();
});
test("upload document", async () => {
// Create file to upload
const TestDataJsonString: string = JSON.stringify(TestData, null, 2);
writeFileSync(uploadDocumentFilePath, TestDataJsonString);
const uploadItemCommandBar = explorer.commandBarButton(CommandBarButton.UploadItem);
await uploadItemCommandBar.click();
// Select file to upload
await explorer.frame.setInputFiles("#importFileInput", uploadDocumentFilePath);
const uploadButton = explorer.frame.getByTestId("Panel/OkButton");
await uploadButton.click();
// Verify upload success message
const fileUploadStatusExpected: string = `${partitionCount * itemsPerPartition} created, 0 throttled, 0 errors`;
const fileUploadStatus = explorer.frame.getByTestId("file-upload-status");
await expect(fileUploadStatus).toContainText(fileUploadStatusExpected, {
timeout: ONE_MINUTE_MS,
});
});
test("upload same document twice", async () => {
// Create file to upload
const TestDataJsonString: string = JSON.stringify(TestData, null, 2);
writeFileSync(uploadDocumentFilePath, TestDataJsonString);
const uploadItemCommandBar = explorer.commandBarButton(CommandBarButton.UploadItem);
await uploadItemCommandBar.click();
// Select file to upload
await explorer.frame.setInputFiles("#importFileInput", uploadDocumentFilePath);
const uploadButton = explorer.frame.getByTestId("Panel/OkButton");
await uploadButton.click();
// Verify upload success message
const fileUploadStatusExpected: string = `${partitionCount * itemsPerPartition} created, 0 throttled, 0 errors`;
const fileUploadStatus = explorer.frame.getByTestId("file-upload-status");
await expect(fileUploadStatus).toContainText(fileUploadStatusExpected, {
timeout: ONE_MINUTE_MS,
});
// Select file to upload again
await explorer.frame.setInputFiles("#importFileInput", uploadDocumentFilePath);
await uploadButton.click();
// Verify upload failure message
const errorIcon = explorer.frame.getByRole("img", { name: "error" });
await expect(errorIcon).toBeVisible({ timeout: ONE_MINUTE_MS });
await expect(fileUploadStatus).toContainText(
`0 created, 0 throttled, ${partitionCount * itemsPerPartition} errors`,
{
timeout: ONE_MINUTE_MS,
},
);
});
test("upload invalid json", async () => {
// Create file to upload
let TestDataJsonString: string = JSON.stringify(TestData, null, 2);
// Remove the first '[' so that it becomes invalid json
TestDataJsonString = TestDataJsonString.substring(1);
writeFileSync(uploadDocumentFilePath, TestDataJsonString);
const uploadItemCommandBar = explorer.commandBarButton(CommandBarButton.UploadItem);
await uploadItemCommandBar.click();
// Select file to upload
await explorer.frame.setInputFiles("#importFileInput", uploadDocumentFilePath);
const uploadButton = explorer.frame.getByTestId("Panel/OkButton");
await uploadButton.click();
// Verify upload failure message
const fileUploadStatusExpected: string = "Unexpected non-whitespace character after JSON";
const fileUploadErrorList = explorer.frame.getByLabel("error list");
await expect(fileUploadErrorList).toContainText(fileUploadStatusExpected, {
timeout: ONE_MINUTE_MS,
});
});
});

View File

@@ -1,103 +0,0 @@
import { expect, test } from "@playwright/test";
import * as DataModels from "../../../src/Contracts/DataModels";
import { CommandBarButton, DataExplorer, ONE_MINUTE_MS, TestAccount } from "../../fx";
import { createTestSQLContainer, TestContainerContext } from "../../testData";
test.describe("Computed Properties", () => {
let context: TestContainerContext = null!;
let explorer: DataExplorer = null!;
test.beforeAll("Create Test Database", async () => {
context = await createTestSQLContainer(true);
});
test.beforeEach("Open Settings tab under Scale & Settings", async ({ page }) => {
explorer = await DataExplorer.open(page, TestAccount.SQL);
const containerNode = await explorer.waitForContainerNode(context.database.id, context.container.id);
await containerNode.expand();
// Click Scale & Settings and open Settings tab
await explorer.openScaleAndSettings(context);
const computedPropertiesTab = explorer.frame.getByTestId("settings-tab-header/ComputedPropertiesTab");
await computedPropertiesTab.click();
});
test.afterAll("Delete Test Database", async () => {
await context?.dispose();
});
test("Add valid computed property", async ({ page }) => {
await clearComputedPropertiesTextBoxContent({ page });
// Create computed property
const computedProperties: DataModels.ComputedProperties = [
{
name: "cp_lowerName",
query: "SELECT VALUE LOWER(c.name) FROM c",
},
];
const computedPropertiesString: string = JSON.stringify(computedProperties);
await page.keyboard.type(computedPropertiesString);
// Save changes
const saveButton = explorer.commandBarButton(CommandBarButton.Save);
await expect(saveButton).toBeEnabled();
await saveButton.click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
});
test("Add computed property with invalid query", async ({ page }) => {
await clearComputedPropertiesTextBoxContent({ page });
// Create computed property with no VALUE keyword in query
const computedProperties: DataModels.ComputedProperties = [
{
name: "cp_lowerName",
query: "SELECT LOWER(c.name) FROM c",
},
];
const computedPropertiesString: string = JSON.stringify(computedProperties);
await page.keyboard.type(computedPropertiesString);
// Save changes
const saveButton = explorer.commandBarButton(CommandBarButton.Save);
await expect(saveButton).toBeEnabled();
await saveButton.click();
await expect(explorer.getConsoleMessage()).toContainText(`Failed to update container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
});
test("Add computed property with invalid json", async ({ page }) => {
await clearComputedPropertiesTextBoxContent({ page });
// Create computed property with no VALUE keyword in query
const computedProperties: DataModels.ComputedProperties = [
{
name: "cp_lowerName",
query: "SELECT LOWER(c.name) FROM c",
},
];
const computedPropertiesString: string = JSON.stringify(computedProperties);
await page.keyboard.type(computedPropertiesString + "]");
// Save button should remain disabled due to invalid json
const saveButton = explorer.commandBarButton(CommandBarButton.Save);
await expect(saveButton).toBeDisabled();
});
const clearComputedPropertiesTextBoxContent = async ({ page }): Promise<void> => {
// Get computed properties text box
const computedPropertiesTextBox = explorer.frame.getByRole("textbox", { name: "Computed properties" });
await computedPropertiesTextBox.waitFor();
const computedPropertiesEditor = explorer.frame.getByTestId("computed-properties-editor");
await computedPropertiesEditor.click();
// Clear existing content
const isMac: boolean = process.platform === "darwin";
await page.keyboard.press(isMac ? "Meta+A" : "Control+A");
await page.keyboard.press("Backspace");
};
});

View File

@@ -15,7 +15,7 @@ test.describe("Settings under Scale & Settings", () => {
const containerNode = await explorer.waitForContainerNode(context.database.id, context.container.id);
await containerNode.expand();
// Click Scale & Settings and open Settings tab
// Click Scale & Settings and open Scale tab
await explorer.openScaleAndSettings(context);
const settingsTab = explorer.frame.getByTestId("settings-tab-header/SubSettingsTab");
await settingsTab.click();
@@ -25,86 +25,46 @@ test.describe("Settings under Scale & Settings", () => {
await context?.dispose();
});
test.describe("Set TTL", () => {
test("Update TTL to On (no default)", async () => {
const ttlOnNoDefaultRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-no-default-option" });
await ttlOnNoDefaultRadioButton.click();
test("Update TTL to On (no default)", async () => {
const ttlOnNoDefaultRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-no-default-option" });
await ttlOnNoDefaultRadioButton.click();
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(
`Successfully updated container ${context.container.id}`,
{
timeout: ONE_MINUTE_MS,
},
);
});
test("Update TTL to On (with user entry)", async () => {
const ttlOnRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-option" });
await ttlOnRadioButton.click();
// Enter TTL seconds
const ttlInput = explorer.frame.getByTestId("ttl-input");
await ttlInput.fill("30000");
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(
`Successfully updated container ${context.container.id}`,
{
timeout: ONE_MINUTE_MS,
},
);
});
test("Update TTL to Off", async () => {
// By default TTL is set to off so we need to first set it to On
const ttlOnNoDefaultRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-no-default-option" });
await ttlOnNoDefaultRadioButton.click();
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(
`Successfully updated container ${context.container.id}`,
{
timeout: ONE_MINUTE_MS,
},
);
// Set it to Off
const ttlOffRadioButton = explorer.frame.getByRole("radio", { name: "ttl-off-option" });
await ttlOffRadioButton.click();
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(
`Successfully updated container ${context.container.id}`,
{
timeout: ONE_MINUTE_MS,
},
);
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
});
test.describe("Set Geospatial Config", () => {
test("Set Geospatial Config to Geometry then Geography", async () => {
const geometryRadioButton = explorer.frame.getByRole("radio", { name: "geometry-option" });
await geometryRadioButton.click();
test("Update TTL to On (with user entry)", async () => {
const ttlOnRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-option" });
await ttlOnRadioButton.click();
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(
`Successfully updated container ${context.container.id}`,
{
timeout: ONE_MINUTE_MS,
},
);
// Enter TTL seconds
const ttlInput = explorer.frame.getByTestId("ttl-input");
await ttlInput.fill("30000");
const geographyRadioButton = explorer.frame.getByRole("radio", { name: "geography-option" });
await geographyRadioButton.click();
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
});
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(
`Successfully updated container ${context.container.id}`,
{
timeout: ONE_MINUTE_MS,
},
);
test("Update TTL to Off", async () => {
// By default TTL is set to off so we need to first set it to On
const ttlOnNoDefaultRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-no-default-option" });
await ttlOnNoDefaultRadioButton.click();
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
// Set it to Off
const ttlOffRadioButton = explorer.frame.getByRole("radio", { name: "ttl-off-option" });
await ttlOffRadioButton.click();
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
});
});

View File

@@ -37,35 +37,27 @@ export interface PartitionKey {
value: string | null;
}
export const partitionCount = 4;
const partitionCount = 4;
// If we increase this number, we need to split bulk creates into multiple batches.
// Bulk operations are limited to 100 items per partition.
export const itemsPerPartition = 100;
const itemsPerPartition = 100;
function createTestItems(): TestItem[] {
const items: TestItem[] = [];
for (let i = 0; i < partitionCount; i++) {
for (let j = 0; j < itemsPerPartition; j++) {
const id = createSafeRandomString(32);
const id = crypto.randomBytes(32).toString("base64");
items.push({
id,
partitionKey: `partition_${i}`,
randomData: createSafeRandomString(32),
randomData: crypto.randomBytes(32).toString("base64"),
});
}
}
return items;
}
// Document IDs cannot contain '/', '\', or '#'
function createSafeRandomString(byteLength: number): string {
return crypto
.randomBytes(byteLength)
.toString("base64")
.replace(/[/\\#]/g, "_");
}
export const TestData: TestItem[] = createTestItems();
export class TestContainerContext {