@carbon/ibm-products
Version:
Carbon for IBM Products
553 lines (547 loc) • 17.5 kB
JavaScript
/**
* Copyright IBM Corp. 2020, 2025
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js';
import React__default, { forwardRef, useState, useRef, useEffect } from 'react';
import PropTypes from '../../_virtual/index.js';
import cx from 'classnames';
import { unstable_FeatureFlags, ComposedModal, ModalHeader, ModalBody, PasswordInput, TextInput, Form, InlineLoading, ModalFooter, Button } from '@carbon/react';
import { ErrorFilled, InformationFilled, CheckmarkFilled, Copy } from '@carbon/react/icons';
import { APIKeyDownloader } from './APIKeyDownloader.js';
import { pkg } from '../../settings.js';
import { usePortalTarget } from '../../global/js/hooks/usePortalTarget.js';
import { getDevtoolsProps } from '../../global/js/utils/devtools.js';
import { isRequiredIf } from '../../global/js/utils/props-helper.js';
import uuidv4 from '../../global/js/utils/uuidv4.js';
import { useFocus } from '../../global/js/hooks/useFocus.js';
import { usePreviousValue } from '../../global/js/hooks/usePreviousValue.js';
var _ErrorFilled, _InformationFilled;
const componentName = 'APIKeyModal';
// Default values for props
const defaults = {
apiKeyName: '',
customSteps: []
};
let APIKeyModal = /*#__PURE__*/forwardRef((_ref, ref) => {
let {
// The component props, in alphabetical order (for consistency).
apiKey,
apiKeyLabel,
apiKeyName = defaults.apiKeyName,
body,
className,
closeButtonText,
copyButtonText,
copyErrorText,
copyIconDescription,
customSteps = defaults.customSteps,
downloadBodyText,
downloadFileName,
downloadFileType,
downloadLinkText,
downloadLinkLabel = downloadLinkText,
editButtonText,
editSuccess,
editSuccessTitle,
editSuccessMessage,
editing,
error,
errorText,
generateButtonText,
generateSuccessBody,
generateSuccessTitle,
generateSuccessMessage,
generateTitle,
hasAPIKeyVisibilityToggle,
hasDownloadLink,
hideAPIKeyLabel,
launcherButtonRef,
loading,
loadingText,
modalLabel,
nameHelperText,
nameLabel,
namePlaceholder,
nameRequired,
nextStepButtonText,
onClose,
onCopy,
onRequestEdit,
onRequestGenerate,
open,
portalTarget: portalTargetIn,
previousStepButtonText,
showAPIKeyLabel,
helperText,
// Collect any other property values passed in.
...rest
} = _ref;
const [title, setTitle] = useState(null);
const [successMessage, setSuccessMessage] = useState(null);
const [copyError, setCopyError] = useState(false);
const [name, setName] = useState(apiKeyName);
const [currentStep, setCurrentStep] = useState(0);
const copyRef = useRef(undefined);
const apiKeyInputId = useRef(uuidv4());
const nameInputId = useRef(uuidv4());
const renderPortalUse = usePortalTarget(portalTargetIn);
const hasSteps = Boolean(customSteps.length);
const apiKeyLoaded = apiKey && !loading;
const hasNextStep = hasSteps && currentStep < customSteps.length - 1;
const hasPreviousStep = hasSteps && currentStep !== 0;
const copyButtonProps = {
renderIcon: props => /*#__PURE__*/React__default.createElement(Copy, _extends({
size: 16
}, props)),
iconDescription: copyIconDescription,
ref: copyRef
};
const blockClass = `${pkg.prefix}--apikey-modal`;
const localRef = useRef(undefined);
const PasswordInputRef = useRef(null);
const modalRef = ref || localRef;
const {
firstElement,
keyDownListener,
claimFocus
} = useFocus(modalRef);
const prevOpen = usePreviousValue(open);
useEffect(() => {
if (copyRef.current && open && apiKeyLoaded) {
copyRef.current.focus();
}
if (PasswordInputRef?.current) {
PasswordInputRef?.current.setAttribute('readOnly', 'true');
}
}, [open, apiKeyLoaded]);
useEffect(() => {
if (open) {
// Focusing the first element or selectorPrimaryFocus element
claimFocus();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [modalRef, open, firstElement]);
useEffect(() => {
if (prevOpen && !open && launcherButtonRef) {
setTimeout(() => {
launcherButtonRef.current.focus();
}, 0);
}
}, [launcherButtonRef, open, prevOpen]);
const isPrimaryButtonDisabled = () => {
if (loading) {
return true;
}
if (hasSteps && 'valid' in (customSteps?.[currentStep] || [])) {
return !customSteps[currentStep]?.valid;
}
if (!hasSteps && nameRequired && !name) {
return true;
}
return false;
};
const getPrimaryButtonText = () => {
if (hasNextStep) {
return nextStepButtonText;
}
if (apiKeyLoaded) {
return copyButtonText;
}
if (editing) {
return editButtonText;
}
return generateButtonText;
};
const getSecondaryButtonText = () => {
if (hasPreviousStep && !apiKeyLoaded) {
return previousStepButtonText;
}
return closeButtonText;
};
useEffect(() => {
if (editing && editSuccess) {
setTitle(generateTitle);
setSuccessMessage(editSuccessMessage ?? editSuccessTitle);
} else if (apiKeyLoaded) {
setTitle(generateTitle);
setSuccessMessage(generateSuccessMessage ?? generateSuccessTitle);
} else if (hasSteps) {
setTitle(customSteps[currentStep]?.title);
} else {
setTitle(generateTitle);
}
}, [apiKeyLoaded, loading, editing, editSuccess, editSuccessTitle, editSuccessMessage, hasSteps, generateSuccessTitle, generateSuccessMessage, generateTitle, currentStep, customSteps]);
const setNameHandler = evt => {
setName(evt.target.value);
};
const onCloseHandler = () => {
setName('');
setCurrentStep(0);
onClose?.();
};
const submitHandler = async e => {
e.preventDefault();
if (hasNextStep) {
setCurrentStep(currentStep + 1);
} else if (apiKeyLoaded) {
if (onCopy) {
onCopy(apiKey);
} else {
try {
await navigator.clipboard.writeText(apiKey);
} catch (e) {
console.error(e);
setCopyError(true);
}
}
} else if (editing) {
onRequestEdit?.(name);
} else {
onRequestGenerate?.(name);
}
};
const onBackHandler = () => {
if (hasPreviousStep && !apiKeyLoaded) {
setCurrentStep(currentStep - 1);
} else {
onCloseHandler();
}
};
return renderPortalUse(/*#__PURE__*/React__default.createElement(unstable_FeatureFlags, {
enableExperimentalFocusWrapWithoutSentinels: true
}, /*#__PURE__*/React__default.createElement(ComposedModal, _extends({}, rest, {
open
}, getDevtoolsProps(componentName), {
ref: modalRef,
onKeyDown: keyDownListener,
className: cx(className, blockClass),
onClose: onCloseHandler,
size: "sm",
"aria-label": modalLabel,
preventCloseOnClickOutside: true
}), /*#__PURE__*/React__default.createElement(ModalHeader, {
className: `${blockClass}__header`,
title: title,
label: modalLabel
}), /*#__PURE__*/React__default.createElement(ModalBody, {
className: `${blockClass}__body-container`
}, hasSteps && !apiKeyLoaded ? customSteps[currentStep]?.content : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, body && /*#__PURE__*/React__default.createElement("p", {
className: `${blockClass}__body`
}, body), !editing && apiKey && hasAPIKeyVisibilityToggle && /*#__PURE__*/React__default.createElement(PasswordInput, {
value: apiKey,
labelText: apiKeyLabel,
id: apiKeyInputId.current,
showPasswordLabel: showAPIKeyLabel,
hidePasswordLabel: hideAPIKeyLabel,
tooltipPosition: "left",
helperText: helperText,
ref: PasswordInputRef
}), !editing && apiKey && !hasAPIKeyVisibilityToggle && /*#__PURE__*/React__default.createElement(TextInput, {
value: apiKey,
labelText: apiKeyLabel,
id: apiKeyInputId.current
}), (editing || !apiKeyLoaded && nameRequired) && /*#__PURE__*/React__default.createElement(Form, {
onSubmit: submitHandler,
"aria-label": title ?? undefined
}, /*#__PURE__*/React__default.createElement(TextInput, {
helperText: nameHelperText,
placeholder: namePlaceholder,
labelText: nameLabel,
onChange: setNameHandler,
value: name,
id: nameInputId.current,
disabled: loading,
required: nameRequired,
"data-modal-primary-focus": true
})), loading && /*#__PURE__*/React__default.createElement(InlineLoading, {
description: loadingText,
className: `${blockClass}__loader`
}), (copyError || error) && /*#__PURE__*/React__default.createElement("div", {
className: `${blockClass}__messaging`
}, /*#__PURE__*/React__default.createElement("div", {
className: `${blockClass}__error-icon`
}, _ErrorFilled || (_ErrorFilled = /*#__PURE__*/React__default.createElement(ErrorFilled, {
size: 16
}))), /*#__PURE__*/React__default.createElement("p", {
className: `${blockClass}__messaging-text`,
role: "alert",
"aria-live": "assertive"
}, copyError ? copyErrorText : errorText)), apiKeyLoaded && /*#__PURE__*/React__default.createElement("div", {
className: `${blockClass}__messaging`
}, _InformationFilled || (_InformationFilled = /*#__PURE__*/React__default.createElement(InformationFilled, {
size: 16
})), hasDownloadLink ? /*#__PURE__*/React__default.createElement(APIKeyDownloader, {
apiKey: apiKey,
body: downloadBodyText,
fileName: downloadFileName,
linkText: downloadLinkText,
fileType: downloadFileType,
downloadLinkLabel: downloadLinkLabel
}) : /*#__PURE__*/React__default.createElement("div", {
className: `${blockClass}__messaging-text`,
role: "alert",
"aria-live": "assertive"
}, generateSuccessBody)), (editSuccess || apiKeyLoaded && successMessage) && /*#__PURE__*/React__default.createElement("div", {
className: `${blockClass}__messaging`
}, /*#__PURE__*/React__default.createElement(CheckmarkFilled, {
size: 16,
className: `${blockClass}__checkmark-icon`
}), /*#__PURE__*/React__default.createElement("p", {
className: `${blockClass}__messaging-text`,
role: "alert",
"aria-live": "assertive"
}, successMessage)))), /*#__PURE__*/React__default.createElement(ModalFooter, {
className: `${blockClass}__footer`
}, /*#__PURE__*/React__default.createElement(Button, {
type: "button",
kind: "secondary",
onClick: onBackHandler
}, getSecondaryButtonText()), /*#__PURE__*/React__default.createElement(Button, _extends({}, apiKeyLoaded && copyButtonProps, {
type: "submit",
kind: "primary",
onClick: submitHandler,
disabled: isPrimaryButtonDisabled()
}), getPrimaryButtonText())))));
});
const customStepsRequiredProps = type => isRequiredIf(type, _ref2 => {
let {
customSteps
} = _ref2;
return customSteps && customSteps.length > 1;
});
const editRequiredProps = type => isRequiredIf(type, _ref3 => {
let {
editing
} = _ref3;
return editing;
});
const downloadRequiredProps = type => isRequiredIf(type, _ref4 => {
let {
hasDownloadLink
} = _ref4;
return hasDownloadLink;
});
// Return a placeholder if not released and not enabled by feature flag
APIKeyModal = pkg.checkComponentEnabled(APIKeyModal, componentName);
const deprecatedProps = {
/**
* deprecated
* title for a successful edit
*/
editSuccessTitle: PropTypes.string,
/**
* deprecated
* title for a successful key generation
*/
generateSuccessTitle: PropTypes.string
};
APIKeyModal.propTypes = {
/**
* the api key that's displayed to the user when a request to create is fulfilled.
*/
apiKey: PropTypes.string,
/**
* label for the text input that holds the api key.
*/
apiKeyLabel: PropTypes.string,
/**
* the name of the api key. should only be supplied in edit mode.
*/
apiKeyName: PropTypes.string,
/**
* body content for the modal
*/
body: PropTypes.string,
/**
* optional class name
*/
className: PropTypes.string,
/**
* text for the close button
*/
closeButtonText: PropTypes.string,
/**
* text for the copy button
*/
copyButtonText: PropTypes.string,
/**
* Error message for when the copy function fails
*/
copyErrorText: PropTypes.string,
/**
* text description for the copy button icon
*/
copyIconDescription: PropTypes.string,
/**
* if you need more options for key creation beyond just the name use custom steps to obtain whatever data is required.
*/
/**@ts-ignore*/
customSteps: PropTypes.arrayOf(PropTypes.shape({
/**
* designates if the step has passed whatever validation rules are in place.
*/
valid: PropTypes.bool,
/**
* designates content is the JSX that holds whatever inputs you need
*/
content: PropTypes.node,
/**
* designates the title that's displayed at the top of the modal for each step
*/
title: PropTypes.string
})),
/**
* the content that appears that indicates the key is downloadable
*/
downloadBodyText: PropTypes.string,
/**
* designates the name of downloadable json file with the key. if not specified will default to 'apikey'
*/
downloadFileName: downloadRequiredProps(PropTypes.string),
/**
* designates the file type for the downloadable key
*/
downloadFileType: downloadRequiredProps(PropTypes.oneOf(['txt', 'json'])),
/**
* aria-label for the download link
*/
downloadLinkLabel: downloadRequiredProps(PropTypes.string),
/**
* anchor text for the download link
*/
downloadLinkText: downloadRequiredProps(PropTypes.string),
/**
* text for the edit button
*/
editButtonText: editRequiredProps(PropTypes.string),
/**
* designates if the edit request was successful
*/
editSuccess: editRequiredProps(PropTypes.bool),
/**
* title for a successful edit
*/
editSuccessMessage: editRequiredProps(PropTypes.string),
/**
* designates if the modal is in the edit mode
*/
editing: PropTypes.bool,
/**
* designates if an error has occurred in a request
*/
error: PropTypes.bool,
/**
* text to display if an error has occurred
*/
errorText: PropTypes.string,
/**
* default primary button text for modal in assumed default mode create or generate.
* in create mode this is the button text prior to supplying an api key, which then
* uses copyButtonText
*/
generateButtonText: PropTypes.string,
/**
* content to display if generate request was successful
*/
generateSuccessBody: PropTypes.node,
/**
* title for a successful key generation
*/
generateSuccessMessage: PropTypes.string,
/**
* default title for the modal in generate key mode
*/
generateTitle: PropTypes.string,
/**
* designates if the api input has the visibility toggle enabled
*/
hasAPIKeyVisibilityToggle: PropTypes.bool,
/**
* designates if user is able to download the api key
*/
hasDownloadLink: PropTypes.bool,
/**
* helper text for password input
*/
helperText: PropTypes.string,
/**
* label text that's displayed when hovering over visibility toggler to hide key
*/
hideAPIKeyLabel: PropTypes.string,
/**
* Provide a ref to return focus to once the tearsheet is closed.
*/
/**@ts-ignore */
launcherButtonRef: PropTypes.any,
/**
* designates if the modal is in a loading state via a request or some other in progress operation
*/
loading: PropTypes.bool,
/**
* text that displays while modal is in the loading state
*/
loadingText: PropTypes.string,
/**
* general label text for modal
*/
modalLabel: PropTypes.string,
/**
* helper text for name input
*/
nameHelperText: PropTypes.string,
/**
* label for api key name input
*/
nameLabel: PropTypes.string,
/**
* placeholder text for api key name input
*/
namePlaceholder: PropTypes.string,
/**
* designates if a name is required or not for key generation. NOTE- if using custom steps set this to false since you will be using your own validation
*/
nameRequired: PropTypes.bool,
/**
* text that displays in the primary button when using custom steps to indicate to the user that there is a next step
*/
nextStepButtonText: customStepsRequiredProps(PropTypes.string),
/**
* handler for on modal close
*/
onClose: PropTypes.func,
/**
* Optional callback if you want to use your own copy function instead of the build in one
* onCopy(apiKey)
*/
onCopy: PropTypes.func,
/**
* handler for api key edit
*/
onRequestEdit: PropTypes.func,
/**
* handler for api key generation
*/
onRequestGenerate: PropTypes.func,
/**
* designates if modal is open or closed
*/
open: PropTypes.bool.isRequired,
/**
* The DOM node the tearsheet should be rendered within. Defaults to document.body.
*/
portalTarget: PropTypes.node,
/**
* text that displays in the secondary button when using custom steps to indicate to the user that there is a previous step
*/
previousStepButtonText: customStepsRequiredProps(PropTypes.string),
/**
* label text that's displayed when hovering over visibility toggler to show key
*/
showAPIKeyLabel: PropTypes.string,
...deprecatedProps
};
APIKeyModal.displayName = componentName;
export { APIKeyModal, deprecatedProps };