diff --git a/ui/package-lock.json b/ui/package-lock.json index 52911411d4..715c52d98f 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -34,6 +34,7 @@ "react-redux": "^8.1.1", "react-router-dom": "^6.14.1", "swr": "^2.2.0", + "tailwind-merge": "^1.14.0", "uuid": "^9.0.0", "yup": "^0.32.11" }, @@ -11960,6 +11961,15 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tailwind-merge": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz", + "integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", @@ -21272,6 +21282,11 @@ "tslib": "^2.5.0" } }, + "tailwind-merge": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz", + "integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==" + }, "tailwindcss": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", diff --git a/ui/package.json b/ui/package.json index fe0879490a..1abff5a842 100644 --- a/ui/package.json +++ b/ui/package.json @@ -38,6 +38,7 @@ "react-redux": "^8.1.1", "react-router-dom": "^6.14.1", "swr": "^2.2.0", + "tailwind-merge": "^1.14.0", "uuid": "^9.0.0", "yup": "^0.32.11" }, diff --git a/ui/src/components/forms/Combobox.tsx b/ui/src/components/forms/Combobox.tsx index 6685896998..daff047069 100644 --- a/ui/src/components/forms/Combobox.tsx +++ b/ui/src/components/forms/Combobox.tsx @@ -2,6 +2,7 @@ import { Combobox as C } from '@headlessui/react'; import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/24/outline'; import { useField } from 'formik'; import { useState } from 'react'; +import { twMerge } from 'tailwind-merge'; import { IFilterable } from '~/types/Selectable'; import { classNames } from '~/utils/helpers'; @@ -14,6 +15,7 @@ type ComboboxProps = { setSelected?: (v: T | null) => void; disabled?: boolean; className?: string; + inputClassNames?: string; }; export default function Combobox( @@ -22,6 +24,7 @@ export default function Combobox( const { id, className, + inputClassNames, values, selected, setSelected, @@ -51,7 +54,9 @@ export default function Combobox(
) => { setQuery(e.target.value); }} diff --git a/ui/src/components/forms/SegmentsPicker.tsx b/ui/src/components/forms/SegmentsPicker.tsx index 6dec4472b8..e844c5871e 100644 --- a/ui/src/components/forms/SegmentsPicker.tsx +++ b/ui/src/components/forms/SegmentsPicker.tsx @@ -1,10 +1,12 @@ import { MinusSmallIcon, PlusSmallIcon } from '@heroicons/react/24/outline'; import { useRef, useState } from 'react'; +import { twMerge } from 'tailwind-merge'; import Combobox from '~/components/forms/Combobox'; import { FilterableSegment, ISegment } from '~/types/Segment'; import { truncateKey } from '~/utils/helpers'; type SegmentPickerProps = { + readonly?: boolean; editMode?: boolean; segments: ISegment[]; selectedSegments: FilterableSegment[]; @@ -14,6 +16,7 @@ type SegmentPickerProps = { }; export default function SegmentsPicker({ + readonly = false, editMode = false, segments, selectedSegments: parentSegments, @@ -25,6 +28,8 @@ export default function SegmentsPicker({ new Set(parentSegments.map((s) => s.key)) ); + const [editing, setEditing] = useState(editMode); + const handleSegmentRemove = (index: number) => { const filterableSegment = parentSegments[index]; @@ -37,8 +42,6 @@ export default function SegmentsPicker({ } }; - const [editing, setEditing] = useState(editMode); - const handleSegmentSelected = ( index: number, segment: FilterableSegment | null @@ -76,18 +79,29 @@ export default function SegmentsPicker({ filterValue: truncateKey(s.key), displayValue: s.name }))} + disabled={readonly} selected={selectedSegment} setSelected={(filterableSegment) => { handleSegmentSelected(index, filterableSegment); }} + inputClassNames={ + readonly + ? 'cursor-not-allowed bg-gray-100 text-gray-500' + : undefined + } />
{editing && parentSegments.length - 1 === index ? (
@@ -98,6 +112,8 @@ export default function SegmentsPicker({ type="button" className="text-gray-400 mt-2 hover:text-gray-500" onClick={() => handleSegmentRemove(index)} + title={readonly ? 'Not allowed in Read-Only mode' : undefined} + disabled={readonly} >
diff --git a/ui/src/components/rollouts/forms/QuickEditRolloutForm.tsx b/ui/src/components/rollouts/forms/QuickEditRolloutForm.tsx index 1ee81a8dad..0a767308a6 100644 --- a/ui/src/components/rollouts/forms/QuickEditRolloutForm.tsx +++ b/ui/src/components/rollouts/forms/QuickEditRolloutForm.tsx @@ -1,5 +1,7 @@ import { FieldArray, Form, Formik } from 'formik'; import { useSelector } from 'react-redux'; +import { twMerge } from 'tailwind-merge'; +import { selectReadonly } from '~/app/meta/metaSlice'; import { selectCurrentNamespace } from '~/app/namespaces/namespacesSlice'; import TextButton from '~/components/forms/buttons/TextButton'; import Input from '~/components/forms/Input'; @@ -45,6 +47,8 @@ export default function QuickEditRolloutForm(props: QuickEditRolloutFormProps) { ? rollout.segment.segmentOperator : SegmentOperatorType.OR; + const readOnly = useSelector(selectReadonly); + const handleSegmentSubmit = (values: RolloutFormValues) => { let rolloutSegment = rollout; rolloutSegment.threshold = undefined; @@ -188,6 +192,7 @@ export default function QuickEditRolloutForm(props: QuickEditRolloutFormProps) { name="segmentKeys" render={(arrayHelpers) => ( @@ -213,7 +218,11 @@ export default function QuickEditRolloutForm(props: QuickEditRolloutFormProps) { id={segmentOperator.id} name="operator" type="radio" - className="text-violet-400 border-gray-300 h-4 w-4 focus:ring-violet-400" + className={twMerge( + `text-violet-400 border-gray-300 h-4 w-4 focus:ring-violet-400 ${ + readOnly ? 'cursor-not-allowed' : undefined + }` + )} onChange={() => { formik.setFieldValue( 'operator', @@ -224,6 +233,12 @@ export default function QuickEditRolloutForm(props: QuickEditRolloutFormProps) { segmentOperator.id === formik.values.operator } value={segmentOperator.id} + disabled={readOnly} + title={ + readOnly + ? 'Not allowed in Read-Only mode' + : undefined + } />
@@ -258,7 +273,14 @@ export default function QuickEditRolloutForm(props: QuickEditRolloutFormProps) { { label: 'True', value: 'true' }, { label: 'False', value: 'false' } ]} - className="w-full cursor-pointer appearance-none self-center rounded-lg py-1 align-middle" + className={twMerge( + `w-full cursor-pointer appearance-none self-center rounded-lg py-1 align-middle ${ + readOnly + ? 'text-gray-500 bg-gray-100 cursor-not-allowed' + : undefined + }` + )} + disabled={readOnly} />
diff --git a/ui/src/components/rules/forms/QuickEditRuleForm.tsx b/ui/src/components/rules/forms/QuickEditRuleForm.tsx index 2cd137e68b..884cb9d705 100644 --- a/ui/src/components/rules/forms/QuickEditRuleForm.tsx +++ b/ui/src/components/rules/forms/QuickEditRuleForm.tsx @@ -1,6 +1,8 @@ import { Field, FieldArray, Form, Formik } from 'formik'; import { useState } from 'react'; import { useSelector } from 'react-redux'; +import { twMerge } from 'tailwind-merge'; +import { selectReadonly } from '~/app/meta/metaSlice'; import { selectCurrentNamespace } from '~/app/namespaces/namespacesSlice'; import TextButton from '~/components/forms/buttons/TextButton'; import Combobox from '~/components/forms/Combobox'; @@ -19,7 +21,7 @@ import { SegmentOperatorType } from '~/types/Segment'; import { FilterableVariant } from '~/types/Variant'; -import { truncateKey } from '~/utils/helpers'; +import { classNames, truncateKey } from '~/utils/helpers'; import { distTypes } from './RuleForm'; type QuickEditRuleFormProps = { @@ -78,7 +80,7 @@ export default function QuickEditRuleForm(props: QuickEditRuleFormProps) { return null; }); - const [operator, setOperator] = useState(rule.operator); + const readOnly = useSelector(selectReadonly); const handleSubmit = async (values: RuleFormValues) => { const originalRuleSegments = rule.segments.map((s) => s.key); @@ -206,6 +208,7 @@ export default function QuickEditRuleForm(props: QuickEditRuleFormProps) { name="segmentKeys" render={(arrayHelpers) => ( @@ -231,7 +234,11 @@ export default function QuickEditRuleForm(props: QuickEditRuleFormProps) { id={segmentOperator.id} name="operator" type="radio" - className="text-violet-400 border-gray-300 h-4 w-4 focus:ring-violet-400" + className={twMerge( + `text-violet-400 border-gray-300 h-4 w-4 focus:ring-violet-400 ${ + readOnly ? 'cursor-not-allowed' : undefined + }` + )} onChange={() => { formik.setFieldValue( 'operator', @@ -242,6 +249,12 @@ export default function QuickEditRuleForm(props: QuickEditRuleFormProps) { segmentOperator.id === formik.values.operator } value={segmentOperator.id} + disabled={readOnly} + title={ + readOnly + ? 'Not allowed in Read-Only mode' + : undefined + } />
@@ -324,7 +337,7 @@ export default function QuickEditRuleForm(props: QuickEditRuleFormProps) { }))} selected={selectedVariant} setSelected={setSelectedVariant} - disabled + disabled={readOnly} />
@@ -364,7 +377,13 @@ export default function QuickEditRuleForm(props: QuickEditRuleFormProps) {