Added Copy Job prerequisites screen

This commit is contained in:
Bikram Choudhury
2025-10-15 12:21:42 +05:30
parent 9227ad379b
commit 9bfb6aecc9
19 changed files with 505 additions and 7 deletions

View File

@@ -39,4 +39,48 @@ export default {
sourceContainerLabel: "Source container",
targetDatabaseLabel: "Destination database",
targetContainerLabel: "Destination container",
// Assign Permissions Screen
assignPermissions: {
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
},
toggleBtn: {
onText: "On",
offText: "Off"
},
addManagedIdentity: {
title: "System assigned managed identity enabled",
description: "Enable a system assigned managed identity for the destination account to allow the copy job to access it.",
toggleLabel: "System assigned managed identity",
managedIdentityTooltip: "A system assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. 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 don't have to store any credentials in code.",
userAssignedIdentityTooltip: "You can select an existing user assigned identity or create a new one.",
userAssignedIdentityLabel: "You may also select a user assigned managed identity.",
createUserAssignedIdentityLink: "Create User Assigned Managed Identity",
enablementTitle: "Enable system assigned managed identity",
enablementDescription: (identityName: string) => identityName ? `'${identityName}' will be registered with Microsoft Entra ID. Once it is registered, '${identityName}' can be granted permissions to access resources protected by Microsoft Entra ID. Do you want to enable the system assigned managed identity for '${identityName}'?` : "",
},
defaultManagedIdentity: {
title: "System assigned managed identity enabled as default",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
tooltip: "A system assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. 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 don't have to store any credentials in code.",
popoverTitle: "System assigned managed identity set as default",
popoverDescription: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco.",
},
readPermissionAssigned: {
title: "Read permission assigned to default identity",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
tooltip: "A system assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. 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 don't have to store any credentials in code.",
popoverTitle: "Read permission assigned to default identity",
popoverDescription: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco.",
},
pointInTimeRestore: {
title: "Point In Time Restore enabled",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
buttonText: "Enable Point In Time Restore",
},
onlineCopyEnabled: {
title: "Online copy enabled",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
buttonText: "Enable Online Copy",
}
}

View File

@@ -0,0 +1,7 @@
import { DatabaseAccount } from "Contracts/DataModels";
export const buildResourceLink = (resource: DatabaseAccount): string => {
const resourceId = resource.id;
// TODO: update "ms.portal.azure.com" based on environment (e.g. for PROD or Fairfax)
return `https://ms.portal.azure.com/#resource${resourceId}`;
}

View File

@@ -0,0 +1,58 @@
import { Link, Stack, Text, Toggle } from "@fluentui/react";
import React, { useMemo } from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { buildResourceLink } from "../../../CopyJobUtils";
import InfoTooltip from "../Components/InfoTooltip";
import PopoverMessage from "../Components/PopoverContainer";
import useToggle from "./hooks/useToggle";
const managedIdentityTooltip = ContainerCopyMessages.addManagedIdentity.managedIdentityTooltip;
const userAssignedTooltip = ContainerCopyMessages.addManagedIdentity.userAssignedIdentityTooltip;
const textStyle = { display: "flex", alignItems: "center" };
const AddManagedIdentity: React.FC = () => {
const { copyJobState } = useCopyJobContext();
const [systemAssigned, onToggle] = useToggle(false);
const manageIdentityLink = useMemo(() => {
const { target } = copyJobState;
const resourceUri = buildResourceLink(target.account);
return target?.account?.id ? `${resourceUri}/ManagedIdentitiesBlade` : "#";
}, [copyJobState]);
return (
<Stack className="addManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<Toggle
label={
<Text className="toggle-label" style={textStyle}>
{ContainerCopyMessages.addManagedIdentity.toggleLabel}&nbsp;<InfoTooltip content={managedIdentityTooltip} />
</Text>
}
checked={systemAssigned}
onText={ContainerCopyMessages.toggleBtn.onText}
offText={ContainerCopyMessages.toggleBtn.offText}
onChange={onToggle}
/>
<Text className="user-assigned-label" style={textStyle}>
{ContainerCopyMessages.addManagedIdentity.userAssignedIdentityLabel}&nbsp;<InfoTooltip content={userAssignedTooltip} />
</Text>
<div style={{ marginTop: 8 }}>
<Link href={manageIdentityLink} target="_blank" rel="noopener noreferrer">
{ContainerCopyMessages.addManagedIdentity.createUserAssignedIdentityLink}
</Link>
</div>
<PopoverMessage
visible={systemAssigned}
title={ContainerCopyMessages.addManagedIdentity.enablementTitle}
onCancel={() => onToggle(null, false)}
onPrimary={() => console.log('Primary action taken')}
>
{ContainerCopyMessages.addManagedIdentity.enablementDescription(copyJobState.target?.account?.name)}
</PopoverMessage>
</Stack>
);
};
export default AddManagedIdentity;

View File

@@ -0,0 +1,42 @@
import { ITooltipHostStyles, Stack, Toggle } from "@fluentui/react";
import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import InfoTooltip from "../Components/InfoTooltip";
import PopoverMessage from "../Components/PopoverContainer";
import useToggle from "./hooks/useToggle";
const TooltipContent = ContainerCopyMessages.readPermissionAssigned.tooltip;
const hostStyles: Partial<ITooltipHostStyles> = { root: { display: 'inline-block' } };
const AddReadPermissionToDefaultIdentity: React.FC = () => {
const [readPermissionAssigned, onToggle] = useToggle(false);
return (
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<div className="toggle-label">
{ContainerCopyMessages.readPermissionAssigned.description} &nbsp;<InfoTooltip content={TooltipContent} />
</div>
<Toggle
checked={readPermissionAssigned}
onText={ContainerCopyMessages.toggleBtn.onText}
offText={ContainerCopyMessages.toggleBtn.offText}
onChange={onToggle}
inlineLabel
styles={{
root: { marginTop: 8, marginBottom: 12 },
label: { display: "none" },
}}
/>
<PopoverMessage
visible={readPermissionAssigned}
title={ContainerCopyMessages.readPermissionAssigned.popoverTitle}
onCancel={() => onToggle(null, false)}
onPrimary={() => console.log('Primary action taken')}
>
{ContainerCopyMessages.readPermissionAssigned.popoverDescription}
</PopoverMessage>
</Stack>
);
};
export default AddReadPermissionToDefaultIdentity;

View File

@@ -0,0 +1,41 @@
import { Stack, Toggle } from "@fluentui/react";
import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import InfoTooltip from "../Components/InfoTooltip";
import PopoverMessage from "../Components/PopoverContainer";
import useToggle from "./hooks/useToggle";
const managedIdentityTooltip = ContainerCopyMessages.defaultManagedIdentity.tooltip;
const DefaultManagedIdentity: React.FC = () => {
const [defaultSystemAssigned, onToggle] = useToggle(false);
return (
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<div className="toggle-label">
{ContainerCopyMessages.defaultManagedIdentity.description} &nbsp;<InfoTooltip content={managedIdentityTooltip} />
</div>
<Toggle
checked={defaultSystemAssigned}
onText={ContainerCopyMessages.toggleBtn.onText}
offText={ContainerCopyMessages.toggleBtn.offText}
onChange={onToggle}
inlineLabel
styles={{
root: { marginTop: 8, marginBottom: 12 },
label: { display: "none" },
}}
/>
<PopoverMessage
visible={defaultSystemAssigned}
title={ContainerCopyMessages.defaultManagedIdentity.popoverTitle}
onCancel={() => onToggle(null, false)}
onPrimary={() => console.log('Primary action taken')}
>
{ContainerCopyMessages.defaultManagedIdentity.popoverDescription}
</PopoverMessage>
</Stack>
);
};
export default DefaultManagedIdentity;

View File

@@ -0,0 +1,30 @@
import { PrimaryButton, Stack } from "@fluentui/react";
import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { buildResourceLink } from "../../../CopyJobUtils";
import useWindowOpenMonitor from "./hooks/useWindowOpenMonitor";
const OnlineCopyEnabled: React.FC = () => {
const { copyJobState: { source } = {} } = useCopyJobContext();
const sourceAccountLink = buildResourceLink(source?.account);
const onlineCopyUrl = `${sourceAccountLink}/Features`;
const onWindowClosed = () => {
console.log('Online copy window closed');
};
const openWindowAndMonitor = useWindowOpenMonitor(onlineCopyUrl, onWindowClosed);
return (
<Stack className="onlineCopyContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<div className="toggle-label">
{ContainerCopyMessages.onlineCopyEnabled.description}
</div>
<PrimaryButton
text={ContainerCopyMessages.onlineCopyEnabled.buttonText}
onClick={openWindowAndMonitor}
/>
</Stack>
);
};
export default OnlineCopyEnabled;

View File

@@ -0,0 +1,31 @@
import { PrimaryButton, Stack } from "@fluentui/react";
import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { buildResourceLink } from "../../../CopyJobUtils";
import useWindowOpenMonitor from "./hooks/useWindowOpenMonitor";
const PointInTimeRestore: React.FC = () => {
const { copyJobState: { source } = {} } = useCopyJobContext();
const sourceAccountLink = buildResourceLink(source?.account);
const pitrUrl = `${sourceAccountLink}/backupRestore`;
const onWindowClosed = () => {
console.log('Point-in-time restore window closed');
};
const openWindowAndMonitor = useWindowOpenMonitor(pitrUrl, onWindowClosed);
return (
<Stack className="pointInTimeRestoreContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<div className="toggle-label">
{ContainerCopyMessages.pointInTimeRestore.description}
</div>
<PrimaryButton
text={ContainerCopyMessages.pointInTimeRestore.buttonText}
onClick={openWindowAndMonitor}
/>
</Stack>
);
};
export default PointInTimeRestore;

View File

@@ -0,0 +1,57 @@
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { CopyJobMigrationType } from "../../../../Enums";
import { CopyJobContextState } from "../../../../Types";
import AddManagedIdentity from "../AddManagedIdentity";
import AddReadPermissionToDefaultIdentity from "../AddReadPermissionToDefaultIdentity";
import DefaultManagedIdentity from "../DefaultManagedIdentity";
import OnlineCopyEnabled from "../OnlineCopyEnabled";
import PointInTimeRestore from "../PointInTimeRestore";
// Define a typed config for permission sections
export interface PermissionSectionConfig {
id: string;
title: string;
Component: React.FC;
shouldShow?: (state: CopyJobContextState) => boolean; // optional conditional rendering
}
// Base permission sections with dynamic visibility logic
const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
{
id: "addManagedIdentity",
title: ContainerCopyMessages.addManagedIdentity.title,
Component: AddManagedIdentity,
},
{
id: "defaultManagedIdentity",
title: ContainerCopyMessages.defaultManagedIdentity.title,
Component: DefaultManagedIdentity,
},
{
id: "readPermissionAssigned",
title: ContainerCopyMessages.readPermissionAssigned.title,
Component: AddReadPermissionToDefaultIdentity,
}
];
const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [
{
id: "pointInTimeRestore",
title: ContainerCopyMessages.pointInTimeRestore.title,
Component: PointInTimeRestore,
},
{
id: "onlineCopyEnabled",
title: ContainerCopyMessages.onlineCopyEnabled.title,
Component: OnlineCopyEnabled,
}
];
const usePermissionSections = (state: CopyJobContextState): PermissionSectionConfig[] => {
return [
...PERMISSION_SECTIONS_CONFIG,
...(state.migrationType !== CopyJobMigrationType.Offline ? PERMISSION_SECTIONS_FOR_ONLINE_JOBS : []),
];
};
export default usePermissionSections;

View File

@@ -0,0 +1,11 @@
import { useCallback, useState } from "react";
const useToggle = (initialState = false) => {
const [state, setState] = useState<boolean>(initialState);
const onToggle = useCallback((_, checked?: boolean) => {
setState(!!checked);
}, []);
return [state, onToggle] as const;
};
export default useToggle;

View File

@@ -0,0 +1,32 @@
import { useEffect, useRef } from "react";
const useWindowOpenMonitor = (url: string, onClose?: () => void, intervalMs = 500) => {
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const openWindowAndMonitor = () => {
const newWindow = window.open(url, '_blank');
intervalRef.current = setInterval(() => {
if (newWindow?.closed) {
clearInterval(intervalRef.current!);
intervalRef.current = null;
console.log('New window has been closed!');
if (onClose) {
onClose();
}
}
}, intervalMs);
};
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, []);
return openWindowAndMonitor;
};
export default useWindowOpenMonitor;

View File

@@ -0,0 +1,40 @@
import { Image, Stack, Text } from "@fluentui/react";
import { Accordion, AccordionHeader, AccordionItem, AccordionPanel } from "@fluentui/react-components";
import React from "react";
import WarningIcon from "../../../../../../images/warning.svg";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext";
import usePermissionSections, { PermissionSectionConfig } from "./hooks/usePermissionsSection";
const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Component }) => (
<AccordionItem key={id} value={id}>
<AccordionHeader className="accordionHeader">
<Text className="accordionHeaderText" variant="medium">{title}</Text>
<Image className="statusIcon" src={WarningIcon} alt="Warning icon" width={24} height={24} />
</AccordionHeader>
<AccordionPanel>
<Component />
</AccordionPanel>
</AccordionItem>
);
const AssignPermissions = () => {
const { copyJobState } = useCopyJobContext();
const permissionSections = usePermissionSections(copyJobState);
return (
<Stack className="assignPermissionsContainer" tokens={{ childrenGap: 15 }}>
<span>
{ContainerCopyMessages.assignPermissions.description}
</span>
<Accordion className="permissionsAccordion" collapsible>
{
permissionSections.map(section => (
<PermissionSection key={section.id} {...section} />
))
}
</Accordion>
</Stack>
);
};
export default AssignPermissions;

View File

@@ -0,0 +1,15 @@
import { Image, ITooltipHostStyles, TooltipHost } from "@fluentui/react";
import React from "react";
import InfoIcon from "../../../../../../images/Info.svg";
const InfoTooltip: React.FC<{ content?: string }> = ({ content }) => {
if (!content) return null;
const hostStyles: Partial<ITooltipHostStyles> = { root: { display: 'inline-block' } };
return (
<TooltipHost content={content} calloutProps={{ gapSpace: 0 }} styles={hostStyles}>
<Image src={InfoIcon} alt="Information" width={14} height={14} />
</TooltipHost>
);
};
export default React.memo(InfoTooltip);

View File

@@ -0,0 +1,41 @@
import { DefaultButton, PrimaryButton, Stack, Text } from "@fluentui/react";
import React from "react";
interface PopoverContainerProps {
title?: string;
children?: React.ReactNode;
onPrimary: () => void;
onCancel: () => void;
}
const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(({ title, children, onPrimary, onCancel }) => {
return (
<Stack className="foreground" tokens={{ childrenGap: 20 }} style={{ maxWidth: 450 }}>
<Text variant="mediumPlus" style={{ fontWeight: 600 }}>{title}</Text>
<Text>{children}</Text>
<Stack horizontal tokens={{ childrenGap: 20 }}>
<PrimaryButton text="Yes" onClick={onPrimary} />
<DefaultButton text="No" onClick={onCancel} />
</Stack>
</Stack>
);
});
interface PopoverMessageProps {
visible: boolean;
title: string;
onCancel: () => void;
onPrimary: () => void;
children: React.ReactNode;
}
const PopoverMessage: React.FC<PopoverMessageProps> = ({ visible, title, onCancel, onPrimary, children }) => {
if (!visible) return null;
return (
<PopoverContainer title={title} onCancel={onCancel} onPrimary={onPrimary}>
{children}
</PopoverContainer>
);
};
export default PopoverMessage;

View File

@@ -1,5 +1,6 @@
import React from "react";
import { DatabaseAccount, Subscription } from "../../../../../../Contracts/DataModels";
import { CopyJobMigrationType } from "../../../../Enums";
import { CopyJobContextProviderType, CopyJobContextState, DropdownOptionType } from "../../../../Types";
export function useDropdownOptions(
@@ -67,7 +68,7 @@ export function useEventHandlers(setCopyJobState: setCopyJobStateType) {
(_ev?: React.FormEvent<HTMLElement>, checked?: boolean) => {
setCopyJobState((prevState: CopyJobContextState) => ({
...prevState,
migrationType: checked ? "offline" : "online",
migrationType: checked ? CopyJobMigrationType.Offline : CopyJobMigrationType.Online,
}));
},
[setCopyJobState]

View File

@@ -5,6 +5,7 @@ import { useDatabaseAccounts } from "../../../../../hooks/useDatabaseAccounts";
import { useSubscriptions } from "../../../../../hooks/useSubscriptions";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../Enums";
import { AccountDropdown } from "./Components/AccountDropdown";
import { MigrationTypeCheckbox } from "./Components/MigrationTypeCheckbox";
import { SubscriptionDropdown } from "./Components/SubscriptionDropdown";
@@ -18,12 +19,13 @@ const SelectAccount = React.memo(
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
const subscriptions: Subscription[] = useSubscriptions(armToken);
const accounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId, armToken);
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId, armToken);
const sqlApiOnlyAccounts: DatabaseAccount[] = allAccounts?.filter(account => account.type === "SQL" || account.kind === "GlobalDocumentDB");
const { subscriptionOptions, accountOptions } = useDropdownOptions(subscriptions, accounts);
const { subscriptionOptions, accountOptions } = useDropdownOptions(subscriptions, sqlApiOnlyAccounts);
const { handleSelectSourceAccount, handleMigrationTypeChange } = useEventHandlers(setCopyJobState);
const migrationTypeChecked = copyJobState?.migrationType === "offline";
const migrationTypeChecked = copyJobState?.migrationType === CopyJobMigrationType.Offline;
return (
<Stack className="selectAccountContainer" tokens={{ childrenGap: 15 }}>

View File

@@ -1,5 +1,6 @@
import React from "react";
import { CopyJobContextState } from "../../Types";
import AssignPermissions from "../Screens/AssignPermissions";
import PreviewCopyJob from "../Screens/PreviewCopyJob";
import SelectAccount from "../Screens/SelectAccount";
import SelectSourceAndTargetContainers from "../Screens/SelectSourceAndTargetContainers";
@@ -8,6 +9,7 @@ const SCREEN_KEYS = {
SelectAccount: "SelectAccount",
SelectSourceAndTargetContainers: "SelectSourceAndTargetContainers",
PreviewCopyJob: "PreviewCopyJob",
AssignPermissions: "AssignPermissions",
};
type Validation = {
@@ -29,7 +31,7 @@ function useCreateCopyJobScreensList() {
component: <SelectAccount />,
validations: [
{
validate: (state) => !!state?.source?.subscription && !!state?.source?.account,
validate: (state: CopyJobContextState) => !!state?.source?.subscription && !!state?.source?.account,
message: "Please select a subscription and account to proceed",
},
],
@@ -39,7 +41,7 @@ function useCreateCopyJobScreensList() {
component: <SelectSourceAndTargetContainers />,
validations: [
{
validate: (state) => (
validate: (state: CopyJobContextState) => (
!!state?.source?.databaseId && !!state?.source?.containerId && !!state?.target?.databaseId && !!state?.target?.containerId
),
message: "Please select source and target containers to proceed",
@@ -49,6 +51,16 @@ function useCreateCopyJobScreensList() {
{
key: SCREEN_KEYS.PreviewCopyJob,
component: <PreviewCopyJob />,
validations: [
{
validate: (state: CopyJobContextState) => !!state?.jobName,
message: "Please enter a job name to proceed",
},
],
},
{
key: SCREEN_KEYS.AssignPermissions,
component: <AssignPermissions />,
validations: [],
},
],

View File

@@ -0,0 +1,10 @@
export enum CopyJobMigrationType {
Offline = "offline",
Online = "online",
}
export enum CopyJobMigrationStatus {
Pause = "Pause",
Resume = "Resume",
Cancel = "Cancel",
}

View File

@@ -2,6 +2,7 @@ import { DatabaseAccount, Subscription } from "Contracts/DataModels";
import React from "react";
import { ApiType } from "UserContext";
import Explorer from "../../Explorer";
import { CopyJobMigrationType } from "../Enums";
export interface ContainerCopyProps {
container: Explorer;
@@ -68,7 +69,7 @@ export interface DatabaseContainerSectionProps {
export interface CopyJobContextState {
jobName: string;
migrationType: "online" | "offline";
migrationType: CopyJobMigrationType;
// source details
source: {
subscription: Subscription;

View File

@@ -46,4 +46,27 @@
font-weight: 600;
}
}
.accordionHeader {
button {
display: flex;
align-items: center;
.accordionHeaderText {
margin-left: 5px;
font-weight: 600;
}
.statusIcon {
margin-left: auto;
}
}
}
.foreground {
z-index: 10;
background-color: white;
padding: 20px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
// transform: translate(0%, -5%);
position: absolute;
}
}