From a1432625a2390de1b0a2c06402f2d7a8ae185c22 Mon Sep 17 00:00:00 2001 From: Kyryl Perepelytsia <46731109+kyryl-perepelytsia@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:45:06 +0300 Subject: [PATCH] feat: allow to copy evaluate command in a CLI format (#3154) * feat: allow to copy evaluate command in a CLI format Signed-off-by: kyryl.perepelytsia * fix: pass each key/value as a seprate context option Signed-off-by: kyryl.perepelytsia --------- Signed-off-by: kyryl.perepelytsia --- ui/src/app/console/Console.tsx | 86 +++++++++++++++++++++------- ui/src/components/forms/Dropdown.tsx | 2 +- ui/src/types/Cli.ts | 14 +++++ ui/src/utils/helpers.ts | 5 ++ 4 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 ui/src/types/Cli.ts diff --git a/ui/src/app/console/Console.tsx b/ui/src/app/console/Console.tsx index a04de6c42e..de4592e224 100644 --- a/ui/src/app/console/Console.tsx +++ b/ui/src/app/console/Console.tsx @@ -1,5 +1,6 @@ import { json } from '@codemirror/lang-json'; import { ArrowPathIcon } from '@heroicons/react/20/solid'; +import { DocumentDuplicateIcon } from '@heroicons/react/24/outline'; import { tokyoNight } from '@uiw/codemirror-theme-tokyo-night'; import CodeMirror from '@uiw/react-codemirror'; import { Form, Formik, useFormikContext } from 'formik'; @@ -11,10 +12,12 @@ import * as Yup from 'yup'; import { useListAuthProvidersQuery } from '~/app/auth/authApi'; import { useListFlagsQuery } from '~/app/flags/flagsApi'; import { selectCurrentNamespace } from '~/app/namespaces/namespacesSlice'; +import { selectCurrentRef } from '~/app/refs/refsSlice'; import { ContextEditor } from '~/components/console/ContextEditor'; import EmptyState from '~/components/EmptyState'; import Button from '~/components/forms/buttons/Button'; import Combobox from '~/components/forms/Combobox'; +import Dropdown from '~/components/forms/Dropdown'; import Input from '~/components/forms/Input'; import { evaluateURL, evaluateV2 } from '~/data/api'; import { useError } from '~/data/hooks/error'; @@ -25,14 +28,15 @@ import { requiredValidation } from '~/data/validations'; import { IAuthMethod } from '~/types/Auth'; +import { Command } from '~/types/Cli'; import { FilterableFlag, FlagType, flagTypeToLabel, IFlag } from '~/types/Flag'; import { INamespace } from '~/types/Namespace'; import { copyTextToClipboard, + generateCliCommand, generateCurlCommand, getErrorMessage } from '~/utils/helpers'; -import { selectCurrentRef } from '~/app/refs/refsSlice'; function ResetOnNamespaceChange({ namespace }: { namespace: INamespace }) { const { resetForm } = useFormikContext(); @@ -133,6 +137,35 @@ export default function Console() { }); }; + const handleCopyAsCli = (values: ConsoleFormValues) => { + let parsed = null; + try { + // need to unescape the context string + parsed = JSON.parse(values.context); + } catch (err) { + setHasEvaluationError(true); + setError('Context provided is invalid.'); + return; + } + + const contextOptions = Object.entries(parsed).map(([key, value]) => ({ + key: '--context', + value: `${key}=${value}` + })); + + const command = generateCliCommand({ + commandName: Command.Evaluate, + arguments: [values.flagKey], + options: [ + { key: '--entity-id', value: values.entityId }, + { key: '--namespace', value: namespace.key }, + ...contextOptions + ] + }); + copyTextToClipboard(command); + setSuccess('Command copied to clipboard'); + }; + const handleCopyAsCurl = (values: ConsoleFormValues) => { let parsed = null; try { @@ -180,7 +213,7 @@ export default function Console() { return ( <> -
+

Console

@@ -265,24 +298,37 @@ export default function Console() {
- - +
+ handleCopyAsCurl(formik.values), + icon: DocumentDuplicateIcon + }, + { + id: 'cli', + disabled: !(formik.dirty && formik.isValid), + label: 'Flipt CLI', + onClick: () => handleCopyAsCli(formik.values), + icon: DocumentDuplicateIcon + } + ]} + /> +
+
+ +
diff --git a/ui/src/components/forms/Dropdown.tsx b/ui/src/components/forms/Dropdown.tsx index edd97f8142..5d9fa88150 100644 --- a/ui/src/components/forms/Dropdown.tsx +++ b/ui/src/components/forms/Dropdown.tsx @@ -44,7 +44,7 @@ export default function Dropdown(props: DropdownProps) { leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > - + {actions.map((action) => (
{!action.disabled && ( diff --git a/ui/src/types/Cli.ts b/ui/src/types/Cli.ts new file mode 100644 index 0000000000..7790aae5c9 --- /dev/null +++ b/ui/src/types/Cli.ts @@ -0,0 +1,14 @@ +export enum Command { + Evaluate = 'evaluate' +} + +export interface IOption { + key: string; + value: string; +} + +export interface ICommand { + commandName: Command; + arguments?: string[]; + options?: IOption[]; +} diff --git a/ui/src/utils/helpers.ts b/ui/src/utils/helpers.ts index c8f21b02ec..8bd9f02219 100644 --- a/ui/src/utils/helpers.ts +++ b/ui/src/utils/helpers.ts @@ -1,6 +1,7 @@ import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; import { defaultHeaders } from '~/data/api'; +import { ICommand } from '~/types/Cli'; import { ICurlOptions } from '~/types/Curl'; export function cls(...args: ClassValue[]) { @@ -116,3 +117,7 @@ export function generateCurlCommand(curlOptions: ICurlOptions) { curlOptions.uri ].join(' '); } + +export function generateCliCommand(command: ICommand): string { + return `flipt ${command.commandName} ${command.arguments?.join(' ')} ${command.options?.map(({ key, value }) => `${key} ${value}`).join(' ')}`; +}