diff --git a/js/apps/admin-ui/package.json b/js/apps/admin-ui/package.json index 27af8c8286..a080c12cee 100644 --- a/js/apps/admin-ui/package.json +++ b/js/apps/admin-ui/package.json @@ -82,7 +82,6 @@ "admin-ui": "file:", "dagre": "^0.8.5", "file-saver": "^2.0.5", - "file-selector": "^1.1.0", "flat": "^6.0.1", "i18next": "^23.16.3", "i18next-http-backend": "^2.6.2", @@ -92,7 +91,6 @@ "p-debounce": "^4.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-dropzone": "^14.2.10", "react-hook-form": "^7.53.1", "react-i18next": "^15.1.0", "react-router-dom": "^6.27.0", diff --git a/js/apps/admin-ui/src/clients/keys/GenerateKeyDialog.tsx b/js/apps/admin-ui/src/clients/keys/GenerateKeyDialog.tsx index 2f685a0b13..0260ee4136 100644 --- a/js/apps/admin-ui/src/clients/keys/GenerateKeyDialog.tsx +++ b/js/apps/admin-ui/src/clients/keys/GenerateKeyDialog.tsx @@ -1,14 +1,9 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { - Controller, - FormProvider, - useForm, - useFormContext, -} from "react-hook-form"; +import type KeyStoreConfig from "@keycloak/keycloak-admin-client/lib/defs/keystoreConfig"; +import { HelpItem, SelectControl } from "@keycloak/keycloak-ui-shared"; import { Button, ButtonVariant, + FileUpload, Form, FormGroup, Modal, @@ -16,12 +11,16 @@ import { Text, TextContent, } from "@patternfly/react-core"; - -import type KeyStoreConfig from "@keycloak/keycloak-admin-client/lib/defs/keystoreConfig"; -import { HelpItem, SelectControl } from "@keycloak/keycloak-ui-shared"; -import { StoreSettings } from "./StoreSettings"; -import { FileUpload } from "../../components/json-file-upload/patternfly/FileUpload"; +import { useState } from "react"; +import { + Controller, + FormProvider, + useForm, + useFormContext, +} from "react-hook-form"; +import { useTranslation } from "react-i18next"; import { useServerInfo } from "../../context/server-info/ServerInfoProvider"; +import { StoreSettings } from "./StoreSettings"; type GenerateKeyDialogProps = { clientId: string; @@ -100,10 +99,10 @@ export const KeyForm = ({ value={field.value} filename={filename} browseButtonText={t("browse")} - onChange={(value, filename) => { - setFilename(filename); + onTextChange={(value) => { field.onChange(value); }} + onFileInputChange={(_, file) => setFilename(file.name)} /> )} /> diff --git a/js/apps/admin-ui/src/clients/keys/ImportKeyDialog.tsx b/js/apps/admin-ui/src/clients/keys/ImportKeyDialog.tsx index cf6f5c5863..028d4ac580 100644 --- a/js/apps/admin-ui/src/clients/keys/ImportKeyDialog.tsx +++ b/js/apps/admin-ui/src/clients/keys/ImportKeyDialog.tsx @@ -1,6 +1,8 @@ +import { SelectControl } from "@keycloak/keycloak-ui-shared"; import { Button, ButtonVariant, + FileUpload, Form, FormGroup, Modal, @@ -10,8 +12,6 @@ import { } from "@patternfly/react-core"; import { Controller, FormProvider, useForm, useWatch } from "react-hook-form"; import { useTranslation } from "react-i18next"; -import { SelectControl } from "@keycloak/keycloak-ui-shared"; -import { FileUpload } from "../../components/json-file-upload/patternfly/FileUpload"; import { useServerInfo } from "../../context/server-info/ServerInfoProvider"; import { StoreSettings } from "./StoreSettings"; @@ -107,8 +107,11 @@ export const ImportKeyDialog = ({ id="importFile" value={field.value.value} filename={field.value.filename} - onChange={(value, filename) => - field.onChange({ value, filename }) + onTextChange={(value) => + field.onChange({ ...field.value, value }) + } + onFileInputChange={(_, file) => + field.onChange({ ...field.value, filename: file.name }) } /> )} diff --git a/js/apps/admin-ui/src/components/dynamic/FileComponent.tsx b/js/apps/admin-ui/src/components/dynamic/FileComponent.tsx index aec17e9e42..d251eb29eb 100644 --- a/js/apps/admin-ui/src/components/dynamic/FileComponent.tsx +++ b/js/apps/admin-ui/src/components/dynamic/FileComponent.tsx @@ -1,10 +1,8 @@ -import { FormGroup } from "@patternfly/react-core"; +import { HelpItem } from "@keycloak/keycloak-ui-shared"; +import { FileUpload, FormGroup } from "@patternfly/react-core"; import { useState } from "react"; import { Controller, useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; - -import { HelpItem } from "@keycloak/keycloak-ui-shared"; -import { FileUpload } from "../json-file-upload/patternfly/FileUpload"; import type { ComponentProps } from "./components"; import { convertToName } from "./DynamicComponents"; @@ -48,9 +46,8 @@ export const FileComponent = ({ }} isLoading={isLoading} allowEditingUploadedText={false} - onChange={(value, filename) => { + onTextChange={(value) => { field.onChange(value); - setFilename(filename); }} /> )} diff --git a/js/apps/admin-ui/src/components/json-file-upload/FileUploadForm.tsx b/js/apps/admin-ui/src/components/json-file-upload/FileUploadForm.tsx index 5666a6ea37..71bf23001a 100644 --- a/js/apps/admin-ui/src/components/json-file-upload/FileUploadForm.tsx +++ b/js/apps/admin-ui/src/components/json-file-upload/FileUploadForm.tsx @@ -1,6 +1,9 @@ import { CodeEditor, Language } from "@patternfly/react-code-editor"; import { Button, + DropEvent, + FileUpload, + FileUploadProps, FormGroup, FormHelperText, HelperText, @@ -14,9 +17,7 @@ import { MouseEvent as ReactMouseEvent, useState, } from "react"; -import { DropEvent } from "react-dropzone"; import { useTranslation } from "react-i18next"; -import { FileUpload, FileUploadProps } from "./patternfly/FileUpload"; type FileUploadType = { value: string; @@ -112,8 +113,8 @@ export const FileUploadForm = ({ value={fileUpload.value} filename={fileUpload.filename} onFileInputChange={handleFileInputChange} - onDataChange={handleTextOrDataChange} - onTextChange={handleTextOrDataChange} + onDataChange={(_, value) => handleTextOrDataChange(value)} + onTextChange={(_, value) => handleTextOrDataChange(value)} onClearClick={handleClear} onReadStarted={() => setFileUpload({ ...fileUpload, isLoading: true }) @@ -137,8 +138,8 @@ export const FileUploadForm = ({ value={fileUpload.value} filename={fileUpload.filename} onFileInputChange={handleFileInputChange} - onDataChange={handleTextOrDataChange} - onTextChange={handleTextOrDataChange} + onDataChange={(_, value) => handleTextOrDataChange(value)} + onTextChange={(_, value) => handleTextOrDataChange(value)} onClearClick={handleClear} onReadStarted={() => setFileUpload({ ...fileUpload, isLoading: true }) diff --git a/js/apps/admin-ui/src/components/json-file-upload/patternfly/FileUpload.tsx b/js/apps/admin-ui/src/components/json-file-upload/patternfly/FileUpload.tsx deleted file mode 100644 index 760d689ff9..0000000000 --- a/js/apps/admin-ui/src/components/json-file-upload/patternfly/FileUpload.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import { fromEvent } from "file-selector"; -import { PropsWithChildren } from "react"; -import { - DropEvent, - DropzoneInputProps, - DropzoneOptions, - FileRejection, - useDropzone, -} from "react-dropzone"; - -import { FileUploadField, FileUploadFieldProps } from "./FileUploadField"; -import { fileReaderType, readFile } from "./fileUtils"; - -export interface FileUploadProps - extends Omit< - FileUploadFieldProps, - | "children" - | "onBrowseButtonClick" - | "onClearButtonClick" - | "isDragActive" - | "containerRef" - > { - /** Unique id for the TextArea, also used to generate ids for accessible labels. */ - id: string; - /** What type of file. Determines what is is passed to `onChange` and expected by `value` - * (a string for 'text' and 'dataURL', or a File object otherwise. */ - type?: "text" | "dataURL"; - /** Value of the file's contents - * (string if text file, File object otherwise) */ - value?: string | File; - /** Value to be shown in the read-only filename field. */ - filename?: string; - /** @deprecated A callback for when the file contents change. Please instead use onFileInputChange, onTextChange, onDataChange, onClearClick individually. */ - onChange?: ( - value: string | File, - filename: string, - event: - | React.MouseEvent // Clear button was clicked - | React.ChangeEvent // User typed in the TextArea - | DropEvent, - ) => void; - /** Change event emitted from the hidden \ field associated with the component */ - onFileInputChange?: (event: DropEvent, file: File) => void; - /** Callback for clicking on the FileUploadField text area. By default, prevents a click in the text area from opening file dialog. */ - onClick?: (event: React.MouseEvent) => void; - /** Additional classes added to the FileUpload container element. */ - className?: string; - /** Flag to show if the field is disabled. */ - isDisabled?: boolean; - /** Flag to show if the field is read only. */ - isReadOnly?: boolean; - /** Flag to show if a file is being loaded. */ - isLoading?: boolean; - /** Aria-valuetext for the loading spinner */ - spinnerAriaValueText?: string; - /** Flag to show if the field is required. */ - isRequired?: boolean; - /** Value to indicate if the field is modified to show that validation state. - * If set to success, field will be modified to indicate valid state. - * If set to error, field will be modified to indicate error state. - */ - validated?: "success" | "error" | "default"; - /** Aria-label for the TextArea. */ - "aria-label"?: string; - /** Placeholder string to display in the empty filename field */ - filenamePlaceholder?: string; - /** Aria-label for the read-only filename field */ - filenameAriaLabel?: string; - /** Text for the Browse button */ - browseButtonText?: string; - /** Text for the Clear button */ - clearButtonText?: string; - /** Flag to hide the built-in preview of the file (where available). - * If true, you can use children to render an alternate preview. */ - hideDefaultPreview?: boolean; - /** Flag to allow editing of a text file's contents after it is selected from disk */ - allowEditingUploadedText?: boolean; - /** Additional children to render after (or instead of) the file preview. */ - children?: React.ReactNode; - - // Props available in FileUpload but not FileUploadField: - - /** A callback for when a selected file starts loading */ - onReadStarted?: (fileHandle: File) => void; - /** A callback for when a selected file finishes loading */ - onReadFinished?: (fileHandle: File) => void; - /** A callback for when the FileReader API fails */ - onReadFailed?: (error: DOMException, fileHandle: File) => void; - /** Optional extra props to customize react-dropzone. */ - dropzoneProps?: DropzoneOptions; - /** Clear button was clicked */ - onClearClick?: React.MouseEventHandler; - /** Text area text changed */ - onTextChange?: (text: string) => void; - /** On data changed - if type='text' or type='dataURL' and file was loaded it will call this method */ - onDataChange?: (data: string) => void; -} - -export const FileUpload = ({ - id, - type, - value = type === fileReaderType.text || type === fileReaderType.dataURL - ? "" - : undefined, - filename = "", - children = null, - // TODO: This should be removed as part of https://github.com/keycloak/keycloak/issues/32420 - // eslint-disable-next-line @typescript-eslint/no-deprecated - onChange, - onFileInputChange, - onReadStarted, - onReadFinished, - onReadFailed, - onClearClick, - onClick = (event) => event.preventDefault(), - onTextChange, - onDataChange, - dropzoneProps = {}, - ...props -}: PropsWithChildren) => { - const onDropAccepted = (acceptedFiles: File[], event: DropEvent) => { - if (acceptedFiles.length > 0) { - const fileHandle = acceptedFiles[0]; - - if (event?.type === "drop") { - onFileInputChange?.(event, fileHandle); - } - if (type === fileReaderType.text || type === fileReaderType.dataURL) { - onChange?.("", fileHandle.name, event); // Show the filename while reading - onReadStarted?.(fileHandle); - readFile(fileHandle, type as fileReaderType) - .then((data) => { - onReadFinished?.(fileHandle); - onChange?.(data as string, fileHandle.name, event); - onDataChange?.(data as string); - }) - .catch((error: DOMException) => { - onReadFailed?.(error, fileHandle); - onReadFinished?.(fileHandle); - onChange?.("", "", event); // Clear the filename field on a failure - onDataChange?.(""); - }); - } else { - onChange?.(fileHandle, fileHandle.name, event); - } - } - dropzoneProps.onDropAccepted?.(acceptedFiles, event); - }; - - const onDropRejected = (rejectedFiles: FileRejection[], event: DropEvent) => { - if (rejectedFiles.length > 0) { - onChange?.("", rejectedFiles[0].file.name, event); - } - - dropzoneProps.onDropRejected?.(rejectedFiles, event); - }; - - const onClearButtonClick = ( - event: React.MouseEvent, - ) => { - onChange?.("", "", event); - onClearClick?.(event); - setFileValue(""); - }; - - const { getRootProps, getInputProps, isDragActive, open, inputRef } = - useDropzone({ - multiple: false, - ...dropzoneProps, - onDropAccepted, - onDropRejected, - }); - - const setFileValue = (filename: string) => { - if (!inputRef.current) { - return; - } - - inputRef.current.value = filename; - }; - - const oldInputProps = getInputProps(); - const inputProps: DropzoneInputProps = { - ...oldInputProps, - onChange: async (e: React.ChangeEvent) => { - oldInputProps.onChange?.(e); - const files = await fromEvent(e.nativeEvent); - if (files.length === 1) { - onFileInputChange?.(e, files[0] as File); - } - }, - }; - - return ( - event.preventDefault(), - })} - tabIndex={undefined} // Omit the unwanted tabIndex from react-dropzone's getRootProps - id={id} - type={type} - filename={filename} - value={value} - onChange={onChange} - isDragActive={isDragActive} - onBrowseButtonClick={open} - onClearButtonClick={onClearButtonClick} - onTextAreaClick={onClick} - onTextChange={onTextChange} - onClick={(e) => e.stopPropagation()} - > - - {children} - - ); -}; - -FileUpload.displayName = "FileUpload"; diff --git a/js/apps/admin-ui/src/components/json-file-upload/patternfly/FileUploadField.tsx b/js/apps/admin-ui/src/components/json-file-upload/patternfly/FileUploadField.tsx deleted file mode 100644 index c7eee6ab79..0000000000 --- a/js/apps/admin-ui/src/components/json-file-upload/patternfly/FileUploadField.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { - Button, - ButtonVariant, - InputGroup, - Spinner, - spinnerSize, - TextArea, - TextAreResizeOrientation, - TextInput, - InputGroupItem, -} from "@patternfly/react-core"; -import { css } from "@patternfly/react-styles"; -import styles from "@patternfly/react-styles/css/components/FileUpload/file-upload"; -import { PropsWithChildren } from "react"; - -import { fileReaderType } from "./fileUtils"; - -export interface FileUploadFieldProps - extends Omit, "value" | "onChange"> { - /** Unique id for the TextArea, also used to generate ids for accessible labels */ - id: string; - /** What type of file. Determines what is is expected by `value` - * (a string for 'text' and 'dataURL', or a File object otherwise). */ - type?: "text" | "dataURL"; - /** Value of the file's contents - * (string if text file, File object otherwise) */ - value?: string | File; - /** Value to be shown in the read-only filename field. */ - filename?: string; - /** A callback for when the TextArea value changes. */ - onChange?: ( - value: string, - filename: string, - event: - | React.ChangeEvent // User typed in the TextArea - | React.MouseEvent, // User clicked Clear button - ) => void; - /** Additional classes added to the FileUploadField container element. */ - className?: string; - /** Flag to show if the field is disabled. */ - isDisabled?: boolean; - /** Flag to show if the field is read only. */ - isReadOnly?: boolean; - /** Flag to show if a file is being loaded. */ - isLoading?: boolean; - /** Aria-valuetext for the loading spinner */ - spinnerAriaValueText?: string; - /** Flag to show if the field is required. */ - isRequired?: boolean; - /** Value to indicate if the field is modified to show that validation state. - * If set to success, field will be modified to indicate valid state. - * If set to error, field will be modified to indicate error state. - */ - validated?: "success" | "error" | "default"; - /** Aria-label for the TextArea. */ - "aria-label"?: string; - /** Placeholder string to display in the empty filename field */ - filenamePlaceholder?: string; - /** Aria-label for the read-only filename field */ - filenameAriaLabel?: string; - /** Text for the Browse button */ - browseButtonText?: string; - /** Text for the Clear button */ - clearButtonText?: string; - /** Flag to disable the Clear button */ - isClearButtonDisabled?: boolean; - /** Flag to hide the built-in preview of the file (where available). - * If true, you can use children to render an alternate preview. */ - hideDefaultPreview?: boolean; - /** Flag to allow editing of a text file's contents after it is selected from disk */ - allowEditingUploadedText?: boolean; - /** Additional children to render after (or instead of) the file preview. */ - children?: React.ReactNode; - - // Props available in FileUploadField but not FileUpload: - - /** A callback for when the Browse button is clicked. */ - onBrowseButtonClick?: ( - event: React.MouseEvent, - ) => void; - /** A callback for when the Clear button is clicked. */ - onClearButtonClick?: ( - event: React.MouseEvent, - ) => void; - /** A callback from when the text area is clicked. Can also be set via the onClick property of FileUpload. */ - onTextAreaClick?: ( - event: React.MouseEvent, - ) => void; - /** Flag to show if a file is being dragged over the field */ - isDragActive?: boolean; - /** A reference object to attach to the FileUploadField container element. */ - containerRef?: React.Ref; - /** Text area text changed */ - onTextChange?: (text: string) => void; -} - -export const FileUploadField = ({ - id, - type, - value = "", - filename = "", - onChange, - onBrowseButtonClick, - onClearButtonClick, - onTextAreaClick, - onTextChange, - className = "", - isDisabled = false, - isReadOnly = false, - isLoading = false, - spinnerAriaValueText, - isRequired = false, - isDragActive = false, - validated = "default" as "success" | "error" | "default", - "aria-label": ariaLabel = "File upload", - filenamePlaceholder = "Drag a file here or browse to upload", - filenameAriaLabel = filename ? "Read only filename" : filenamePlaceholder, - browseButtonText = "Browse...", - clearButtonText = "Clear", - isClearButtonDisabled = !filename && !value, - containerRef = null as React.Ref, - allowEditingUploadedText = false, - hideDefaultPreview = false, - children = null, - - ...props -}: PropsWithChildren) => { - const onTextAreaChange = ( - newValue: string, - event: React.ChangeEvent, - ) => { - onChange?.(newValue, filename, event); - onTextChange?.(newValue); - }; - return ( -
-
- - - - - - - - - - - -
-
- {!hideDefaultPreview && type === fileReaderType.text && ( -