From 7fe5dcd0458aa2661b028541b33b1a00338cb54b Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Wed, 21 May 2025 13:32:18 -0400 Subject: [PATCH] Fleet UI components; Editor copy button added, File details/uploader gitopsCompatible can now be false (#29307) --- frontend/components/Editor/Editor.stories.tsx | 170 ++++++++++++++++++ frontend/components/Editor/Editor.tsx | 55 +++++- frontend/components/Editor/_styles.scss | 15 ++ .../components/FileDetails/FileDetails.tsx | 75 +++++--- .../components/FileUploader/FileUploader.tsx | 3 +- .../forms/fields/InputField/InputField.jsx | 24 +-- .../forms/fields/InputField/_styles.scss | 10 +- .../EditScriptModal/EditScriptModal.tsx | 1 - .../AdvancedOptionsModal.tsx | 2 - .../AdvancedOptionsFields.tsx | 3 - 10 files changed, 303 insertions(+), 55 deletions(-) create mode 100644 frontend/components/Editor/Editor.stories.tsx diff --git a/frontend/components/Editor/Editor.stories.tsx b/frontend/components/Editor/Editor.stories.tsx new file mode 100644 index 0000000000..cc516fbc86 --- /dev/null +++ b/frontend/components/Editor/Editor.stories.tsx @@ -0,0 +1,170 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { action } from "@storybook/addon-actions"; + +import Editor from "."; + +const meta: Meta = { + component: Editor, + title: "Components/FormFields/Editor", + argTypes: { + mode: { + control: "select", + options: ["sh", "powershell"], + description: "Syntax highlighting mode", + }, + readOnly: { control: "boolean" }, + enableCopy: { control: "boolean" }, + wrapEnabled: { control: "boolean" }, + isFormField: { control: "boolean" }, + focus: { control: "boolean" }, + label: { control: "text" }, + labelTooltip: { control: "text" }, + error: { control: "text" }, + helpText: { control: "text" }, + value: { control: "text" }, + defaultValue: { control: "text" }, + maxLines: { control: "number" }, + name: { control: "text" }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + name: "default-editor", + label: "Shell Script Editor", + value: "#!/bin/bash\necho 'Hello, World!'", + mode: "sh", + onChange: action("onChange"), + onBlur: action("onBlur"), + }, +}; + +export const WithError: Story = { + args: { + ...Default.args, + name: "error-editor", + label: "Editor with Error", + error: "There is a syntax error", + value: "echo 'Missing closing quote", + }, +}; + +export const WithHelpText: Story = { + args: { + ...Default.args, + name: "help-text-editor", + label: "Editor with Help Text", + helpText: "Write your shell script here. Supports Bash and PowerShell.", + }, +}; + +export const WithTooltip: Story = { + args: { + ...Default.args, + name: "tooltip-editor", + label: "Editor with Tooltip", + labelTooltip: "This editor supports syntax highlighting for shell scripts.", + }, +}; + +export const ReadOnly: Story = { + args: { + ...Default.args, + name: "readonly-editor", + label: "Read-only Editor", + readOnly: true, + value: "# This editor is read-only\nls -la", + }, +}; + +export const WithCopyButton: Story = { + args: { + ...Default.args, + name: "copy-editor", + label: "Editor with Copy Button", + value: "echo 'Copy this script!'", + enableCopy: true, + }, +}; + +export const PowerShellMode: Story = { + args: { + ...Default.args, + name: "powershell-editor", + label: "PowerShell Editor", + value: "Write-Host 'Hello from PowerShell!'", + mode: "powershell", + }, +}; + +export const WrappedLines: Story = { + args: { + ...Default.args, + name: "wrapped-lines-editor", + label: "Editor with Wrapped Lines", + value: + "# This is a very long line that should wrap to the next line in the editor to demonstrate the wrapEnabled prop in action.", + wrapEnabled: true, + }, +}; + +export const CustomMaxLines: Story = { + args: { + ...Default.args, + name: "custom-maxlines-editor", + label: "Editor with Custom Max Lines", + value: "echo 'Line 1'\necho 'Line 2'\necho 'Line 3'\n", + maxLines: 3, + }, +}; + +export const LongScriptWithCopy: Story = { + args: { + name: "long-script-editor", + label: "Long Script with Copy Button", + enableCopy: true, + mode: "sh", + value: `#!/bin/bash +# This is a long example script to test the editor's UI with overflow and copy button interaction. + +echo "Starting system update..." +sudo apt-get update -y && sudo apt-get upgrade -y + +echo "Installing dependencies..." +sudo apt-get install -y git curl wget unzip build-essential python3 python3-pip + +echo "Cloning repository..." +git clone https://github.com/example/repo.git /opt/example-repo + +echo "Setting up environment variables..." +export APP_ENV=production +export DB_HOST=localhost +export DB_PORT=5432 +export DB_USER=admin +export DB_PASSWORD=supersecurepassword1234567890 + +echo "Configuring application..." +cd /opt/example-repo +cp config.example.json config.json +sed -i 's/localhost/127.0.0.1/g' config.json + +echo "Running database migrations..." +python3 manage.py migrate + +echo "Starting application..." +nohup python3 manage.py runserver 0.0.0.0:8000 & + +echo "Setup complete! Application is running." +`, + wrapEnabled: false, // Try toggling this to true to see the difference! + maxLines: 20, + helpText: + "This is a realistic, long shell script. Try copying it or scrolling horizontally.", + onChange: action("onChange"), + onBlur: action("onBlur"), + }, +}; diff --git a/frontend/components/Editor/Editor.tsx b/frontend/components/Editor/Editor.tsx index a68604257a..afbc625420 100644 --- a/frontend/components/Editor/Editor.tsx +++ b/frontend/components/Editor/Editor.tsx @@ -1,11 +1,17 @@ +import React, { MouseEvent, ReactNode, useState, useCallback } from "react"; + import classnames from "classnames"; -import TooltipWrapper from "components/TooltipWrapper"; -import React, { ReactNode } from "react"; import AceEditor from "react-ace"; import "ace-builds/src-noconflict/mode-sh"; import "ace-builds/src-noconflict/mode-powershell"; import { IAceEditor } from "react-ace/lib/types"; +import { stringToClipboard } from "utilities/copy_text"; + +import TooltipWrapper from "components/TooltipWrapper"; +import Button from "components/buttons/Button"; +import Icon from "components/Icon"; + const baseClass = "editor"; interface IEditorProps { @@ -24,6 +30,10 @@ interface IEditorProps { /** Sets the default value of the input. Use this if you'd like the editor * to be an uncontrolled component */ defaultValue?: string; + /** Enable copying the value of the editor. + * @default false + */ + enableCopy?: boolean; /** Enabled wrapping lines. * @default false */ @@ -36,7 +46,7 @@ interface IEditorProps { */ mode?: string; /** Include correct styles as a form field. - * @default false + * @default true */ isFormField?: boolean; maxLines?: number; @@ -61,10 +71,11 @@ const Editor = ({ value, defaultValue, readOnly = false, + enableCopy = false, wrapEnabled = false, name = "editor", mode, - isFormField = false, + isFormField = true, maxLines = 20, className, onChange, @@ -75,6 +86,41 @@ const Editor = ({ [`${baseClass}__error`]: !!error, }); + const [showCopiedMessage, setShowCopiedMessage] = useState(false); + + const onClickCopy = useCallback( + (e: MouseEvent) => { + e.preventDefault(); + stringToClipboard(value).then(() => { + setShowCopiedMessage(true); + setTimeout(() => { + setShowCopiedMessage(false); + }, 2000); + }); + }, + [value] + ); + + const renderCopyButton = () => { + const copyButtonValue = ; + const wrapperClasses = classnames(`${baseClass}__copy-wrapper`); + + const copiedConfirmationClasses = classnames( + `${baseClass}__copied-confirmation` + ); + + return ( +
+ {showCopiedMessage && ( + Copied! + )} + +
+ ); + }; + const onLoadHandler = (editor: IAceEditor) => { // Lose focus using the Escape key so you can Tab forward (or Shift+Tab backwards) through app editor.commands.addCommand({ @@ -123,6 +169,7 @@ const Editor = ({ return (
{renderLabel()} + {enableCopy && renderCopyButton()} ) => void; accept?: string; progress?: number; + /** Set to false for one instance we allow users to edit a file as it shows them the YAML */ + gitopsCompatible?: boolean; gitOpsModeEnabled?: boolean; } @@ -35,10 +37,12 @@ const FileDetails = ({ onFileSelect, accept, progress, + gitopsCompatible = true, gitOpsModeEnabled = false, }: IFileDetailsProps) => { const infoClasses = classnames(`${baseClass}__info`, { - [`${baseClass}__info--disabled-by-gitops-mode`]: gitOpsModeEnabled, + [`${baseClass}__info--disabled-by-gitops-mode`]: + gitOpsModeEnabled && gitopsCompatible, }); return (
@@ -58,32 +62,49 @@ const FileDetails = ({ )}
- {!progress && canEdit && onFileSelect && ( - ( -
- - -
- )} - /> - )} + {!progress && + canEdit && + onFileSelect && + (gitopsCompatible ? ( + ( +
+ + +
+ )} + /> + ) : ( +
+ + +
+ ))} {!!progress && (
diff --git a/frontend/components/FileUploader/FileUploader.tsx b/frontend/components/FileUploader/FileUploader.tsx index 41a960f50d..9d79dc680a 100644 --- a/frontend/components/FileUploader/FileUploader.tsx +++ b/frontend/components/FileUploader/FileUploader.tsx @@ -216,7 +216,8 @@ export const FileUploader = ({ canEdit={canEdit} onFileSelect={onFileSelect} accept={accept} - gitOpsModeEnabled={gitopsCompatible && gitOpsModeEnabled} + gitopsCompatible={gitopsCompatible} + gitOpsModeEnabled={gitOpsModeEnabled} /> ) : ( renderFileUploader() diff --git a/frontend/components/forms/fields/InputField/InputField.jsx b/frontend/components/forms/fields/InputField/InputField.jsx index f9f6da8157..f4d2c56eac 100644 --- a/frontend/components/forms/fields/InputField/InputField.jsx +++ b/frontend/components/forms/fields/InputField/InputField.jsx @@ -107,6 +107,16 @@ class InputField extends Component { return false; }; + onClickCopy = (e) => { + e.preventDefault(); + stringToClipboard(this.props.value).then(() => { + this.setState({ copied: true }); + setTimeout(() => { + this.setState({ copied: false }); + }, 2000); + }); + }; + renderShowSecretButton = () => { const { onToggleSecret } = this; @@ -122,17 +132,7 @@ class InputField extends Component { }; renderCopyButton = () => { - const { value } = this.props; - - const copyValue = (e) => { - e.preventDefault(); - stringToClipboard(value).then(() => { - this.setState({ copied: true }); - setTimeout(() => { - this.setState({ copied: false }); - }, 2000); - }); - }; + const { onClickCopy } = this; const copyButtonValue = ; const wrapperClasses = classnames(`${baseClass}__copy-wrapper`); @@ -146,7 +146,7 @@ class InputField extends Component { {this.state.copied && ( Copied! )} - {this.props.enableShowSecret && this.renderShowSecretButton()} diff --git a/frontend/components/forms/fields/InputField/_styles.scss b/frontend/components/forms/fields/InputField/_styles.scss index 092e355b95..15f45232be 100644 --- a/frontend/components/forms/fields/InputField/_styles.scss +++ b/frontend/components/forms/fields/InputField/_styles.scss @@ -110,14 +110,14 @@ &__input-container.copy-enabled { position: relative; + + .input-field { + padding-right: 35px; // horizontal scroll of long inputs will not be hidden by copy button + } } &__copied-confirmation { - font-size: $x-small; - background-color: $ui-light-grey; - border: solid 1px $ui-fleet-black-10; - border-radius: $border-radius-xlarge; - padding: $pad-xxsmall 6px; + @include copy-message; } &__copied-confirmation-outside { diff --git a/frontend/pages/ManageControlsPage/Scripts/components/EditScriptModal/EditScriptModal.tsx b/frontend/pages/ManageControlsPage/Scripts/components/EditScriptModal/EditScriptModal.tsx index c91d3a145e..768cd63120 100644 --- a/frontend/pages/ManageControlsPage/Scripts/components/EditScriptModal/EditScriptModal.tsx +++ b/frontend/pages/ManageControlsPage/Scripts/components/EditScriptModal/EditScriptModal.tsx @@ -172,7 +172,6 @@ const EditScriptModal = ({ {preInstallQuery && (
@@ -73,7 +72,6 @@ const AdvancedOptionsModal = ({ maxLines={10} value={postInstallScript} helpText="Shell (macOS and Linux) or PowerShell (Windows)." - isFormField />
)} diff --git a/frontend/pages/SoftwarePage/components/forms/AdvancedOptionsFields/AdvancedOptionsFields.tsx b/frontend/pages/SoftwarePage/components/forms/AdvancedOptionsFields/AdvancedOptionsFields.tsx index 70d1599025..2d8eff9bbb 100644 --- a/frontend/pages/SoftwarePage/components/forms/AdvancedOptionsFields/AdvancedOptionsFields.tsx +++ b/frontend/pages/SoftwarePage/components/forms/AdvancedOptionsFields/AdvancedOptionsFields.tsx @@ -86,7 +86,6 @@ const AdvancedOptionsFields = ({ helpText={installScriptHelpText} label="Install script" labelTooltip={installScriptTooltip} - isFormField />
);