import React, { useState, useRef } from "react"; import classnames from "classnames"; import Button from "components/buttons/Button"; import Card from "components/Card"; import { GraphicNames } from "components/graphics"; import Icon from "components/Icon"; import Graphic from "components/Graphic"; import FileDetails from "components/FileDetails"; import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper"; import TooltipWrapper from "components/TooltipWrapper"; const baseClass = "file-uploader"; export type ISupportedGraphicNames = Extract< GraphicNames, | "file-configuration-profile" | "file-sh" | "file-ps1" | "file-py" | "file-script" | "file-pdf" | "file-pkg" | "file-p7m" | "file-pem" | "file-vpp" | "file-png" >; interface IFileUploaderProps { label?: React.ReactNode; graphicName: ISupportedGraphicNames | ISupportedGraphicNames[]; message: React.ReactNode; title?: string; /** allow error state within the file uploader, as opposed to on its label */ internalError?: string; additionalInfo?: string; /** Controls the loading spinner on the upload button */ isLoading?: boolean; /** Disables the upload button */ disabled?: boolean; /** A comma separated string of one or more file types accepted to upload. * This is the same as the html accept attribute. * https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept */ accept?: string; /** The text to display on the upload button * @default "Upload" */ buttonMessage?: string; className?: string; /** renders the button to open the file uploader to appear as a button or * a link. * @default "button" */ buttonType?: "button" | "brand-inverse-icon"; /** renders a tooltip for the button. If `gitopsCompatible` is set to `true` * this tooltip will not be rendered if gitops mode is enabled. */ buttonTooltip?: React.ReactNode; onFileUpload: (files: FileList | null) => void; /** renders the current file with the edit pencil button */ canEdit?: boolean; /** renders a custom editor for the current file replacing the edit pencil button */ customEditor?: () => React.ReactNode; /** renders the current file with the delete trash button */ onDeleteFile?: () => void; /** if provided, will be called when the button is clicked * instead of opening the file selector. Useful if you want to * show the file selector UI but handle the file selection * in a modal. */ onButtonClick?: () => void; fileDetails?: { name: string; description?: React.ReactNode; }; /** Indicates that this file uploader deals with an entity that can be managed by GitOps, and so should be disabled when gitops mode is enabled */ gitopsCompatible?: boolean; /** Whether or not GitOpsMode is enabled. Has no effect if `gitopsCompatible` is false */ gitOpsModeEnabled?: boolean; } /** * A component that encapsulates the UI for uploading a file and a file selected. */ export const FileUploader = ({ label, graphicName: graphicNames, message, title, internalError, additionalInfo, isLoading = false, disabled = false, accept, className, buttonMessage = "Upload", buttonType = "button", buttonTooltip, onButtonClick, onFileUpload, canEdit = false, customEditor, onDeleteFile, fileDetails, gitopsCompatible = false, gitOpsModeEnabled = false, }: IFileUploaderProps) => { const [isFileSelected, setIsFileSelected] = useState(!!fileDetails); const fileInputRef = useRef(null); const classes = classnames(baseClass, className, { [`${baseClass}__file-preview`]: isFileSelected, [`${baseClass}__error`]: !!internalError, }); const buttonVariant = buttonType === "button" ? "default" : "brand-inverse-icon"; const triggerFileInput = () => { fileInputRef.current?.click(); }; const onFileSelect = (e: React.ChangeEvent) => { const target = e.currentTarget; // Ensure target is the expected input element to prevent DOM manipulation if (target && target.type === "file") { const files = target.files; onFileUpload(files); setIsFileSelected(true); if (fileInputRef.current) { fileInputRef.current.value = ""; } } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); triggerFileInput(); } }; const renderLabel = () => { return label ? (
{label}
) : null; }; const renderGraphics = () => { const graphicNamesArr = typeof graphicNames === "string" ? [graphicNames] : graphicNames; return graphicNamesArr.map((graphicName) => ( )); }; const renderUploadButton = () => { let buttonMarkup = ( <> {buttonMessage} {buttonType === "brand-inverse-icon" && ( )} ); // If we want to actual do file uploading, wrap in a label that // references the hidden file input. Otherwise just use a span. if (!onButtonClick) { buttonMarkup = ; } else { buttonMarkup = {buttonMarkup}; } // the gitops mode tooltip wrapper takes presedence over other button // renderings if (gitopsCompatible) { return ( ( )} /> ); } return ( ); }; const renderTitle = () => { if (internalError) { return (
{internalError}
); } if (title) { return
{title}
; } return null; }; const renderFileUploader = () => { return (
{renderGraphics()}
{renderTitle()}

{message}

{additionalInfo && (

{additionalInfo}

)}
{renderUploadButton()} {/* If onButtonClick is provided, we're not actually uploading files here. */} {!onButtonClick && ( )}
); }; return (
{renderLabel()} {fileDetails ? ( ) : ( renderFileUploader() )}
); }; export default FileUploader;