Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow to copy evaluate command in a CLI format #3154

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 66 additions & 20 deletions ui/src/app/console/Console.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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();
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -180,7 +213,7 @@ export default function Console() {

return (
<>
<div className="flex flex-col">
<div className="relative flex flex-col">
<h1 className="text-gray-900 text-2xl font-bold leading-7 sm:truncate sm:text-3xl">
Console
</h1>
Expand Down Expand Up @@ -265,24 +298,37 @@ export default function Console() {
</div>
</div>
<div className="flex justify-end">
<Button
className="ml-3"
type="button"
disabled={!(formik.dirty && formik.isValid)}
onClick={() => {
handleCopyAsCurl(formik.values);
}}
>
Copy as curl
</Button>
<Button
variant="primary"
className="ml-3"
type="submit"
disabled={!(formik.dirty && formik.isValid)}
>
Evaluate
</Button>
<div className="absolute left-[45%] mt-0.5">
<Dropdown
label="Copy"
actions={[
{
id: 'curl',
disabled: !(formik.dirty && formik.isValid),
label: 'Curl Request',
onClick: () => handleCopyAsCurl(formik.values),
icon: DocumentDuplicateIcon
},
{
id: 'cli',
disabled: !(formik.dirty && formik.isValid),
label: 'Flipt CLI',
onClick: () => handleCopyAsCli(formik.values),
icon: DocumentDuplicateIcon
}
]}
/>
</div>
<div>
<Button
variant="primary"
className="ml-3"
type="submit"
disabled={!(formik.dirty && formik.isValid)}
>
Evaluate
</Button>
</div>
</div>
</div>
<ResetOnNamespaceChange namespace={namespace} />
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/forms/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default function Dropdown(props: DropdownProps) {
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="bg-white absolute right-0 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<Menu.Items className="bg-white absolute right-0 z-10 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{actions.map((action) => (
<div className="py-1" key={action.id}>
{!action.disabled && (
Expand Down
14 changes: 14 additions & 0 deletions ui/src/types/Cli.ts
Original file line number Diff line number Diff line change
@@ -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[];
}
5 changes: 5 additions & 0 deletions ui/src/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -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[]) {
Expand Down Expand Up @@ -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(' ')}`;
}
Loading