mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-05-15 09:47:30 +01:00
5bf0970b5e
* feat: add copyable ID to delete confirmation dialogs When deleting databases or containers, the confirmation dialog now displays the resource ID in a read-only text field with a copy button, allowing users to copy-paste the ID into the confirmation input instead of typing it manually. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fixed formatting. * revert non-en locale changes; add localization instruction Revert changes to non-English locale files — translations are managed by a separate localization process. Add a note to copilot instructions clarifying that only en/Resources.json should be modified. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: capitalize 'Id' in copyable resource ID labels Changed 'id:' to 'Id:' in the copyable ID labels for delete confirmation dialogs (both database and collection). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: capitalize collection name in copyable ID label Use getCollectionName() directly (returns 'Container', 'Collection', etc.) instead of the lowercased collectionName variable for the copyable ID label. The database panel already used getDatabaseName() which returns capitalized. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add warning message to delete container confirmation dialog Added the same warning banner that exists in the delete database dialog to the delete container dialog, informing users that the action cannot be undone and will permanently delete the resource and its children. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
221 lines
8.2 KiB
TypeScript
221 lines
8.2 KiB
TypeScript
import { IconButton, Text, TextField } from "@fluentui/react";
|
|
import { Areas } from "Common/Constants";
|
|
import DeleteFeedback from "Common/DeleteFeedback";
|
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
|
import { deleteCollection } from "Common/dataAccess/deleteCollection";
|
|
import { Collection } from "Contracts/ViewModels";
|
|
import { Keys, t } from "Localization";
|
|
import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
|
|
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
|
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
|
import { userContext } from "UserContext";
|
|
import { getCollectionName } from "Utils/APITypeUtils";
|
|
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
|
import { useSidePanel } from "hooks/useSidePanel";
|
|
import { useTabs } from "hooks/useTabs";
|
|
import React, { FunctionComponent, useState } from "react";
|
|
import { useDatabases } from "../../useDatabases";
|
|
import { useSelectedNode } from "../../useSelectedNode";
|
|
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
|
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "../PanelInfoErrorComponent";
|
|
|
|
const themedTextFieldStyles = {
|
|
fieldGroup: {
|
|
width: 300,
|
|
backgroundColor: "var(--colorNeutralBackground1)",
|
|
borderColor: "var(--colorNeutralStroke1)",
|
|
selectors: {
|
|
":hover": { borderColor: "var(--colorNeutralStroke1Hover)" },
|
|
},
|
|
},
|
|
field: {
|
|
color: "var(--colorNeutralForeground1)",
|
|
backgroundColor: "var(--colorNeutralBackground1)",
|
|
},
|
|
subComponentStyles: {
|
|
label: { root: { color: "var(--colorNeutralForeground1)" } },
|
|
},
|
|
};
|
|
|
|
export interface DeleteCollectionConfirmationPaneProps {
|
|
refreshDatabases: () => Promise<void>;
|
|
}
|
|
|
|
export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({
|
|
refreshDatabases,
|
|
}: DeleteCollectionConfirmationPaneProps) => {
|
|
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
|
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
|
|
const [inputCollectionName, setInputCollectionName] = useState<string>("");
|
|
const [formError, setFormError] = useState<string>("");
|
|
const [isExecuting, setIsExecuting] = useState(false);
|
|
|
|
const shouldRecordFeedback = (): boolean =>
|
|
useDatabases.getState().isLastCollection() && !useDatabases.getState().findSelectedDatabase()?.isDatabaseShared();
|
|
|
|
const collectionName = getCollectionName().toLocaleLowerCase();
|
|
const paneTitle = t(Keys.panes.deleteCollection.panelTitle, { collectionName });
|
|
const selectedCollection = useSelectedNode.getState().selectedNode
|
|
? useSelectedNode.getState().findSelectedCollection()
|
|
: undefined;
|
|
const selectedCollectionId = selectedCollection?.id() ?? "";
|
|
|
|
const onSubmit = async (): Promise<void> => {
|
|
const collection = useSelectedNode.getState().findSelectedCollection();
|
|
if (!collection || inputCollectionName !== collection.id()) {
|
|
const errorMessage = t(Keys.panes.deleteCollection.inputMismatch, {
|
|
input: inputCollectionName,
|
|
selectedId: collection.id(),
|
|
});
|
|
setFormError(errorMessage);
|
|
NotificationConsoleUtils.logConsoleError(
|
|
`Error while deleting ${collectionName} ${collection.id()}: ${errorMessage}`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
const paneInfo = {
|
|
collectionId: collection.id(),
|
|
dataExplorerArea: Areas.ContextualPane,
|
|
paneTitle,
|
|
};
|
|
|
|
setFormError("");
|
|
setIsExecuting(true);
|
|
|
|
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteCollection, paneInfo);
|
|
|
|
try {
|
|
await deleteCollection(collection.databaseId, collection.id());
|
|
|
|
setIsExecuting(false);
|
|
useSelectedNode.getState().setSelectedNode(collection.database);
|
|
useTabs
|
|
.getState()
|
|
.closeTabsByComparator(
|
|
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId,
|
|
);
|
|
refreshDatabases();
|
|
|
|
TelemetryProcessor.traceSuccess(Action.DeleteCollection, paneInfo, startKey);
|
|
|
|
if (shouldRecordFeedback()) {
|
|
const deleteFeedback = new DeleteFeedback(
|
|
userContext.databaseAccount?.id,
|
|
userContext.databaseAccount?.name,
|
|
DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.apiType),
|
|
deleteCollectionFeedback,
|
|
);
|
|
|
|
TelemetryProcessor.trace(Action.DeleteCollection, ActionModifiers.Mark, {
|
|
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
|
|
});
|
|
}
|
|
|
|
closeSidePanel();
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
|
|
setFormError(errorMessage);
|
|
setIsExecuting(false);
|
|
|
|
TelemetryProcessor.traceFailure(
|
|
Action.DeleteCollection,
|
|
{
|
|
...paneInfo,
|
|
error: errorMessage,
|
|
errorStack: getErrorStack(error),
|
|
},
|
|
startKey,
|
|
);
|
|
}
|
|
};
|
|
const props: RightPaneFormProps = {
|
|
formError: formError,
|
|
isExecuting,
|
|
submitButtonText: t(Keys.common.ok),
|
|
onSubmit,
|
|
};
|
|
const errorProps: PanelInfoErrorProps = {
|
|
messageType: "warning",
|
|
showErrorDetails: false,
|
|
message: t(Keys.panes.deleteCollection.warningMessage),
|
|
};
|
|
const copyableIdLabel = t(Keys.panes.deleteCollection.copyableId, {
|
|
collectionName: getCollectionName(),
|
|
});
|
|
const confirmContainer = t(Keys.panes.deleteCollection.confirmPrompt, {
|
|
collectionName: collectionName.toLowerCase(),
|
|
});
|
|
const reasonInfo =
|
|
t(Keys.panes.deleteCollection.feedbackTitle) +
|
|
" " +
|
|
t(Keys.panes.deleteCollection.feedbackReason, { collectionName });
|
|
return (
|
|
<RightPaneForm {...props}>
|
|
{!formError && <PanelInfoErrorComponent {...errorProps} />}
|
|
<div className="panelFormWrapper">
|
|
<div className="panelMainContent">
|
|
<div className="confirmDeleteInput">
|
|
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
|
{copyableIdLabel}
|
|
</Text>
|
|
<TextField
|
|
id="copyableCollectionId"
|
|
readOnly
|
|
value={selectedCollectionId}
|
|
styles={themedTextFieldStyles}
|
|
onRenderSuffix={() => (
|
|
<IconButton
|
|
iconProps={{ iconName: "Copy" }}
|
|
title={t(Keys.common.copy)}
|
|
ariaLabel={t(Keys.common.copy)}
|
|
onClick={() => navigator.clipboard.writeText(selectedCollectionId)}
|
|
styles={{ root: { height: "100%" } }}
|
|
/>
|
|
)}
|
|
ariaLabel={copyableIdLabel}
|
|
/>
|
|
<span className="mandatoryStar">* </span>
|
|
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
|
{confirmContainer}
|
|
</Text>
|
|
<TextField
|
|
id="confirmCollectionId"
|
|
autoFocus
|
|
value={inputCollectionName}
|
|
styles={themedTextFieldStyles}
|
|
onChange={(event, newInput?: string) => {
|
|
setInputCollectionName(newInput);
|
|
}}
|
|
ariaLabel={confirmContainer}
|
|
required
|
|
/>
|
|
</div>
|
|
{shouldRecordFeedback() && (
|
|
<div className="deleteCollectionFeedback">
|
|
<Text variant="small" block style={{ color: "var(--colorNeutralForeground1)" }}>
|
|
{t(Keys.panes.deleteCollection.feedbackTitle)}
|
|
</Text>
|
|
<Text variant="small" block style={{ color: "var(--colorNeutralForeground1)" }}>
|
|
{t(Keys.panes.deleteCollection.feedbackReason, { collectionName })}
|
|
</Text>
|
|
<TextField
|
|
id="deleteCollectionFeedbackInput"
|
|
styles={themedTextFieldStyles}
|
|
multiline
|
|
value={deleteCollectionFeedback}
|
|
rows={3}
|
|
onChange={(event, newInput?: string) => {
|
|
setDeleteCollectionFeedback(newInput);
|
|
}}
|
|
ariaLabel={reasonInfo}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</RightPaneForm>
|
|
);
|
|
};
|